feat(shadow-infra): step 7a infrastructure — capture, artifact, engine protocol, AnalysisTrace, first-divergence reduction (#260/#269 cp1–4) - #338
Merged
Conversation
…eproduction artifact (#260/#269 cp1) P-022 step 7a, checkpoint 1 — INFRASTRUCTURE for shadow mode. Not shadow mode, and not a parity claim: comparing two engines' end diagnostics as an acceptance surface is #260's acceptance and is blocked by #259 (cp5 and 4b). Nothing here attempts it. Two things have to be settled before two engines can be compared at all, and neither is a comparison: * a canonical identity for an OwnIR document, so "both engines saw the same input" is a checked fact rather than an assumption about which file was passed where; * one self-contained reproduction artifact carrying the input, its schema version, its hash, the engine identifiers and each engine's outputs per layer. What landed: * ownlang/repro.py — the authoritative emitter, an OBSERVER like lowered.py and verdicts.py: it never mutates facts, never changes a verdict, and is imported by nothing in the production path. It composes the three frozen layer surfaces and never re-encodes them. Its docstring freezes the canonical form and the artifact format. * rust/crates/own-shadow — the replaying half, zero Python: canonical form, digest, artifact verification and rendering. `verify` is an independent reading of the same frozen rule, not a port of the reference's code, so a divergence between the two is itself a finding. * tests/fixtures/repro/ — digests.json pins the canonical hash of every facts document in the shared corpora; a curated set of artifacts is committed and replayed byte-for-byte. * scripts/mutate_campaign.py, scripts/render_checkpoint_status.py, tests/test_generated_docs.py — the campaign as data and the census as a generated document, both gated. No count in a document is typed by hand. Measured (generated: docs/generated/p022-shadow-cp1-census.md): 80 documents digest-pinned across five corpora and re-hashed by the port, 0/0/0; 8 artifacts round-tripped and self-verifying; 80 tamper controls; 6 executable domain refusals; 12 structural + 5 backstop negative controls per side; 30 mutations, 30 caught, 0 survivors. Ordering-only is named inapplicable at this layer rather than reported as a meaningless zero, and the unmeasured set is named in the census. One FINDING, recorded and closed as a contract decision rather than by bending either engine: CPython's json reads the literal -0 as the integer 0 and serde_json reads it as the float -0.0. Refused by both engines instead of reconciled — a canonical form that hashed it would assert that two engines saw the same document while they held different values. Because the disagreement is invisible after parsing on the reference side, the domain is enforced on the LITERALS, under an executable domain_refusals ledger. No reference behaviour changed, no golden regenerated, no OwnIR schema change. Two harness defects the campaign paid for, both fixed in the tooling so the next campaign inherits the lesson: controls that proved "something refused" rather than "the parse boundary refused" (round 1, 3 survivors), and a same-size mutation poisoning __pycache__ across a restore (round 2, fifteen impossible catchers). own-shadow is entry-point class, added to the allowed DAG edge set with an empty dependency set; a named test asserts no core crate — nor own-bridge — depends on it. Gates: cargo fmt --check, cargo clippy --workspace --all-targets (zero warnings), cargo test --workspace --no-fail-fast, ruff check ., mypy, python tests/run_tests.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ers, and declares what it could produce (#260/#269 cp2) P-022 step 7a, checkpoint 2. Still INFRASTRUCTURE for shadow mode, still no parity claim, and — this checkpoint's sharpest line — still not a comparison: an artifact now carries two engines' captures side by side and nothing reads one against the other. Comparing them is #260's acceptance, blocked by #259. What landed: * own_shadow::capture — the port's half of the protocol, driving own-bridge's three layer surfaces (lower, dump_summaries, check_facts) into the shared envelope. The reference's half is ownlang/repro.py::project_layers, and the two stay independent readings of one frozen format. * `projection` on the layer envelope (format version 2): each layer declares what its engine could PRODUCE — {"kind":"full"} or {"kind":"partial", "members":[...], "reason":"..."}. * An engine writes only its own entry. The reference authors python-ownlang and carries foreign entries through untouched; the port authors rust-own-bridge under OWN_SHADOW_WRITE=1. Each half is produced with zero of the other's runtime, and neither can quietly become a comparison of one implementation against itself. * own-shadow gains own-ir/own-lowered/own-bridge in the allowed DAG edge set — a deliberate, reviewed edit. The constraint running the other way did not move: no core crate, nor own-bridge, may depend on the harness. Why a projection: the port is mid-migration. Two of its layers emit the whole frozen surface; check_facts is at the #259 cp4 projection, missing message, related and flow. Without the field it could only emit a short document a comparison would score as agreement, or refuse a layer it can mostly produce. A test holds the claim to the records, so the field cannot be prose. Measured (generated: docs/generated/p022-shadow-census.md): 9 artifacts carrying both engines; 4 layer envelopes where the statuses differ, every one a boundary the port DECLARES (the OBL protocol analysis; the typed door, which is upstream of every layer and so refuses all three). Campaign (docs/notes/p022-shadow-infra-checkpoint2-data/): round 1 was 8 caught, 1 compile error, 2 survivors. M39 found genuinely untested code — no committed artifact had a document the typed door refuses — fixed by promoting verdict_door_effect_deps_not_strings to an artifact. M41 found a control that stopped at a missing reason and never tried an empty one. M37 did not compile and is recorded as a compile error, never as "caught". Round 2: 11/11 caught, 0 survivors, control clean. The --check gate earned its keep: reshaping the layer envelope broke two of checkpoint 1's mutation anchors (M11, M15), and the gate caught it before the recorded result could go on describing a tree that no longer exists. They were re-anchored and cp1 re-run: 30/30, unchanged. The census is now one live view of the slice rather than a per-checkpoint snapshot that would be stale by design; each checkpoint's recorded campaign stays frozen at what it measured. Gates: cargo fmt --check, cargo clippy --workspace --all-targets (zero warnings), cargo test --workspace --no-fail-fast, ruff check ., mypy, python tests/run_tests.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
…clare the order (#260/#269 cp3) P-022 step 7a, checkpoint 3. Still INFRASTRUCTURE for shadow mode, no parity claim, and still not a comparison: the trace is the SHAPE a comparison would need, and producing it is not performing one. Checkpoint 2 left two captures side by side. Two things stand between that pairing and a comparison, and only one should be removed: * Internal identifiers. The Layer 2 handles are minted from global counters in document order (BR-L2) — positions wearing the costume of names. Measured on handles_global_counters, reversing the component list turns cap_0/sub_1/sub_2/cap_3 into sub_0/cap_1/cap_2/sub_3: four unchanged facts, eight names, none shared. A raw comparison would report every handle as a difference and bury the one real one. * Order, which must NOT be normalized away: document and lowering order is semantic (BR-D4/BR-L5), and BR-V8 leaves verdict ties in construction order. So the trace normalizes the identifiers and DECLARES the order. * Stable ids are `component|file|line|event|handler`, rebuilt from the record the bridge attached to the handle; every occurrence is rewritten; the rename is a bijection and total, both asserted. The mint KIND is preserved as `mint`, so a routing difference stays a comparable value on one step instead of splitting into a pair of "only in one engine" addresses. * `order` is `significant` for lowered and verdicts, `canonical` for summaries — read by a comparison to CLASSIFY an ordering difference, never to license a sort. * Both sides project BOTH engines: projecting a capture is not authoring it, and doing it twice is what cross-checks the normalization itself. Proven on the property it exists for, over the whole captured corpus: a permutation that reshuffles the mint counters must not move a stable id, and must still change the lowered layer's step order. FINDING: the two implementations disagreed, and the disagreement was in the schema. For two functions named `Take`, the reference addressed the second as functions[Take~1] and the port as functions[Take]~1 — both faithful readings, and the reference was inconsistent with itself (inside the bracket for functions, outside for every other list). Resolved by making the rule explicit and uniform: inside the bracket, everywhere, so a nested prefix composes. A single implementation would have shipped the inconsistency. A second defect surfaced from this family's own step-id control: the function disambiguator reset per function, so two `Take`s collided on one address. Campaign: round 1 was 10 caught, 1 survivor. M51 guards a state the corpus cannot reach, so it needed a synthetic unit-level control (added both sides) — and then still survived, because the campaign never RAN the layer that catches it. A campaign that does not run a layer cannot see it catch. A `rust-unit` layer was added to all three campaigns and all three re-run: 30/30, 11/11, 11/11, controls clean. Gates: cargo fmt --check, cargo clippy --workspace --all-targets (zero warnings), cargo test --workspace --no-fail-fast, ruff check ., mypy, python tests/run_tests.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
…verdicts refused (#260/#269 cp4) P-022 step 7a, the last checkpoint of the row-7a slice. Still INFRASTRUCTURE for shadow mode and still not a parity claim: this reducer REFUSES the verdict layer, because comparing final diagnostics is #260's acceptance and is blocked by #259 (cp5 and 4b). Checkpoints 1-3 built the pair and made it walkable. This one walks it — over `lowered` and `summaries` only — and names the FIRST place two engines part company: the layer, the step address, and the minimal difference inside it. * reduce_traces on both sides, independent readings of the same rules. A comparison is the last thing to have only one implementation of, and having two paid for itself twice here. * <case>.reduction.json committed and replayed byte-for-byte by the port. * The census's divergence counters are now COMPUTED by the reducer rather than implied by a green build: left-only 0 / right-only 0 / changed 0 / ordering-only 0 / unexplained 0 over 9 reductions, with 2 `status` observations, both boundaries the port declares in its own error text. Decisions: the scope is a contract, and `verdicts` is refused rather than skipped — the refusal is carried in every reduction and asserted, so "not compared" can never be read as "compared and agreed". `status` and `projection` are counted apart from the four content classes; neither is a difference in what an engine computed. When both engines refused a layer the reducer compares THAT they refused, never how they phrased it. The difference is minimal — the field, not the step body. Object key order is significant, because the surfaces fix field order byte-exactly. TWO FINDINGS, both from having two implementations: 1. The capture carried the MOS document in `dump_summaries`' insertion order while the port read the same surface back from its rendered, sorted form — the form tests/fixtures/summaries pins. The first reduction reported a `changed` step for a difference NEITHER surface has. Fixed at the capture: each layer document is carried in the key order its own surface fixes. 2. The two reducers disagreed about what "the same" means. Python's dict compares order-insensitively and True == 1; the port's value type distinguishes both. The reference now has an explicit `_same` matching the port. Neither finding would have surfaced with one implementation. The reducer is shown to work, not assumed to: seven controls per side over a real Layer 2 output — silence on unchanged data, and one controlled change each for changed/left-only/right-only/ordering-only/key-order, plus two differently-worded refusals reading as agreement. The changed-field control moved case twice before it tested the right thing (`di` has no line-bearing lowered step, so one side ADDED a key while the other saw a no-op) — again surfaced by the two halves disagreeing. Campaign: round 1 was 8 caught, 3 survivors. M57/M62 showed the "both refused" short-circuit was real but unreachable from the corpus; M59 showed key-order sensitivity had lost its only control to finding 1. Synthetic controls added on both sides. Round 2: 11/11 caught, control clean. Gates: cargo fmt --check, cargo clippy --workspace --all-targets (zero warnings), cargo test --workspace --no-fail-fast, ruff check ., mypy, python tests/run_tests.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
…heckpoint 1 spec/Bridge.md §6 named the reproduction artifact as "checkpoint 1, landed" while checkpoints 2-4 had since landed too. Status surfaces move together, so it now describes the composition the slice actually built — the artifact, the engine protocol's per-layer projection, the AnalysisTrace, and the reducer. Two points are the spec's rather than a note's, and were missing: * "Verbatim" means in the key order the layer's OWN surface fixes. INF-R1 (Inference.md 8) makes the MOS dump's field order part of the contract, and the concrete order is the sorted rendering tests/fixtures/summaries pins byte-for-byte — not dump_summaries' dict insertion order. Carrying the insertion order made two engines' MOS documents differ in key order alone, a difference neither surface has. * The reducer's scope is the Layer 2 lowered document and the MOS summaries sub-surface only. Layer 3 is REFUSED and the refusal is recorded, so "not compared" can never be read as "compared and agreed". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
…el boundary Closure for the step-7a slice. No code, no fixture, no recorded campaign result changes; the checkpoints stay as they landed. Three departures from the slice's brief are now decisions on the record instead of a brief quietly re-read to match the tree, in a new ledger at docs/notes/p022-shadow-infra-owner-decisions.md: - D-1: capture and the reproduction format stay ONE checkpoint, where the brief asked for a checkpoint each. Accepted — the format's central field is the hash, so the two halves have one indivisible piece of evidence — and recorded rather than renumbered. - D-2: the `-0` canonical-domain narrowing. An engine divergence was the brief's stop-and-report trigger; checkpoint 1 reported it AND decided it, in a paragraph that disclaims the standing to decide. The outcome is ratified as an owner decision (neither engine's semantics move, both refuse at the literal); the process correction is that discovering it was the trigger to stop. - D-3: `sha2` accepted for `own-shadow` only, bounded by the existing dag.rs test that no core crate depends on it. And one boundary the slice worded too strongly. #260's acceptance invariant is byte-level: hash the raw OwnIR bytes, feed THOSE bytes to both engines. What checkpoint 1 proves is shared CANONICAL document identity — and canonical-equivalent input is not byte-identical input, since ignoring whitespace, key order and duplicate-key resolution is exactly the canonical form's job. So the raw-byte invariant is now named as unmeasured in the generated census, in spec/Bridge.md §6 (a fifth point), and in P-022 row 7a plus the proposals index, before "same-input" and a green 80/80 can be read as #260's acceptance already closed. The ledger also records what CI does not attest: CodeRabbit's green line on the PR is "review skipped, manual review required for this OSS repository", not a review. Gates: ruff, mypy, python tests/run_tests.py, cargo fmt --check, cargo clippy --workspace --all-targets (zero warnings), cargo test --workspace --no-fail-fast, render_checkpoint_status.py --check. Campaigns not re-run: every mutation anchors into ownlang/repro.py or own-shadow/src/, neither of which this commit touches, and CI re-anchors each definition. Refs #260, #269, #250. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
#337 and this branch independently added `scripts/mutate_campaign.py` and `scripts/render_checkpoint_status.py`. Resolved by making #337's the shared infrastructure and folding this slice in as a mode of it, rather than keeping two mutation harnesses that would drift apart. scripts/mutate_campaign.py — #337's contract, kept whole: schema-validated definitions, exactly-once regex anchors, per-mutation expected_catchers, the compile-error / invalid-mutation / runner-error vocabulary, the clean-tree contract, and provenance (definition sha256 + the commit the run was taken on, gated as an ancestor of HEAD). Two things this branch needed are added as generalizations, not as a second code path: * `layers` — a campaign may declare explicit commands instead of a cargo workspace, because the shadow campaigns' catchers are a Python harness plus four cargo test targets. `workspace` is unchanged and derives its layers from `cargo metadata` as before; exactly one of the two must be declared. * Python-source hygiene — a mutation that edits a `.py` file drops the cached bytecode on every write and runs Python layers with PYTHONDONTWRITEBYTECODE. CPython validates a `.pyc` by integer mtime and size, so a same-size mutation survived restoration and made fifteen later mutations report a catcher that could not exist. The cargo-only campaigns never hit this. scripts/render_checkpoint_status.py — #337's fragment/check/write structure, extended with two fragments: `p022-shadow-census.md` (computed by a new `tests/shadow_census.py`, the house pattern `tests/verdict_census.py` set) and `p022-shadow-mutations.md` (the slice's four campaigns through the SAME `summarize()` as cp4's — one interpreter per run, in one place). #337's own two fragments reproduce byte-for-byte. The four shadow campaigns are migrated onto the shared schema at docs/evidence/p022-shadow-cp{1,2,3,4}.json; all 63 mutations still anchor exactly once. Their recorded results are NOT carried over: they lack the provenance and required-catcher fields the shared gate needs, so under it they would not be evidence. The campaigns are re-run on this merge commit and their results committed next. tests/test_generated_docs.py is superseded by tests/test_checkpoint_status.py, which now also gates that every campaign definition still anchors — kept as a separate named failure so a fragment's content stays independent of HEAD, which is what lets a recorded run survive an unrelated refactor. docs/proposals/README.md — #337's cp4 row (typed counts replaced by the generated census) is the base; this branch's step-7a clause is appended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
The mypy --strict pass over the merged tree flagged `name = e.get("name")`
shadowing an outer `for name in ...` loop variable with `Any | None`. Renaming
only the assignment left three later uses in that loop reading the stale outer
name — including `plan.pop(name, None)`, whose job is to take the six
domain-refusal controls back out of the capturable set. They stayed in, and the
harness tried to capture a document whose whole purpose is that neither engine
can name it (`domain_exponent.facts.json`, the `1e3` literal).
Caught by the campaign's own honesty control, before a single mutation ran: the
unmutated tree did not pass, so all four runs were declared void and nothing
was recorded. That is the control working exactly as designed — a campaign that
reports "caught" over a red baseline has measured nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
…give the release job the history its gate needs Two things the merged tree surfaced, both by a gate rather than by luck. 1. cp3's M42 and cp4's M55 declared `python::non-zero exit with no FAIL line` — the OLD runner's name for a Python layer that failed without printing a `FAIL[<check>]:` line. The shared runner names that case `<layer>::<non-zero exit with no reported failure>`, so the declared catcher could never fire. Both mutations were still caught, and by that exact layer; what was wrong was the name the definition promised. #337's expected-catchers rule is what turned "caught, near enough" into a reported problem: a mutation caught only by something other than the test the definition names does not prove what it claims. 2. `.github/workflows/owen-cli-release.yml`'s `build + test + pack` job runs the whole Python suite on a depth-1 checkout, so tests/test_checkpoint_status.py cannot resolve the commit a recorded campaign names and fails with the shallow-clone message the runner writes for exactly this case. #337 gave ci.yml's tests job `fetch-depth: 0` for the same reason; this job needed it too and did not get it, because the workflow only triggers on paths (`ownlang/**` among them) that #337 never touched. Porting the same one-line fix here rather than leaving it for the next PR that edits ownlang/ to trip over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
63 mutations across the slice's four campaigns, re-run on the merged tree through #337's runner: 30/30, 11/11, 11/11, 11/11 caught, no survivors, no compile errors, no invalid mutations, the honesty control surviving in each, and — after the two mis-declared catchers this run exposed — every mutation caught by the test its definition names. The recorded results now carry what the shared contract requires and the old ones could not: the definition's sha256, the commit the run was taken on (gated as an ancestor of HEAD), the clean-tree assertion, and every catching test by name. Interpreted once, by `summarize()`, into docs/generated/p022-shadow-mutations.md. The owner-decision ledger gains M-1: why the harnesses were merged rather than kept side by side, why these campaigns were re-run when the closure commit deliberately did not re-run them, and the two defects the merge surfaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM
This was referenced Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Что и зачем
Инфраструктурный срез P-022 step 7a (#260/#269), все четыре checkpoint'а: каноническая идентичность OwnIR-входа, формат reproduction-артефакта, протокол движка, схема
AnalysisTrace(#269) с нормализацией стабильных ID, и редукция до первого расхождения над слоями lowered/MOS. Каждая половина воспроизводится без единой строки рантайма другого движка.Это инфраструктура для shadow mode, а не shadow mode, и не заявление о parity. Редуктор отказывается от слоя вердиктов и записывает отказ: сравнение конечных диагностик — это acceptance #260, заблокированный #259 (cp5 и 4b). Пересечение этой границы сделано решением о контракте: scope редукции, словарь движков и словарь слоёв — замороженные константы с тестами, которые краснеют при их сдвиге.
Тип изменения
Как проверено
python tests/run_tests.pyruff check .иmypy(скрипты evidence-оснастки — под--strict, как их поставил docs(P-022): generate the cp4 census and mutation evidence from the tree #337)cargo fmt --check,cargo clippy --workspace --all-targets(ноль предупреждений),cargo test --workspace --no-fail-fastpython scripts/mutate_campaign.py --campaign docs/evidence/p022-shadow-cp{1,2,3,4}.json --runpython scripts/render_checkpoint_status.py --check(гейт —tests/test_checkpoint_status.py)Связанные issue
Refs #260, #269, #250. Не закрывает #260: построено ровно то, что строка 7a перечисляла как «sliceable now»; acceptance остаётся заблокированным #259, а byte-level инвариант same-input — недоказанным (см. «Что НЕ измерено»).
Чеклист
feat:,fix:,docs:…)Коммиты
fc3031948f50afAnalysisTrace(#269): нормализация стабильных ID, при этом семантика порядка каждого слоя объявляется, а не нормализуется прочь59fdf312c64cefspec/Bridge.md§6 доведён до всего среза52866892b4f971mainпосле #337: один mutation harness вместо двух2adc2e80cdbd0ffetch-depth: 0релизной джобе001f6fd3fe6f69ownlang/repro.py— авторитетный эмиттер (единственный новый модуль вownlang/, строго наблюдатель).rust/crates/own-shadow— половина порта.Слияние с #337: один mutation harness, а не два
#337 приземлился в
main, пока ветка была открыта, и обе стороны независимо добавилиscripts/mutate_campaign.pyиscripts/render_checkpoint_status.py. Разрешать это кнопкой «ours»/«theirs» значило бы оставить в дереве две расходящиеся оснастки, поэтому:База — оснастка #337, целиком. Её контракт строго лучше: схемно-валидируемые определения, якоря-регекспы с ровно одним совпадением,
expected_catchersна каждую мутацию, словарь исходов (compile-error/invalid-mutation/runner-error), контракт чистого дерева и provenance (sha256 определения + коммит прогона, проверяемый как предок HEAD). То, что нужно этому срезу, вошло обобщениями этой оснастки, а не вторым путём исполнения:layers— кампания может объявить явные команды вместо cargo-workspace: catcher'ы shadow-кампаний — это Python-харнесс плюс четыре cargo-таргета. Режимworkspaceне тронут и по-прежнему выводит слои изcargo metadata.__pycache__при каждой записи, раз общий раннер теперь правит.py. Чисто-cargo кампаниям docs(P-022): generate the cp4 census and mutation evidence from the tree #337 это было не нужно; следующей, которая тронет Python, уже понадобилось бы.render_checkpoint_status.pyсохраняет структуру #337 (fragments/check/write) и получает два фрагмента:p022-shadow-census.md(считает новыйtests/shadow_census.py— тот же паттерн, чтоtests/verdict_census.pyу #337) иp022-shadow-mutations.md— четыре кампании через тот жеsummarize(), что и cp4. Один интерпретатор на прогон. Оба фрагмента #337 воспроизводятся побайтно.tests/test_generated_docs.pyупразднён в пользуtests/test_checkpoint_status.py, который дополнительно гейтит, что каждое определение всё ещё якорится — отдельным именованным падением, чтобы содержимое фрагмента осталось независимым от HEAD (именно это позволяет записанному прогону пережить посторонний рефакторинг).Кампании перезапущены, а не перенесены. Старые результаты не несут provenance и объявленных catcher'ов, поэтому под общим гейтом они не были бы evidence. Все 63 мутации по-прежнему якорятся; все 63 пойманы.
Два дефекта, которые слияние вскрыло — и оба поймал гейт, а не глаз
mypy --strictосталось незаконченным.name = e.get("name")затеняло переменную внешнего цикла (за что mypy и ругался); переименовав только присваивание, я оставил три поздних использования читать устаревшее имя — включаяplan.pop(name, None), чья работа — убрать шесть domain-refusal контролей из множества захватываемых. Они остались, и харнесс попробовал захватить документ, чей смысл в том, что ни один движок не может его назвать. Honesty-control кампании отказался работать по красной базе — все четыре прогона объявлены void, ничего не записано. Все положительные проверки при этом проходили.python::non-zero exit with no FAIL line— имя старого раннера. Мутации были пойманы, и именно тем слоем, но правилоexpected_catchersиз docs(P-022): generate the cp4 census and mutation evidence from the tree #337 сообщает о мутации, пойманной не тем, что названо в определении: это и есть разница между evidence и зелёной галочкой.Один фикс вынесен наружу: джоба
build + test + packвowen-cli-release.ymlгоняет весь Python-набор на depth-1 checkout, и гейт provenance не может разрешить коммит записанной кампании. #337 выдалfetch-depth: 0джобе тестов вci.ymlпо той же причине; этот workflow триггерится по путям (в т.ч.ownlang/**), которых #337 не касался, поэтому наmainдыра осталась зелёной.Что доказано, и на чём
Все числа генерируются:
docs/generated/p022-shadow-census.mdиdocs/generated/p022-shadow-mutations.md. Ни одно не набрано руками.«Оба движка видели один вход» здесь означает ровно одно: совпадение канонической идентичности документа. Это слабее acceptance-инварианта #260 — см. первый пункт «Что НЕ измерено».
Классификация, ВЫЧИСЛЕННАЯ редуктором, а не подразумеваемая зелёной сборкой (scope = lowered + summaries, 9 редукций, 8
identical):Python-only 0 / Rust-only 0 / Changed 0 / Ordering-only 0 / Unexplained 0, плюс 2 наблюдения класса
status— оба границы, которые порт объявляет своим текстом ошибки (непортированный анализ обязательств; типизированная дверь).statusиprojectionсчитаются отдельно от четырёх контентных классов: ни одно из них не является различием в том, что движок вычислил.Счётчики слоя same-input остаются гейт-обеспеченными, а не вычисленными: порт утверждает поэлементное равенство идентичности и побайтное равенство каждого артефакта и трассы, поэтому ненулевой счётчик там не представим в зелёной сборке. Census перечисляет тесты-гейты поимённо, вычитывая их из исходников — и заодно здесь исправлен регексп, который спотыкался на
#[allow(...)] // комментарийи тихо недосчитывал один реальный гейт.Что НЕ измерено
then/else/whileвходят в шаг охватывающего оператора.serde_jsonразличаются; §4.2spec/OwnIR.mdдержит конформный документ внутри обоих.Решения владельца
Ledger:
docs/notes/p022-shadow-infra-owner-decisions.md.-0принято владельцем ретроспективно. Расхождение движков было стоп-условием: доложить, но не решать. Checkpoint 1 доложил и решил — в абзаце, который сам же отрицает наличие такого мандата. Исход утверждён; поправка процесса — обнаружение расхождения и было триггером остановиться.sha2принят, только дляown-shadow; границу держит существующий тестdag.rs.spec/Bridge.md§6 и строке 7a.Четыре finding'а — каждый закрыт решением о контракте, не подгонкой движка
Ни один референс не менялся, ни один порт не учили «соглашаться», ни один существующий голден не регенерировался.
1. Литерал
-0. CPythonjsonчитает его как целое0;serde_json— как float-0.0.Канонический домен сужен так, чтобы
-0в него не входил: форма, которая его хэширует, утверждала бы «оба движка видели один документ», пока движки держат разные значения. На стороне референса расхождение невидимо после разбора, поэтому домен держится на литералах — под исполняемым ledger'омdomain_refusalsиз шести документов, которые оба движка обязаны отвергнуть. Само сужение домена — решение владельца (D-2): по брифу здесь следовало остановиться и доложить.2. Схема трассы была неоднозначна. Два независимых прочтения адресовали вторую функцию с именем
Takeкакfunctions[Take~1]иfunctions[Take]~1. Оба верны — и референс был несогласован сам с собой. Правило сделано явным и единообразным: внутри скобки, везде.3. Захват нёс MOS-документ в чужом порядке ключей. Первая же редукция сообщила
changedо различии, которого нет ни у одной поверхности. Починено в захвате: каждый документ слоя несётся в порядке ключей, который фиксирует его собственная поверхность.4. Два редуктора расходились в том, что значит «одинаково».
dictв Python сравнивается без учёта порядка ключей иTrue == 1; типизированное значение порта различает и то, и другое. Референс получил явный_same.Ни один из findings 2–4 не всплыл бы при одной реализации.
Правила, которые соблюдены
ownlang/не менялся, кроме одного наблюдателя-проектораrepro.py— без изменения поведения (в слиянии у него поправлена только ссылка в docstring на переехавшую evidence-директорию).dag.rsявно; именованный тест утверждает, что ни один core-крейт — иown-bridgeтоже — от него не зависит.docs/proposals/README.md,spec/Bridge.md§6.Стоп-условия: одно сработало, и было отработано неправильно
Семантика вердиктов не менялась, схема OwnIR не менялась, голдены не трогались,
ownlang/не менялся сверх наблюдателя.Но стоп-условие «обнаружено расхождение движков → остановись и доложи, решение не принимай» сработало на
-0, и checkpoint 1 доложил и решил. Решение верное и теперь утверждено владельцем (D-2), но это ратификация постфактум, а не соблюдение брифа.CI
Ветка
claude/shadow-mode-infrastructure-0uv2n1, HEAD3fe6f69, база —mainпосле #337.CodeRabbit=successревью не является: его description — «Review skipped: manual review required for this OSS repository» (репозиторий ниже порога звёзд для автоматического ревью), submitted reviews на PR нет вообще. Зелёная строка с именем ревью-инструмента — не ревью, и цитировать её как независимую проверку нельзя.🤖 Generated with Claude Code
https://claude.ai/code/session_01Kh6wpfbBUNLbeCJZ263gYM