Repository navigation
P-022 diagnostics: normalized AnalysisTrace and first-divergence minimizer for Python↔Rust shadow mode #269
Description
Classification
- Change type: enhancement / migration-debugging infrastructure
- Priority: P1
- Expected value: very high during
own-bridgeand dual-engine cutover work - Estimated effort: medium-high
- Implementation risk: medium — trace normalization must not become a second semantics
- Recommendation: YES, strongly recommended before P-022 step 6b: implement Rust own-bridge with layered OwnIR parity #259/P-022 step 7a: add dual-engine shadow mode and zero-diff reproduction artifacts #260 become the daily debugging path
- Extends, does not replace: P-022 step 7a: add dual-engine shadow mode and zero-diff reproduction artifacts #260
- Informed by: P-022 step 6a: formalize OwnIR bridge semantics before the Rust port #258 and P-022 step 6b: implement Rust own-bridge with layered OwnIR parity #259
Problem
The P-022 roadmap correctly requires Python↔Rust behavioral parity and #260 requires dual-engine shadow mode plus standalone reproduction artifacts.
A final diagnostic diff answers:
What disagreed?
It does not answer:
Where did the two implementations first diverge?
Was the first mismatch in lowering, state transfer, merge, fixpoint convergence,
MOS inference, evidence construction, or final rendering?
Without a normalized internal trace, every mismatch risks becoming a manual binary search through two implementations. That is slower than a sedated sloth and considerably less charming.
Goal
Add an optional, deterministic AnalysisTrace contract shared by Python and Rust, then provide tooling that reports the first semantic divergence and minimizes the reproduction input.
The trace is diagnostic infrastructure only. Final diagnostics/evidence remain the production contract and Python remains authoritative until explicit cutover.
Trace vocabulary
Names are reviewable. The trace should cover semantically load-bearing checkpoints, not every incidental allocation or hash-map iteration:
InputAccepted
FunctionEntered
BlockInputState
InstructionApplied
BlockOutputState
MergePerformed
WorkItemScheduled
FixpointReached
SummaryInferred
EvidenceEdgeAdded
DiagnosticConstructed
Each event should carry stable typed identifiers and normalized semantic payloads, for example:
function_id
block_id
instruction_id/op
resource/handle IDs normalized by construction order
before_state_digest
after_state_digest
predecessor state digests
diagnostic code/evidence edge
Normalization requirements
- No pointer/object-identity values.
- No hash-map iteration order.
- IDs must be stable for the same normalized input across Python and Rust.
- Equivalent sets/maps are sorted canonically before hashing/rendering.
- Source locations are normalized separately from semantic state so harmless line drift does not hide the first semantic mismatch.
- Trace schema is versioned and unknown events fail loudly in the comparison tool.
- Tracing is off by default and must not affect verdicts.
Tooling surface
A narrow dev surface is sufficient:
own-oracle explain-divergence repro.ownir.json
Expected output shape:
first divergence:
phase: transfer
function: CustomerViewModel::.ctor
block: B7
instruction: Release(subscription#12)
Python:
before: ...
after: Released
Rust:
before: ...
after: {Owned, Released}
The command should also emit machine-readable JSON for CI artifacts.
Minimization
When a mismatch exists, provide a reducer that attempts to remove irrelevant:
- components/functions;
- resources/handles;
- flow statements/branches;
- DI/effect facts;
- unrelated evidence locations;
while preserving the same first-divergence signature.
The minimized reproduction becomes a permanent fixture once the divergence is understood and fixed.
The reducer may be deliberately conservative. A small valid repro is more useful than an aggressive reducer that silently changes semantics.
Integration with #260
Shadow mode should continue comparing normalized final outputs first.
On mismatch only:
- retain the exact OwnIR bytes;
- rerun both engines with trace enabled;
- compute the first-divergence report;
- optionally run bounded minimization;
- store original input, traces, report and minimized fixture as the zero-diff reproduction artifact.
This keeps successful runs cheap and makes failed runs explainable.
Required controls
- Identical engines/traces compare equal byte-for-byte after normalization.
- Deliberate transfer-function mutation reports the exact first differing instruction.
- Deliberate merge mutation reports merge as the first divergence, not a later diagnostic symptom.
- Different internal allocation/object IDs normalize to equality.
- Different map iteration order normalizes to equality.
- A rendering-only difference does not masquerade as an analysis-state divergence.
- Minimizer removes unrelated components while preserving the mismatch.
- Reducer refuses malformed/semantically invalid candidates instead of manufacturing a tiny lie.
- Trace-off and trace-on runs produce identical final diagnostics/evidence.
- A fixed divergence lands with a minimized permanent regression fixture.
Acceptance
- Versioned normalized AnalysisTrace contract is documented.
- Python and Rust emit comparable traces for currently ported layers.
- First-divergence CLI/report exists and identifies the earliest semantic mismatch.
- P-022 step 7a: add dual-engine shadow mode and zero-diff reproduction artifacts #260 mismatch artifacts can include traces without changing success-path cost materially.
- A bounded minimizer produces valid smaller repros for at least ownership and one fact-level family.
- Trace generation cannot change final verdicts.
- Regression tests prove normalization is independent of incidental identity/order.
Non-goals
- No production user-facing trace format commitment.
- No replacement of diagnostics, Evidence or SARIF.
- No behavior change or new analyzer rule.
- No generic debugger UI.
- No requirement to trace every parser token or allocation.
- No cutover decision in this issue.
Activity
test(ownir): discover the parser boundary instead of sampling under it
test(ownir): discover the parser boundary instead of sampling under it
feat(shadow-infra): the engine protocol — each engine reports its lay…
feat(shadow-infra): the AnalysisTrace — normalize the identifiers, de…
feat(shadow-infra): first-divergence reduction over lowered and MOS, …
docs(shadow-infra): record the owner decisions, and name the byte-lev…
Merge pull request #338 from PhysShell/claude/shadow-mode-infrastruct…
docs(p022): the recorded run names documents portably, not by machine…
docs(p022): record the sweep run and level the surfaces to what it pr…
docs(p022): inventory the #260 sweep before running it (#260 sweep.0)
feat(shadow): adapter identity, manifest runs and the empty-set rule …
ci: the shadow sweep — five pinned repositories, large-solution contr…
docs(p022): the sweep ledger, its one interpreter and its gate (#260 …
fix(campaign): the mutation harness restores bytes, not text (#260 sw…
fix(campaign): a catcher's name is an identity, not the host's spelli…
fix(campaign): a layer's output is UTF-8, not the console codepage (#260
fix(shadow-sweep): an aggregation with nothing to aggregate fails leg…
docs(p022): record the sweep run and level the surfaces to what it pr…
docs(p022): record the sweep run and level the surfaces to what it pr…