core/evals/gsm8k_math/equivalence/trace.py
Shay ad75a2c79a test(derivation): add ADR-0184 semantic replay equivalence harness
ADR-0184 S4b — the replay/provenance equivalence boundary, the
precondition for any semantic-primary path. After #685 the in-repo
legacy path delegates to the semantic boundary, so old-vs-new is
vacuous; the durable reference is a pinned canonical trace artifact
(evals/gsm8k_math/equivalence/v1/, 937 problems) whose authority is
the #684/#685 cross-tree differentials (documented provenance chain).

- state/provenance.py: replay_is_faithful — the replay bridge's law as
  a checkable structural property (no verify/pool import; covered by
  the #685 authority scan).
- equivalence/trace.py: canonical traces (worlds, candidates, order,
  multiplicity, classifications, resolutions) + authority_violations
  re-deriving the pool commit law from trace content.
- scripts/verify_semantic_equivalence.py: check / --update gate
  mirroring verify_lane_shas.py.
- tests/test_adr_0184_s4b_replay_equivalence.py: full-corpus live-vs-
  pinned equality, corpus-wide authority + faithfulness, and 17
  single-mutation non-vacuity proofs.

Pure additions; verify.py/pool.py untouched; no serving, CLAIMS,
metrics, or lane pins moved.
2026-06-10 14:35:22 -07:00

263 lines
11 KiB
Python

"""ADR-0184 S4b — deterministic candidate-trace canonicalization and checks.
One canonical trace per corpus problem captures everything the equivalence proof
needs:
* the semantic worlds the boundary enumerates (provenance);
* the candidates it emits — values, order, AND duplicate multiplicity;
* the pooled candidate set and each candidate's commit-eligibility class
(``complete`` / ``exempt`` / ``None``);
* the resolutions (``compose_accumulation``, ``resolve_pooled``), including
refusals (``None`` — fail-closed is part of the contract being pinned).
Traces are pure data (sorted keys, fixed separators), so the corpus-level SHA-256
is a deterministic fingerprint of derivation-lane behavior. The committed
artifact under ``v1/`` is the stable reference: it pins the behavior proven
byte-equal to the pre-ledger legacy path by the #684/#685 cross-tree
differentials. Comparing live traces against it is therefore NOT a
self-comparison — the reference is frozen evidence, the live run is the code
under test.
This module also re-derives the pool's commit law from trace content
(:func:`authority_violations`) so a bypass of verifier/pool authority is
*detectable from the trace itself*, and ties every emitted candidate back to its
world via the S4b faithfulness checker
(:func:`generate.derivation.state.provenance.faithfulness_violations`).
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from typing import Any
from generate.derivation.accumulate import accumulation_candidates, compose_accumulation
from generate.derivation.model import GroundedDerivation
from generate.derivation.pool import pooled_candidates, resolve_pooled
from generate.derivation.state.model import SemanticLedger
from generate.derivation.state.provenance import faithfulness_violations
from generate.derivation.state.replay import replay_accumulation_ledger
from generate.derivation.state.source import (
accumulation_ledger_worlds,
semantic_state_candidates,
)
from generate.derivation.verify import Resolution, classify_derivation
_EQUIVALENCE_DIR = Path(__file__).resolve().parent
REPO_ROOT = _EQUIVALENCE_DIR.parent.parent.parent
EXPECTED_TRACES_PATH = _EQUIVALENCE_DIR / "v1" / "expected_traces.jsonl"
MANIFEST_PATH = _EQUIVALENCE_DIR / "v1" / "manifest.json"
# The corpus is every problem under evals/gsm8k_math/**/cases.jsonl, de-duplicated
# by exact text — the same definition the #684/#685 differentials used.
_CASES_GLOB = "evals/gsm8k_math/**/cases.jsonl"
def corpus_problems() -> tuple[str, ...]:
"""Every unique problem text in the corpus, sorted (deterministic order)."""
problems: set[str] = set()
for path in sorted(REPO_ROOT.glob(_CASES_GLOB)):
with path.open() as handle:
for line in handle:
line = line.strip()
if not line:
continue
case = json.loads(line)
text = case.get("problem") or case.get("question") or case.get("text")
if text:
problems.add(text)
return tuple(sorted(problems))
def canonical_derivation(derivation: GroundedDerivation) -> dict[str, Any]:
return {
"start": [derivation.start.value, derivation.start.unit, derivation.start.source_token],
"steps": [
[
step.op,
step.operand.value,
step.operand.unit,
step.operand.source_token,
step.cue,
step.comparative,
]
for step in derivation.steps
],
"answer": derivation.answer,
"answer_unit": derivation.answer_unit,
}
def canonical_resolution(resolution: Resolution | None) -> dict[str, Any] | None:
if resolution is None:
return None
return {
"answer": resolution.answer,
"answer_unit": resolution.answer_unit,
"derivation": canonical_derivation(resolution.derivation),
}
def canonical_world(world: SemanticLedger) -> list[list[Any]]:
return [
[
transition.op,
transition.key.entity,
transition.key.unit,
transition.quantity.value,
transition.quantity.unit,
transition.quantity.source_token,
transition.cue,
transition.clause_index,
]
for transition in world.transitions
]
def problem_sha(problem_text: str) -> str:
return hashlib.sha256(problem_text.encode("utf-8")).hexdigest()
def problem_trace(problem_text: str) -> dict[str, Any]:
"""The canonical behavior trace for one problem (pure data, sorted keys)."""
worlds = accumulation_ledger_worlds(problem_text)
semantic = semantic_state_candidates(problem_text)
pooled = pooled_candidates(problem_text)
return {
"problem_sha": problem_sha(problem_text),
"preview": problem_text[:64],
"worlds": [canonical_world(world) for world in worlds],
"semantic": [canonical_derivation(d) for d in semantic],
"wrapper_equal": accumulation_candidates(problem_text) == semantic,
"compose": canonical_resolution(compose_accumulation(problem_text)),
"pooled": [canonical_derivation(d) for d in pooled],
"classifications": [classify_derivation(d, problem_text) for d in pooled],
"resolution": canonical_resolution(resolve_pooled(problem_text)),
}
def corpus_traces() -> list[dict[str, Any]]:
"""Canonical traces for the whole corpus, sorted by problem SHA."""
traces = [problem_trace(text) for text in corpus_problems()]
traces.sort(key=lambda trace: str(trace["problem_sha"]))
return traces
def trace_line(trace: dict[str, Any]) -> str:
"""The canonical single-line JSON encoding (artifact + hashing format)."""
return json.dumps(trace, sort_keys=True, separators=(",", ":"))
def traces_sha(traces: list[dict[str, Any]]) -> str:
digest = hashlib.sha256()
for trace in traces:
digest.update(trace_line(trace).encode("utf-8"))
digest.update(b"\n")
return digest.hexdigest()
def load_expected_traces() -> list[dict[str, Any]]:
with EXPECTED_TRACES_PATH.open() as handle:
return [json.loads(line) for line in handle if line.strip()]
def load_manifest() -> dict[str, Any]:
return json.loads(MANIFEST_PATH.read_text())
def compare_traces(
expected: list[dict[str, Any]], live: list[dict[str, Any]]
) -> tuple[str, ...]:
"""Every per-problem, per-dimension difference between two trace lists.
Empty iff equivalent. Dimension-labelled so a failure names what drifted
(candidate values, order/multiplicity, classifications, refusals, ...).
"""
differences: list[str] = []
expected_by_sha = {str(t["problem_sha"]): t for t in expected}
live_by_sha = {str(t["problem_sha"]): t for t in live}
for sha in sorted(set(expected_by_sha) - set(live_by_sha)):
differences.append(f"{sha[:16]}: problem missing from live corpus")
for sha in sorted(set(live_by_sha) - set(expected_by_sha)):
differences.append(f"{sha[:16]}: problem missing from expected artifact")
for sha in sorted(set(expected_by_sha) & set(live_by_sha)):
exp, liv = expected_by_sha[sha], live_by_sha[sha]
for dimension in sorted(set(exp) | set(liv)):
if exp.get(dimension) != liv.get(dimension):
differences.append(
f"{sha[:16]} ({str(exp.get('preview', ''))[:40]!r}): "
f"{dimension} differs"
)
return tuple(differences)
def authority_violations(trace: dict[str, Any]) -> tuple[str, ...]:
"""Ways ``trace`` violates the verifier/pool commit law — empty iff lawful.
Re-derives the *commit-licensing* direction of ``resolve_pooled`` from trace
content alone, so a bypassed authority is detectable from the trace:
* a resolution requires at least one ``complete``-classified pooled candidate
(an exempt-only or unclassified pool must refuse);
* a resolution requires agreement — every classified candidate's answer must
match the committed answer (disagreement must refuse);
* an empty pool must refuse (fail-closed).
These are necessary conditions for any commit, not a re-implementation of the
refusal side: a refusal (``resolution: null``) is always lawful here. That
asymmetry is deliberate — wrong=0 hazards live only on the commit side.
"""
violations: list[str] = []
resolution = trace.get("resolution")
pooled = trace.get("pooled") or []
classifications = trace.get("classifications") or []
if resolution is None:
return ()
if not isinstance(resolution, dict):
return (f"resolution has non-canonical type {type(resolution).__name__}",)
if not pooled:
violations.append("resolution committed from an empty pool (fail-closed broken)")
if "complete" not in classifications:
violations.append(
"resolution committed without any 'complete'-classified candidate "
"(exempt-only/unclassified commit — verifier/pool authority bypassed)"
)
committed_answer = resolution.get("answer")
for candidate, kind in zip(pooled, classifications):
if kind is None or not isinstance(candidate, dict):
continue
answer = candidate.get("answer")
if (
isinstance(answer, (int, float))
and isinstance(committed_answer, (int, float))
and round(float(answer), 9) != round(float(committed_answer), 9)
):
violations.append(
"resolution committed despite a disagreeing classified candidate "
f"({answer!r} != {committed_answer!r})"
)
break
return tuple(violations)
def replay_faithfulness_report(problem_text: str) -> tuple[str, ...]:
"""Faithfulness violations for every (world, replayed candidate) pair of a
problem, plus structural consistency of the boundary enumeration itself."""
violations: list[str] = []
replayed: list[GroundedDerivation] = []
for index, world in enumerate(accumulation_ledger_worlds(problem_text)):
derivation = replay_accumulation_ledger(world)
if derivation is None:
# builder-produced worlds always replay today; a refusal here is a
# structural change worth flagging loudly, not skipping silently.
violations.append(f"world {index} refused replay")
continue
replayed.append(derivation)
violations.extend(
f"world {index}: {violation}"
for violation in faithfulness_violations(world, derivation)
)
if tuple(replayed) != semantic_state_candidates(problem_text):
violations.append("boundary candidates are not the in-order replay of the worlds")
return tuple(violations)