diff --git a/core/cognition/pipeline.py b/core/cognition/pipeline.py index 6e3a3bc9..f530510d 100644 --- a/core/cognition/pipeline.py +++ b/core/cognition/pipeline.py @@ -863,6 +863,7 @@ class CognitiveTurnPipeline: proposition=response.proposition, articulation=response.articulation, surface=surface, + hash_surface=hash_surface, walk_surface=response.walk_surface, articulation_surface=articulation_surface, dialogue_role=response.dialogue_role, diff --git a/core/cognition/result.py b/core/cognition/result.py index 4b2d511f..1eb137ae 100644 --- a/core/cognition/result.py +++ b/core/cognition/result.py @@ -70,6 +70,19 @@ class CognitiveTurnResult: vault_hits: int recall_energy_class: str | None = None + #: The register-INVARIANT truth-path bytes — what ``compute_trace_hash`` + #: actually folds (ADR-0069 inv C, ADR-0077 R6). Distinct from ``surface``, + #: which is the served, register-decorated string the user reads. + #: + #: Exposed 2026-07-25. It was pipeline-local before that, which made the + #: truth-path-isolation invariant unobservable from a turn result — so + #: ``test_register_matrix_canonical_surface_byte_identical`` asserted it on + #: ``surface`` instead, and went red across all 99 registers the moment + #: Phase 0 flipped ``resolve_surface`` to response-first precedence and + #: moved the invariant here. The contract was never broken; it just could + #: not be seen. Empty string ⇒ identical to ``surface`` (no divergence). + hash_surface: str = "" + # --- intent / graph telemetry --- intent: DialogueIntent | None = None proposition_graph: PropositionGraph | None = None diff --git a/evals/cognition/runner.py b/evals/cognition/runner.py index 1b1ffa7f..055434a2 100644 --- a/evals/cognition/runner.py +++ b/evals/cognition/runner.py @@ -30,6 +30,10 @@ class CaseResult: versor_condition: float trace_hash: str surface: str + #: The register-invariant truth-path bytes (what compute_trace_hash folds). + #: ``surface`` is the served, register-decorated string; these two are + #: different fields with different contracts — see CognitiveTurnResult. + hash_surface: str = "" @dataclass(slots=True) @@ -71,6 +75,7 @@ def _run_case(case: dict[str, Any], pipeline: CognitiveTurnPipeline) -> CaseResu versor_condition=result.versor_condition, trace_hash=result.trace_hash, surface=result.surface, + hash_surface=result.hash_surface or result.surface, ) diff --git a/tests/test_cognition_eval_register_matrix.py b/tests/test_cognition_eval_register_matrix.py index 51696411..62ab3cf5 100644 --- a/tests/test_cognition_eval_register_matrix.py +++ b/tests/test_cognition_eval_register_matrix.py @@ -302,24 +302,42 @@ def test_register_matrix_versor_condition_byte_identical( def test_register_matrix_canonical_surface_byte_identical( register_report, baseline_by_id, ): - """``CognitiveTurnResult.surface`` is the pre-decoration / - canonical composer output (the truth-path field - ``compute_trace_hash`` consumes). Substantive register transforms - apply downstream on ``turn_log[-1].surface``; the canonical - must remain byte-identical across the register axis. + """``hash_surface`` — the truth-path bytes ``compute_trace_hash`` folds — + must be byte-identical across the register axis. - If this test ever fails for a non-null register, the substantive - transform has leaked into the canonical surface and the - truth-path-isolation contract is broken. + If this fails for a non-null register, a substantive transform has leaked + into the truth path and the isolation contract is broken. + + ASSERTED ON ``hash_surface``, NOT ``surface`` (corrected 2026-07-25). + This test used to read ``surface``, on the docstring's claim that it was + "the pre-decoration / canonical composer output". That stopped being true + on 2026-07-23: Phase 0 flipped ``resolve_surface`` to response-first + precedence and introduced ``SurfaceResolution.hash_surface`` as the + register-invariant capture, so ``pipeline.py`` now folds *that* into + ``compute_trace_hash`` while ``surface`` carries the served, decorated + bytes (``CognitiveTurnResult.surface``: "final voiced surface (what the + user sees)"). + + Asserting byte-identity on ``surface`` therefore demanded that registers + NOT differ — the exact opposite of what the register axis is for, and in + direct contradiction with ``test_register_substantive_consumption.py``, + which pins that they DO differ and is green in smoke. This file is in + neither `smoke` nor `deductive`, so it went red on main across all 99 + registers and stayed there. + + The invariant itself was never broken: ``test_register_matrix_trace_hash_invariant`` + passed throughout, which is the load-bearing proof, since a leak into + ``hash_surface`` would move ``trace_hash`` immediately. What was missing was + the ability to *observe* the field — now exposed on the turn result. """ register_id, report = register_report rows = _diff_rows( - register_id, baseline_by_id, _by_id(report), "surface", + register_id, baseline_by_id, _by_id(report), "hash_surface", ) assert not rows, ( - f"CANONICAL SURFACE LEAK under register {register_id!r} — " - "substantive transforms have escaped into the pre-decoration " - "surface that feeds compute_trace_hash.\n" + "\n".join(rows) + f"TRUTH-PATH SURFACE LEAK under register {register_id!r} — " + "substantive transforms have escaped into hash_surface, the bytes " + "compute_trace_hash folds.\n" + "\n".join(rows) )