evals/constraint_oracle: span-free canonical setup signature (unknowns/facts/constraints/query; terms merged+sorted, lhs constant folded into rhs), a 13-fixture gold (7 solved / 3 solver_refuses / 3 reader_refuses) with a closed expect+refusal taxonomy, and a gold-validation runner. No reader yet — this proves the ruler is coherent (13/13 valid), not capability. ADR-0211 ratifies the R2 finite-integer two-category constraint compiler: the IR, the canonical signature, the closed taxonomy, the four wrong=0 gates (setup oracle / reader refusal / exact solver / answer-choice contradiction flag), off-serving disjointness, and the C1-C9 ladder. Cross-refs ADR-0207/0175/0083/0055-0057; reuses the R1 setup_oracle pattern. Off-serving (no generate.derivation / reliability_gate). 13 oracle tests incl. per-branch meaningful-fail.
172 lines
7.1 KiB
Python
172 lines
7.1 KiB
Python
"""R2 constraint setup-oracle runner — the RULER, before any reader capability (C2).
|
|
|
|
There is no R2 reader yet (it lands C5+). This lane validates that the independent gold is
|
|
internally coherent and canonicalizes stably: every fixture deserializes into the typed
|
|
:class:`ConstraintProblem` IR, its setup signature is deterministic, its taxonomy is closed,
|
|
and — for ``solved`` fixtures — the provided multiple-choice key agrees with the gold value.
|
|
When the reader lands, a grading lane compares the reader's signature against these same gold
|
|
signatures; the solver lane (C3) verifies that each ``solved`` setup actually computes ``gold``.
|
|
|
|
Exit 0 iff ``invalid == 0``. "Zero capability" is the point: this proves the ruler, not a reader.
|
|
|
|
Gold fixture ``expect`` taxonomy (closed):
|
|
- ``solved`` — well-formed two-category / two-constraint setup; ``gold`` is the int
|
|
answer; ``options[answer] == gold`` (coherent key).
|
|
- ``solver_refuses`` — well-formed setup, but unsolvable; ``solver_reason`` says why; no gold.
|
|
- ``reader_refuses`` — incomplete/ambiguous prose the reader must refuse to assemble;
|
|
``reader_reason`` says why; no setup fields required, no gold.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from evals.constraint_oracle.signature import constraint_setup_signature
|
|
from generate.constraint_comprehension.expr import LinearConstraint, LinearExpr
|
|
from generate.constraint_comprehension.model import (
|
|
AttributeFact,
|
|
ConstraintProblem,
|
|
ConstraintQuery,
|
|
Unknown,
|
|
)
|
|
|
|
_R2_GOLD_PATH = Path(__file__).resolve().parent / "r2_gold.jsonl"
|
|
|
|
#: Closed taxonomies. ``READER_REASONS`` grows as reader slices (C6/C8) add coefficient-level
|
|
#: refusals (equal coefficients, unit mismatch, …); each addition is ratified with its fixture.
|
|
EXPECTATIONS = frozenset({"solved", "solver_refuses", "reader_refuses"})
|
|
SOLVER_REASONS = frozenset(
|
|
{"indistinguishable_weights", "non_integer_solution", "negative_solution", "verification_failed"}
|
|
)
|
|
READER_REASONS = frozenset({"missing_total_count", "missing_weighted_total", "too_many_categories"})
|
|
DOMAINS = frozenset({"nonnegative_integer", "integer"})
|
|
|
|
|
|
def _load_r2_gold() -> list[dict[str, Any]]:
|
|
return [
|
|
json.loads(line)
|
|
for line in _R2_GOLD_PATH.read_text(encoding="utf-8").splitlines()
|
|
if line.strip()
|
|
]
|
|
|
|
|
|
def gold_to_problem(fx: dict[str, Any]) -> ConstraintProblem:
|
|
"""Deserialize a gold fixture's setup fields into the typed ConstraintProblem IR.
|
|
|
|
Terms arrive as ``[symbol, coefficient]`` pairs (the pinned serialization). Raises the
|
|
standard ``KeyError``/``TypeError``/``ValueError`` on a malformed fixture — the validator
|
|
turns that into an ``invalid`` outcome rather than a crash.
|
|
"""
|
|
unknowns = tuple(
|
|
Unknown(u["symbol"], u["entity"], u["unit"], u["domain"]) for u in fx["unknowns"]
|
|
)
|
|
facts = tuple(
|
|
AttributeFact(f["category"], f["measured_unit"], int(f["value"]))
|
|
for f in fx.get("facts", [])
|
|
)
|
|
constraints = tuple(
|
|
LinearConstraint(
|
|
LinearExpr(
|
|
tuple((str(s), int(c)) for s, c in con["terms"]),
|
|
int(con.get("constant", 0)),
|
|
),
|
|
con["relation"],
|
|
int(con["rhs"]),
|
|
)
|
|
for con in fx["constraints"]
|
|
)
|
|
q = fx["query"]
|
|
return ConstraintProblem(unknowns, facts, constraints, ConstraintQuery(q["symbol"], q["unit"]))
|
|
|
|
|
|
def validate_fixture(fx: dict[str, Any]) -> tuple[str, str | None]:
|
|
"""Validate one gold fixture's internal coherence. Returns ``(outcome, reason)`` where
|
|
``outcome`` is ``"valid"`` or ``"invalid"`` and ``reason`` names the failing check."""
|
|
expect = fx.get("expect")
|
|
if expect not in EXPECTATIONS:
|
|
return "invalid", f"unknown_expect:{expect!r}"
|
|
|
|
if expect == "reader_refuses":
|
|
if fx.get("reader_reason") not in READER_REASONS:
|
|
return "invalid", f"unknown_reader_reason:{fx.get('reader_reason')!r}"
|
|
if fx.get("gold") is not None:
|
|
return "invalid", "reader_refuses_has_gold"
|
|
return "valid", None
|
|
|
|
# solved | solver_refuses both require a well-formed two-category, two-constraint setup.
|
|
try:
|
|
problem = gold_to_problem(fx)
|
|
except (KeyError, TypeError, ValueError) as exc:
|
|
return "invalid", f"malformed_setup:{exc}"
|
|
|
|
symbols = {u.symbol for u in problem.unknowns}
|
|
if len(problem.unknowns) != 2 or len(symbols) != 2:
|
|
return "invalid", "v1_requires_exactly_two_distinct_categories"
|
|
if any(u.domain not in DOMAINS for u in problem.unknowns):
|
|
return "invalid", "bad_domain"
|
|
if len(problem.constraints) != 2:
|
|
return "invalid", "v1_requires_exactly_two_constraints"
|
|
if any(sym not in symbols for c in problem.constraints for sym, _ in c.lhs.terms):
|
|
return "invalid", "constraint_references_unknown_symbol"
|
|
if any(f.category not in symbols for f in problem.facts):
|
|
return "invalid", "fact_references_unknown_symbol"
|
|
if problem.query.symbol not in symbols:
|
|
return "invalid", "query_target_not_a_category"
|
|
if constraint_setup_signature(problem) != constraint_setup_signature(problem):
|
|
return "invalid", "nondeterministic_signature" # pragma: no cover - determinism guard
|
|
|
|
if expect == "solver_refuses":
|
|
if fx.get("solver_reason") not in SOLVER_REASONS:
|
|
return "invalid", f"unknown_solver_reason:{fx.get('solver_reason')!r}"
|
|
if fx.get("gold") is not None:
|
|
return "invalid", "solver_refuses_has_gold"
|
|
return "valid", None
|
|
|
|
# solved: an integer gold and a coherent multiple-choice key.
|
|
gold = fx.get("gold")
|
|
if not isinstance(gold, int) or isinstance(gold, bool):
|
|
return "invalid", "solved_needs_int_gold"
|
|
options, answer = fx.get("options"), fx.get("answer")
|
|
if not isinstance(options, dict) or answer not in options:
|
|
return "invalid", "missing_or_unlabeled_answer"
|
|
if options[answer] != gold:
|
|
return "invalid", "answer_key_incoherent"
|
|
return "valid", None
|
|
|
|
|
|
def run() -> dict[str, Any]:
|
|
"""Validate every R2 gold fixture. Exit-0 criterion for the lane is ``invalid == 0``."""
|
|
fixtures = _load_r2_gold()
|
|
valid = invalid = 0
|
|
by_expect: dict[str, int] = {}
|
|
details: list[dict[str, Any]] = []
|
|
for fx in fixtures:
|
|
outcome, reason = validate_fixture(fx)
|
|
expect = fx.get("expect", "?")
|
|
by_expect[expect] = by_expect.get(expect, 0) + 1
|
|
if outcome == "valid":
|
|
valid += 1
|
|
details.append({"id": fx.get("id"), "outcome": "valid", "expect": expect})
|
|
else:
|
|
invalid += 1
|
|
details.append({"id": fx.get("id"), "outcome": "invalid", "reason": reason})
|
|
return {
|
|
"lane": "constraint_oracle_gold_validation",
|
|
"total": len(fixtures),
|
|
"valid": valid,
|
|
"invalid": invalid,
|
|
"by_expect": by_expect,
|
|
"details": details,
|
|
}
|
|
|
|
|
|
__all__ = [
|
|
"EXPECTATIONS",
|
|
"READER_REASONS",
|
|
"SOLVER_REASONS",
|
|
"gold_to_problem",
|
|
"run",
|
|
"validate_fixture",
|
|
]
|