feat(reader-arc): compare compiler-side guards for inverse + chained shapes

Two shapes (PR ruling) pinned compiler-side with tests — reader direction errors
surface as refusals, never wrongs:
- Chained / ordering: a compare whose reference is neither seeded nor
  already-defined at that point refuses (compare_reference_not_a_register).
  Pinned explicitly with a test (story order != dependency order), not left to
  fall out of existing checks.
- Inverse mis-direction: a compare that would DEFINE an already-known (seeded or
  earlier-defined) register refuses (compare_redefines_register) — the reader
  must bind the unknown side as the target with a fraction factor; the compiler
  never overwrites a known quantity.

The 5 real compare cases still solve wrong=0. 27/27 (2 new guards + 25).

[Verification]: uv run python -m pytest tests/test_adr_0250_compare.py tests/test_adr_0250_multi_register.py tests/test_adr_0250_summation.py -q
This commit is contained in:
Shay 2026-07-18 17:14:16 -07:00
parent cd0df2019f
commit 260e56ca5c
2 changed files with 40 additions and 3 deletions

View file

@ -208,9 +208,16 @@ def compile_multi_register_program(graph: MathProblemGraph) -> MultiRegisterProg
raise MultiRegisterError("compare_factor_not_positive", factor=factor)
if op.actor == reference:
raise MultiRegisterError("compare_self_reference", actor=op.actor)
if op.actor not in units: # the comparison DEFINES the actor register
units[op.actor] = units[reference] # unit propagation from the reference
order.append(op.actor)
# The comparison DEFINES the actor. If the actor is already known
# (seeded or defined earlier), this is a redefine — most often a
# mis-directed inverse compare (the known side used as the target).
# Refuse rather than overwrite: a reader direction error becomes a
# refusal, never a wrong (the reader must bind the unknown side as
# the target, with a fraction factor).
if op.actor in units:
raise MultiRegisterError("compare_redefines_register", actor=op.actor)
units[op.actor] = units[reference] # unit propagation from the reference
order.append(op.actor)
turns.append(
RegisterTurn(_COMPARE_MULT, op.actor, factor, 0.0, factor, reference=reference)
)

View file

@ -153,6 +153,36 @@ def test_summation_record_has_no_source_entity() -> None:
# --- Fail-closed refusals ----------------------------------------------------
def test_refuses_chained_reference_before_definition() -> None:
# A := 2×B, but B is never seeded or defined at that point (story order ≠
# dependency order): fail-closed, don't guess. Pins the ordering rule.
graph = MathProblemGraph(
entities=("A", "B", "C"),
initial_state=(InitialPossession("C", Quantity(5, "x")),),
operations=(Operation("A", "compare_multiplicative", Comparison("B", None, 2.0, "times")),),
unknown=Unknown("A", "x"),
)
with pytest.raises(MultiRegisterError):
compile_multi_register_program(graph)
def test_refuses_compare_redefining_known_register() -> None:
# A seeded; "B is twice A" mis-directed so the target is the KNOWN side —
# a redefine of a seeded register. Refuse (the reader must invert to the
# unknown side); never overwrite a known quantity.
graph = MathProblemGraph(
entities=("A", "B"),
initial_state=(
InitialPossession("A", Quantity(5, "x")),
InitialPossession("B", Quantity(9, "x")),
),
operations=(Operation("B", "compare_multiplicative", Comparison("A", None, 2.0, "times")),),
unknown=Unknown(None, "x"),
)
with pytest.raises(MultiRegisterError):
compile_multi_register_program(graph)
def test_refuses_compare_additive() -> None:
# compare_additive ("more"/"fewer") is a later increment — out of scope now.
graph = MathProblemGraph(