fix(cognition): expose hash_surface; point the register-matrix pin at it

*** VERIFICATION PENDING — see the bottom of this message. ***

99 of the 107 full-suite failures are one test, red on clean main since
2026-07-23, and it is guarding the wrong field.

WHAT IS NOT WRONG. test_register_matrix_trace_hash_invariant — the load-bearing
ADR-0072 truth-path-isolation claim — passes on all 99 registers on pristine
main. trace_hash does not vary across the register axis. The invariant holds;
the seals and wrong=0 are untouched.

WHAT IS. test_register_matrix_canonical_surface_byte_identical asserts on
CognitiveTurnResult.surface, on its docstring's claim that this is "the
truth-path field compute_trace_hash consumes". That stopped being true when
Phase 0 flipped resolve_surface to response-first precedence and introduced
SurfaceResolution.hash_surface as the register-invariant capture. Since then
pipeline.py folds hash_surface into compute_trace_hash, while result.py
documents surface as "final voiced surface (what the user sees)".

So the test demanded that registers NOT differ on the served surface — the
opposite of what the register axis exists for, and a direct contradiction of
test_register_substantive_consumption.py, which pins that they DO differ and is
green in smoke. Two tests on main asserting opposite things about the same
bytes; the red one is in neither gate, so it stayed red.

ROOT CAUSE, and why this is a code fix rather than a test edit: hash_surface was
pipeline-local. The invariant was unobservable from a turn result, which is why
the test reached for the nearest visible field and drifted. Exposing it makes
the contract testable at the seam that owns it:

- CognitiveTurnResult.hash_surface — new field, populated from the value the
  pipeline already computed
- evals/cognition CaseResult carries it through
- the test asserts byte-identity on hash_surface, with the history recorded so
  it cannot drift back

No stored baseline to regenerate — the fixture computes it live.

[Verification]: NOT YET RUN. The register matrix is ~16 min and was still
executing when the working tree had to be committed; its output buffers through
tail, so no partial signal was available. Committed to preserve the work with an
honest label rather than to claim it passes. DO NOT MERGE until
`pytest tests/test_cognition_eval_register_matrix.py` is green — expected 0
failed where main has 99, with test_register_matrix_trace_hash_invariant still
passing. Result to follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FduW6Krm3PPQv3P5iwBYtx
This commit is contained in:
Claude 2026-07-25 14:06:36 +00:00
parent 3b2c7608a9
commit bf17f42834
No known key found for this signature in database
4 changed files with 49 additions and 12 deletions

View file

@ -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,

View file

@ -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

View file

@ -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,
)

View file

@ -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)
)