core/tests/test_quantitative_expr.py
Shay e9cbe65d77 feat(comprehension): the multiplicative comparative frame — first R1 capability (PR-5c)
The first capability slice on the R1 arc, gated by the setup-oracle: turn the
"twice / N times as many" reading from REFUSED into a correct setup, without a single
misread. Builds on the typed IR (PR-4) and the R1 gold (PR-5b).

- IR: a Mul(symbol, literal-factor) node — to_canonical_string "ref * factor",
  operation_kind "multiply", dependencies {ref}, to_relation -> times_as_many. The
  product keeps the symbol's unit (count * scalar = count), admitted by the REAL
  check_admissibility multiply path (the literal factor is dimensionless, not a dep).
- Reader: a multiplicative template "Y has <factor> as many <unit> as X" (factor word:
  twice/double/triple/quadruple) and "Y has <N> times as many <unit> as X", checked
  BEFORE the digit gate (the factor may be a word). 'half' (a /2) is deliberately
  deferred — divide-by-literal is a separate admissibility path.
- setup-oracle: relation_signature now canonicalizes times_as_many.

Setup-oracle R1 result: 2 setup_correct (r1-01 twice; r1-05 the multi-step chain
ivy/jon=3*ivy/kim=jon+2), 0 setup_WRONG, 8 setup_refused. Every hard negative stays a
safe refusal: missing-base (Rosa ungrounded), ambiguous referent, distractor, inverse,
partition, 'altogether'/'in total' phrasings, and 'half' (divide). wrong=0 held through
the first capability addition.

Gates green: setup-oracle R1 setup_wrong=0; 15-case setup gate 15/15 setup_wrong=0;
relational_metric answer lane 15/15 wrong=0; binding-graph admissibility + realize +
architectural invariants + chat-runtime + pipeline (122+). No serving path touched (this
reader feeds the relational_metric / setup-oracle lanes, not the candidate-graph serving).
2026-06-06 17:29:23 -07:00

88 lines
3.5 KiB
Python

"""Typed expression IR (PR-4) — the reader's source of meaning for an equation rhs.
Pins: the canonical serialization is byte-identical to the pre-IR string format (so the
binding-graph + every downstream hash is unchanged), the structured projection reads the
IR (never the string), and dependencies/operation_kind derive from the IR.
"""
from __future__ import annotations
from generate.quantitative_comprehension import comprehend_quantitative
from generate.quantitative_expr import (
Add,
Literal,
Sub,
SumOf,
Symbol,
dependencies,
operation_kind,
to_canonical_string,
to_relation,
)
def test_canonical_string_is_byte_identical_to_legacy_format() -> None:
assert to_canonical_string(Add(Symbol("liam"), Literal(4))) == "liam + 4"
assert to_canonical_string(Sub(Symbol("noah"), Literal(6))) == "noah - 6"
assert to_canonical_string(SumOf((Symbol("dan"), Symbol("eva")))) == "dan + eva"
assert to_canonical_string(Symbol("x")) == "x"
assert to_canonical_string(Literal(7)) == "7"
def test_dependencies_from_structure() -> None:
assert dependencies(Add(Symbol("liam"), Literal(4))) == frozenset({"liam"})
assert dependencies(Sub(Symbol("noah"), Literal(6))) == frozenset({"noah"})
assert dependencies(SumOf((Symbol("dan"), Symbol("eva")))) == frozenset({"dan", "eva"})
assert dependencies(Literal(3)) == frozenset()
def test_operation_kind_from_structure() -> None:
assert operation_kind(Add(Symbol("a"), Literal(1))) == "add"
assert operation_kind(SumOf((Symbol("a"), Symbol("b")))) == "add"
assert operation_kind(Sub(Symbol("a"), Literal(1))) == "subtract"
def test_to_relation_reads_structure_not_string() -> None:
assert to_relation("mia", Add(Symbol("liam"), Literal(4))) == {
"kind": "more_than", "entity": "mia", "ref": "liam", "delta": 4,
}
assert to_relation("olivia", Sub(Symbol("noah"), Literal(6))) == {
"kind": "fewer_than", "entity": "olivia", "ref": "noah", "delta": 6,
}
assert to_relation("total", SumOf((Symbol("dan"), Symbol("eva")))) == {
"kind": "sum_of", "entity": "total", "parts": ["dan", "eva"],
}
def test_to_relation_refuses_unhandled_shape() -> None:
# A literal-only or nested shape the projection doesn't handle returns None (refuse).
assert to_relation("x", Literal(5)) is None
assert to_relation("x", Add(Literal(1), Literal(2))) is None # no symbol ref
def test_reader_carries_ir_consistent_with_rhs_canonical() -> None:
# The IR the reader attaches serializes EXACTLY to the equation's rhs_canonical.
comp = comprehend_quantitative(
"Liam has 6 stickers. Mia has 4 more stickers than Liam. How many stickers does Mia have?"
)
by_lhs = {lhs: expr for lhs, expr in comp.equation_exprs}
for eq in comp.binding_graph.equations:
assert to_canonical_string(by_lhs[eq.lhs_symbol_id]) == eq.rhs_canonical
assert dependencies(by_lhs[eq.lhs_symbol_id]) == eq.dependencies
# --------------------------------------------------------------------------- #
# PR-5c — the multiplicative comparative (Mul)
# --------------------------------------------------------------------------- #
def test_mul_serialization_and_derivations() -> None:
from generate.quantitative_expr import Mul
m = Mul(Symbol("anna"), Literal(2))
assert to_canonical_string(m) == "anna * 2"
assert dependencies(m) == frozenset({"anna"})
assert operation_kind(m) == "multiply"
assert to_relation("bella", m) == {
"kind": "times_as_many", "entity": "bella", "ref": "anna", "factor": 2,
}