Skip to content

Commit 0df77f7

Browse files
committed
refactor: make ADR32 M2 evidence/receipt parity opt-in
The M2 fold commit (2763a1c) ran the legacy-vs-typed parity comparison on the default production path: build_run_evidence_bundle_from_events assembled both the folded and legacy bundles plus two to_dict() serializations every call, and build_run_receipt rendered the receipt twice for a shadow gate. That doubled evidence work on the hot path and let a mismatch silently revert the M6 typed source-of-truth cutover. Make parity diagnostics opt-in without weakening the guarantee: - build_run_evidence_bundle_from_events(..., verify_parity=False) folds once by default; parity/gap comparison only when verify_parity=True; - build_run_receipt(..., shadow_parity=False) renders once by default; the receipt_event_derived_mismatch shadow warning only when shadow_parity=True; - ADR32-M2 parity tests now pass the flags explicitly so the guarantee still holds where it belongs (tests, not runtime); - add a regression guard asserting the default fold assembles once and verify_parity adds exactly one more assembly. Action: G-P2-8 Roadmap-Status: unchanged Constraint: behavior-preserving refactor of ADR32-M2 fold; default evidence/receipt output unchanged; parity still proven in tests; no fixture retirement; no roadmap holds touched Tested: ruff format/check; mypy teaagent/run_evidence.py teaagent/run_receipt.py teaagent/runner/ --explicit-package-bases; pytest across evidence/receipt/model-route/goal-record/skill-activation/event-stream/conversation-ux/five-minute-proof/adversarial/evidence-completeness (150 passed); scripts/validate_wiring.py; scripts/validate_docs_consistency.py (29/29 risk, 21/21 ticket); docs-inventory --check Not-tested: full acceptance/nightly suite, live/paid providers Confidence: high
1 parent 2763a1c commit 0df77f7

5 files changed

Lines changed: 87 additions & 12 deletions

File tree

.omo/plans/adr-0032-m2-t003-work-order.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ Switch `build_run_receipt()` to consume event-derived evidence (via `build_run_e
1111
Synthetic fixtures that construct receipt test data from hardcoded audit event dicts mask real-path gaps. Once the event stream can produce byte-equivalent evidence, those fixtures become liabilities — they test a path that no longer matches production. M2-T003 eliminates this gap.
1212

1313
**2026-07-22 execution note:** the safe local implementation added
14-
`build_run_receipt(..., use_event_stream=True|False)`, receipt parity coverage,
15-
and a best-effort `receipt_event_derived_mismatch` audit warning when the
16-
event-derived receipt diverges from the legacy receipt. The synthetic-fixture
17-
retirement remains Human Review work; no fixture was deleted by the agent.
14+
`build_run_receipt(..., use_event_stream=True|False, shadow_parity=False)` and
15+
receipt parity coverage. The `receipt_event_derived_mismatch` audit warning and
16+
the legacy shadow rendering are gated behind the opt-in `shadow_parity` flag so
17+
the default receipt path renders once (FR-02 called the shallow gate temporary;
18+
it is off by default rather than always-on). The synthetic-fixture retirement
19+
remains Human Review work; no fixture was deleted by the agent.
1820

1921
## 3. Scope
2022

teaagent/run_evidence.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -946,16 +946,22 @@ def build_run_evidence_bundle_from_events(
946946
raw_audit_events: list[JsonMapping],
947947
*,
948948
goal_id: str = '',
949+
verify_parity: bool = False,
949950
) -> EventDerivedEvidenceResult:
950-
"""Build evidence from typed RunEvents, preserving raw-audit parity.
951-
952-
If typed folding ever diverges from the legacy raw-audit assembly, return
953-
the legacy bundle and record the missing/raw categories. This preserves
954-
receipt correctness while making the gap explicit for ADR32-M2 review.
951+
"""Build evidence from typed RunEvents.
952+
953+
By default this folds the typed stream once (the production path). When
954+
``verify_parity`` is True it additionally assembles the legacy raw-audit
955+
bundle and, if the two diverge, returns the legacy bundle plus the gap
956+
categories. The parity path is opt-in because the default runtime must not
957+
pay for a second full assembly and two ``to_dict`` serializations on every
958+
call; parity is asserted by the ADR32-M2 tests, not the hot path.
955959
"""
956960
folded = build_evidence_from_events(
957961
events, root=root, run_id=run_id, goal_id=goal_id
958962
)
963+
if not verify_parity:
964+
return EventDerivedEvidenceResult(bundle=folded)
959965
legacy = _assemble_evidence_bundle(
960966
raw_audit_events, root=root, run_id=run_id, goal_id=goal_id
961967
)

teaagent/run_receipt.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,15 @@ def build_run_receipt(
374374
*,
375375
budget_cap_cents: int | None = None,
376376
use_event_stream: bool = True,
377+
shadow_parity: bool = False,
377378
) -> str:
378-
"""Build a formatted run receipt for *run_id*."""
379+
"""Build a formatted run receipt for *run_id*.
380+
381+
``shadow_parity`` is an opt-in ADR32-M2 diagnostic: when enabled alongside
382+
``use_event_stream`` it also renders the legacy receipt and records a
383+
``receipt_event_derived_mismatch`` audit warning on divergence. It is off by
384+
default so the production receipt path renders once instead of twice.
385+
"""
379386
try:
380387
events = store.show_run(run_id)
381388
except FileNotFoundError:
@@ -396,7 +403,7 @@ def build_run_receipt(
396403
raw_audit_events=events,
397404
)
398405
receipt = format_run_receipt(summary, context, bundle=bundle, events=events)
399-
if use_event_stream:
406+
if use_event_stream and shadow_parity:
400407
legacy_bundle = build_run_evidence_bundle(
401408
root,
402409
run_id,

tests/test_run_evidence.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,70 @@ def _assert_fold_matches_legacy(events: list[dict], run_id: str) -> RunEvidenceB
343343
run_id,
344344
typed,
345345
events,
346+
verify_parity=True,
346347
)
347348
assert result.gap_categories == []
348349
assert result.bundle.to_dict() == legacy.to_dict()
349350
return legacy
350351

351352

353+
def test_from_events_parity_adds_exactly_one_assembly() -> None:
354+
"""The default fold does one assembly; verify_parity adds exactly one more.
355+
356+
Regression guard for the ADR32-M2 receipt/evidence fold. The typed fold
357+
(``build_evidence_from_events``) legitimately calls ``_assemble_evidence_bundle``
358+
once as its shared projection step. The parity comparison must be opt-in: the
359+
default hot path must NOT assemble the legacy bundle a second time (the earlier
360+
implementation did, doubling evidence work on every production call).
361+
"""
362+
import teaagent.run_evidence as run_evidence
363+
364+
events = [
365+
{
366+
'event_type': 'run_started',
367+
'run_id': 'r-once',
368+
'payload': {},
369+
'created_at': '2026-06-13T10:00:00+00:00',
370+
},
371+
{
372+
'event_type': 'run_completed',
373+
'run_id': 'r-once',
374+
'payload': {'status': 'completed'},
375+
'created_at': '2026-06-13T10:00:01+00:00',
376+
},
377+
]
378+
from teaagent.runner._events import read_run_events_from_audit
379+
380+
typed = read_run_events_from_audit(events)
381+
calls = {'legacy': 0}
382+
original = run_evidence._assemble_evidence_bundle
383+
384+
def _counting_assemble(*args: object, **kwargs: object):
385+
calls['legacy'] += 1
386+
return original(*args, **kwargs)
387+
388+
run_evidence._assemble_evidence_bundle = _counting_assemble
389+
try:
390+
calls['legacy'] = 0
391+
default_result = build_run_evidence_bundle_from_events(
392+
'.', 'r-once', typed, events
393+
)
394+
default_calls = calls['legacy']
395+
396+
calls['legacy'] = 0
397+
parity_result = build_run_evidence_bundle_from_events(
398+
'.', 'r-once', typed, events, verify_parity=True
399+
)
400+
parity_calls = calls['legacy']
401+
finally:
402+
run_evidence._assemble_evidence_bundle = original
403+
404+
assert default_calls == 1, 'default fold should assemble exactly once'
405+
assert parity_calls == default_calls + 1, 'parity must add exactly one assembly'
406+
assert default_result.gap_categories == []
407+
assert default_result.bundle.to_dict() == parity_result.bundle.to_dict()
408+
409+
352410
def test_m6_fold_equals_legacy_on_success_run():
353411
"""Success run with commands, tests, and an approval folds losslessly."""
354412
events = [

tests/test_run_receipt.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,9 @@ def test_build_run_receipt_event_stream_matches_legacy_path() -> None:
189189
run_id = 'receipt-parity'
190190
_write_run(tmpdir, run_id, events)
191191
store = RunStore(tmpdir)
192-
event_receipt = build_run_receipt(store, run_id, tmpdir, use_event_stream=True)
192+
event_receipt = build_run_receipt(
193+
store, run_id, tmpdir, use_event_stream=True, shadow_parity=True
194+
)
193195
legacy_receipt = build_run_receipt(
194196
store, run_id, tmpdir, use_event_stream=False
195197
)

0 commit comments

Comments
 (0)