CMB (r4_combined_rate) now participates in route_setup and bounded contemplation. R1/R2/R3 readers/ solvers, the CMB reader/solver/oracle, and serving are all UNCHANGED — pure shared-infra integration, no new comprehension capability (that was CMB-c). - model.py: Organ += r4_combined_rate. - classify.py: classify_cmb + _cmb_signature (organ-tagged, cross-organ-distinct) + cmb_reason — namespaces CMB reasons cmb_* before the reason-string-keyed registry sees them, so a CMB boundary never inherits R2/R3's family for the SAME bare string (R3 rate_unit_mismatch is a GROWTH surface; R2/R3 non_integer_solution carry other owners). not_combined_rate_shaped/empty stay bare -> the cross input_shape family (router hygiene). - failure_family.py: Owner += r4; input_shape += not_combined_rate_shaped; 8 CMB families (must_remain_refused: cmb_unit_mismatch [until a dimension registry exists — NOT forever], cmb_combine_ambiguous, cmb_underdetermined, cmb_non_positive_net, cmb_non_integer; proposal_allowed -> cmb_gold_fixture: cmb_unsupported_rate_count/_reciprocal/_clock_interval). - pass_manager.py: _solve_and_verify_cmb (solver Refusal -> REFUSED_KNOWN_BOUNDARY, never a proposal). CMB-over-R3 domain-precedence rule (cmb_is_authoritative): when CMB POSITIVELY recognizes combined-rate shape (a setup, or a substantive cmb_* refusal — NOT the input_shape step-aside), R3's broader single-rate over-read of the SAME text is inadmissible — for BOTH routing (veto R3 setup_correct) and family attribution (suppress R3 so CMB's sharper diagnosis owns the terminal/proposal). This came from a pre-build cross-organ pressure-test: R3 reads 'Anna and Ben paint together; Anna paints 3 rooms/hour. How many do THEY paint in 4 hours?' as a single rate and would confidently answer a wrong 12. The rule fixes that (-> REFUSED) without touching R3. It does NOT mean 'CMB always wins': on input_shape CMB cedes (a plain single-rate car problem routes to R3 and solves 180). Narrow CMB<->R3 instance of a future general domain-specificity adjudication. Tests: new test_cmb_router_contemplation (full terminal matrix; cmb-11 veto-no-wrong-answer; cmb-15 cede-to-R3; solver-refusal-never-proposes; CMB-owned proposal-only artifacts mounted:false; no gold ambiguous; off-serving AST). Extended router-organ-hygiene to CMB x R1/R2/R3 (0 breaches). test_setup_router/test_r3_router_contemplation attempt-count 3->4; test_failure_family ALL_REASONS + growth set. 97 CMB-d tests; 597 broad-net pass (1 unrelated pre-existing red); CMB organ lanes green.
159 lines
7.2 KiB
Python
159 lines
7.2 KiB
Python
"""Normalize R1/R2 organ output into a typed ``ComprehensionAttempt`` (N2, setup-level).
|
|
|
|
`classify_r1` / `classify_r2` run their organ and report **produce-mode** setup outcomes:
|
|
`setup_refused` (with the organ's typed reason) or `setup_correct` (an admissible setup was
|
|
produced, with a deterministic signature). They do NOT solve, do NOT compare to gold, and import
|
|
nothing from `evals` (signatures are computed inline) — keeping this a thin, dependency-light
|
|
normalizer. Answer-level outcomes are reached downstream (N6) when the solver/verifier run.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from core.comprehension_attempt.model import ComprehensionAttempt
|
|
from generate.combined_rate_comprehension.model import CombinedRateProblem
|
|
from generate.combined_rate_comprehension.reader import read_combined_rate_problem
|
|
from generate.constraint_comprehension.model import ConstraintProblem
|
|
from generate.constraint_comprehension.reader import read_constraint_problem
|
|
from generate.meaning_graph.reader import Refusal
|
|
from generate.quantitative_comprehension import comprehend_quantitative, to_relational_metric
|
|
from generate.rate_comprehension.model import RateProblem
|
|
from generate.rate_comprehension.reader import read_rate_problem
|
|
|
|
#: CMB reader/solver reasons that are namespaced ``cmb_*`` before the (reason-string-keyed) failure
|
|
#: registry sees them, so a CMB boundary never inherits R2/R3's family for the SAME bare string
|
|
#: (R3 ``rate_unit_mismatch`` is a growth surface; R2/R3 ``non_integer_solution`` carry other
|
|
#: owners). The two ``input_shape`` reasons stay bare so they map to the cross ``input_shape``
|
|
#: family (router hygiene). A ``cmb_``-prefixed attempt reason is also the signal that CMB
|
|
#: *substantively* recognized the text (used by the CMB-over-R3 precedence rule in the router).
|
|
_CMB_BARE_REASONS = frozenset({"not_combined_rate_shaped", "empty"})
|
|
|
|
|
|
def cmb_reason(reason: str) -> str:
|
|
"""Namespace a CMB refusal reason for the failure registry (see :data:`_CMB_BARE_REASONS`)."""
|
|
return reason if reason in _CMB_BARE_REASONS else f"cmb_{reason}"
|
|
|
|
|
|
def _r1_signature(relations: list[dict[str, Any]]) -> str:
|
|
"""Deterministic, order-independent string signature of the projected R1 relations."""
|
|
items: list[tuple] = []
|
|
for r in relations:
|
|
kind = r["kind"]
|
|
if kind == "fact":
|
|
items.append((kind, r["entity"], int(r["value"])))
|
|
elif kind in ("more_than", "fewer_than"):
|
|
items.append((kind, r["entity"], r["ref"], int(r["delta"])))
|
|
elif kind == "times_as_many":
|
|
items.append((kind, r["entity"], r["ref"], int(r["factor"])))
|
|
elif kind == "divide_by":
|
|
items.append((kind, r["entity"], r["ref"], int(r["divisor"])))
|
|
elif kind == "sum_of":
|
|
items.append((kind, r["entity"], tuple(sorted(r["parts"]))))
|
|
else: # pragma: no cover - defensive
|
|
items.append(("unhandled", kind, r.get("entity", "")))
|
|
return repr(tuple(sorted(items, key=repr)))
|
|
|
|
|
|
def _r2_signature(problem: ConstraintProblem) -> str:
|
|
"""Deterministic, order-independent string signature of an R2 ConstraintProblem setup."""
|
|
unknowns = tuple(sorted((u.symbol, u.unit, u.domain) for u in problem.unknowns))
|
|
constraints: list[tuple] = []
|
|
for c in problem.constraints:
|
|
merged: dict[str, int] = {}
|
|
for symbol, coeff in c.lhs.terms:
|
|
merged[symbol] = merged.get(symbol, 0) + coeff
|
|
terms = tuple(sorted((s, v) for s, v in merged.items() if v != 0))
|
|
constraints.append((terms, c.relation, c.rhs - c.lhs.constant))
|
|
query = (problem.query.symbol, problem.query.unit)
|
|
return repr((unknowns, tuple(sorted(constraints, key=repr)), query))
|
|
|
|
|
|
def classify_r1(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
|
|
"""Attempt the R1 relational-arithmetic setup compiler on *text*."""
|
|
comp = comprehend_quantitative(text)
|
|
if isinstance(comp, Refusal):
|
|
return ComprehensionAttempt(
|
|
"r1_quantitative", "setup_refused", case_id=case_id, refusal_reason=comp.reason
|
|
)
|
|
projected = to_relational_metric(comp)
|
|
if projected is None:
|
|
return ComprehensionAttempt(
|
|
"r1_quantitative", "setup_refused", case_id=case_id, refusal_reason="unprojectable"
|
|
)
|
|
relations, _query = projected
|
|
return ComprehensionAttempt(
|
|
"r1_quantitative", "setup_correct", case_id=case_id, setup_signature=_r1_signature(relations)
|
|
)
|
|
|
|
|
|
def classify_r2(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
|
|
"""Attempt the R2 two-category constraint setup compiler on *text*."""
|
|
problem = read_constraint_problem(text)
|
|
if isinstance(problem, Refusal):
|
|
return ComprehensionAttempt(
|
|
"r2_constraints", "setup_refused", case_id=case_id, refusal_reason=problem.reason
|
|
)
|
|
return ComprehensionAttempt(
|
|
"r2_constraints", "setup_correct", case_id=case_id, setup_signature=_r2_signature(problem)
|
|
)
|
|
|
|
|
|
def _r3_signature(problem: RateProblem) -> str:
|
|
"""Deterministic string signature of an R3 single-rate setup."""
|
|
return repr(
|
|
(
|
|
(problem.rate_unit.numerator, problem.rate_unit.denominator),
|
|
("rate", problem.rate),
|
|
("time", problem.time),
|
|
("quantity", problem.quantity),
|
|
problem.query,
|
|
)
|
|
)
|
|
|
|
|
|
def classify_r3(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
|
|
"""Attempt the R3 single-rate setup compiler on *text*."""
|
|
problem = read_rate_problem(text)
|
|
if isinstance(problem, Refusal):
|
|
return ComprehensionAttempt(
|
|
"r3_rate", "setup_refused", case_id=case_id, refusal_reason=problem.reason
|
|
)
|
|
return ComprehensionAttempt(
|
|
"r3_rate", "setup_correct", case_id=case_id, setup_signature=_r3_signature(problem)
|
|
)
|
|
|
|
|
|
def _cmb_signature(problem: CombinedRateProblem) -> str:
|
|
"""Deterministic string signature of an R4 combined-rate setup. The leading ``"cmb"`` tag
|
|
guarantees it never coincides with an R1/R2/R3 signature (cross-organ distinctness). ``sum`` is
|
|
commutative, so its two rates are sorted; ``difference`` keeps order (which rate is the drain)."""
|
|
rates = (problem.rate_a, problem.rate_b)
|
|
if problem.combine_mode == "sum":
|
|
rates = tuple(sorted(rates))
|
|
return repr(
|
|
(
|
|
"cmb",
|
|
problem.combine_mode,
|
|
(problem.rate_unit.numerator, problem.rate_unit.denominator),
|
|
rates,
|
|
("time", problem.time),
|
|
("quantity", problem.quantity),
|
|
problem.query,
|
|
)
|
|
)
|
|
|
|
|
|
def classify_cmb(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
|
|
"""Attempt the R4 combined-rate setup compiler on *text* (refusal reasons namespaced ``cmb_*``)."""
|
|
problem = read_combined_rate_problem(text)
|
|
if isinstance(problem, Refusal):
|
|
return ComprehensionAttempt(
|
|
"r4_combined_rate", "setup_refused", case_id=case_id, refusal_reason=cmb_reason(problem.reason)
|
|
)
|
|
return ComprehensionAttempt(
|
|
"r4_combined_rate", "setup_correct", case_id=case_id, setup_signature=_cmb_signature(problem)
|
|
)
|
|
|
|
|
|
__all__ = ["classify_cmb", "classify_r1", "classify_r2", "classify_r3", "cmb_reason"]
|