feat(field-wedge): geometric field reader — relational-metric lane wrong=0 (Phase W.1)

Measurement #1 of the field-reasoner falsifiable experiment: does the CL(4,1) field,
given an honest metric encoding, read forward-substitutable quantitative-relational
problems from TEXT with wrong==0? It does — 14/15 correct, 0 wrong, 1 refused
(precision ceiling), scored against an independent arithmetic oracle.

- generate/relational_field_reader.py: reads problem text into conformal points on
  the e1 number line; additive/part-whole relations are conformal TRANSLATOR versors
  (versor_apply(T_delta, embed[x]) == embed[x+delta], exact); the answer reads back
  by projective dehomogenization. Refusal-first: fences multiplicative/ratio (the
  sign/orientation-blind cases), the precision ceiling, non-forward-substitutable
  references, negatives. A per-step exactness self-check turns any f64 translator
  drift into a refusal (precision_drift) — it NEVER commits a wrong integer. Its
  parser is an independent reimplementation importing no generate.derivation/math_*.
- evals/relational_metric/: independent arithmetic oracle (computes gold from the
  STRUCTURE, shares no code with the reader), 15-case fixture, and a runner that
  enforces gold integrity + wrong==0.
- INV-25: relational_metric registered in INDEPENDENT_GOLD_LANES (oracle proven
  code-disjoint from the field reader and the algebra engine). The independently
  golded panel is now three domains: deductive, dimensional, relational-metric.

Green: smoke 87, 53 architectural invariants, 16 new tests; deductive + dimensional
lanes unperturbed (wrong=0).
This commit is contained in:
Shay 2026-06-04 19:34:43 -07:00
parent 568face63e
commit 145d797196
8 changed files with 593 additions and 0 deletions

View file

@ -0,0 +1,10 @@
"""Relational-metric lane — the field-reasoner wedge's golded micro-domain.
A panel of forward-substitutable quantitative-relational word problems (additive /
part-whole). The system under test is the geometric FIELD reader
(``generate.relational_field_reader``), which reads problem TEXT into conformal
points on the e1 number line and reads the answer back by projective
dehomogenization. The gold is an INDEPENDENT, pure-arithmetic oracle
(``evals.relational_metric.oracle``) that computes the answer from the structured
relations and shares no code with the reader (INV-25).
"""

View file

@ -0,0 +1,78 @@
"""Independent arithmetic oracle — the gold for the relational-metric lane.
This is **deliberately a second, independent decision procedure**: it computes the
answer to a forward-substitutable quantitative-relational problem by plain integer
arithmetic over the *structured* relations. It never reads problem text, and it
shares **no code** with the geometric field reader under test
(``generate.relational_field_reader``) it imports no ``algebra`` / ``field`` /
``generate`` module. Two independent procedures (the field reader off the TEXT, the
oracle off the STRUCTURE) agreeing is real evidence the field READ the text
correctly; a shared-code "gold" would only prove the reader agrees with itself
(INV-25).
Supported relation kinds (the v1 forward-substitutable grammar):
- ``fact`` : ``entity = value`` (a given quantity)
- ``more_than`` : ``entity = ref + delta``
- ``fewer_than`` : ``entity = ref - delta``
- ``sum_of`` : ``entity = sum(parts)`` (part-whole / total)
Every relation's references must already be resolved (forward-substitutable /
triangular). The oracle refuses anything else, never guesses.
"""
from __future__ import annotations
from typing import Any
class OracleError(ValueError):
"""Malformed or out-of-grammar case — the oracle refuses, never guesses."""
_SUPPORTED = frozenset({"fact", "more_than", "fewer_than", "sum_of"})
def oracle_answer(relations: list[dict[str, Any]], query: dict[str, Any]) -> int:
"""Compute the integer answer by forward substitution over the relations.
Raises :class:`OracleError` on an unknown relation kind, a forward reference to
an unresolved entity, a duplicate definition, or a missing query entity.
"""
values: dict[str, int] = {}
for rel in relations:
kind = rel.get("kind")
entity = rel.get("entity")
if kind not in _SUPPORTED:
raise OracleError(f"unsupported relation kind: {kind!r}")
if not isinstance(entity, str) or not entity:
raise OracleError(f"relation missing entity: {rel!r}")
if entity in values:
raise OracleError(f"duplicate definition of {entity!r}")
if kind == "fact":
value = rel.get("value")
if not isinstance(value, int) or isinstance(value, bool):
raise OracleError(f"fact value must be int: {rel!r}")
values[entity] = value
elif kind in ("more_than", "fewer_than"):
ref = rel.get("ref")
delta = rel.get("delta")
if ref not in values:
raise OracleError(f"forward reference to unresolved {ref!r}")
if not isinstance(delta, int) or isinstance(delta, bool):
raise OracleError(f"delta must be int: {rel!r}")
values[entity] = values[ref] + (delta if kind == "more_than" else -delta)
else: # sum_of
parts = rel.get("parts")
if not isinstance(parts, list) or not parts:
raise OracleError(f"sum_of needs non-empty parts: {rel!r}")
if any(p not in values for p in parts):
raise OracleError(f"sum_of references unresolved part: {rel!r}")
values[entity] = sum(values[p] for p in parts)
target = query.get("entity")
if target not in values:
raise OracleError(f"query entity {target!r} not resolved")
return values[target]

View file

@ -0,0 +1,98 @@
"""Relational-metric lane runner — field reader vs the independent arithmetic gold.
For each committed case:
1. the independent oracle recomputes the gold from the STRUCTURED relations
(gold-integrity: a committed gold that the oracle cannot reproduce is rejected,
so the gold can never be field-derived INV-25);
2. the geometric field reader reads the TEXT into an answer (or refuses);
3. the field's committed answer is scored against that gold.
Buckets: ``correct`` (field == gold), ``wrong`` (field committed a different
integer), ``refused`` (field declined). ``wrong`` must be 0 the runner exits 1
otherwise. A refusal is honest coverage, not a wrong answer.
Run: PYTHONPATH=. .venv/bin/python -m evals.relational_metric.runner
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
from evals.relational_metric.oracle import OracleError, oracle_answer
from generate.relational_field_reader import read_relational
_CASES = Path(__file__).resolve().parent / "v1" / "cases.jsonl"
def _load_cases() -> list[dict[str, Any]]:
cases: list[dict[str, Any]] = []
for line in _CASES.read_text(encoding="utf-8").splitlines():
if line.strip():
cases.append(json.loads(line))
return cases
def run() -> dict[str, Any]:
"""Run the lane and return a structured report."""
cases = _load_cases()
correct = 0
wrong: list[dict[str, Any]] = []
refused: list[str] = []
gold_integrity_failures: list[str] = []
for case in cases:
cid = case["id"]
committed_gold = case["gold"]
# 1 — independent gold integrity (the oracle must reproduce the committed gold)
try:
recomputed = oracle_answer(case["relations"], case["query"])
except OracleError:
gold_integrity_failures.append(f"{cid}: oracle could not reproduce gold")
continue
if recomputed != committed_gold:
gold_integrity_failures.append(
f"{cid}: oracle={recomputed} != committed gold={committed_gold}"
)
continue
# 2/3 — the field reads the TEXT, scored against the independent gold
reading = read_relational(case["text"])
if reading.refused:
refused.append(f"{cid}: {reading.refusal_reason}")
elif reading.answer == committed_gold:
correct += 1
else:
wrong.append(
{"id": cid, "field": reading.answer, "gold": committed_gold}
)
return {
"total": len(cases),
"correct": correct,
"wrong": len(wrong),
"refused": len(refused),
"wrong_detail": wrong,
"refused_detail": refused,
"gold_integrity_failures": gold_integrity_failures,
}
def main() -> int:
report = run()
print(json.dumps({k: v for k, v in report.items()
if k not in ("wrong_detail",)}, indent=2))
if report["gold_integrity_failures"]:
print("GOLD INTEGRITY FAILURE:", report["gold_integrity_failures"], file=sys.stderr)
return 1
if report["wrong"] > 0:
print("WRONG ANSWERS (wrong!=0 breach):", report["wrong_detail"], file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,15 @@
{"id": "rm-v1-0001", "text": "Liam has 6 stickers. Mia has 4 more stickers than Liam. How many stickers does Mia have?", "relations": [{"kind": "fact", "entity": "liam", "value": 6}, {"kind": "more_than", "entity": "mia", "ref": "liam", "delta": 4}], "query": {"entity": "mia", "unit": "stickers"}, "gold": 10, "class": "additive_more_than"}
{"id": "rm-v1-0002", "text": "Noah has 15 cards. Olivia has 6 fewer cards than Noah. How many cards does Olivia have?", "relations": [{"kind": "fact", "entity": "noah", "value": 15}, {"kind": "fewer_than", "entity": "olivia", "ref": "noah", "delta": 6}], "query": {"entity": "olivia", "unit": "cards"}, "gold": 9, "class": "additive_fewer_than"}
{"id": "rm-v1-0003", "text": "Ava has 8 pencils. Ben has 5 more pencils than Ava. Cara has 3 more pencils than Ben. How many pencils does Cara have?", "relations": [{"kind": "fact", "entity": "ava", "value": 8}, {"kind": "more_than", "entity": "ben", "ref": "ava", "delta": 5}, {"kind": "more_than", "entity": "cara", "ref": "ben", "delta": 3}], "query": {"entity": "cara", "unit": "pencils"}, "gold": 16, "class": "additive_chain"}
{"id": "rm-v1-0004", "text": "Dan has 7 coins. Eva has 9 more coins than Dan. How many coins do Dan and Eva have?", "relations": [{"kind": "fact", "entity": "dan", "value": 7}, {"kind": "more_than", "entity": "eva", "ref": "dan", "delta": 9}, {"kind": "sum_of", "entity": "total", "parts": ["dan", "eva"]}], "query": {"entity": "total", "unit": "coins"}, "gold": 23, "class": "part_whole_sum"}
{"id": "rm-v1-0005", "text": "Finn has 12 books. How many books does Finn have?", "relations": [{"kind": "fact", "entity": "finn", "value": 12}], "query": {"entity": "finn", "unit": "books"}, "gold": 12, "class": "fact_only"}
{"id": "rm-v1-0006", "text": "Gabe has 30 apples. Hana has 10 fewer apples than Gabe. How many apples does Hana have?", "relations": [{"kind": "fact", "entity": "gabe", "value": 30}, {"kind": "fewer_than", "entity": "hana", "ref": "gabe", "delta": 10}], "query": {"entity": "hana", "unit": "apples"}, "gold": 20, "class": "additive_fewer_than"}
{"id": "rm-v1-0007", "text": "Iris has 100 dollars. Jack has 250 more dollars than Iris. How many dollars does Jack have?", "relations": [{"kind": "fact", "entity": "iris", "value": 100}, {"kind": "more_than", "entity": "jack", "ref": "iris", "delta": 250}], "query": {"entity": "jack", "unit": "dollars"}, "gold": 350, "class": "additive_more_than"}
{"id": "rm-v1-0008", "text": "Kim has 2 marbles. Leo has 3 more marbles than Kim. How many marbles do Kim and Leo have?", "relations": [{"kind": "fact", "entity": "kim", "value": 2}, {"kind": "more_than", "entity": "leo", "ref": "kim", "delta": 3}, {"kind": "sum_of", "entity": "total", "parts": ["kim", "leo"]}], "query": {"entity": "total", "unit": "marbles"}, "gold": 7, "class": "part_whole_sum"}
{"id": "rm-v1-0009", "text": "Maya has 40 beads. Nico has 18 fewer beads than Maya. How many beads does Nico have?", "relations": [{"kind": "fact", "entity": "maya", "value": 40}, {"kind": "fewer_than", "entity": "nico", "ref": "maya", "delta": 18}], "query": {"entity": "nico", "unit": "beads"}, "gold": 22, "class": "additive_fewer_than"}
{"id": "rm-v1-0010", "text": "Omar has 5 tokens. Pia has 5 more tokens than Omar. Quinn has 5 more tokens than Pia. How many tokens does Quinn have?", "relations": [{"kind": "fact", "entity": "omar", "value": 5}, {"kind": "more_than", "entity": "pia", "ref": "omar", "delta": 5}, {"kind": "more_than", "entity": "quinn", "ref": "pia", "delta": 5}], "query": {"entity": "quinn", "unit": "tokens"}, "gold": 15, "class": "additive_chain"}
{"id": "rm-v1-0011", "text": "Rosa has 9 ribbons. Sam has 14 more ribbons than Rosa. How many ribbons do Rosa and Sam have?", "relations": [{"kind": "fact", "entity": "rosa", "value": 9}, {"kind": "more_than", "entity": "sam", "ref": "rosa", "delta": 14}, {"kind": "sum_of", "entity": "total", "parts": ["rosa", "sam"]}], "query": {"entity": "total", "unit": "ribbons"}, "gold": 32, "class": "part_whole_sum"}
{"id": "rm-v1-0012", "text": "Tara has 11 stamps. Uma has 11 fewer stamps than Tara. How many stamps does Uma have?", "relations": [{"kind": "fact", "entity": "tara", "value": 11}, {"kind": "fewer_than", "entity": "uma", "ref": "tara", "delta": 11}], "query": {"entity": "uma", "unit": "stamps"}, "gold": 0, "class": "additive_fewer_than"}
{"id": "rm-v1-0013", "text": "Vera has 1200 points. Will has 800 more points than Vera. How many points does Will have?", "relations": [{"kind": "fact", "entity": "vera", "value": 1200}, {"kind": "more_than", "entity": "will", "ref": "vera", "delta": 800}], "query": {"entity": "will", "unit": "points"}, "gold": 2000, "class": "additive_more_than"}
{"id": "rm-v1-0014", "text": "Xena has 25 shells. Yara has 13 more shells than Xena. Zane has 7 fewer shells than Yara. How many shells does Zane have?", "relations": [{"kind": "fact", "entity": "xena", "value": 25}, {"kind": "more_than", "entity": "yara", "ref": "xena", "delta": 13}, {"kind": "fewer_than", "entity": "zane", "ref": "yara", "delta": 7}], "query": {"entity": "zane", "unit": "shells"}, "gold": 31, "class": "additive_chain"}
{"id": "rm-v1-0015", "text": "Gus has 9000000 apples. How many apples does Gus have?", "relations": [{"kind": "fact", "entity": "gus", "value": 9000000}], "query": {"entity": "gus", "unit": "apples"}, "gold": 9000000, "class": "coverage_over_ceiling"}

View file

@ -0,0 +1,211 @@
"""The geometric FIELD reader for forward-substitutable quantitative relations.
Phase W of the field-reasoner wedge
(docs/analysis/field-reasoner-wedge-design-and-falsification-2026-06-04.md).
This is the system under test: it reads problem **text** into conformal points on
the e1 number line (``algebra.cga.embed_point`` at float64), resolves each unknown
by applying a conformal **translator versor** (``algebra.versor.versor_apply``), and
reads the answer back by **projective dehomogenization** (``read_scalar_e1``). The
resolution is genuinely geometric: ``versor_apply(T_delta, embed([x])) == embed([x+δ])``
exactly (verified), so "A is δ more than B" is a translation, not a hidden add.
It is a **second, hand-written reader**: its tokenizer / number extraction / relation
classification are an independent reimplementation. It imports **nothing** from
``generate.derivation`` / ``generate.math_candidate_parser`` / ``generate.math_*`` /
``WORD_NUMBERS`` so its agreement with a symbolic reader is not common-mode by
construction (the disjointness is *proven* by INV-27, not asserted here).
**Refusal-first.** It commits an answer only inside a narrow, sealed grammar
(digit integers; ``has``/``more``/``fewer``/``than``/``how many`` cues; additive and
part-whole only). It REFUSES never guesses on:
- multiplicative / ratio cues (``times``/``twice``/``double``/``ratio`` ) fenced,
because ``cga_inner`` is sign/orientation-blind (A=2B vs A=2B);
- any quantity above :data:`algebra.cga.EMBED_EXACT_MAX` (precision ceiling);
- a non-forward-substitutable reference (an unknown referenced before it is defined);
- a negative resolved quantity, a non-integer read-back, or any unparsed sentence.
The answer is exact-integer; there is no float tolerance anywhere on the commit path.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
import numpy as np
from algebra.cga import EMBED_EXACT_MAX, embed_point, read_scalar_e1
from algebra.cl41 import N_COMPONENTS, geometric_product
from algebra.versor import versor_apply
# The reader's stable identity for the Tier-2 gate (registered in INV-27).
READER_LINEAGE: str = "field.relational_number_line"
_F64 = np.float64
# Multiplicative / ratio cues are out of the sealed metric domain — refuse.
_FENCED_CUE = re.compile(
r"\b(times|twice|double|triple|thrice|half|quarter|ratio|per|each)\b"
)
_MORE_THAN = re.compile(r"\b(\w+)\s+(?:has|have)\s+(\d+)\s+more\s+\w+\s+than\s+(\w+)")
_FEWER_THAN = re.compile(
r"\b(\w+)\s+(?:has|have)\s+(\d+)\s+(?:fewer|less)\s+\w+\s+than\s+(\w+)"
)
_FACT = re.compile(r"\b(\w+)\s+(?:has|have)\s+(\d+)\s+(\w+)")
_QUERY_SINGLE = re.compile(r"how many\s+(\w+)\s+does\s+(\w+)\s+have")
_QUERY_SUM = re.compile(
r"how many\s+(\w+)\s+do\s+(\w+)\s+and\s+(\w+)(?:\s+and\s+(\w+))?\s+have"
)
class FieldReaderError(ValueError):
"""Internal signal that a case is out of the sealed grammar (→ typed refusal)."""
@dataclass(frozen=True, slots=True)
class FieldReading:
"""Result of the geometric reading. ``answer is None`` iff ``refused``."""
refused: bool
answer: int | None = None
answer_unit: str | None = None
refusal_reason: str | None = None
reader_lineage: str = READER_LINEAGE
def _refuse(reason: str) -> FieldReading:
return FieldReading(refused=True, refusal_reason=reason)
def _translator_e1(delta: int) -> np.ndarray:
"""The conformal translator versor that shifts an e1-axis point by ``delta``.
``T = 1 ½·(δ·e1)·n_inf`` with ``n_inf = e4 + e5``. For null n_inf the
exponential series truncates exactly, so ``versor_apply(T, embed([x]))`` lands
on ``embed([x+δ])`` with zero residual (the metric stays exact in f64).
"""
n_inf = np.zeros(N_COMPONENTS, dtype=_F64)
n_inf[4] = 1.0
n_inf[5] = 1.0
e1 = np.zeros(N_COMPONENTS, dtype=_F64)
e1[1] = float(delta)
t = geometric_product(e1, n_inf)
rotor = np.zeros(N_COMPONENTS, dtype=_F64)
rotor[0] = 1.0
return rotor - 0.5 * t
def _check_magnitude(value: int) -> None:
if abs(value) > EMBED_EXACT_MAX:
raise FieldReaderError("over_ceiling")
def _apply_delta(point: np.ndarray, delta: int) -> np.ndarray:
"""Translate an e1 point by ``delta`` and PROVE the move was exact.
The translator sandwich loses f64 integer exactness when ``δ·`` approaches
2^52, which would silently commit a plausible-but-wrong integer (the resolve_pooled
failure mode). So we verify the read-back moved by *exactly* ``delta`` and refuse
(``precision_drift``) otherwise the field never commits a drifted answer.
"""
before = _read_int(point)
moved = versor_apply(_translator_e1(delta), point)
if read_scalar_e1(moved) != before + delta: # exact integer move, or refuse
raise FieldReaderError("precision_drift")
return moved
def _sentences(text: str) -> list[str]:
return [s.strip() for s in re.split(r"[.?!]", text.lower()) if s.strip()]
def read_relational(text: str) -> FieldReading:
"""Read problem ``text`` geometrically into an exact integer answer, or refuse."""
if not isinstance(text, str) or not text.strip():
return _refuse("empty_input")
if _FENCED_CUE.search(text.lower()):
return _refuse("fenced_multiplicative")
try:
return _read(text)
except FieldReaderError as exc:
return _refuse(str(exc))
def _read(text: str) -> FieldReading:
sentences = _sentences(text)
# --- locate the query (the question sentence) -------------------------------
query_unit: str | None = None
query_single: str | None = None
query_parts: list[str] | None = None
for s in sentences:
if "how many" in s:
if (m := _QUERY_SUM.search(s)) is not None:
query_unit = m.group(1)
query_parts = [g for g in m.groups()[1:] if g]
elif (m := _QUERY_SINGLE.search(s)) is not None:
query_unit = m.group(1)
query_single = m.group(2)
else:
raise FieldReaderError("unparsed_query")
break
if query_single is None and query_parts is None:
raise FieldReaderError("no_query")
# --- parse the declarative sentences, in order ------------------------------
points: dict[str, np.ndarray] = {}
def resolve(entity: str) -> None:
if entity not in points:
raise FieldReaderError("non_forward_substitutable")
for s in sentences:
if "how many" in s:
continue
if (m := _MORE_THAN.search(s)) is not None:
entity, n, ref = m.group(1), int(m.group(2)), m.group(3)
_check_magnitude(n)
resolve(ref)
points[entity] = _apply_delta(points[ref], n)
elif (m := _FEWER_THAN.search(s)) is not None:
entity, n, ref = m.group(1), int(m.group(2)), m.group(3)
_check_magnitude(n)
resolve(ref)
points[entity] = _apply_delta(points[ref], -n)
elif (m := _FACT.search(s)) is not None:
entity, n = m.group(1), int(m.group(2))
_check_magnitude(n)
points[entity] = embed_point(np.array([float(n), 0.0, 0.0]), dtype=_F64)
else:
raise FieldReaderError("unparsed_sentence")
# --- read the answer back off the geometry ----------------------------------
if query_single is not None:
if query_single not in points:
raise FieldReaderError("query_entity_unresolved")
answer = _read_int(points[query_single])
else:
assert query_parts is not None
total = embed_point(np.array([0.0, 0.0, 0.0]), dtype=_F64)
for part in query_parts:
if part not in points:
raise FieldReaderError("query_entity_unresolved")
total = _apply_delta(total, _read_int(points[part]))
answer = _read_int(total)
if answer < 0:
raise FieldReaderError("negative_quantity")
return FieldReading(refused=False, answer=answer, answer_unit=query_unit)
def _read_int(point: np.ndarray) -> int:
"""Projective read-back, refusing any non-integer coordinate (no float slack)."""
value = read_scalar_e1(point)
rounded = round(value)
if abs(value - rounded) > 0.0: # exact-integer commit path; no tolerance
raise FieldReaderError("non_integer_readback")
return int(rounded)

View file

@ -1103,6 +1103,19 @@ INDEPENDENT_GOLD_LANES: tuple[IndependentGoldLane, ...] = (
oracle_module="evals/dimensional/oracle.py",
sut_import_prefixes=("generate.binding_graph",),
),
# The relational-metric lane (field-reasoner wedge): the geometric field reader
# (generate.relational_field_reader, which reads TEXT into conformal points) is
# the SUT, so its arithmetic gold oracle must share no code with the reader or
# the field engine it rides on.
IndependentGoldLane(
name="relational_metric",
oracle_module="evals/relational_metric/oracle.py",
sut_import_prefixes=(
"generate.relational_field_reader",
"algebra",
"field",
),
),
)
_DEDUCTIVE_CASE_FILES: tuple[str, ...] = (

View file

@ -0,0 +1,133 @@
"""Phase W — the geometric field reader on forward-substitutable relations.
Proves the field reads problem TEXT into an exact integer answer via conformal
translators + projective read-back, and REFUSES (never guesses) outside its sealed
metric grammar. wrong==0 is structural: every commit is an exact-integer read-back.
"""
from __future__ import annotations
from generate.relational_field_reader import READER_LINEAGE, read_relational
# --- commits: the field reads correctly ------------------------------------
def test_fact_then_more_than():
r = read_relational(
"Tom has 3 marbles. Jane has 5 more marbles than Tom. "
"How many marbles does Jane have?"
)
assert not r.refused
assert r.answer == 8
assert r.answer_unit == "marbles"
assert r.reader_lineage == READER_LINEAGE
def test_fewer_than():
r = read_relational(
"Anna has 20 apples. Ben has 7 fewer apples than Anna. "
"How many apples does Ben have?"
)
assert not r.refused
assert r.answer == 13
def test_chained_forward_substitution():
r = read_relational(
"Tom has 4 coins. Jane has 6 more coins than Tom. "
"Sara has 10 more coins than Jane. How many coins does Sara have?"
)
assert not r.refused
assert r.answer == 20 # 4 -> 10 -> 20
def test_part_whole_sum_query():
r = read_relational(
"Tom has 3 marbles. Jane has 5 more marbles than Tom. "
"How many marbles do Tom and Jane have?"
)
assert not r.refused
assert r.answer == 11 # 3 + 8
def test_large_value_within_ceiling_is_exact():
# x=12345 already collapses f32 (the n_o weight loses the ±1 past ~4096);
# f64 + translator stays exact here.
r = read_relational(
"Tom has 12345 dollars. Jane has 5000 more dollars than Tom. "
"How many dollars does Jane have?"
)
assert not r.refused
assert r.answer == 17345
def test_never_commits_a_drifted_answer():
"""wrong==0 guard: at a scale where the translator sandwich loses f64 integer
exactness, the field REFUSES (precision_drift) rather than commit a wrong int."""
r = read_relational(
"Tom has 123456 dollars. Jane has 654321 more dollars than Tom. "
"How many dollars does Jane have?"
)
# Either it commits the EXACT answer, or it refuses — never a wrong integer.
assert r.refused or r.answer == 777777
if r.refused:
assert r.refusal_reason == "precision_drift"
# --- refusals: the field declines outside its sealed grammar ----------------
def test_refuses_multiplicative():
r = read_relational(
"Tom has 3 marbles. Jane has twice as many marbles as Tom. "
"How many marbles does Jane have?"
)
assert r.refused
assert r.refusal_reason == "fenced_multiplicative"
def test_refuses_times_cue():
r = read_relational(
"Tom has 3 marbles. Jane has 4 times as many marbles as Tom. "
"How many marbles does Jane have?"
)
assert r.refused
assert r.refusal_reason == "fenced_multiplicative"
def test_refuses_over_ceiling():
r = read_relational(
"Tom has 9000000 marbles. How many marbles does Tom have?"
)
assert r.refused
assert r.refusal_reason == "over_ceiling"
def test_refuses_forward_reference():
r = read_relational(
"Jane has 5 more marbles than Tom. Tom has 3 marbles. "
"How many marbles does Jane have?"
)
assert r.refused
assert r.refusal_reason == "non_forward_substitutable"
def test_refuses_no_question():
r = read_relational("Tom has 3 marbles. Jane has 5 more marbles than Tom.")
assert r.refused
assert r.refusal_reason == "no_query"
def test_refuses_negative_quantity():
r = read_relational(
"Tom has 3 marbles. Jane has 5 fewer marbles than Tom. "
"How many marbles does Jane have?"
)
assert r.refused
assert r.refusal_reason == "negative_quantity"
def test_refuses_empty():
assert read_relational("").refused
assert read_relational(" ").refusal_reason == "empty_input"

View file

@ -0,0 +1,35 @@
"""Relational-metric gold lane — the field reader is wrong==0 vs an independent gold.
This is measurement #1 of the field-reasoner wedge falsifiable experiment: does the
geometric field reader, reading problem TEXT, commit answers that match an
independent arithmetic oracle (computed from the STRUCTURE) with zero wrong? The
oracle shares no code with the reader (enforced structurally by INV-25's
INDEPENDENT_GOLD_LANES registration).
"""
from __future__ import annotations
from evals.relational_metric.runner import run
def test_lane_is_wrong_zero_with_independent_gold():
report = run()
# The committed gold is reproducible by the independent oracle (never field-derived).
assert report["gold_integrity_failures"] == [], report["gold_integrity_failures"]
# wrong==0 is the prime directive on this lane.
assert report["wrong"] == 0, report["wrong_detail"]
# Buckets account for every case.
assert report["total"] == report["correct"] + report["wrong"] + report["refused"]
def test_field_actually_commits_not_a_refusal_floor():
"""A refusal floor (refuse everything) would be wrong==0 too — and worthless.
The capability claim is COVERAGE with wrong==0: the field must commit real cases."""
report = run()
assert report["correct"] >= 10
def test_over_ceiling_is_refused_not_wrong():
"""The precision ceiling is honest coverage (a refusal), never a wrong commit."""
report = run()
assert any("over_ceiling" in r for r in report["refused_detail"])