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:
commit
5c2f005e96
21 changed files with 1867 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)) = −½‖a−b‖²` | **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 Knight–Leveson, 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
|
||||
Knight–Leveson'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 (Knight–Leveson). 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.**
|
||||
83
docs/analysis/field-wedge-ablation-result-2026-06-04.md
Normal file
83
docs/analysis/field-wedge-ablation-result-2026-06-04.md
Normal 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 Knight–Leveson), 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.
|
||||
10
evals/relational_metric/__init__.py
Normal file
10
evals/relational_metric/__init__.py
Normal 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).
|
||||
"""
|
||||
162
evals/relational_metric/ablation.py
Normal file
162
evals/relational_metric/ablation.py
Normal 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())
|
||||
78
evals/relational_metric/oracle.py
Normal file
78
evals/relational_metric/oracle.py
Normal 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]
|
||||
98
evals/relational_metric/runner.py
Normal file
98
evals/relational_metric/runner.py
Normal 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())
|
||||
15
evals/relational_metric/v1/cases.jsonl
Normal file
15
evals/relational_metric/v1/cases.jsonl
Normal 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"}
|
||||
211
generate/relational_field_reader.py
Normal file
211
generate/relational_field_reader.py
Normal 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 ``δ·x²`` 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)
|
||||
127
generate/relational_symbolic_reader.py
Normal file
127
generate/relational_symbolic_reader.py
Normal 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)
|
||||
|
|
@ -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."
|
||||
)
|
||||
|
|
|
|||
112
tests/test_cga_f64_exactness.py
Normal file
112
tests/test_cga_f64_exactness.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
52
tests/test_relational_ablation.py
Normal file
52
tests/test_relational_ablation.py
Normal 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"]
|
||||
133
tests/test_relational_field_reader.py
Normal file
133
tests/test_relational_field_reader.py
Normal 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"
|
||||
35
tests/test_relational_metric_lane.py
Normal file
35
tests/test_relational_metric_lane.py
Normal 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"])
|
||||
59
tests/test_relational_symbolic_reader.py
Normal file
59
tests/test_relational_symbolic_reader.py
Normal 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"
|
||||
Loading…
Reference in a new issue