diff --git a/algebra/__init__.py b/algebra/__init__.py index cf1aa1f0..6d6c3abf 100644 --- a/algebra/__init__.py +++ b/algebra/__init__.py @@ -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 diff --git a/algebra/cga.py b/algebra/cga.py index 8eae034f..42ff6b23 100644 --- a/algebra/cga.py +++ b/algebra/cga.py @@ -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 diff --git a/core/reasoning/__init__.py b/core/reasoning/__init__.py index ccba2161..b363dd4b 100644 --- a/core/reasoning/__init__.py +++ b/core/reasoning/__init__.py @@ -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", diff --git a/core/reasoning/adapters.py b/core/reasoning/adapters.py index 2b9cd1ff..df5f01bb 100644 --- a/core/reasoning/adapters.py +++ b/core/reasoning/adapters.py @@ -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, diff --git a/core/reasoning/evidence.py b/core/reasoning/evidence.py index c0ea18bd..00d1e911 100644 --- a/core/reasoning/evidence.py +++ b/core/reasoning/evidence.py @@ -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))), ) diff --git a/tests/test_architectural_invariants.py b/tests/test_architectural_invariants.py index 26cd7bdc..ddc78d9e 100644 --- a/tests/test_architectural_invariants.py +++ b/tests/test_architectural_invariants.py @@ -1392,3 +1392,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." + ) diff --git a/tests/test_cga_f64_exactness.py b/tests/test_cga_f64_exactness.py new file mode 100644 index 00000000..2fb971b9 --- /dev/null +++ b/tests/test_cga_f64_exactness.py @@ -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 diff --git a/tests/test_reasoning_evidence.py b/tests/test_reasoning_evidence.py index f23277aa..ab6eeb2b 100644 --- a/tests/test_reasoning_evidence.py +++ b/tests/test_reasoning_evidence.py @@ -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