You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parent: #250
Recommended tier: strong/local-agent collaboration
Blocked by: nothing — final acceptance reached (PR #343); the two owner decisions it waited on were ratified and landed (PR #342)
Preferably after: #256 (satisfied)
Checkpoint status
Per the status-drift rule in #250, this body is a status surface and moves together with docs/proposals/P-022-rust-core-migration.md (row 7a) and the proposals index. Reconciled at 4520a543e0c47a886217d065b3d80920199d4f93 (PR #343 merge) and at f48780630780b7f38f239469a1d7d0a87f30d356 (PR #344 merge, the CI re-record of the sweep).
checkpoint
status
1 — same-input OwnIR capture + canonical hash, the reproduction-artifact format
reached — the five pinned OSS repositories at their verified pins, the largest classic solution of every target that has one, the examples/ tree and the Windows path forms, recorded in docs/notes/p022-shadow-sweep.md with every count in p022-shadow-sweep.md (PR #343); the record on file has since been re-taken from a CI run of the workflow (PR #344)
Both blocking decisions are resolved. They were ratified as D-4..D-7, B-2, B-3, R-1 and R-2 and are recorded verbatim in the owner-decision ledger; the work that lands them over the committed corpus is the acceptance note.
What compare mode reports over the committed corpus: zero acceptance-unexplained at all three layers and on the derived SARIF, on byte-attested same input, with the two #294 OD-1 typed-door boundaries declared by policy. The policy has three entries — one (layer, status, OD-1) per layer, because the typed door refuses every layer at once — and the two OD-1 documents therefore produce six declared-boundary observations and nothing unexplained. Counts live only in p022-shadow-census.md and p022-shadow-mutations.md.
The C# samples gate measured rather than predicted: the OwnIR the wpf-extractor job already produces is handed to the driver through one upload-artifact handshake — no second extraction — and the one sample document compared clean. One document is a sample, not a sweep.
This issue's acceptance is reached; shadow mode is still not claimed. The test matrix below is taken in full: the committed corpus and the C# samples by the two ci.yml gates, and the five pinned OSS repositories, the large-solution controls and the examples/ tree by the recorded sweep. What compare mode reports over that matrix: zero acceptance-unexplained at all three layers and on the derived SARIF, on byte-attested same input, with the two #294 OD-1 typed-door boundaries declared by policy. It is still not "shadow mode achieved", not "P-022 done" and not "Rust is the default" — that is #262's cutover behind #261, Python remains the public engine, and no production behaviour changed.
A repository is not covered because its extraction succeeded: coverage is a recorded, non-empty set of documents fed byte-identically to both engines and judged, per target, with the denominator on the record. The driver fails a run that compared zero documents and a declared target it never reached; the denominators are in the generated fragment. The sweep is a committed definition plus one recorded run: the run names its Own.NET commit and the adapter it executed by sha256 and byte length, and tests/shadow_sweep.py refuses a run that is short, long, or measured at another commit, in another mode or by another command than the definition declares.
Taking the measurement found defects in the harnesses and none in either engine — a cross-drive relpath that killed the driver on a label, a timeout that never returned when the adapter had children, a control group that could not execute on Windows at all, and three in the mutation harness that between them meant no campaign could be recorded anywhere but Linux. Each is written out in the sweep note's §5. MaterialDesignInXamlToolkit carries only an .slnx solution at its pin, which the extractor's classic-format resolver does not read: that target is covered by its directory walk alone, and the record says so per target rather than averaging it away.
What the byte-level measurement settled (recorded in the note's §2.1 and in tests/fixtures/repro/variants/byte_variants.json, one column per engine): CRLF, trailing-newline, whitespace and key-order variants are accepted by both readers with one canonical identity and distinct raw identities; a UTF-8 BOM, invalid UTF-8 and truncated JSON are refused by both, and are negative compare-driver controls rather than artifacts.
Goal
Run the Python and Rust cores against the exact same OwnIR bytes, normalize their outputs, and fail on any unexplained difference while preserving a standalone reproduction artifact.
This is the safety gate before Rust CLI/default-engine work.
Engine modes
Provide internal/dev surfaces equivalent to:
python
rust
compare
The final flag names may differ, but semantics must not:
python: current reference behavior;
rust: Rust bridge/core only;
compare: extract OwnIR once, feed identical bytes to both engines, compare normalized outputs.
Python remains the public default in this issue. (Landed as scripts/shadow_compare.py --engine python|rust|compare, a dev surface, with the dev-only own-shadow-engine adapter — stdin bytes in, one capture out, no paths, no fallback.)
Same-input invariant
Compare mode must:
Produce or load OwnIR exactly once.
Hash the raw OwnIR bytes.
Feed those exact bytes to Python and Rust.
Record the hash in the diff artifact.
Running the extractor twice is forbidden because frontend nondeterminism would contaminate the comparison.
(Landed as artifact v3: input.raw carries the byte-exact input, and each engine independently records the digest and length of what it consumed; both must equal input.raw, and the raw bytes must parse back to the existing canonical identity. Every consumed claim comes from an actual run of that engine — B-2, B-3.)
Normalized comparison
Compare at least:
path
line
code
severity
message
subject
resource_kind
ordered Evidence
(canonical .ownreport.json was listed here originally; it was struck at #256 — measured, it is a buffer-storage report needing the AST, not a diagnostics surface, and the project had already refused that shape. See #250's #256 entry. Canonical SARIF is compared as a derived zero-diff surface, not as an AnalysisTrace layer — D-6.)
Classify differences as:
Python-only;
Rust-only;
changed diagnostic;
ordering-only;
explicitly ignored volatile metadata;
unexplained.
(Landed as two orthogonal axes — observation kind and acceptance — with missing-layer as a kind, every content difference acceptance-unexplained, and status/projection explained only by an exact (layer, kind, class) entry of a frozen policy — D-5.)
Reproduction artifact
On mismatch, emit a self-contained artifact containing:
Include the OwnIR input or a stable artifact reference sufficient to replay without the original source repository.
(A crash, timeout or non-zero exit of an engine is a run-level hard failure with a failure report — raw input, engine, exit code, signal, stderr — never a layer refusal and never a synthetic capture — R-2.)
The five-repository sweep may use a local/corpus-capable agent, but its commands, commits and output artifacts must be recorded.
Reliability controls
Rust panic/crash is a hard failure, never a Python-fallback success.
Python crash is also visible and classified.
Timeout/cancellation behavior is explicit.
Ordering instability is treated as a bug.
Re-running the same OwnIR produces byte-identical normalized outputs.
Compare mode never mutates user source or caches in a way that changes the second engine's input.
CI strategy
Fast PR CI:
committed fixtures;
C# samples;
generated compact controls.
Scheduled/manual broader gate:
five pinned OSS repos;
large-solution controls;
reproduction artifacts uploaded on failure.
(The fast gate is landed as two ci.yml jobs: shadow compare (committed corpus) and shadow compare (C# samples). The scheduled/manual gate is .github/workflows/shadow-sweep.yml (PR #343): one leg per document, the pin verified before anything runs, one extraction per document, OWN_SHADOW_ENGINE always explicit and the adapter named by digest, and an aggregation that checks the assembled run against the committed sweep definition rather than trusting the legs' ticks. It had not executed when this issue closed — a workflow_dispatch workflow is dispatchable only from the default branch — so the first record was a local one, which this issue allows. It has since executed, and the record on file is a CI run of the workflow (PR #344): workflow_run_url names it, every leg ran on GitHub-hosted runners, and each leg names its document by id rather than by a runner path — the workflow's very first execution had recorded the runner's temp path as every document's source, a constant dressed as provenance, and was superseded without being committed.)
Guardrails
No Rust-default cutover.
No removal of Python.
No new diagnostics.
No separate extraction per engine.
No normalization that deletes semantic fields.
No automatic acceptance of a diff because one engine produced fewer findings.
Tails carried here from #259's final-acceptance review
Three of the four are closed by PR #342: one pub(crate) tolerant line reader in own-bridge (with p022-coord-2 re-anchored and re-run); the §3.2 diff-stat label in docs/notes/p022-bridge-verdict-final-acceptance.md; and scripts/mutate_campaign.py --validate resolving every named catcher, refusing a Python mutation that does not parse, with the re-run rule written into the acceptance note.
The fourth closed in PR #343: the header of docs/notes/p022-bridge-verdict-final-acceptance.md now reads final acceptance reached, as its §7 records.
The #342 review's suggestion landed in PR #343: every result and failure report names the adapter by sha256 and byte length as well as by path, taken from the file that ran (shadow_compare_version 2), so a stale build can never stand in for the engine that was meant. The digest names the build, not the port's source — measured: two release builds of one unchanged tree gave two digests at one byte length — which is why the record carries the set of adapters a run executed.
None blocks the acceptance; each is small and named so it is not assumed done:
the sweep evidence carries no dirty bit — the driver records HEAD only; the run summary, the collector and the interpreter should carry and require one, as the campaign harness already does;
tests/shadow_sweep.py does not verify the record's definition_sha256 against the definition's bytes; it should, as the campaign renderer does for a campaign's definition;
byte-sensitive evidence depends on the operator's checkout settings (core.autocrlf changes the on-disk digest of a definition without changing a character): a researched .gitattributes for docs/evidence, docs/generated and tests/fixtures, never an operator's global configuration;
derived-status hygiene: the P-022 row 7a and the proposals index carry definition-level counts in prose;
(closed by PR docs(p022): re-record the #260 sweep from a CI run of its workflow #344) re-record the sweep from a CI run of .github/workflows/shadow-sweep.yml — done: the record on file is a CI run with its workflow_run_url, and each leg names its document by id rather than by a runner path;
the .slnx solution format is an extractor gap outside P-022;
process: work committed from a local machine enters the tree under a repository identity (repo-local git config), and a pre-push report compares recorded artifacts and commit metadata as bytes, never as console text.
Acceptance
Compare mode feeds identical OwnIR bytes to both engines — read once, as bytes, no engine ever sees a path; attested by input.raw against each engine's consumed.
Fast CI reports zero unexplained differences — shadow compare (committed corpus) and shadow compare (C# samples) in ci.yml.
Five-repository sweep reports zero unexplained differences — every document of the committed sweep definition, each repository at its verified pin, recorded in docs/evidence/p022-shadow-sweep.result.json and rendered to docs/generated/p022-shadow-sweep.md; the scheduled/manual gate is .github/workflows/shadow-sweep.yml.
Mismatches produce replayable artifacts — artifact v3 on divergence, a failure report on an execution failure, both naming the adapter by sha256.
Rust crashes cannot be hidden by fallback — a crash, timeout or non-zero exit is a run-level hard failure (R-2), and the driver has no path to the reference's answer.
Python remains the default engine.
Commands and normalization policy are documented — docs/notes/p022-shadow-sweep.md, docs/notes/p022-shadow-acceptance.md, spec/Bridge.md §6, and the commands carried as data in docs/evidence/p022-shadow-sweep.json.
Parent: #250
Recommended tier: strong/local-agent collaboration
Blocked by: nothing — final acceptance reached (PR #343); the two owner decisions it waited on were ratified and landed (PR #342)
Preferably after: #256 (satisfied)
Checkpoint status
Per the status-drift rule in #250, this body is a status surface and moves together with
docs/proposals/P-022-rust-core-migration.md(row 7a) and the proposals index. Reconciled at4520a543e0c47a886217d065b3d80920199d4f93(PR #343 merge) and atf48780630780b7f38f239469a1d7d0a87f30d356(PR #344 merge, the CI re-record of the sweep).OwnIRcapture + canonical hash, the reproduction-artifact formatAnalysisTrace(#269)input.raw, per-engineconsumed, B-2/B-3)own-shadow-engineadapter and the compare driver (R-1, R-2)examples/tree and the Windows path forms, recorded indocs/notes/p022-shadow-sweep.mdwith every count inp022-shadow-sweep.md(PR #343); the record on file has since been re-taken from a CI run of the workflow (PR #344)Both blocking decisions are resolved. They were ratified as D-4..D-7, B-2, B-3, R-1 and R-2 and are recorded verbatim in the owner-decision ledger; the work that lands them over the committed corpus is the acceptance note.
What compare mode reports over the committed corpus: zero acceptance-unexplained at all three layers and on the derived SARIF, on byte-attested same input, with the two #294 OD-1 typed-door boundaries declared by policy. The policy has three entries — one
(layer, status, OD-1)per layer, because the typed door refuses every layer at once — and the two OD-1 documents therefore produce sixdeclared-boundaryobservations and nothingunexplained. Counts live only inp022-shadow-census.mdandp022-shadow-mutations.md.The C# samples gate measured rather than predicted: the OwnIR the
wpf-extractorjob already produces is handed to the driver through one upload-artifact handshake — no second extraction — and the one sample document compared clean. One document is a sample, not a sweep.This issue's acceptance is reached; shadow mode is still not claimed. The test matrix below is taken in full: the committed corpus and the C# samples by the two
ci.ymlgates, and the five pinned OSS repositories, the large-solution controls and theexamples/tree by the recorded sweep. What compare mode reports over that matrix: zero acceptance-unexplained at all three layers and on the derived SARIF, on byte-attested same input, with the two #294 OD-1 typed-door boundaries declared by policy. It is still not "shadow mode achieved", not "P-022 done" and not "Rust is the default" — that is #262's cutover behind #261, Python remains the public engine, and no production behaviour changed.A repository is not covered because its extraction succeeded: coverage is a recorded, non-empty set of documents fed byte-identically to both engines and judged, per target, with the denominator on the record. The driver fails a run that compared zero documents and a declared target it never reached; the denominators are in the generated fragment. The sweep is a committed definition plus one recorded run: the run names its Own.NET commit and the adapter it executed by
sha256and byte length, andtests/shadow_sweep.pyrefuses a run that is short, long, or measured at another commit, in another mode or by another command than the definition declares.Taking the measurement found defects in the harnesses and none in either engine — a cross-drive
relpaththat killed the driver on a label, a timeout that never returned when the adapter had children, a control group that could not execute on Windows at all, and three in the mutation harness that between them meant no campaign could be recorded anywhere but Linux. Each is written out in the sweep note's §5. MaterialDesignInXamlToolkit carries only an.slnxsolution at its pin, which the extractor's classic-format resolver does not read: that target is covered by its directory walk alone, and the record says so per target rather than averaging it away.What the byte-level measurement settled (recorded in the note's §2.1 and in
tests/fixtures/repro/variants/byte_variants.json, one column per engine): CRLF, trailing-newline, whitespace and key-order variants are accepted by both readers with one canonical identity and distinct raw identities; a UTF-8 BOM, invalid UTF-8 and truncated JSON are refused by both, and are negative compare-driver controls rather than artifacts.Goal
Run the Python and Rust cores against the exact same OwnIR bytes, normalize their outputs, and fail on any unexplained difference while preserving a standalone reproduction artifact.
This is the safety gate before Rust CLI/default-engine work.
Engine modes
Provide internal/dev surfaces equivalent to:
The final flag names may differ, but semantics must not:
python: current reference behavior;rust: Rust bridge/core only;compare: extract OwnIR once, feed identical bytes to both engines, compare normalized outputs.Python remains the public default in this issue. (Landed as
scripts/shadow_compare.py --engine python|rust|compare, a dev surface, with the dev-onlyown-shadow-engineadapter — stdin bytes in, one capture out, no paths, no fallback.)Same-input invariant
Compare mode must:
Running the extractor twice is forbidden because frontend nondeterminism would contaminate the comparison.
(Landed as artifact v3:
input.rawcarries the byte-exact input, and each engine independently records the digest and length of what it consumed; both must equalinput.raw, and the raw bytes must parse back to the existing canonical identity. Everyconsumedclaim comes from an actual run of that engine — B-2, B-3.)Normalized comparison
Compare at least:
When #256 is complete, also compare:
(
canonical .ownreport.jsonwas listed here originally; it was struck at #256 — measured, it is a buffer-storage report needing the AST, not a diagnostics surface, and the project had already refused that shape. See #250's #256 entry. Canonical SARIF is compared as a derived zero-diff surface, not as anAnalysisTracelayer — D-6.)Classify differences as:
(Landed as two orthogonal axes — observation kind and acceptance — with
missing-layeras a kind, every content difference acceptance-unexplained, andstatus/projectionexplained only by an exact(layer, kind, class)entry of a frozen policy — D-5.)Reproduction artifact
On mismatch, emit a self-contained artifact containing:
{ "input": "...", "ownir_sha256": "...", "python_engine": "...", "rust_engine": "...", "python_only": [], "rust_only": [], "changed": [], "ordering_only": [], "ignored_metadata": [], "unexplained": [] }Include the OwnIR input or a stable artifact reference sufficient to replay without the original source repository.
(A crash, timeout or non-zero exit of an engine is a run-level hard failure with a failure report — raw input, engine, exit code, signal, stderr — never a layer refusal and never a synthetic capture — R-2.)
Test matrix
Run compare mode over:
The five-repository sweep may use a local/corpus-capable agent, but its commands, commits and output artifacts must be recorded.
Reliability controls
CI strategy
Fast PR CI:
Scheduled/manual broader gate:
(The fast gate is landed as two
ci.ymljobs:shadow compare (committed corpus)andshadow compare (C# samples). The scheduled/manual gate is.github/workflows/shadow-sweep.yml(PR #343): one leg per document, the pin verified before anything runs, one extraction per document,OWN_SHADOW_ENGINEalways explicit and the adapter named by digest, and an aggregation that checks the assembled run against the committed sweep definition rather than trusting the legs' ticks. It had not executed when this issue closed — aworkflow_dispatchworkflow is dispatchable only from the default branch — so the first record was a local one, which this issue allows. It has since executed, and the record on file is a CI run of the workflow (PR #344):workflow_run_urlnames it, every leg ran on GitHub-hosted runners, and each leg names its document by id rather than by a runner path — the workflow's very first execution had recorded the runner's temp path as every document'ssource, a constant dressed as provenance, and was superseded without being committed.)Guardrails
Tails carried here from #259's final-acceptance review
Three of the four are closed by PR #342: one
pub(crate)tolerant line reader inown-bridge(withp022-coord-2re-anchored and re-run); the§3.2diff-stat label indocs/notes/p022-bridge-verdict-final-acceptance.md; andscripts/mutate_campaign.py --validateresolving every named catcher, refusing a Python mutation that does not parse, with the re-run rule written into the acceptance note.The fourth closed in PR #343: the header of
docs/notes/p022-bridge-verdict-final-acceptance.mdnow reads final acceptance reached, as its §7 records.The #342 review's suggestion landed in PR #343: every result and failure report names the adapter by
sha256and byte length as well as by path, taken from the file that ran (shadow_compare_version2), so a stale build can never stand in for the engine that was meant. The digest names the build, not the port's source — measured: two release builds of one unchanged tree gave two digests at one byte length — which is why the record carries the set of adapters a run executed.Tails carried out of this issue, to #250
None blocks the acceptance; each is small and named so it is not assumed done:
dirtybit — the driver records HEAD only; the run summary, the collector and the interpreter should carry and require one, as the campaign harness already does;tests/shadow_sweep.pydoes not verify the record'sdefinition_sha256against the definition's bytes; it should, as the campaign renderer does for a campaign's definition;core.autocrlfchanges the on-disk digest of a definition without changing a character): a researched.gitattributesfordocs/evidence,docs/generatedandtests/fixtures, never an operator's global configuration;.github/workflows/shadow-sweep.yml— done: the record on file is a CI run with itsworkflow_run_url, and each leg names its document by id rather than by a runner path;.slnxsolution format is an extractor gap outside P-022;Acceptance
input.rawagainst each engine'sconsumed.shadow compare (committed corpus)andshadow compare (C# samples)inci.yml.docs/evidence/p022-shadow-sweep.result.jsonand rendered todocs/generated/p022-shadow-sweep.md; the scheduled/manual gate is.github/workflows/shadow-sweep.yml.sha256.docs/notes/p022-shadow-sweep.md,docs/notes/p022-shadow-acceptance.md,spec/Bridge.md§6, and the commands carried as data indocs/evidence/p022-shadow-sweep.json.