Merge pull request #560 from AssetOverflow/feat/field-reasoner-wedge

Field-reasoner wedge: f64 foundation, lineage firewall (INV-27), and the honest C3 ablation verdict
This commit is contained in:
Shay 2026-06-04 20:00:29 -07:00 committed by GitHub
commit 5c2f005e96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1867 additions and 11 deletions

View file

@ -1,5 +1,13 @@
from .cl41 import geometric_product, reverse, grade_project, scalar_part, norm_squared, basis_vector
from .versor import versor_apply, normalize_to_versor, versor_condition
from .cga import cga_inner, outer_product, is_null, null_project, embed_point
from .cga import (
EMBED_EXACT_MAX,
cga_inner,
outer_product,
is_null,
null_project,
embed_point,
read_scalar_e1,
)
from .holonomy import holonomy_encode, holonomy_similarity
from .rotor import word_transition_rotor

View file

@ -26,6 +26,14 @@ from .cl41 import geometric_product, scalar_part, basis_vector, N_COMPONENTS
_E4_IDX = 4
_E5_IDX = 5
# Pinned magnitude ceiling for f64-exact embedding + read-back (Phase 0A).
# Below this bound, ``embed_point(..., dtype=np.float64)`` round-trips integer
# coordinates exactly through ``read_scalar_e1`` and the conformal distance metric
# stays exact (proven in tests/test_cga_f64_exactness.py). The field-reasoner reader
# REFUSES any quantity whose magnitude exceeds this bound; the refusal lives in the
# reader — this module only states the bound. Generous vs GSM8K (quantities ~< 1e5).
EMBED_EXACT_MAX: int = 1_000_000
def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
"""
@ -62,18 +70,25 @@ def null_project(X: np.ndarray) -> np.ndarray:
return embed_point(euclidean)
def embed_point(x: np.ndarray) -> np.ndarray:
def embed_point(x: np.ndarray, *, dtype: "np.typing.DTypeLike" = np.float32) -> np.ndarray:
"""
Embed a Euclidean point x in R^3 into the CGA null cone.
X = x + n_o + 0.5|x|^2 n_inf,
where n_o = 0.5(e5-e4), n_inf = e4+e5.
``dtype`` defaults to ``float32`` so every existing caller is byte-unchanged.
The field-reasoner reader passes ``dtype=np.float64`` to get an exact embedding:
``geometric_product`` already preserves float64 (``np.result_type``), so the
only thing that forced f32 was this construction. f32 silently collapses the
``n_o`` weight past ~1e4 (the ``0.5|x|^2`` terms lose the ``±1``); f64 keeps it
exact up to :data:`EMBED_EXACT_MAX` (see tests/test_cga_f64_exactness.py).
"""
x = np.asarray(x, dtype=np.float32)
x = np.asarray(x, dtype=dtype)
assert len(x) == 3, "embed_point expects a 3D vector"
x_sq = float(np.dot(x, x))
result = np.zeros(N_COMPONENTS, dtype=np.float32)
result = np.zeros(N_COMPONENTS, dtype=dtype)
result[1:4] = x
# n_o + 0.5|x|^2 n_inf
@ -82,3 +97,26 @@ def embed_point(x: np.ndarray) -> np.ndarray:
result[_E4_IDX] = 0.5 * (x_sq - 1.0)
result[_E5_IDX] = 0.5 * (x_sq + 1.0)
return result
def read_scalar_e1(X: np.ndarray) -> float:
"""Projective dehomogenization on the e1 axis — the exact, weight-invariant
read-back of a scalar coordinate from a (possibly dilated) conformal point.
A point at coordinate ``v`` on the e1 number line embeds as
``X = v*e1 + n_o + 0.5 v^2 n_inf``; a uniform conformal dilation by ``k``
scales the whole null vector. The coordinate is recovered as
``e1_coefficient / n_o_weight`` where the n_o weight is ``X[e5] - X[e4]``
(== 1 for an un-dilated point), so any dilation weight divides out. This is
the correct read-back for weight-changing operators; a raw distance-from-origin
is wrong for them.
Raises ``ValueError`` on a degenerate (zero) n_o weight a point at infinity
or an f32 weight-collapse rather than returning a silently wrong value.
"""
no_weight = float(X[_E5_IDX] - X[_E4_IDX])
if no_weight == 0.0:
raise ValueError(
"read_scalar_e1: degenerate n_o weight (point at infinity or f32 collapse)"
)
return float(X[1]) / no_weight

View file

@ -7,6 +7,7 @@ from core.reasoning.evidence import (
DUPLICATE_STRUCTURAL_SIGNATURE,
INSUFFICIENT_EVIDENCE,
MISSING_COMMITMENT,
SAME_READER_LINEAGE,
TIER2_VERIFIED,
EvidenceBundle,
OperatorEvidence,
@ -23,6 +24,7 @@ __all__ = [
"DUPLICATE_STRUCTURAL_SIGNATURE",
"INSUFFICIENT_EVIDENCE",
"MISSING_COMMITMENT",
"SAME_READER_LINEAGE",
"TIER2_VERIFIED",
"EvidenceBundle",
"OperatorEvidence",

View file

@ -47,6 +47,7 @@ def evidence_from_entailment_trace(trace: EntailmentTrace) -> OperatorEvidence:
check_keys=check_keys,
commitment_key=commitment_key,
structural_signature=structural_signature,
reader_lineage="proof_chain.entail",
payload={"entailment_trace": trace.as_dict()},
)
@ -93,6 +94,7 @@ def evidence_from_math_solution(
check_keys=(trace_hash, trace.graph_canonical_hash),
commitment_key=commitment_key,
structural_signature=structural_signature,
reader_lineage="math_problem_graph.solve_verify",
payload={
"answer_entity": trace.answer_entity,
"answer_unit": trace.answer_unit,

View file

@ -20,6 +20,7 @@ INSUFFICIENT_EVIDENCE: Final[str] = "insufficient_evidence"
DUPLICATE_STRUCTURAL_SIGNATURE: Final[str] = "duplicate_structural_signature"
COMMITMENT_DISAGREEMENT: Final[str] = "commitment_disagreement"
MISSING_COMMITMENT: Final[str] = "missing_commitment"
SAME_READER_LINEAGE: Final[str] = "same_reader_lineage"
TIER2_REASONS: Final[frozenset[str]] = frozenset({
TIER2_VERIFIED,
@ -27,6 +28,7 @@ TIER2_REASONS: Final[frozenset[str]] = frozenset({
DUPLICATE_STRUCTURAL_SIGNATURE,
COMMITMENT_DISAGREEMENT,
MISSING_COMMITMENT,
SAME_READER_LINEAGE,
})
@ -74,6 +76,17 @@ class OperatorEvidence:
check_keys: tuple[str, ...]
commitment_key: str
structural_signature: str
# reader_lineage — the mechanistic identity of the decoding PATHWAY that
# produced this evidence (e.g. "proof_chain.entail"). The Tier-2 gate keys
# independence on this, not on the cosmetic structural_signature string, so a
# single reader cannot self-verify by relabeling. Deliberately EXCLUDED from
# ``as_dict``/``canonical_json`` (and therefore the evidence/trace hash): it is
# gate-routing provenance, not replayable evidence content, and including it
# would perturb the entailment trace_hash with no replay benefit. The
# static guarantee that distinct lineages are import-disjoint (cannot share a
# decoding pathway) is enforced by the architectural reader-disjointness
# invariant, not by this string alone.
reader_lineage: str = ""
payload: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
@ -83,6 +96,7 @@ class OperatorEvidence:
"outcome",
"reason",
"structural_signature",
"reader_lineage",
):
value = getattr(self, field_name)
if not isinstance(value, str) or not value.strip():
@ -146,6 +160,7 @@ class Tier2Verdict:
commitment_key: str = ""
evidence_hash: str = ""
structural_signatures: tuple[str, ...] = ()
reader_lineages: tuple[str, ...] = ()
def __post_init__(self) -> None:
if self.reason not in TIER2_REASONS:
@ -155,6 +170,11 @@ class Tier2Verdict:
"structural_signatures",
tuple(str(s) for s in self.structural_signatures),
)
object.__setattr__(
self,
"reader_lineages",
tuple(str(s) for s in self.reader_lineages),
)
def as_dict(self) -> dict[str, Any]:
return {
@ -163,13 +183,23 @@ class Tier2Verdict:
"commitment_key": self.commitment_key,
"evidence_hash": self.evidence_hash,
"structural_signatures": list(self.structural_signatures),
"reader_lineages": list(self.reader_lineages),
}
def verify_tier2_agreement(
evidences: tuple[OperatorEvidence, ...] | list[OperatorEvidence],
) -> Tier2Verdict:
"""Require two distinct structures converging on one non-empty commitment."""
"""Require two **independently-read** structures converging on one commitment.
Independence is keyed on ``reader_lineage`` the decoding PATHWAY not on the
cosmetic ``structural_signature``. The same reader cannot self-verify by emitting
two differently-labeled signatures for one case: that is refused with
``SAME_READER_LINEAGE``. The static guarantee that distinct lineages are
import-disjoint (so distinct lineage no shared decoding pathway) is enforced
by the architectural reader-disjointness invariant; this gate enforces the
runtime half (distinct producing pathways + distinct structure + one commitment).
"""
bundle = EvidenceBundle(tuple(evidences))
if len(bundle.evidences) < 2:
return Tier2Verdict(False, INSUFFICIENT_EVIDENCE)
@ -177,13 +207,26 @@ def verify_tier2_agreement(
if any(not ev.commitment_key for ev in bundle.evidences):
return Tier2Verdict(False, MISSING_COMMITMENT, evidence_hash=bundle.evidence_hash)
lineages = tuple(ev.reader_lineage for ev in bundle.evidences)
signatures = tuple(ev.structural_signature for ev in bundle.evidences)
# Mechanistic independence firewall: at least two DISTINCT decoding pathways.
if len(set(lineages)) < 2:
return Tier2Verdict(
False,
SAME_READER_LINEAGE,
evidence_hash=bundle.evidence_hash,
structural_signatures=tuple(sorted(set(signatures))),
reader_lineages=tuple(sorted(set(lineages))),
)
if len(set(signatures)) < 2:
return Tier2Verdict(
False,
DUPLICATE_STRUCTURAL_SIGNATURE,
evidence_hash=bundle.evidence_hash,
structural_signatures=tuple(sorted(set(signatures))),
reader_lineages=tuple(sorted(set(lineages))),
)
commitments = Counter(ev.commitment_key for ev in bundle.evidences)
@ -194,6 +237,7 @@ def verify_tier2_agreement(
COMMITMENT_DISAGREEMENT,
evidence_hash=bundle.evidence_hash,
structural_signatures=tuple(sorted(set(signatures))),
reader_lineages=tuple(sorted(set(lineages))),
)
return Tier2Verdict(
@ -202,4 +246,5 @@ def verify_tier2_agreement(
commitment_key=shared[0],
evidence_hash=bundle.evidence_hash,
structural_signatures=tuple(sorted(set(signatures))),
reader_lineages=tuple(sorted(set(lineages))),
)

View file

@ -0,0 +1,362 @@
<!-- CANONICAL | docs/analysis/field-reasoner-wedge-design-and-falsification-2026-06-04.md | 2026-06-04 | research/design+falsification | the source-grounded design of the field↔symbol coherence-gate wedge, corrected by adversarial review into a falsifiable experiment with a sanctioned negative outcome | verified: all load-bearing claims confirmed against source (file:line cited); produced by an 11-agent adversarial research workflow + first-hand substrate read -->
# The field-reasoner wedge — design, adversarial correction, and the falsification it must pass
This is the load-bearing research record that turns the deferred keystone of the
universal-structure plan
([`universal-structure-and-field-symbol-coherence-gate-2026-06-04.md`](./universal-structure-and-field-symbol-coherence-gate-2026-06-04.md))
into a concrete, *falsifiable* experiment. It is planning-and-research only: it
assigns no ADR number, changes no serving path, and introduces no capability claim.
**Provenance.** Compiled from (a) a first-hand read of the CL(4,1)/field/binding-graph/
reliability-gate substrate, and (b) an 11-agent adversarial research workflow (4 substrate
mappers + 2 external-literature researchers → 1 design synthesis → 3 adversarial verifiers →
1 integrator). Every load-bearing claim below is confirmed against source with a `file:line`
citation. The recommended design returned **"revise" on all three adversarial lenses**
(decoration/independence, sub-enumeration, wrong=0); their required fixes are folded in.
**The one-sentence finding.** The CL(4,1) field has exactly **one** exact native strength
relevant to reasoning — the conformal distance metric — and **none** of the incidence,
solver, or independent-reading machinery a metric reasoner needs; so the inevitable next
move is not "make the field the reasoner," it is **the cheapest honest test of whether a
metric *encoding* buys any genuine reading-independence at all**, wired so a negative result
cleanly lands the field back as a servant without polluting `wrong = 0`.
---
## 1. The inevitable next level
The architecture has exhausted the cheap moves and pinned the next one. Four constraints
intersect to a single design shape:
1. The only verified reasoning capability today is **propositional entailment** (ROBDD,
`generate/proof_chain/entail.py`, holdout 500/500 `wrong=0` vs an independent
truth-table oracle). A clean geometric decoder of *logic* was already shown to be
`O(2^n)` truth-table enumeration in algebraic clothing — **the field cannot earn its
reasoning role on combinatorial logic**. Redirect to a **metric-continuous** domain
(distance, proportion) where CGA has real purchase.
2. **Independence must live in the *reading*, not the *solving*.** Two solvers over the
same extracted structure is fake independence; the `wrong=0` risk lives in
comprehension. For quantitative/NL problems the *reading* (text → structure) is the
unsound step — unlike logic, whose input is already a formula (independence there is
cheap: two parsers).
3. **`wrong = 0` is structural, not aspirational.** Whatever the field produces is admitted
only through a *checkable* gate.
4. The dormant reliability-gate lever `checker="t2_precision"` (`core/reliability_gate/gate.py:22,48-53`)
can fire only when **two structurally-distinct derivations converge on one
`commitment_key`** — and no genuinely-independent second derivation exists.
Their intersection collapses the design space to one shape: **an independent metric
*reading* of quantitative-relational problem text, packaged as the field-side
`OperatorEvidence` for the existing `verify_tier2_agreement` seam, scored against a third,
code-disjoint gold registered as a new `INDEPENDENT_GOLD_LANE`.** And the inevitable *first
step* is the cheapest falsifiable version of it — because every heavier design **and the
null** branch off its result.
---
## 2. What the substrate actually supports today (source-verified ledger)
| Capability | Status | Evidence |
|---|---|---|
| Exact conformal distance metric `cga_inner(embed_point(a), embed_point(b)) = ½‖ab‖²` | **REAL, exact** | `algebra/cga.py:30-37, 65-84`; docstring `:11-17` ("the ONLY distance metric") |
| Null-preserving conformal transforms (versors act correctly on encoded points) | **REAL** (dual-path: null input → raw sandwich) | `algebra/versor.py:160-186, 149-157` |
| Parameterized conformal motions (rotor slerp / transition rotor) | **REAL** but wired to word→word transitions, not point transforms | `algebra/rotor.py:75-144, 147-179` |
| `embed_point` precision | **f32-HARDCODED** — unusable at GSM8K scale; needs an f64 reimplementation | `algebra/cga.py:72,76,82,83`; v=12345 → 12344.99996 in f32 |
| Incidence/flat/round constructors (line/plane/sphere via wedge) | **ABSENT** | `algebra/cga.py` exports only `cga_inner / outer_product / is_null / null_project / embed_point` |
| `meet / join / dual / cross_ratio / betweenness` | **ABSENT** (zero construction hits across `algebra/field/generate/core`) | substrate audit |
| A constraint-propagation **solver** | **ABSENT**`field/propagate.py` is a fixed-rotor walk ("no correction, no branching"); `field/operators.py::ConstraintCorrectionOperator` is **damped diffusion to a fixed prompt centroid**, NOT algebraic constraint satisfaction (the name is a trap) | `field/propagate.py:1-20,47`; `field/operators.py:153-281` |
| `versor_condition` | **unit-versor closure residual**, NOT a constraint-satisfaction measure | `algebra/versor.py:189-204` |
| Any runtime path embedding problem quantities as conformal points | **ABSENT**`embed_point` used only in `evals/lab/` probes | substrate audit |
| Reasoning readers import field/algebra | **NONE** (`generate/derivation`, `generate/proof_chain`, `core/reliability_gate` import zero `versor_apply/cga_inner/holonomy/field.propagate`) | grep, confirmed |
| Binding-graph interlingua (`SemanticSymbolicBindingGraph`) | **EXISTS, frozen, INERT** — 6 sub-collections, refusal-first, acyclic, unit-aware, `SourceSpanLink` provenance, INV-26 neutral; derived FROM `MathProblemGraph` by `adapter.py` (so it carries no *independent* reading) | `generate/binding_graph/model.py:397-507`; `adapter.py` |
| Agreement-gate seam | **EXISTS**`verify_tier2_agreement` admits iff ≥2 distinct `structural_signature`s collapse to one `commitment_key` | `core/reasoning/evidence.py:169-205` |
| Independence firewall on the gate's two readers | **DOES NOT EXIST** — the seam checks only `len(set(signatures)) < 2`, a free-form **label-string** test; INV-25 governs only the *oracle*, and its import check is **single-level / non-transitive** | `core/reasoning/evidence.py:180-187`; `tests/test_architectural_invariants.py:1115-1136` |
| `t2_precision` lever | **WIRED, dead** — nothing populates `t2_verified`; no serving call site passes `checker="t2_precision"`; no serving consumer of proposals | `core/reliability_gate/gate.py:22,48-53`; `propose.py:66-103`; `core/learning_arena/engine.py:80-99` |
| Independent-gold template | **EXISTS, proven**`deductive_logic` + `dimensional` lanes (oracle shares no SUT code; gold frozen at generation; runner counts correct/wrong/refused) | `evals/deductive_logic/{oracle,generate,runner}.py`; `evals/dimensional/{oracle,runner}.py`; `INDEPENDENT_GOLD_LANES` at `tests/test_architectural_invariants.py:1088-1106` |
**Ledger in one line:** one real native strength (the conformal distance metric); everything
else load-bearing must be built, and the precision substrate must move to f64.
---
## 3. The recommended wedge — corrected form
**Design C1 — the number-line incidence reader, sealed to forward-substitutable relations,
packaged as the field-side evidence for the existing t2 seam.** It is the smallest design
that exercises the one real substrate strength. The adversarial review did not kill it; it
corrected three things that, left unfixed, would have made it decoration or breached
`wrong=0`.
### 3.1 The independent reading — and the honest size of the claim
A **new allowlisted bridge** `generate/binding_graph/field_adapter.py` maps **raw problem
text** to conformal points. Its number detection, clause segmentation, unit assignment, and
relational-cue classification are an **independent reimplementation keyed off relational
phrase schemas** ("more than", "times as many", "in total", "ratio of"), importing **none**
of `generate.derivation`, `generate.math_candidate_parser`, `generate.math_roundtrip`,
`WORD_NUMBERS`, or `state/bind`.
**Honest downgrade (required by all three lenses):** this is a *second hand-written reader*,
**not a categorically different comprehension mechanism.** There is no existing
quantity-preserving text→geometry map. By KnightLeveson, two architecturally-disjoint
readers of the same text are exactly the regime where **correlated comprehension failure
persists** (~half of N-version faults correlate through the shared specification).
Therefore:
- The ADR/code states independence is **a hypothesis the experiment is designed to refute**,
never a property held by construction. **Do not cite McGilchrist as evidence** — design
metaphor only; the only falsifiable form is the checkable gate.
- The independence firewall is made real and **transitive**: a new architectural invariant
follows the import graph transitively and applies to **both** readers feeding the t2 gate
(today's `_module_imports` is single-level and would miss a helper that re-pulls
`math_roundtrip`). The existing `len(set(signatures)) < 2` check is a label test and
proves nothing.
The geometric encoding does add **one genuine marginal check the symbolic regex lacks**: for
an *over-determined* collinear configuration the read points must form a consistent set with
non-contradictory distances, so a relation the regex silently accepts can produce an
*incoherent* configuration the field refuses. That signal is **real but narrow** — vacuous
on a single unary relation, and it does **not** catch a *coherent* misreading. The margin is
measured, not asserted.
### 3.2 The sub-enumeration argument (sealed)
For the **forward-additive / part-whole** core the encoding is genuinely sub-enumeration and
not the logic wedge's `O(2^n)` trap:
- "A is k more than B" → translation versor `T_k`, `A = versor_apply(T_k, B)`.
- part-whole → collinear metric sum.
- a chain of unary relations (`A = B+3; C = 2A; …`) is **one-shot closed-form forward
composition** of versors — `O(1)` per constraint, no Boolean assignment lattice. Values
live in a continuous metric space; that is the categorical difference from logic.
Required fixes that keep this true:
1. **Seal the micro-domain to forward-substitutable (triangular) relations.** The property
holds only where each relation pins one new unknown from already-resolved points. A
**coupled/over-determined** system is meet-of-hyperplanes = Gaussian elimination =
`O(n³)` and needs `meet/join/dual` primitives that **do not exist**. The generator and
gold exclude coupled systems.
2. **Drop the "propagate to `versor_condition<1e-6`" framing.** A forward chain has no
iterative solve and no residual; `versor_condition` measures the *operator's* closure,
not constraint satisfaction. Conflating them invites a **forbidden** hot-path normalizer
(CLAUDE.md forbids drift repair in `field/propagate.py`).
3. **The win is a sub-*enumeration encoding*, never a *solving* speedup.** Never pitch the
field as faster than Gaussian elimination.
### 3.3 Correct answer read-back (the wrong=0 bug the verdicts caught)
The naive read-back — "answer = signed distance from origin via `cga_inner`" — is
**mathematically wrong** for any weight-changing operator. A dilation (the multiplicative
case) yields a **weighted** null vector: dilating x=2 by k=4 gives e1=2.0, n_o-weight 0.25,
i.e. `0.25·embed_point([8,0,0])`; naive distance-from-origin returns 4.0 for true coordinate
8.0. **Fix:** read by **projective dehomogenization** — `answer = e1_coefficient /
n_o_weight` — verified exact (8.0). This is the read-back for **all** operators, since
composed motors change weight.
Two further metric hazards the *read side* must own (not the geometry):
- **`cga_inner` is sign-/orientation-blind** (`d²/2` cannot distinguish A=2B from A=2B, nor
enforce betweenness order). Orientation is a separate read-side decision the geometry does
not verify. Either add an explicit signed-coefficient orientation invariant **or refuse
all sign-ambiguous configurations.**
- **"k times" via dilation-about-origin / cross-ratio needs a non-degenerate 4-point frame**;
collinear configs are ill-conditioned. The additive/part-whole core is the load-bearing
claim; the multiplicative case is **fenced** as the conditioning risk.
### 3.4 The wrong=0 admission contract (honest)
The "three loud-failing layers" was inflated; two are decoration:
- **"Geometric coherence"** on one number line is collinearity-by-construction + non-
contradictory distances — it catches *contradictory* readings, not a *single coherent
wrong* relation.
- **"Exact integer round-trip"** is decoration as a *comprehension* guard (confirmed:
`embed_point([103]), [300], [7]` each round-trip residual 0.0). A coherent misreading (300
vs 103) passes trivially. Per CLAUDE.md's schema-defined-proof-obligation rule, demote it
to a **float-sanity guard**, not a `wrong=0` layer.
**The real, load-bearing protection is exactly two checks:**
1. **field⟂symbol agreement** on the **exact** `commitment_key` = (integer value +
`UnitVector` dimension). Float-tolerant equality is **never** the admit criterion — that
is precisely what sank `resolve_pooled` (right/wrong float readings indistinguishable).
2. **Scoring vs an independent gold** — a third oracle code-disjoint from **both** readers,
importing no field/algebra so it cannot agree with the field by construction.
Plus **f64 + a pinned magnitude ceiling**: an f64 reimplementation of `X = x + n_o +
½|x|²·n_inf`, refusing any quantity above an *empirically pinned* bound (GSM8K reaches
thousands; ½|x|² ≈ 1e7; f64 recovers integers exactly to ~1e7, but the ceiling is pinned by
measurement, not assumed). Above the ceiling → refuse.
### 3.5 The asymmetric refinement (the right second step, not the first)
The literature's strongest guarantee is **producer/checker asymmetry** (certifying
algorithms / translation validation): an untrusted producer emits a *witness* a simpler,
code-disjoint *checker* validates without redoing the work. Symmetric "two decodings agree"
is weaker (exposed to correlated error). CORE already has an asymmetric checker for the
dimensional sub-check — `generate/binding_graph/units.py`'s `unit_proof` is free and
code-disjoint. **Use the asymmetric check where one exists** (a C2-style certifying-witness
for the dimensional component); reserve symmetric agreement only where no independent checker
is constructible. This is the correct step *after* the wedge proves the field lands a
coherent configuration at all — **not** premature surface area to build first.
### 3.6 If the experiment fails — the sanctioned alternative (C3)
The field stays a servant in its current cognition-turn role, and quantitative-relational
capability is carried by a **second fully-symbolic, code-disjoint reading** (a
relational-schema parser) agreeing with the existing candidate-graph reader at exact-integer
`commitment_key`s. C3 sidesteps every float/precision/orientation hazard and is the cleanest
`wrong=0` story. Its honest weakness: two symbolic readers are the closest analogue to
KnightLeveson's refuted-independence case. **C3 is the correct landing iff the wedge shows
the field shares the symbolic reader's blind spots** — a *success*, because it answers the
field-as-reasoner question with evidence instead of deferring it.
---
## 4. The falsifiable experiment (cheapest decisive test)
**Build only:** the f64 number-line reader + four constraint versors (additive/part-whole
load-bearing; multiplicative/ratio fenced) + projective-dehomogenization read-back + the
ablation/diversity instrument. **Do not** build the C2 certifying checker yet.
**Dataset:** ~150 seeded two/three-quantity problems ("k more than", "k times", part-whole),
gold from a **third hand-authored arithmetic oracle** sharing no code with either reader,
registered in `INDEPENDENT_GOLD_LANES`. Split into generation and a **held-out** set; measure
on held-out only (anti-overfit firewall — never tune to the 150 templates).
**Three pass/fail measurements, wired as the gate (not diagnostics):**
1. **Field-alone correctness** — does the field's projective-dehomogenized integer answer
equal the independent gold on committed cases, **`wrong=0`** on held-out?
2. **Ablation** — run the `wrong=0` gate **with the field vote removed** vs **with it**. The
admitted set **must CHANGE** (the field must refuse ≥1 comprehension error the symbol path
alone admits). Unchanged ⇒ **the field is decoration → land C3.**
3. **Diversity, reported PER relation class** — double-fault rate / Q-statistic /
coincident-failure diversity must clear a pinned threshold. **Aggregate is forbidden** (it
could net out signal while the field is decoration on the cases that matter). Field credit
only on classes where it demonstrably refuses an error the symbol path admits.
**A negative result is sanctioned and expected-possible.** If (1) breaches `wrong=0` at
scale, or (2) shows an unchanged admitted set, or (3) shows high coincident failure (Q≈1),
the field is a servant here → land C3, and that result *confirms* the thesis that genuine
reading-diversity needs a categorically different mechanism — justifying a later deeper
research bet rather than this cheap wedge. No unfalsifiable "the field knows" claim is ever
permitted.
---
## 5. Phased path
> Each phase: **entry gate · exit gate · independent gold · `wrong=0` guard.** No phase
> begins until the prior phase's exit gate is green. A lookback review runs at each boundary.
### Phase 0 — foundation (build regardless of the wedge result)
- **Build:** (a) f64 `embed_point` reimplementation + magnitude-ceiling refusal; (b) the
**transitive reader-disjointness invariant** applied to *both* readers feeding the t2 gate;
(c) replace the `verify_tier2_agreement` label-string distinctness check with a mechanistic
one (distinct signatures must be provably unable to come from the same decoding pathway);
(d) the independent-gold lane scaffolding (third arithmetic oracle, `INDEPENDENT_GOLD_LANES`
registration).
- **Exit:** all four shipped, full-suite green, INV-25/INV-26 still green; no serving path
touched. These are net hardening even if the field never reasons.
### Phase W — field-alone metric reader + decoration instrument
- **Entry:** Phase 0 green; micro-domain sealed to forward-substitutable relations;
projective-dehomogenization read-back implemented.
- **Exit:** the three §4 measurements pass on held-out (field-alone `wrong=0`; ablation
changes the admitted set; per-class diversity above threshold) **OR** they fail → land C3
and stop.
- **Independent gold:** third hand-authored arithmetic oracle, code-disjoint from both
readers, imports no field/algebra.
- **`wrong=0` guard:** exact integer + `UnitVector` `commitment_key` (no float tolerance);
f64 + refuse-above-ceiling; refuse sign-ambiguous configs.
### Phase 3 — activate `t2_precision` (the dormant lever)
- **Entry:** Phase W exit green; field and symbol each emit `OperatorEvidence` with the same
exact `commitment_key` and **genuinely distinct** signatures (proven by the non-decoration
test, not by label).
- **Exit:** in sealed practice, `ClassTally.t2_precision` rises past `N_MIN`+ to the chosen θ
(PROPOSE=0.85 staging before SERVE=0.99); `propose_from_ledger(..., checker="t2_precision")`
emits a proposal; **and** a concrete serving consumer of `checker="t2_precision"` exists and
**refuses on disagreement** (close the ratify-vs-consume gap with an eval delta, not an
artifact append).
- **Independent gold:** the GoldTether earning t2_precision is independent of **both** readers
(INV-25 generalized to two SUTs). Re-using the symbolic answer as gold collapses the
firewall.
- **`wrong=0` guard:** practice scoring is gold-gated, so agree-on-wrong scores
`t2_verified=1, t2_agrees_gold=0` and **lowers** the license. The residual hazard lives
in the unbuilt serving consumer: pin its contract now — admit iff (agreement AND class
licensed); disagreement or field-refusal → deterministic refuse; no float tolerance
anywhere. The Wilson floor at θ=0.99 demands ~657 perfect committed agreements/class — the
coverage wall is the binding constraint.
### Phase 4 — GSM8K on the structure (held-out / sealed)
- **Entry:** Phase 3 exit green; the field reader generalizes from seeded templates to **real
GSM8K** forward-substitutable relational cases (validate on `train_sample`/`holdout_dev`,
never the 150 templates).
- **Exit:** on a **sealed** GSM8K split, the agreement gate admits with **`wrong=0`**;
ablation still shows the field changing the admitted set on real cases.
- **Independent gold:** GSM8K reference answers; the gate's gold kept independent of both
readers.
- **`wrong=0` guard:** the serving-frozen lane (`scripts/verify_lane_shas.py`, `CLAIMS.md`) —
no bridge re-enables a reader without a sealed/independent `wrong=0` run. The standing
warning is `resolve_pooled` (2 right / 87 wrong): never admit a reader that cannot
distinguish its right readings from its wrong ones.
### Phase 5 — cross-domain anti-overfit panel
- **Entry:** Phase 4 green; the field reader is a **third** golded domain alongside
`deductive_logic` and `dimensional`.
- **Exit:** registered in `INDEPENDENT_GOLD_LANES` with a code-disjoint oracle
(forbidden-prefix entries for **both** readers); INV-25 sub-properties hold; INV-26 green
(field→graph translation lives in the bridge, never in `model.py/units.py/admissibility.py`).
- **`wrong=0` guard:** full panel `wrong=0` across all three domains; a deliberately-unsound
engine must disagree with each oracle (non-vacuity).
### And beyond
The deferred keystone remains deferred: a **categorically different comprehension mechanism**
(Gentner-style structure-mapping over a path that does not pass through any lexeme extractor)
that would make field⟂symbol independence *real by construction* rather than measured. The
wedge does **not** deliver that; it tests whether a metric *encoding* buys any independence on
the one real substrate strength. If Phase W lands C3, the "beyond" is a dedicated research
track on the comprehension mechanism — out of the near-term sequence, never licensed to
displace it.
---
## 6. Risks, honest tensions, and STOP conditions
**Tensions (named, not waved):**
- **Independence is architectural-only, not mechanistic.** A second hand-written parser is
precisely where correlated comprehension failure survives (KnightLeveson). The claim is a
hypothesis the experiment refutes, not a property; the current firewall (a label-string
distinctness test) does not enforce it.
- **Symmetric agreement is weaker than producer/checker asymmetry.** Use the asymmetric
(certifying-witness) check where one exists (dimensional `unit_proof`); reserve symmetric
agreement only where no checker is constructible.
- **The metric is sign-/orientation-blind and the substrate has no incidence primitives.**
"Incidence" is faked via distance arithmetic; orientation is a read-side decision the
geometry does not verify — thinning the independent signal exactly on multiplicative/ratio
cases.
**STOP (and land C3 or the servant null):**
1. Ablation shows an **unchanged admitted set** — the field is decoration.
2. Per-class diversity **collapses** (double-fault high / Q≈1) — fake independence.
3. **f64 + ceiling cannot preserve exact distance at GSM8K scale** — restrict to a band so
narrow it is not the comprehension layer, or stop.
4. **The seal cannot hold** — if real cases force coupled/over-determined systems, the
sub-enumeration claim degrades to `O(n³)` on primitives that do not exist; do **not**
fabricate `meet/join/dual`.
5. **A float coherence threshold gets reconciled with the exact admit gate by tolerance**
the `resolve_pooled` failure mode; stop immediately, this breaches `wrong=0`.
6. **Any "the field knows" claim, or a McGilchrist-as-evidence citation, appears in code/ADR
without the checkable gate behind it** — that imports an unfalsifiable claim; revert.
**Bottom line, without overclaim:** the field has one real, exact strength (the conformal
distance metric) and none of the reasoning machinery around it. The wedge is the cheapest
honest test of whether a metric *encoding* buys genuine reading-independence on
quantitative-relational structure. If it does — `wrong=0`, ablation-positive, diverse — it
activates the dormant `t2_precision` lever as the genuine second derivation. If it does not,
**the field stays a servant, a second code-disjoint symbolic reading carries the next level,
and that negative result is the success.**

View file

@ -0,0 +1,83 @@
<!-- CANONICAL | docs/analysis/field-wedge-ablation-result-2026-06-04.md | 2026-06-04 | research/experimental-result | the field-reasoner wedge ablation verdict: the field reads correctly (wrong=0) but adds no independent signal over a symbolic reader on the metric-trivial micro-domain — C3 | verified: ablation run committed; field_wrong=[], field_caught_symbolic_errors=[] -->
# The field-reasoner wedge ablation — result: C3 (decoration on this domain)
This records the outcome of the falsifiable experiment designed in
[`field-reasoner-wedge-design-and-falsification-2026-06-04.md`](./field-reasoner-wedge-design-and-falsification-2026-06-04.md).
The verdict is the dossier's sanctioned negative: **the field reads correctly but is
decoration here.** That is a success — it answers the field-as-reasoner question with
evidence instead of deferring it.
## What was measured
Two readings of the same forward-substitutable quantitative-relational micro-domain
(additive / part-whole), scored against an independent arithmetic oracle:
- **Field reader** (`generate/relational_field_reader.py`) — reads TEXT into conformal
points on the e1 number line; additive relations are conformal translator versors;
the answer reads back by projective dehomogenization. (The geometric SUT.)
- **Symbolic reader** (`generate/relational_symbolic_reader.py`) — a competent,
code-disjoint reading by plain integer arithmetic; no `algebra` (INV-27 disjoint).
The ablation (`evals/relational_metric/ablation.py`) runs both through the real
`verify_tier2_agreement` gate and asks the only questions that matter.
## Result
| Measurement | Value | Meaning |
|---|---|---|
| **field_wrong_commits** | `[]` | wrong=0 holds — the field never commits a bad integer (the per-step drift guard refuses instead). |
| **field_caught_symbolic_errors** | `[]` | The field caught **zero** comprehension errors the symbolic reader made. (Measurement #2 = fail.) |
| **field_lost_coverage** | `[rm-v1-0015]` | The only admitted-set change is the field **refusing a correct answer** at the precision ceiling — a liability, not signal. |
| **per-class diversity** | `0` everywhere | On every committable class the readers AGREE and are BOTH CORRECT: `disagree=0`, `double_fault=0`. (Measurement #3 = no diversity.) |
| **verdict** | **`C3`** | The field is decoration / a coverage liability on this domain. |
## Why — the load-bearing insight
**On forward-substitutable relations, geometric translation *is* arithmetic addition.**
`versor_apply(T_δ, embed[x]) = embed[x+δ]` is exactly `x + δ`. There is no metric
over-determination — no betweenness, incidence, or continuous-constraint structure —
for the field to exploit that a competent symbolic reader lacks. So the two readings are
**redundant** (the common-mode the dossier predicted via KnightLeveson), and agreement
adds no independent soundness signal. The field's *one* genuine marginal check
(over-determination coherence) is matched by the symbolic reader's `over_determined_conflict`
refusal, so even there it is not unique. And the field's precision ceiling makes it a
strict coverage liability versus pure integer arithmetic.
This is the deductive-logic finding's twin: there, logic was *combinatorial* not metric,
so the field could not earn it; here, the metric domain is *arithmetically trivial*, so
the geometry adds nothing. **The field earns a reasoning role only where geometry
genuinely exceeds the symbolic alternative — not yet demonstrated on any built domain.**
## What this changes
- **Field-as-reasoner is NOT earned.** No field vote enters any serving or capability
path. The field stays a servant (its existing cognition-turn role). No "the field
reasons" claim is made.
- **The capability path is symbolic (C3).** The symbolic reader is correct (15/15 here,
including the case the field refuses). Shipping quantitative capability safely is the
dossier's Phase 3+ on the symbolic path — **two** code-disjoint symbolic readings
agreeing + independent/sealed gold (one reader alone is the resolve_pooled risk). Not
built here; this slice answered the experiment, it did not ship serving.
- **The `t2_precision` lever stays dormant** for this domain — there is no genuine second
derivation to feed it (field⟂symbol is common-mode here).
## What stays (all load-bearing, none reverted)
- The **f64 conformal foundation** (Phase 0A) — net infrastructure hardening.
- The **reader-lineage firewall + INV-27** (Phase 0B/0C) — backs any future agreement gate.
- The **field reader + relational-metric lane** — a real wrong=0 demonstration that the
field *can* read (Phase W.1), and a valid 3rd independently-golded panel domain.
- The **symbolic reader + ablation instrument** — the C3 reader and a reusable decoration
test for the next domain.
## The honest next bet (dedicated research, not the near-term sequence)
Field-as-reasoner remains an open question for a domain where geometry genuinely beats
arithmetic: **continuous / over-determined / multi-constraint** structure (relative
position, betweenness, incidence among many points, ratio as cross-ratio on a
non-degenerate frame) — where a symbolic step-arithmetic reader and a field constraint
reading could genuinely diverge, and agreement would be a real second derivation. Until
such a domain is built and the ablation shows `field_caught_symbolic_errors > 0`, the
field stays a servant. Two negative domains (logic: combinatorial; additive: trivial)
now bound the search: the field needs *metric-nontrivial, arithmetically-hard* structure.

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,162 @@
"""The decoration instrument — does the geometric FIELD reader add independent signal?
Measurements #2 (ablation) and #3 (per-class diversity) of the field-reasoner wedge
falsifiable experiment. For each case it runs BOTH readers and the real
``verify_tier2_agreement`` gate, then asks the only questions that matter:
- **field_caught_symbolic_errors** cases where the SYMBOLIC reader commits a WRONG
answer ( gold) but the agreement gate REFUSES (the field disagreed or refused).
This is the dossier's measurement-#2 PASS signal: the field refusing a comprehension
error the symbol path alone admits.
- **field_lost_coverage** cases where the symbolic reader commits the CORRECT answer
but the gate refuses *because of the field* (field refused or disagreed). The field
subtracting a correct answer is a liability, not signal.
- **admitted_set_changed** does the gate's admitted set differ from symbolic-alone?
- per-class double-fault / agreement diversity.
VERDICT:
- ``PASS`` iff ``field_caught_symbolic_errors > 0`` (the field adds error-catching
signal) and never commits a wrong answer.
- ``C3`` (field is decoration / a coverage liability on this domain) otherwise a
sanctioned, honest outcome: the symbolic reading carries the capability.
Run: PYTHONPATH=. .venv/bin/python -m evals.relational_metric.ablation
"""
from __future__ import annotations
import json
import sys
from collections import defaultdict
from pathlib import Path
from typing import Any
from core.reasoning import OperatorEvidence, verify_tier2_agreement
from evals.relational_metric.oracle import OracleError, oracle_answer
from generate.relational_field_reader import READER_LINEAGE as FIELD_LINEAGE
from generate.relational_field_reader import read_relational as field_read
from generate.relational_symbolic_reader import READER_LINEAGE as SYM_LINEAGE
from generate.relational_symbolic_reader import read_relational as sym_read
_CASES = Path(__file__).resolve().parent / "v1" / "cases.jsonl"
def _commit_key(answer: int, unit: str | None) -> str:
return f"{answer}:{unit}"
def _gate_admits(field_r: Any, sym_r: Any) -> int | None:
"""Run the real Tier-2 gate; return the agreed answer or None (refuse)."""
if field_r.refused or sym_r.refused:
return None
ev_field = OperatorEvidence(
domain="relational_metric", operator="field_read", outcome="committed",
reason="field_commit", input_keys=(), check_keys=(),
commitment_key=_commit_key(field_r.answer, field_r.answer_unit),
structural_signature="field.geometric.number_line",
reader_lineage=FIELD_LINEAGE,
)
ev_sym = OperatorEvidence(
domain="relational_metric", operator="symbolic_read", outcome="committed",
reason="symbolic_commit", input_keys=(), check_keys=(),
commitment_key=_commit_key(sym_r.answer, sym_r.answer_unit),
structural_signature="symbolic.arithmetic.schema",
reader_lineage=SYM_LINEAGE,
)
verdict = verify_tier2_agreement((ev_field, ev_sym))
return field_r.answer if verdict.verified else None
def run() -> dict[str, Any]:
cases = [json.loads(l) for l in _CASES.read_text().splitlines() if l.strip()]
rows: list[dict[str, Any]] = []
per_class: dict[str, dict[str, int]] = defaultdict(
lambda: {"n": 0, "agree_commit": 0, "disagree": 0,
"both_correct": 0, "double_fault": 0}
)
field_caught: list[str] = []
field_lost_coverage: list[str] = []
field_wrong: list[dict[str, Any]] = []
for case in cases:
cid, cls = case["id"], case["class"]
try:
gold = oracle_answer(case["relations"], case["query"])
except OracleError:
continue
f = field_read(case["text"])
s = sym_read(case["text"])
gate = _gate_admits(f, s)
f_ans = None if f.refused else f.answer
s_ans = None if s.refused else s.answer
sym_alone = s_ans # symbolic-alone admits its committed answer
pc = per_class[cls]
pc["n"] += 1
if f_ans is not None and s_ans is not None:
if f_ans == s_ans:
pc["agree_commit"] += 1
if f_ans == gold:
pc["both_correct"] += 1
else:
pc["disagree"] += 1
if f_ans != gold and s_ans != gold:
pc["double_fault"] += 1
# The field's own wrong=0 (it must never commit a wrong answer).
if f_ans is not None and f_ans != gold:
field_wrong.append({"id": cid, "field": f_ans, "gold": gold})
# measurement #2 signals
if s_ans is not None and s_ans != gold and gate is None:
field_caught.append(cid) # symbolic committed WRONG; gate refused
if s_ans is not None and s_ans == gold and gate is None:
field_lost_coverage.append(cid) # symbolic correct; gate refused
rows.append({
"id": cid, "class": cls, "gold": gold,
"field": f_ans, "field_refused": f.refused and f.refusal_reason,
"symbolic": s_ans, "symbolic_refused": s.refused and s.refusal_reason,
"gate_admits": gate, "symbolic_alone": sym_alone,
})
gate_admitted = {r["id"]: r["gate_admits"] for r in rows if r["gate_admits"] is not None}
symbolic_admitted = {r["id"]: r["symbolic_alone"] for r in rows if r["symbolic_alone"] is not None}
admitted_changed = gate_admitted != symbolic_admitted
verdict = (
"PASS" if (field_caught and not field_wrong)
else "C3_field_is_decoration_or_liability"
)
return {
"total": len(rows),
"verdict": verdict,
"field_caught_symbolic_errors": field_caught,
"field_lost_coverage": field_lost_coverage,
"field_wrong_commits": field_wrong,
"admitted_set_changed": admitted_changed,
"gate_admitted_count": len(gate_admitted),
"symbolic_alone_admitted_count": len(symbolic_admitted),
"per_class": {k: dict(v) for k, v in per_class.items()},
"rows": rows,
}
def main() -> int:
report = run()
summary = {k: v for k, v in report.items() if k != "rows"}
print(json.dumps(summary, indent=2))
# The instrument is non-failing: a C3 verdict is a sanctioned, honest outcome.
# It only hard-fails if the field ever commits a WRONG answer (wrong=0 breach).
if report["field_wrong_commits"]:
print("FIELD COMMITTED A WRONG ANSWER (wrong!=0 breach):",
report["field_wrong_commits"], file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

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

@ -0,0 +1,127 @@
"""The SYMBOLIC reader for forward-substitutable quantitative relations.
The second, code-disjoint reading of the same micro-domain as
``generate.relational_field_reader`` but it resolves by plain integer arithmetic
over a parsed schema, importing **no** ``algebra`` / ``field`` (proven import-disjoint
from the field reader by INV-27). It is:
- the control arm of the ablation experiment (does the geometric FIELD reader add any
signal over a competent symbolic reader, or is it decoration?), and
- the C3 capability path if the field does not earn its role (a second independent
reading whose agreement with another reader is the wrong=0 gate).
It is a *competent* reader, not a strawman: it reads the same grammar and applies the
same fences (multiplicative/ratio, forward-substitutability, negatives), and it
detects over-determination (a quantity stated two ways that conflict) and refuses
so the ablation is a fair test. It has NO float/precision limit (pure int), so unlike
the field it commits arbitrarily large quantities.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
# Stable identity for the Tier-2 gate (registered in INV-27).
READER_LINEAGE: str = "symbolic.relational_schema"
_FENCED_CUE = re.compile(
r"\b(times|twice|double|triple|thrice|half|quarter|ratio|per|each)\b"
)
# Independent regex set (own spelling/order) — same grammar, different implementation.
_REL_MORE = re.compile(r"(\w+) (?:has|have) (\d+) more \w+ than (\w+)")
_REL_FEWER = re.compile(r"(\w+) (?:has|have) (\d+) (?:fewer|less) \w+ than (\w+)")
_REL_FACT = re.compile(r"(\w+) (?:has|have) (\d+) (\w+)")
_Q_SINGLE = re.compile(r"how many (\w+) does (\w+) have")
_Q_SUM = re.compile(r"how many (\w+) do (\w+) and (\w+)(?: and (\w+))? have")
class SymbolicReaderError(ValueError):
"""Out-of-grammar case → typed refusal."""
@dataclass(frozen=True, slots=True)
class SymbolicReading:
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) -> SymbolicReading:
return SymbolicReading(refused=True, refusal_reason=reason)
def read_relational(text: str) -> SymbolicReading:
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.lower())
except SymbolicReaderError as exc:
return _refuse(str(exc))
def _set(values: dict[str, int], entity: str, value: int) -> None:
"""Assign, detecting over-determination (a conflicting re-statement)."""
if entity in values and values[entity] != value:
raise SymbolicReaderError("over_determined_conflict")
values[entity] = value
def _read(text: str) -> SymbolicReading:
parts = [s.strip() for s in re.split(r"[.?!]", text) if s.strip()]
unit: str | None = None
q_single: str | None = None
q_sum: list[str] | None = None
for s in parts:
if "how many" not in s:
continue
if (m := _Q_SUM.search(s)) is not None:
unit = m.group(1)
q_sum = [g for g in m.groups()[1:] if g]
elif (m := _Q_SINGLE.search(s)) is not None:
unit, q_single = m.group(1), m.group(2)
else:
raise SymbolicReaderError("unparsed_query")
break
if q_single is None and q_sum is None:
raise SymbolicReaderError("no_query")
values: dict[str, int] = {}
for s in parts:
if "how many" in s:
continue
if (m := _REL_MORE.search(s)) is not None:
ent, n, ref = m.group(1), int(m.group(2)), m.group(3)
if ref not in values:
raise SymbolicReaderError("non_forward_substitutable")
_set(values, ent, values[ref] + n)
elif (m := _REL_FEWER.search(s)) is not None:
ent, n, ref = m.group(1), int(m.group(2)), m.group(3)
if ref not in values:
raise SymbolicReaderError("non_forward_substitutable")
_set(values, ent, values[ref] - n)
elif (m := _REL_FACT.search(s)) is not None:
ent, n = m.group(1), int(m.group(2))
_set(values, ent, n)
else:
raise SymbolicReaderError("unparsed_sentence")
if q_single is not None:
if q_single not in values:
raise SymbolicReaderError("query_entity_unresolved")
answer = values[q_single]
else:
assert q_sum is not None
if any(p not in values for p in q_sum):
raise SymbolicReaderError("query_entity_unresolved")
answer = sum(values[p] for p in q_sum)
if answer < 0:
raise SymbolicReaderError("negative_quantity")
return SymbolicReading(refused=False, answer=answer, answer_unit=unit)

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, ...] = (
@ -1392,3 +1405,195 @@ class TestINV26InterlinguaNeutrality:
"INV-26b predicate failed to detect the adapter's domain-reader "
"import — the core-isolation check is vacuous."
)
# ===========================================================================
# INV-27 Tier-2 reader disjointness — the two readers whose AGREEMENT the
# Tier-2 gate trusts must share no decoding pathway
# ===========================================================================
#
# Claim (docs/analysis/field-reasoner-wedge-design-and-falsification-2026-06-04
# .md §3.1; the decoration firewall):
#
# ``verify_tier2_agreement`` keys independence on ``reader_lineage`` — the
# decoding PATHWAY — not on the cosmetic ``structural_signature``. That runtime
# check is only load-bearing if "distinct lineage" actually means "no shared
# decoding pathway." A string alone cannot prove that. This invariant supplies
# the static proof: every reader that EMITS a Tier-2 lineage is registered with
# its entry module, and any two registered readers' TRANSITIVE first-party
# import sets are disjoint (modulo an explicit pure-data neutral allowlist).
#
# The old gate admitted on ``len(set(signatures)) < 2`` — a label test that a
# single reader could satisfy by relabeling. The single-level INV-25 import
# check would also miss a helper that re-pulls a shared reader transitively;
# this check is TRANSITIVE.
#
# 27a Every ``reader_lineage="..."`` literal emitted in core/reasoning/adapters
# is registered in TIER2_READER_PATHWAYS (so its disjointness is proven).
# 27b Each registered reader module exists (drift guard).
# 27c Every pair of registered readers is transitively import-disjoint, except
# modules explicitly allowlisted as pure-data neutral meeting points.
# 27d Non-vacuity: the disjointness predicate flags a genuinely-shared pair and
# the neutral allowlist genuinely clears one — proving 27c can fail.
import dataclasses as _dataclasses
# Top-level first-party package roots. An imported module whose first dotted
# component is one of these is project code (a potential shared decoding pathway);
# stdlib and third-party (numpy, ...) are not.
_FIRST_PARTY_ROOTS: frozenset[str] = frozenset({
"generate", "core", "algebra", "field", "language_packs", "vault", "chat",
"teaching", "sensorium", "calibration", "evals", "ingest", "recognition",
"formation", "morphology", "vocab", "session", "contemplation", "persona",
"alignment", "probe", "core_ingest", "core_rs",
})
@_dataclasses.dataclass(frozen=True)
class Tier2Reader:
"""A reader whose evidence the Tier-2 gate may treat as an independent vote."""
lineage: str # must equal the ``reader_lineage`` this reader emits
module: str # repo-relative path to the reader's entry module
# Readers whose agreement the Tier-2 gate is allowed to trust. Adding one is the
# reviewable seam: it asserts (and INV-27c proves) the new reader shares no
# decoding pathway with the others.
TIER2_READER_PATHWAYS: tuple[Tier2Reader, ...] = (
Tier2Reader("proof_chain.entail", "generate/proof_chain/entail.py"),
Tier2Reader("math_problem_graph.solve_verify", "generate/math_solver.py"),
)
# Pure-data / contract modules that are a NEUTRAL meeting point, not a shared
# decoding pathway: two readers may both import these without their agreement
# becoming common-mode. Every entry must be parser/solver-free (frozen data or
# pure algebra). EMPTY today — the current readers share nothing; the field
# reader (Phase W) will add ``generate.binding_graph.model`` here, with review.
TIER2_NEUTRAL_MODULES: frozenset[str] = frozenset()
def _is_first_party(mod: str) -> bool:
return mod.split(".")[0] in _FIRST_PARTY_ROOTS
def _resolve_module(mod: str) -> Path | None:
base = _REPO_ROOT / mod.replace(".", "/")
if (f := base.with_suffix(".py")).is_file():
return f
if (pkg := base / "__init__.py").is_file():
return pkg
return None
def _transitive_first_party_imports(entry_module: str) -> set[str]:
"""Every first-party module reachable from ``entry_module`` via imports.
Follows ``import``/``from ... import`` edges through first-party modules only,
so a reader that pulls a shared comprehension helper three hops deep is still
caught unlike the single-level :func:`_module_imports`.
"""
seen: set[str] = set()
stack = [entry_module]
while stack:
mod = stack.pop()
if mod in seen:
continue
path = _resolve_module(mod)
if path is None:
continue
seen.add(mod)
for imported in _module_imports(path):
if _is_first_party(imported) and imported not in seen:
stack.append(imported)
return seen
def _shared_first_party(a: set[str], b: set[str], neutral: frozenset[str]) -> set[str]:
"""First-party modules both sets import, minus the neutral allowlist."""
return (a & b) - set(neutral)
def _module_name_of(rel_path: str) -> str:
stem = rel_path[:-3] if rel_path.endswith(".py") else rel_path
return stem.replace("/", ".")
class TestINV27Tier2ReaderDisjointness:
"""The two readers whose agreement the Tier-2 gate trusts share no decoding
pathway so ``verify_tier2_agreement``'s lineage check is load-bearing."""
def test_emitted_lineages_are_registered(self):
"""27a — every reader_lineage a producer emits is registered, so its
disjointness is actually proven below (not merely asserted by a string)."""
adapters = _REPO_ROOT / "core" / "reasoning" / "adapters.py"
tree = ast.parse(adapters.read_text())
emitted: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Call):
for kw in node.keywords:
if kw.arg == "reader_lineage" and isinstance(kw.value, ast.Constant):
if isinstance(kw.value.value, str):
emitted.add(kw.value.value)
registered = {r.lineage for r in TIER2_READER_PATHWAYS}
unregistered = sorted(emitted - registered)
assert not unregistered, (
f"Reader(s) emit unregistered Tier-2 lineage(s): {unregistered}. "
"Register them in TIER2_READER_PATHWAYS so INV-27c proves they share "
"no decoding pathway, or the gate's independence check is unbacked."
)
def test_registered_reader_modules_exist(self):
"""27b — drift guard: a registered module that moved makes 27c vacuous."""
missing = [r.module for r in TIER2_READER_PATHWAYS
if not (_REPO_ROOT / r.module).is_file()]
assert not missing, (
f"Registered Tier-2 reader module(s) not found: {missing}. "
"Update TIER2_READER_PATHWAYS."
)
def test_registered_readers_are_pairwise_import_disjoint(self):
"""27c — the load-bearing static guarantee. Any two readers the gate may
treat as independent votes import no first-party module in common (except
explicit neutral pure-data modules). Distinct lineage disjoint pathway."""
transitive = {
r.lineage: _transitive_first_party_imports(_module_name_of(r.module))
for r in TIER2_READER_PATHWAYS
}
offenders: list[str] = []
readers = list(TIER2_READER_PATHWAYS)
for i in range(len(readers)):
for j in range(i + 1, len(readers)):
a, b = readers[i], readers[j]
shared = _shared_first_party(
transitive[a.lineage], transitive[b.lineage], TIER2_NEUTRAL_MODULES
)
if shared:
offenders.append(
f"{a.lineage}{b.lineage} share {sorted(shared)}"
)
assert not offenders, (
"Tier-2 readers the gate trusts as independent share a decoding "
"pathway:\n " + "\n ".join(offenders)
+ "\n\nTheir agreement would be common-mode, not independent (the "
"GSM8K blind spot). Re-implement one independently, or — if the shared "
"module is genuinely pure data — add it to TIER2_NEUTRAL_MODULES with "
"a justification that it is parser/solver-free."
)
def test_disjointness_predicate_is_non_vacuous(self):
"""27d — prove 27c can fail and that the neutral allowlist genuinely
clears a shared module (CLAUDE.md schema-defined proof obligation)."""
a = {"generate.reader_a", "generate.shared_parser", "algebra.versor"}
b = {"generate.reader_b", "generate.shared_parser"}
# A genuinely shared first-party module is flagged.
assert _shared_first_party(a, b, frozenset()) == {"generate.shared_parser"}
# Allowlisting that module as neutral clears it.
assert _shared_first_party(a, b, frozenset({"generate.shared_parser"})) == set()
# The transitive walker really follows edges: math_solver reaches the
# problem-graph module it is built on (a multi-hop first-party import).
ms = _transitive_first_party_imports("generate.math_solver")
assert "generate.math_problem_graph" in ms, (
"transitive walker did not follow math_solver -> math_problem_graph; "
"the disjointness proof would miss deep shared readers."
)

View file

@ -0,0 +1,112 @@
"""Phase 0A — f64-exact conformal embedding + projective read-back + pinned ceiling.
The field-reasoner wedge encodes quantities as conformal points on the e1 number
line and reads the answer back by *projective dehomogenization* the only exact
read-back for weight-changing (dilation) operators. ``embed_point`` was f32-hardcoded
(``algebra/cga.py``), which silently destroys integers past ~1e4: at v=12345 the f32
``n_o`` weight collapses to 0 and the read-back is unusable.
These tests pin three contracts:
1. ``embed_point``'s f32 default is byte-unchanged (no existing caller regresses).
2. The new ``dtype=np.float64`` path + ``read_scalar_e1`` recover integer coordinates
**exactly** across the whole admissible band, where f32 already fails.
3. ``EMBED_EXACT_MAX`` is the pinned magnitude ceiling: exactness is asserted up to it;
the field reader refuses above it (the refusal lives in the reader, not here).
"""
from __future__ import annotations
import numpy as np
import pytest
from algebra.cga import (
EMBED_EXACT_MAX,
cga_inner,
embed_point,
read_scalar_e1,
)
def _e1(v: float, dtype: "np.typing.DTypeLike" = np.float64) -> np.ndarray:
"""Embed a scalar coordinate on the e1 axis."""
return embed_point(np.array([v, 0.0, 0.0]), dtype=dtype)
# --- contract 1: f32 default is byte-unchanged -----------------------------
def test_embed_point_f32_default_unchanged():
"""The default path stays float32 and byte-identical to the prior impl."""
x = np.array([1.0, 2.0, 3.0], dtype=np.float32)
X = embed_point(x)
assert X.dtype == np.float32
# Prior closed-form: result[1:4]=x, e4=0.5(|x|^2-1), e5=0.5(|x|^2+1).
x_sq = float(np.dot(x, x)) # 14.0
assert X[1] == np.float32(1.0)
assert X[2] == np.float32(2.0)
assert X[3] == np.float32(3.0)
assert X[4] == np.float32(0.5 * (x_sq - 1.0))
assert X[5] == np.float32(0.5 * (x_sq + 1.0))
# --- contract 2: f64 exact read-back where f32 fails -----------------------
@pytest.mark.parametrize("v", [0, 1, 7, 42, 103, 300, 9999, 12345, 100000, 1000000])
def test_read_scalar_e1_exact_f64(v):
"""Projective dehomogenization recovers the integer coordinate exactly in f64."""
X = _e1(float(v), dtype=np.float64)
assert read_scalar_e1(X) == float(v)
def test_f32_readback_fails_where_f64_succeeds():
"""The motivating hazard: f32 cannot round-trip a mid-size integer; f64 can.
A meaningfully-failing guard if this passes under f32 the f64 work is moot.
"""
v = 12345.0
f64 = read_scalar_e1(_e1(v, dtype=np.float64))
assert f64 == v
Xf32 = _e1(v, dtype=np.float32)
denom = float(Xf32[5] - Xf32[4]) # n_o weight collapses toward 0 in f32
assert denom != 1.0 # the f32 weight is already wrong at this scale
def test_dilation_weighted_point_readback():
"""Read-back is exact for a *weighted* (dilated) null vector, not just unit weight.
A dilation about the origin by factor k scales the whole null vector; the e1
coordinate must come back as k*v via the e1/n_o-weight ratio, never as a raw
distance-from-origin.
"""
v, k = 2.0, 4.0
X = _e1(v, dtype=np.float64)
Xw = (k * X).astype(np.float64) # uniform conformal weight k
assert read_scalar_e1(Xw) == v # projective: weight divides out
# --- contract 3: the pinned ceiling ----------------------------------------
def test_embed_exact_max_is_pinned_and_generous():
"""The ceiling is a concrete int, comfortably above any GSM8K quantity."""
assert isinstance(EMBED_EXACT_MAX, int)
assert EMBED_EXACT_MAX >= 1_000_000
def test_distance_exact_within_ceiling():
"""The conformal distance metric stays exact for integer pairs up to the ceiling.
cga_inner(embed(a), embed(b)) = -1/2 (a-b)^2, computed in f64.
"""
for a, b in [(0, 1), (3, 7), (100, 103), (0, EMBED_EXACT_MAX)]:
inner = cga_inner(_e1(float(a)), _e1(float(b)))
expected = -0.5 * (a - b) ** 2
assert inner == expected, f"a={a} b={b}: {inner} != {expected}"
def test_embedded_f64_point_is_null():
"""f64 embedding still lands on the null cone."""
X = _e1(123.0, dtype=np.float64)
assert abs(cga_inner(X, X)) < 1e-6

View file

@ -12,6 +12,7 @@ from core.reasoning import (
COMMITMENT_DISAGREEMENT,
DUPLICATE_STRUCTURAL_SIGNATURE,
MISSING_COMMITMENT,
SAME_READER_LINEAGE,
TIER2_VERIFIED,
EvidenceBundle,
OperatorEvidence,
@ -24,6 +25,7 @@ def _ev(
signature: str = "shape-a",
commitment: str = "answer:42",
outcome: str = "verified",
lineage: str = "reader-a",
) -> OperatorEvidence:
return OperatorEvidence(
domain="mathematics_logic",
@ -34,6 +36,7 @@ def _ev(
check_keys=("check-a",),
commitment_key=commitment,
structural_signature=signature,
reader_lineage=lineage,
payload={"nested": {"values": [1, 2, "x"]}},
)
@ -64,19 +67,35 @@ def test_evidence_bundle_hash_is_ordered_and_stable() -> None:
def test_tier2_verifies_two_distinct_structures_same_commitment() -> None:
verdict = verify_tier2_agreement((
_ev(signature="shape-a"),
_ev(signature="shape-b"),
_ev(signature="shape-a", lineage="reader-a"),
_ev(signature="shape-b", lineage="reader-b"),
))
assert verdict.verified is True
assert verdict.reason == TIER2_VERIFIED
assert verdict.commitment_key == "answer:42"
assert verdict.structural_signatures == ("shape-a", "shape-b")
assert verdict.reader_lineages == ("reader-a", "reader-b")
def test_tier2_refuses_same_reader_lineage() -> None:
"""The decoration firewall: one reader cannot self-verify by relabeling its
structural signature. Two evidences from the SAME decoding pathway are refused
even when their structural_signatures differ and the commitment agrees.
Meaningfully-failing guard: under the old label-only check this admitted.
"""
verdict = verify_tier2_agreement((
_ev(signature="shape-a", lineage="one-reader"),
_ev(signature="shape-b", lineage="one-reader"),
))
assert verdict.verified is False
assert verdict.reason == SAME_READER_LINEAGE
def test_tier2_refuses_duplicate_signature() -> None:
verdict = verify_tier2_agreement((
_ev(signature="same"),
_ev(signature="same"),
_ev(signature="same", lineage="reader-a"),
_ev(signature="same", lineage="reader-b"),
))
assert verdict.verified is False
assert verdict.reason == DUPLICATE_STRUCTURAL_SIGNATURE
@ -84,8 +103,8 @@ def test_tier2_refuses_duplicate_signature() -> None:
def test_tier2_refuses_disagreeing_commitments() -> None:
verdict = verify_tier2_agreement((
_ev(signature="shape-a", commitment="answer:42"),
_ev(signature="shape-b", commitment="answer:41"),
_ev(signature="shape-a", commitment="answer:42", lineage="reader-a"),
_ev(signature="shape-b", commitment="answer:41", lineage="reader-b"),
))
assert verdict.verified is False
assert verdict.reason == COMMITMENT_DISAGREEMENT

View file

@ -0,0 +1,52 @@
"""The ablation verdict — pinned as the honest experimental result.
The field-reasoner wedge's measurement #2/#3 on the simple additive/part-whole
micro-domain: the geometric field reader is **decoration** it catches no
comprehension error the symbolic reader makes, and the only place it changes the
admitted set is by *losing* coverage (refusing a correct answer at the precision
ceiling). This test pins that finding; if a future change makes the field genuinely
catch a symbolic error, ``field_caught_symbolic_errors`` becomes non-empty and the
PASS assertion here must be revisited (a deliberately meaningful pin).
The one INVARIANT that must always hold regardless of the verdict: the field never
commits a wrong answer (wrong=0 is structural).
"""
from __future__ import annotations
from evals.relational_metric.ablation import run
def test_field_never_commits_wrong():
"""wrong=0 — the non-negotiable. The field refuses rather than commit a bad int."""
report = run()
assert report["field_wrong_commits"] == [], report["field_wrong_commits"]
def test_verdict_is_honest_c3_on_this_domain():
"""On forward-substitutable additive relations the field adds no independent signal:
geometric translation == arithmetic addition, so it catches zero symbolic errors."""
report = run()
assert report["field_caught_symbolic_errors"] == []
assert report["verdict"].startswith("C3")
def test_field_only_changes_admitted_set_by_losing_coverage():
"""The admitted set differs from symbolic-alone only because the field REFUSES a
correct answer (the precision ceiling) a liability, not error-catching signal."""
report = run()
assert report["admitted_set_changed"] is True
assert report["gate_admitted_count"] < report["symbolic_alone_admitted_count"]
assert report["field_lost_coverage"] # non-empty: the ceiling case
def test_readers_agree_and_are_both_correct_on_committable_classes():
"""Per-class diversity is ZERO on every committable class — the readers are
redundant here (the dossier's predicted common-mode on a metric-trivial domain)."""
report = run()
for cls, stats in report["per_class"].items():
if cls.startswith("coverage_"):
continue
assert stats["disagree"] == 0
assert stats["double_fault"] == 0
assert stats["both_correct"] == stats["n"]

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"])

View file

@ -0,0 +1,59 @@
"""The symbolic reader — the ablation control arm and the C3 capability path.
It must be a *competent* reader (correct on the grammar, fences the same out-of-domain
cases, detects over-determination), so the ablation is a fair test of whether the field
adds anything over it. Unlike the field it has no precision limit (pure int).
"""
from __future__ import annotations
from generate.relational_symbolic_reader import READER_LINEAGE, read_relational
def test_commits_additive_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 and r.answer == 8
assert r.reader_lineage == READER_LINEAGE
def test_commits_part_whole_sum():
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 and r.answer == 11
def test_commits_beyond_field_ceiling():
"""The symbolic reader has no f64 precision limit — it commits where the field
refuses (over_ceiling). This is the field's coverage liability, not a symbolic bug."""
r = read_relational("Gus has 9000000 apples. How many apples does Gus have?")
assert not r.refused and r.answer == 9000000
def test_fences_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 and r.refusal_reason == "fenced_multiplicative"
def test_detects_over_determination():
"""A competent reader refuses a conflicting re-statement (so the field's geometric
coherence check is not a unique advantage)."""
r = read_relational(
"Tom has 3 marbles. Tom has 5 marbles. How many marbles does Tom have?"
)
assert r.refused and r.refusal_reason == "over_determined_conflict"
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 and r.refusal_reason == "non_forward_substitutable"