Skip to content

P-022 Stage 3: Rust is the public default engine (cutover, rollback gate, decision packet) - #359

Merged
PhysShell merged 21 commits into
mainfrom
claude/p022-stage3-rust-cutover-q0n2q5
Sep 18, 2026
Merged

PhysShell merged 21 commits into
mainfrom
claude/p022-stage3-rust-cutover-q0n2q5

Conversation

@PhysShell

Copy link
Copy Markdown
Owner

Что и зачем

P-022 Stage 3: the Rust core (own-cli ownir) becomes Owen's public default engine on all four launcher surfaces — owen, own-check.sh, own-check.ps1 and the Action — with Python as the explicit, tested rollback (--engine python / -Engine python / engine: python) and no silent fallback anywhere. The default is reachable because the own-cli binary now ships inside the Owen.Cli package (D6: packaged resolution, not discovery). Before the flip, the three Python-first hygiene tails #262 owed were closed in the reference first (invalid UTF-8 → rc 2 at byte parity; V1 to the JSON door; V2's top-level -0 refused, residual CLI-B2). The #262 decision packet is generated from a measurement ledger and held to its rules by a test. Performance is DEFERRED BY OWNER (#263 stays open). Stage 4 (Python distribution removal) is not in this PR.

Qualification, three points kept apart on purpose: the decision was established at f1d3681 (run 35312190310, 31/31); the implementation was re-qualified at b225436 (run 35320877147, 31/31) after a repository-hygiene commit untracked 223 files of Cargo build output under rust/target-fault/ (build output, never evidence); the terminal merge candidate is this PR's head 1bf17e4 (run 35322222621, 31/31), a reconciliation-only commit. The SHA that is merged is the SHA that was qualified.

Тип изменения

  • feat — новая возможность
  • fix — исправление бага
  • docs — документация
  • refactor / chore / test / ci — без изменения поведения

Как проверено

  • python tests/run_tests.py — full suite green locally on b225436 (this tree minus the documentation-only record commit)
  • ruff check . и mypy — clean locally on 1bf17e4
  • python tests/test_stage3_packet.py and python scripts/render_checkpoint_status.py --check — green on 1bf17e4
  • the full ci.yml matrix, 31 jobs, on each of the three qualification points (links in the packet below)

Связанные issue

Refs #262 (P-022 step 8, the cutover; stays open for Stage 4) and #250 (the roadmap). Closes nothing by keyword: the owner closes by hand together with the body update, per #250's status-drift rule.

Чеклист

  • изменение покрыто тестом/селфтестом (или объяснено, почему нет) — tests/test_stage1_engine.py, tests/test_stage2_dogfood.py, tests/test_stage3_rollback.py, tests/test_stage3_cancellation.py, tests/test_stage3_surfaces.py, tests/test_stage3_packet.py, the own-cli parity replay, and the CI legs the packet names
  • README/docs обновлены при необходимости — README.md, docs/notes/p022-stage3-cutover-decision.md, docs/notes/owen-engine-rollback.md, docs/proposals/P-022-rust-core-migration.md, the generated packet
  • коммиты в conventional-commit стиле — the branch keeps the P-022 Stage 3: … subject convention every P-022 checkpoint uses

Acceptance packet (#250, Global PR acceptance packet)

Scope:                          #262 Stage 3, the Rust-default cutover: launcher engine selection on the four surfaces; D6 packaged resolution of own-cli inside Owen.Cli; the rollback contract and its tests; cancellation measured per platform; the Python-first hygiene tails (invalid UTF-8, V1, V2) with the Rust parity reconciliation they require; Action wiring (ACTION-BUILD); the Stage-3 evidence ledger and generated packet; status/doc reconciliation; the repository-hygiene commit that untracked rust/target-fault
Explicit non-goals:             no new analysis features; no P-037 production code; no ConsumesParam fix; no unrelated refactors; no performance optimization; no #263 implementation; no Stage-4 Python removal; no #345 or #257 work
Python source of truth:         ownlang (python -m ownlang) remains the oracle; every behaviour change here landed in the reference first (UTF-8, V1, V2 at the OwnIR input boundary) and Rust followed to byte parity
Frozen fixture:                 tests/fixtures/cli_ownir/ (Python-authored CLI replay, zero Python at steady state); the Stage-1/Stage-2 control campaigns and the Windows-native campaign under docs/evidence/ with their generated fragments; the shadow-compare corpus and the #260 sweep definition, re-taken on this tree (104/104, 1/1, 10/10 agreed)
Fixture regeneration command:   python tests/test_cli_ownir_fixtures.py --write; python scripts/mutate_campaign.py --campaign <definition> --run; python scripts/stage3_packet.py --write; python scripts/render_checkpoint_status.py --write
Steady-state test command:      python tests/run_tests.py; in rust/: cargo fmt --check, cargo clippy --all-targets, cargo test; the ci.yml matrix
Production dependency changes:  Owen.Cli ships the own-cli binary per supported platform (linux-x64, win-x64) and resolves it from AppContext.BaseDirectory; the Action installs a pinned Rust toolchain (1.98.1) and builds own-cli --locked from its pinned ref until the first published own-cli artifact (ACTION-BUILD, accepted by owner as a temporary distribution cost); Python is no longer required for a bare `owen check`
Behavior changes:               default engine Python → Rust on all four surfaces; explicit Python selection is the rollback; a broken Rust candidate with nothing asked is a visible exit 2, never a fallback; declared differences carried in the packet: WIN-ABC, CLI-B1, CLI-B2, V4, CANCEL-TRACEBACK (Linux-only), ACTION-BUILD; closed in the reference first: invalid UTF-8 rc 70 → rc 2, V1 to the JSON door, V2 top-level -0 refused; PS1-CAPTURE and the -notmatch assertion repaired as defects, not recorded as differences
Acceptance changes:             #262's acceptance is split into Stage 3 (met) and Stage 4 (open); the performance gates are deferred by owner, neither passed nor deleted; no fixture or acceptance was weakened
Local commands:                 python tests/run_tests.py; ruff check .; python -m mypy; python tests/test_stage3_packet.py; python scripts/render_checkpoint_status.py --check; git ls-files 'rust/target*' (empty)
GitHub Actions links:           decision        https://github.com/PhysShell/Own.NET/actions/runs/35312190310  (f1d3681, 31/31)
                                implementation  https://github.com/PhysShell/Own.NET/actions/runs/35320877147  (b225436, 31/31)
                                terminal merge  https://github.com/PhysShell/Own.NET/actions/runs/35322222621  (1bf17e4, 31/31)
Known deferred cases:           performance baselines (#263, deferred by owner for this decision); Stage 4 Python distribution removal (a separate PR after the observation period); ACTION-BUILD until the first published own-cli/Owen.Cli artifact; BR-V5 on the protocol path (Python-first, blocks nothing); the three P-037 known-false-positive controls keep their current expectations until A1

Migration report (per #250; compare mode over the committed corpus, the C# samples and the #260 sweep, re-taken on this tree):

Python-only verdict count:        0
Rust-only verdict count:          0
Changed verdict count:            0
Ordering-only difference count:   0
Unexplained difference count:     0

🤖 Generated with Claude Code

https://claude.ai/code/session_01QfeUtLRm6KFRWcTGQZxQQb


Generated by Claude Code

…tion

Phase 0 of the #262 Stage-3 public Rust-default cutover. This note is the
decision surface, written against the tree at base 70189a3 rather than from
prose memory, and committed BEFORE the implementation it authorizes.

What it records:

* the owner ruling that deferred performance evidence for this decision, kept
  as an amendment to #262's evidence policy and never as a claim that the
  measurements happened -- #263 stays open and stays the baseline tracker;
* every Stage-3 acceptance predicate of #262, classified as ALREADY SATISFIED,
  MUST CLOSE NOW, DEFERRED BY OWNER (performance only) or NOT A STAGE-3
  REQUIREMENT. Nothing outside the performance section is deferred;
* the starting state verified from the tree rather than assumed, including that
  all four launcher surfaces still default to Python today;
* D6 -- packaged resolution of the Rust core -- stated as the Stage-3 gap that
  RustCoreLocator.cs itself names and defers here, and defined as packaged
  resolution rather than discovery;
* the four distinct rollback states, so "Rust is broken" and "Python was asked
  for" are never conflated;
* the evidence labels used throughout, so DEFERRED EVIDENCE can never be read
  as PASS.

Verified while writing, not asserted: the V4 reopen predicate has not fired --
the supported Python matrix is still 3.11/3.12/3.13 and the Rust Unicode
snapshot is still unicode-properties 0.1.4.

No production behaviour changes in this commit.

Refs #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
#262 records three input-boundary defects of the Python REFERENCE that are
owed before the public Rust-default cutover. All three are closed here, in the
reference first, with the port reconciled to whatever the repair made true --
never the other way round, because repairing a divergence by changing the port
freezes the reference's accident into the contract instead of removing it.

Measured before (this tree, Linux), and every one of them reproduced:

  invalid UTF-8         py rc 70  "internal error"   rs rc 70
  NaN / Infinity / -Inf py rc  2  via the VERSION door
  top-level literal -0  py rc  0  ACCEPTED AS v0 AND ANALYSED
  nested -0             py rc  2  got [0]            (byte parity, must not move)

Measured after:

  invalid UTF-8         rc 2 both, BYTE PARITY
  NaN / Infinity / -Inf rc 2 both, JSON door both, CLI-B1 covers the detail
  top-level literal -0  rc 2 both, CLI-B2
  nested -0             rc 2 both, got [0]           unmoved

The third one was not cosmetic: until this commit a facts document declaring
`{"ownir_version": -0}` was silently analysed as v0 by the public default
engine.

Invalid UTF-8 is now byte parity rather than one more declared boundary. That
is a deliberate choice of WORDING, not a concession: #261 declined to make a
CPython exception's text a cross-language contract, and it was right to. So the
reference no longer speaks in its decoder's voice -- it states the fact about
the FILE, the first offending byte and its offset, which both implementations
can report identically. The declared difference disappears because the thing
that was different stopped being part of the message.

V2 keeps a declared boundary, CLI-B2, because reconciling it would mean
teaching one parser the other's reading of `-0` -- the reconciliation #262
ruled out and #260 froze a refusal for instead. What changed is the CLASS of
the difference: it was accept-versus-reject and is now the spelling of one
token inside a refusal both sides make, at the same exit code, at the same
door. CLI-B2 is guarded at least as tightly as CLI-B1: `Version` is otherwise
pinned byte-exact, so the kind alone is nowhere near enough to admit a case,
and eligibility additionally requires the literal top-level `-0` -- read from
the raw token via `RawValue`, because `serde_json::Value` reads `-0` and `-0.0`
into the same f64 and would let a case that needs no boundary borrow one.

Scope, asserted rather than described. The V2 repair is scoped by #262 to the
top-level scalar, so `_NegativeZeroInt` is an `int` subclass that compares,
hashes and formats exactly as 0: a `-0` anywhere else renders byte for byte as
it did before. Both scope controls are mutation-proved -- a marker whose repr
leaked `-0` and a repair widened to float negative zeros are each caught by
name. V3 rides the same `parse_int` hook, so it is re-asserted at both signs
rather than assumed still closed.

Evidence:
* 6 new frozen CLI cases, authored by executing the reference and replayed
  against the built binary with zero Python. No existing case moved -- the
  regeneration diff is additions only;
* tests/test_ownir_input_boundary.py: the reference side, through the
  production module entry point rather than only through load(), with the
  scope controls above;
* rust/crates/own-cli/tests/replay.rs: CLI-B2's eligibility guard and its
  scope control, plus a per-boundary sweep that fails if either boundary's
  last case disappears.

docs/evidence/p022-cli-1.json M24 is re-anchored: it patterns on a comment this
change rewrote. The recorded result stays valid for the commit it names; the
campaign is re-run for this tree in the next commit.

Refs #262, #250, #261

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
The hygiene-tail commit re-anchored M24, so the recorded result no longer
matched the definition it named. A stale run is not evidence for a changed
tree, so the campaign is re-taken rather than re-labelled.

p022-cli-1 @ b5d9272: 24/24 caught, 0 survived, 0 compile-error, 0 invalid,
0 runner-error; expected catchers missed: none.

M24 -- the control that proves CLI-B1's eligibility guard judges the bytes it
was handed rather than a file it reaches for -- is now caught by four tests
instead of one: the two boundaries' controls both fire on it, which is the
first evidence that CLI-B2 is wired into the same guard rather than beside it.

Generated fragments regenerated from the evidence; no count is typed.

Refs #262, #261

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
#262's ruling is that the Windows and Linux reference behaviour is MEASURED
first and that 130 is not invented as a universal contract. This control is
built around that distinction rather than around an exit code:

  INVARIANT   asserted on every platform, because the evidence supports it
  DISPOSITION measured and RECORDED per platform, never asserted

The invariants are the ones the cutover depends on. An interrupted engine must
terminate; it must never return 0 or 1, because the launcher reads those as
"clean" and "findings" and a cancelled run would be published as an answer
nobody computed; and it must leave no ok line or findings summary behind.

MEASURED, Linux, this tree (both engines interrupted while genuinely running):

  python reference   SIGINT -> died by signal 2 (SIGINT)  stdout 0b  stderr 1478b
  rust own-cli       SIGINT -> died by signal 2 (SIGINT)  stdout 0b  stderr 0b

Neither has an exit code at all in that state -- they die BY the signal -- which
is precisely why writing 130 into a contract would have been inventing one.

The 1478 bytes are the reference's KeyboardInterrupt traceback. That is a real,
user-visible difference the cutover changes (a cancelled run stops printing a
stack trace), so it is recorded for the packet's Known differences rather than
quietly fixed: Stage 3 changes the default engine, not the reference.

Windows offers neither the same mechanism nor the same disposition, so the
control sends the event a parent CAN deliver to a child group there
(CTRL_BREAK_EVENT), names it in the record, and leaves the number to the
platform. The Windows measurement is owed from CI and is not predicted here.

Two ways this control could have passed without earning it, both closed: an
interrupt delivered after the process already exited measures nothing, so the
workload escalates until it lands on a live process and FAILS if none does; and
a missing candidate is NOT APPLICABLE rather than a pass, with
OWEN_STAGE3_REQUIRE=1 turning that skip into a failure in a job that exists to
provide it.

No timing is asserted anywhere. The workload is sized to leave a window to
interrupt, not to measure speed; performance is deferred by owner for the
Stage-3 decision and nothing here may be read as a performance claim.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
RustCoreLocator.cs said it, in the Stage-1 text: "Stage 3's packaged resolution
is D6's problem, not this one's." It was the load-bearing gap. OWEN_RUST_CORE is
a DEVELOPMENT locator, so with Rust as the public default a user who sets
nothing -- which is every user -- would have got exit 2.

D6 closes it, and is careful to be packaged resolution rather than discovery,
because the whole of D3 turns on that distinction. Discovery asks the machine
where a binary might be and takes what it finds; the answer then depends on
PATH, on the working directory, on what else is installed. This asks nothing.
There is exactly one path, computed from the running assembly's own location
and this platform's key, and the file is either there or the run fails visibly.
A second candidate can never win because there is never a second candidate.

Precedence: an explicitly set OWEN_RUST_CORE still wins, with Stage 1's
semantics and diagnostics untouched. Only an ABSENT variable reaches the
packaged path; an empty or malformed one stays the error it was. A developer
measuring a specific build must not have the shipped binary substituted for it.

MEASURED on a real packed artifact, installed as a dotnet tool from an isolated
feed into a scratch directory, Linux:

  A  --engine rust, OWEN_RUST_CORE unset      finds OWN001, rc 1
  B  --engine rust, NO python on PATH at all  finds OWN001, rc 1
  C  --engine python, no python on PATH       rc 3, visible, no substitution
  D  packaged binary chmod 644 (the NuGet case) materialised to
     ~/.owen/rust-core/<sha256>/own-cli, runs, rc 1
  E  second run hits that cache
  F  packaged binary absent                   rc 2, and the diagnostic says
                                              Owen did not fall back to Python

B is the distribution gate #262 actually asks for: the packed public artifact
runs the Rust path with no undeclared Python runtime.

D is not hypothetical. A NuGet package is a zip and Unix file modes do not
reliably survive it, so a payload binary can arrive mode 644 and be unspawnable
however correct its contents are. The fallback materialises it into a
content-addressed cache and sets the execute bit there, which is the shape
CoreVendor already uses for the vendored Python core and for the same reasons:
the install directory may be read-only, and keying by the file's own SHA-256
makes a different binary a different path rather than an overwrite of one a
concurrent reader might be executing.

Packaging is staged EXPLICITLY through -p:OwenRustCoreDir and never globbed
from rust/target. If it were globbed, whether a package carried a Rust core --
and which profile's -- would be a function of whether someone had run cargo in
that checkout. A package's contents must not depend on the build machine's
history. A plain `dotnet build` with no property therefore packs nothing and
behaves exactly as before, which is also what keeps the Stage-1 locator
controls meaningful: they need an install that carries no candidate. Verified:
a clean build produces no rust-core directory, and all 18 Stage-1 engine
controls plus the 4 ps1 controls still pass.

The release workflow builds each platform's binary NATIVELY on its own runner
rather than cross-compiling from one. Cross-compiling would prove less: the
binary a Windows user runs would be one no Windows machine had executed, and
#262's distribution section exists precisely because "it built" and "it works
where it lands" are different claims. Each leg runs its own candidate before
staging it, and the pack asserts both platforms' cores are present, non-empty,
and that rust-core/ holds exactly two files -- an emptiness test would pass a
truncated payload, and a payload packed twice already happened here once.

Two packaging defects found by inspecting the built package rather than by
reasoning about it, both fixed and both recorded in the csproj so the tidier-
looking spelling is not restored: NuGet decides file-or-folder by whether a
PackagePath looks like it has an extension, so the extensionless `own-cli` was
packed at a doubled path while `own-cli.exe` was not; and adding an explicit
PackagePath on top of the publish closure packs every binary twice.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
The cutover itself. The four surfaces P-022 names move together, under one
contract, each in its own idiom:

  owen (EngineSelection.Default)   Engine.Python -> Engine.Rust
  scripts/own-check.sh             engine="python" -> engine="rust"
  scripts/own-check.ps1            $Engine = "python" -> "rust"
  action.yml                       engine input default "python" -> "rust"

MEASURED, not read off the diff. On a freshly packed artifact installed as a
dotnet tool into a scratch directory, with no Python of any spelling on PATH
and OWEN_RUST_CORE unset, a BARE `owen check` finds OWN001 and exits 1; and
`--engine python`, on the same install, finds the same OWN001. Both shell
surfaces were driven the same way: bare with a deliberately broken OWEN_PYTHON
(both succeed, so neither resolves Python any more), and bare with no candidate
(own-check.sh exits 2 and says, in as many words, that it did not fall back).

Python is not gone and is not hidden. It is the tested ROLLBACK and it remains
the reference. The Action still sets Python up unconditionally. Removing it is
Stage 4 and none of it happens here.

Two controls turned over with the claim, because they exist to catch exactly
this move happening WITHOUT authorization:

* stage-1 `default-stays-python` becomes `default-is-rust`, and is stronger
  than a renamed assertion. "Did not use Python" is not "used Rust": a default
  that became a no-op would satisfy the negative half. So the default is run
  twice -- once with Python deliberately unusable, where it must SUCCEED, and
  once with the candidate forced to panic, where it must fail on the RUST path
  (exit 5). Only a default that really routes to Rust does both.
* stage-2 `public-default-moved` becomes `public-default-is-rust`, with its
  falsifier inverted the same way, plus a new assertion that `python` is still
  an accepted spelling -- a cutover that quietly took the rollback with it
  would otherwise pass everything else.

The Stage-2 census classifies by ROLE, so it survives the cutover; class A's
text is updated because what a public-contract verifier verifies changed while
its job did not.

Five CI call sites would have gone GREEN WHILE PROVING NOTHING, and each is now
explicit about the engine it is actually about. They all inject a broken or
crashing PYTHON through OWEN_PYTHON/OWN_PYTHON and then invoke the surface
bare; after the cutover a bare invocation never consults those variables, so
the injected fault would simply not happen and every assertion would pass
vacuously: wpf-extractor's own.toml plumbing, the core-crash exit-5 step and
its --debug twin, the OWEN_PYTHON-not-found step, and the legacy OWN_PYTHON
deprecation step. This is the failure mode a cutover most easily hides behind,
so it is named here rather than left to be discovered.

The class-A jobs that invoke a surface bare now build the production own-cli
and select it, because that is what the public default resolves to; and
ownsharp-cli-smoke packs it into the package it installs, natively on each leg
of its ubuntu/windows matrix, which makes that job the Windows packed-path
evidence as well as the Linux one.

The Action builds the production own-cli crate from its own pinned ref, with a
cache keyed on that ref. It is not a developer adapter and never
own-shadow-engine or a stub. It is nevertheless a real cost to every consumer
-- a Rust toolchain and a first build -- and it is what is available today,
because this repository publishes no release: there is no own-cli artifact to
download and no published Owen.Cli package to install. Recorded in action.yml
rather than smoothed over, and it should be revisited the moment a release
exists.

Refs #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
#262 requires a documented, explicit, tested way back, and requires that it
never be automatic. Those are two claims and the second does not follow from
the first: a launcher with a hidden fallback passes any test that only ever
asks for Python on purpose, because it would answer correctly every time.

So the states are driven apart rather than described:

  1  default, candidate fine            -> Rust runs
  2  --engine python                    -> Python runs, and AGREES with 1
  3  candidate BROKEN, nothing asked    -> visible failure (exit 2), and the
                                          diagnostic denies a fallback in as
                                          many words
  4  candidate BROKEN, python asked     -> Python runs anyway

3 and 4 are one flag apart and are the pair that matters: a hidden fallback
makes 3 look like 4 -- the user gets an answer, from an engine they did not
choose, and nothing says so. 1 and 2 are what stop 3 passing for a launcher
that simply never worked. 4 additionally proves the way back does not depend on
the engine it is a way back FROM: the candidate is broken and the rollback runs
regardless.

State 2 also compares Python's finding against the Rust finding from state 1 on
the same sample. That is not a parity gate (#260 owns parity over the whole
matrix); it is the rollback-specific question of whether the way back leads to
the same place, because a rollback that answered something else would not be
one.

MEASURED: both runnable surfaces (own-check.sh and owen) hold all four states
on Linux. The Windows leg runs in CI, on the job that already builds the
candidate, the fault-injection binary and the launcher.

docs/notes/owen-engine-rollback.md is the canonical mechanism, in one place, and
says what rollback is NOT: not automatic, not a moved tag (a broken release is
corrected by a new patch release), and not Python's removal or deprecation --
that is Stage 4, and until then Python ships, works and is supported.

The Stage-3 cancellation control is wired into the same job, on both platforms,
so the Windows disposition gets measured rather than predicted.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
Caught by the Stage-1 controls on the full-suite run after the cutover, which
is what they are for: `bad-locator-is-2` listed `unset` among the INVALID
OWEN_RUST_CORE values and asserted exit 2 for it. At Stage 3 an absent variable
stopped meaning "no candidate" and started meaning "use the one this install
ships" (D6), so on an install that carries a packaged core that assertion was
asserting that D6 does not work -- and it duly failed with exit 0 and a verdict.

The control was wrong, not the behaviour, so `unset` moves out of the
invalid-locator list into a control of its own that asks the question which
actually survived the cutover: an unset locator must never produce a PYTHON
answer. Which side of that it proves depends on how the tree was built, so the
control MEASURES whether this install ships a packaged core rather than assuming:

  ships one     -> an unset locator must produce a verdict, and must not have
                   gone through the development locator's diagnostic to do it
  ships none    -> exit 2 naming the packaging problem, denying a fallback

Both branches are verified here, the second by hiding the packaged core and
re-running. 19 Stage-1 engine controls pass, full Python suite green.

Before Stage 3 this case could not go wrong: the default was Python and the
locator was never consulted on a bare run. After Stage 3 it is consulted on
every bare run, which is exactly why it earned a control instead of a list entry.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…tree

The #260 sweep record was taken at 321ab8b. That predates #261's changes to
own-ir's strict door (+61/-11) AND this branch's hygiene repair to the
reference's OwnIR input boundary (+112/-6) -- both of which sit in the compare
path. The sweep's own contract permits reuse when the recorded commit is an
ancestor of HEAD, and it is, so the committed pair validates. It is still not
evidence for THIS candidate, and a cutover decision is exactly the wrong place
to spend a contractual allowance.

So it was re-taken, not reused. Same frozen definition, same pins, same driver,
all ten documents over six targets:

  ShareX .repo/.sln           MahApps.Metro .repo/.sln
  MaterialDesignInXamlToolkit AvalonEdit .repo/.sln
  ClosedXML .repo/.sln        examples

  10 extracted, 10 compared, 10 agreed, 0 diverged, 0 execution-failure,
  0 input-refused, 0 input-disagreement,
  0 declared-boundary and 0 ACCEPTANCE-UNEXPLAINED observations,
  recorded at b590bf4, adapter e28fde7a0c1d

The fast half was re-taken too: 104/104 on the committed corpus, 1/1 on the
extracted C# samples, and the compare driver's own controls (including the
raw-variant, negative and execution-failure ones) through the real adapter.

The sweep gate caught me getting it wrong once, which is the best argument for
it: I gave the `examples` leg a WindowsDesktop reference pack its definition
does not declare, and the record was refused with "the run measured something,
but not this". Re-extracted without it.

The record lands BESIDE #260's rather than replacing it, because they answer
different questions -- that one is #260's acceptance, anchored to a CI run;
this one is whether the same measurement holds on the tree that moves the
default. Its workflow_run_url is null and stays null: taken locally, a weaker
anchor than CI, recorded as such rather than dressed up.

Two more CI surfaces would have broken or gone hollow, found by auditing every
launcher invocation rather than by grepping for the ones I remembered:

* shadow-sweep.yml extracts through a BARE own-check.sh, which after the
  cutover exits 2 before writing a fact. Its extraction command is part of the
  sweep's frozen definition and is compared against it, so the candidate is
  supplied rather than the command changed -- which also keeps the extraction
  running the real public default;
* oracle.yml's cross-tool comparison against Infer# and CodeQL is a class-B
  site where the reference IS the instrument, and a bare call would have
  silently changed which engine the oracle is of. Now explicit.

tests/test_stage3_surfaces.py turns that audit into a control: 59 launcher
invocations over 11 workflows, each either naming its engine or living in a job
that can resolve the public default. It also refuses the subtler shape -- a
step that injects a broken Python and then invokes BARE while asserting the
injection took effect, which since the cutover can only pass by proving
nothing. It distinguishes that from the legitimate inverse (inject, invoke
bare, assert it is IGNORED) by reading the step's own assertion rather than its
environment, because those two look identical from the invocation line. Both
failure modes are mutation-proved against the real defects they were written
for.

Refs #262, #260

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
Seven jobs failed on b590bf4. Every one was a real consequence of the default
moving, and none of them was a flake:

* cargo fmt on a line of replay.rs I wrote;
* ownsharp-cli-smoke, BOTH platforms: "expected a fresh ~/.owen unpack". The
  vendored-Python-core cache steps invoke `owen check` bare, and only the
  PYTHON engine unpacks ~/.owen/core -- after the cutover those assertions are
  about a directory nothing wrote. Six such invocations are now explicit;
* corpus-benchmark: scripts/benchmark.py shells out to own-check.sh from
  PYTHON, so no amount of reading the workflows would have found it. It is a
  class-B site in the Stage-2 census, where the reference IS the instrument and
  "changing the engine underneath silently redefines" the regression-pinned
  recall number -- so it is pinned to --engine python rather than handed a
  candidate, because handing it a candidate would have turned a red job green
  while changing what the number means;
* stage1-engine on WINDOWS -- see below;
* the Windows mutation/ps1 legs, downstream of the same two causes.

The Windows failure is the one worth reading twice. My own rollback control
reported that the rollback "answered differently from the default's baseline",
and it was right about the bytes and wrong about the question: it compared raw
stdout between the Rust default and the Python rollback, which on Windows
differ because the reference emits cp1252 and CRLF while the Rust core emits
canonical UTF-8. #262 records that as a deliberate behaviour change and states
that native-Windows Python parity is NOT claimed. So the control was asserting
the single claim the ruling says is not made -- and it could only have been
caught on Windows. It now compares the VERDICT (exit code plus the finding
codes), which is what a rollback actually has to preserve; the byte-level
relationship between the engines remains #260's compare matrix's business.

scripts/mine.sh keeps following the public default and its workflows now build
a candidate: pinning the corpus miner to --engine python would freeze its
reports to an engine users no longer run.

tests/test_stage3_surfaces.py grew to cover scripts, which is where two of the
three breakages hid, and fighting it honest was instructive. Four ways it could
have passed while missing them, each found by mutation and each closed:

  * it read only workflows, so a call site built with os.path.join was invisible;
  * comments count as code, so a matcher found "--engine python is EXPLICIT" in
    the prose after a mutation removed the flag from the argv;
  * prose in ordinary strings looked like invocations, so an error message about
    a missing own-check.sh was reported as a call site;
  * the "follows the default on purpose" escape hatch matched anywhere in a
    file, so one comment exempted every call site in it.

Both script-side rules are now mutation-proved against the real defects.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…m memory

A decision packet is a form, and a form invites being filled in at the end from
what someone remembers. So the packet is a DERIVED artifact: a hand-assembled
ledger of measurements (docs/evidence/p022-stage3-cutover.json) that names for
each row what was asked, where it was taken and what it returned, and a
generator that reads it. No number in the packet is typed.

The performance fields are the exception that proves the rule. They print
exactly "DEFERRED BY OWNER — NOT MEASURED" and no number, and they are NOT
deleted: #262 asked for them, #263 still owes them, and a packet missing the
line is a packet nobody can notice is missing it.

tests/test_stage3_packet.py holds the ledger to the rules that make the
derivation worth anything. Each rule is named for the misreading it stops, and
each is mutation-proved:

  deferred-stays-deferred     softening a deferral to "no material regression"
                              is caught -- a deferral is not a measurement, and
                              that includes "n/a" and "no change"
  no-performance-claim        performance language anywhere outside those
                              fields is caught, so a timing met incidentally
                              cannot be promoted into a claim
  windows-not-inferred        filling an OWED Windows row with the Linux answer
                              beside it is caught
  closure-is-not-a-difference listing a bug this change CLOSED as a standing
                              known difference is caught -- #262 says record
                              the closure separately
  required-differences        dropping a ratified difference (WIN-ABC, CLI-B1,
                              V4) is caught
  stage-4-not-now             Python-removal timing must say Stage 4 / later
  packet-is-generated         the committed packet must match what the ledger
                              produces now

The ledger records six known differences and four closures, kept apart on
purpose. Three of the differences are new and none of them is a regression:
CLI-B2 (a NARROWING of #262's V2, from accept-versus-reject down to one token's
spelling inside a refusal both sides now make); CANCEL-TRACEBACK (an
interrupted Python reference prints a 1478-byte KeyboardInterrupt traceback and
the Rust core prints nothing, so a cancelled run stops printing a stack trace);
and ACTION-BUILD, which is a cost rather than a difference -- the Action builds
own-cli because this repository publishes no release.

Four Windows rows are labelled DEFERRED EVIDENCE and say OWED. They are owed
from CI and are not predicted here, which the control enforces in both
directions: an OWED row must carry that label, and a labelled row must not
quietly acquire a result.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…verted

The Windows Stage-2 gate went red because three of these campaigns' mutants had
quietly stopped being mutations. They mutate the public default from Python to
Rust -- "the covert Stage 3", as S06's description put it -- and Stage 3 made
that the shipped state, so the edit is now a no-op and the pattern no longer
matches anything.

What the mutants are FOR did not change: one edit to one constant moves what
every user gets. So they are INVERTED rather than deleted -- they now mutate
Rust back to Python, the cutover silently reversed, caught by the controls that
turned over with it (stage1::default-is-rust, stage2::public-default-is-rust).

A fourth, S04, became ambiguous rather than inert: it anchors on the dogfood
job's own `core="..."` line and a step I added elsewhere in ci.yml spelled an
identical line, so the pattern matched twice. My variable is renamed; the
mutant keeps its anchor.

The reason this hid until a Windows job ran is worth more than the fix. The
anchor gate's DEFINITIONS list covered the cp4b, cp5, shadow, coord and cli
campaigns and NOT the Stage-1 and Stage-2 ones, so nothing in the ordinary
suite ever asked whether those definitions still applied to the tree. They are
in the list now. A campaign nobody re-anchors is a campaign nobody notices has
stopped applying, and this one had stopped applying in the most flattering way
available: its mutants could not fail.

The recorded results are now stale by construction (the definitions' sha256
moved) and the gate says so. Both campaigns are re-run in the next commit;
this one is the definition change and needs a clean tree to re-run against.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
Four surfaces carried "Python is the default" in their own words, and a cutover
that moved the code without moving them would leave the repository telling
users something untrue in four places:

* docs/proposals/P-022-rust-core-migration.md -- the header status and the
  step-8 row;
* docs/proposals/README.md -- the P-022 index entry;
* README.md -- the quickstart, which told a reader to clone and run
  own-check.sh and would now have them hit exit 2;
* docs/notes/p022-stage3-cutover-decision.md -- the decision surface itself.

All four now say the same thing, precisely:

  Stage 1    DONE
  Stage 2    DONE
  Stage 3    DONE -- Rust is public default
  Stage 4    NOT STARTED -- Python distribution removal remains separate

and none of them says P-022 is complete, because it is not: Stage 4 is open and
#263 still owes the baselines this decision deferred.

The README change is the one a user actually feels. Running the script from a
checkout now needs either a built candidate or `--engine python`, so it says
both, in the two commands that work, and says why there is no third option: no
discovery and no fallback, an unusable locator is a visible exit 2. It also
states the distinction that makes the packaged CLI different -- `owen check`
ships its own Rust core and needs neither an environment variable nor Python.

Refs #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…tants

p022-stage1-1 @ f988c8e: 21/21 caught, 0 survived, 0 compile-error, 0 invalid,
0 runner-error; expected catchers missed: none.

M01 is the one that matters. It used to mutate the default Python -> Rust and
was caught by `default-stays-python`; the cutover made that the shipped state,
so the mutant had become a no-op that could not fail. Inverted, it now mutates
Rust -> Python -- the cutover silently reversed -- and is caught by
`default-is-rust`, which proves the new control is load-bearing rather than
merely renamed.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…e ruling

OWNER RULING: ACTION-BUILD is ACCEPTED for Stage 3 as a declared TEMPORARY
distribution cost -- not a parity difference, not a semantic difference, not a
blocker. The contract is intact: the default is rust, Python stays the explicit
rollback until Stage 4, the Rust toolchain is installed by the Action rather
than assumed, and what is built is the production own-cli crate, never
own-shadow-engine or a test adapter. It is simply a heavy way to deliver a
binary -- a consumer today pays setup-python, setup-dotnet, setup-rust and a
cargo build to run a static analyzer. EXIT CONDITION: the first published
own-cli/Owen.Cli artifact, after which the Action downloads an immutable
platform binary. That is a post-Stage-3 packaging follow-up and deliberately
NOT Stage 4, which is about the Python distribution dependency.

Accepted, but the build had to be made to mean one thing over time. Three of its
four inputs were pinned and the fourth was not:

  source        pinned by the action ref the caller writes
  dependencies  pinned by rust/Cargo.lock ... only with --locked, which was absent
  actions       pinned by SHA
  rustc         FLOATING on `stable`

A caller who pins PhysShell/Own.NET@<tag> is entitled to have that tag mean one
thing. With a moving channel the same tag builds with whatever rustc shipped
that month, so a future release that compiled the crate differently -- or
refused it -- would change the behaviour of a revision nobody touched. rustc is
now pinned to 1.98.1, the concrete stable the qualification CI already runs, and
the build is `--locked` so an unchanged source revision cannot silently resolve
a different dependency graph. Verified: the locked build succeeds on this tree.

The cache-key comment claimed more than the key delivers, and the claim was
worth more than the key. It said a caller who bumps the pinned version gets a
rebuild rather than a stale binary; `github.action_ref` is whatever the caller
wrote, so a moving major tag like `v1` keeps ONE key across every commit it ever
points at. It is not a correctness bug -- Cargo rebuilds a local crate whose
sources changed, cache hit or not -- so the claim is withdrawn rather than
propped up with a digest-identity gate nobody needs: the cache is an
optimization and the Cargo build is authoritative.

Both properties are asserted, not just commented: tests/test_stage3_surfaces.py
refuses a floating channel and a build without --locked, and both are
mutation-proved. That control also learned, for the third time in this file,
that comments are not code: the prose justifying these pins contains the phrase
"cargo build" and was duly reported as an unlocked build.

Evidence recorded with it: `packaging-windows` moves from OWED to MEASURED. The
`owen CLI (gate A)` job on windows-latest concluded success at 2ecf501 carrying
the unconditional Stage-3 assertion that a bare `owen check` with an unusable
OWEN_PYTHON still finds OWN001 -- under `bash -e` with an explicit `exit 1`, so
success is that step passing. Nothing is carried over from Linux; that is
windows-latest reporting on itself. A new row records what the same run proved
about the Stage-1 controls there: 19 passed natively, including default-is-rust
and an unexpected child status mapped to exit 5 with the raw Windows-native
status retained. rollback-windows and cancellation-windows remain OWED.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…caught

p022-stage2-1 @ 0d6686e: 11/11 caught, 0 survived, 0 compile-error, 0 invalid,
0 runner-error; expected catchers missed: none.

S06 and S07 are the pair the cutover had turned into no-ops -- the product
default and the Action's engine input flipping Python -> Rust, which Stage 3
made the shipped state. Inverted, they now flip Rust -> Python and are both
caught by `stage2::public-default-is-rust`, which is what makes that control's
rename load-bearing rather than cosmetic: the same mutant that proved the old
claim now proves its inverse.

The anchor gate went from 16 definitions to 19. The three it gained are the
Stage-1 and Stage-2 campaigns whose absence let this drift hide in the first
place.

Generated fragments regenerated from the evidence; no count is typed. Full
Python suite green, ruff and mypy clean.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…troduced

The qualification round found a real defect in a product surface, which is what
a qualification round is for. 30 of 31 jobs were green; the one failure was
own-check.ps1's exit-code tiers, reporting "expected OWN001 in the annotated
output" over output that visibly CONTAINED OWN001.

It was not a harness fault. PowerShell's call operator routes a child's stdout
through its PIPELINE, and that is what makes

    $out = & ./scripts/own-check.ps1 -Format github -Paths src

capture anything at all. The Python branch uses the call operator. The Rust
branch spawned a process that inherited the console handle and so bypassed the
pipeline entirely. While Python was the default nobody noticed; the moment Rust
became the default, every caller capturing or piping this script silently got
NOTHING -- and because the text still appeared on screen, it looked fine.

The in-code comment asserted the opposite, and that is where the error lived:
it said an inherited handle leaves the child's output "live, exactly as the call
operator's did". The first half is true and the second half is false.

MEASURED both ways on one tree, and it is not Windows-specific -- it reproduces
on Linux pwsh; the Windows job is simply the only place own-check.ps1 is
exercised with output capture:

  before   -Engine python captured 1 line with OWN001
           -Engine rust   captured 0 lines
  after    both capture 1 line with OWN001, and the captured arrays are IDENTICAL

The candidate's streams are now redirected and replayed by the caller: stdout to
the pipeline, stderr to the error stream, decoded as UTF-8 with NO BOM -- a byte
order mark in front of a SARIF document or a workflow command is not a decoding
detail, it is corrupt output -- and both pipes drained CONCURRENTLY, because
reading one to the end and then the other deadlocks as soon as the child fills
the pipe nobody is reading, which for a large SARIF log is not hypothetical.

Nothing else moved: exit tiers still 1 with -FailOnFinding and 0 without, SARIF
still 1413 bytes from either engine with no BOM and one result, and the four
Stage-1 ps1 controls still pass. Recorded as a CLOSURE, not as a known
difference: an accidental behaviour change beyond the engine is what #262's
guardrails forbid, so it is repaired rather than declared.

Also recorded from the same run, measured and not predicted:

* rollback-windows PASS -- all four states distinct on both surfaces;
* cancellation-windows PASS, and the disposition vindicates refusing to invent
  130. Both engines on Windows EXIT with code 3221225786 (0xC000013A,
  STATUS_CONTROL_C_EXIT); both on Linux DIE BY SIGNAL 2 and have no exit code
  at all. Neither platform is 130, and a universal contract would have been
  wrong on both;
* CANCEL-TRACEBACK is now known to be LINUX-ONLY. Python prints a 1478-byte
  KeyboardInterrupt traceback there and nothing on Windows, where the default
  CTRL_BREAK handler terminates the process before Python's handler runs. A
  single-platform measurement would have got this wrong in either direction.

No OWED rows remain in the decision packet.

One item is knowingly left red, and named rather than worked around:
p022-stage1-windows's P07 mutates this very function back to the call operator,
so re-anchoring it to the new lines invalidates the committed result -- which
can only be re-recorded on a Windows runner. The workflow prints that result in
full for exactly this purpose, so it is taken from the next run rather than
guessed at here. Until then the checkpoint gate correctly says so.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…t could not fail

P07 is closed. The Windows-native campaign ran on a Windows runner against the
re-anchored definition and its result is taken from the log the workflow prints
for exactly this purpose -- not reconstructed, not guessed:

  p022-stage1-windows @ baf3771: 9/9 caught, 0 survived, 0 compile-error,
  0 invalid, 0 runner-error; expected catchers missed: none

Verified before installing it: the result's definition_sha256
(656c5e69e4ec...) equals the sha256 of the definition in this tree, the honesty
control M00 survived the unmutated tree, and P07 is caught by
`ps1::ps1-not-started-is-2`, the catcher its definition names. The checkpoint
gate is green and all 19 campaign definitions anchor.

The Windows dogfood leg then failed, and it is the same class of defect as the
regression that caused it -- a step that could not fail:

  if ($out -notmatch 'OWN001') { throw "the Rust core found no OWN001" }

On an ARRAY, PowerShell's -notmatch is a FILTER, not a boolean. Over a 166-line
capture where 137 lines carry OWN001 it returns the other 29, and a non-empty
result is truthy, so the assertion fires on correct output. The mirror image is
how it passed for the whole of Stage 2: while the Rust branch wrote to an
inherited console handle $out was $null, -notmatch filtered an empty collection
to an empty collection, that is falsy, and the step went green having captured
and verified NOTHING. So the OWN001 half of this gate has never actually held
on the Rust path.

`-not ($out -match 'OWN001')` is the idiom the rest of this file already uses,
and it is correct in both directions -- MEASURED: with the real 166-line capture
it does not throw; with an empty capture it does. It was the only instance in
any workflow.

Full Python suite green, ruff and mypy clean, checkpoint gate green.

Refs #262

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
Terminal candidate f1d3681, qualified by run 35312190310: 31/31 jobs green, 0
failures. Every Stage-3 predicate is confirmed on THAT commit rather than
assembled from earlier ones -- both Stage-1 control legs, both packed-artifact
legs, both dogfood legs, the Windows-native and Stage-2 Windows campaigns,
own-check.ps1's exit-code tiers, both shadow-compare gates, the Rust-default
code-scanning dogfood, rust fmt/clippy/tests, and the Python suite on
3.11/3.12/3.13.

  public default        Rust on owen / own-check.sh / own-check.ps1 / Action
  rollback              explicit --engine python, tested, four states distinct
  silent fallback       none
  compare               zero acceptance-unexplained
  Linux packed path     PASS      Windows packed path     PASS
  Windows rollback      PASS      Windows cancellation    PASS
  Stage-1 campaign      21/21     Stage-2 campaign        11/11
  Windows campaign       9/9      checkpoint gate         green
  decision packet       zero OWED rows
  performance           DEFERRED BY OWNER, #263 open
  Stage 4               NOT STARTED

The three qualification findings are recorded as DEFECTS FOUND AND CLOSED, not
as known differences. That distinction is the point: after the fixes they are
not permitted Stage-3 behaviour. They also form one sequence worth seeing whole
-- an invocation that could reach no engine, a capture that captured nothing,
and an assertion that passed BECAUSE the capture was empty -- each of which
looked perfectly respectable alone, and all three of which were invisible while
Python was the default.

PS1-NOTMATCH-ASSERTION is scoped precisely rather than loosely, because the
loose version would be untrue: it is the WINDOWS STAGE-2 DOGFOOD'S OWN001
OUTPUT-OBSERVATION assertion that had never constituted evidence on the Rust
path. The other Stage-2 controls and the actual Rust-default execution (exit
code, no-fallback, candidate identity) remain separately evidenced. Stage 2 as
a whole is not being called unproven.

Governance hand-off, recorded on the decision surface:

  P-037 A1 PRODUCTION GATE:  BLOCKED BY DESIGN  ->  UNBLOCKED

A1 is NOT started here and not in this PR. It begins on a new branch from the
Rust-default baseline, so that nobody has to work out later whether the first
ConsumesParam change belonged to the Rust migration or to a new inference
feature.

Refs #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…directories

`rust/target-fault/` is the build directory of the fault-injection candidate
(`cargo build -p own-cli --release --features fault-injection --target-dir
target-fault`, ci.yml). Commit 04c3303 tracked its contents by accident: 223
files and 57 MB of Cargo internals (.fingerprint, .rustc_info.json,
build-script outputs, compiled metadata), present in the qualified candidate
f1d3681 and rightly not a functional failure for the 31/31 qualification run,
which checks the program and not the tree's manners. Build output is not
evidence and is never versioned.

`rust/.gitignore` already ignores `target/`, which is why `rust/target/` never
entered the tree and why `target-fault` did: that pattern does not match a
sibling with a different name. Both directories are now ignored explicitly at
the root.

- every tracked file under rust/target-fault/ is removed
- /rust/target/ and /rust/target-fault/ are ignored in the root .gitignore
- `git ls-files 'rust/target*'` is empty

No functional change, and nothing in the tree read those files: ci.yml builds
the fault-injection candidate into that directory itself. The full
qualification CI runs on this head because the terminal candidate is the SHA
that goes to main, not its almost identical ancestor.

Refs #262

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfeUtLRm6KFRWcTGQZxQQb
… head

Run 35320877147 is 31/31 green on b225436, the head after the
repository-hygiene commit that untracked rust/target-fault. That makes
b225436 the implementation qualification point, and this record keeps three
points apart because they answer three different questions:

  decision qualification        f1d3681   run 35312190310   31/31
  implementation qualification  b225436   run 35320877147   31/31
  terminal merge qualification  CI on the reconciliation-only head that
                                carries this record; canonical SHA and run
                                recorded on #262

The decision stays where it was established: on f1d3681, every predicate
confirmed on that commit. The implementation candidate differs from it by the
reconciliation record 7622a3b and the hygiene commit alone -- no production
behaviour changed -- and the matrix was re-run on it rather than inherited,
because the SHA that goes to main is qualified as itself.

What this commit deliberately does NOT do is name the SHA that goes to main:
that SHA is this commit's own, and a commit cannot name its own SHA without a
fixed-point hunt. The ledger, the decision note and P-022 therefore describe
the terminal merge qualification as CI on this head, and the canonical
SHA/run live on #262 and #250, outside the tree.

Documentation only: the ledger (candidate_sha, the terminal-ci row, the
qualification points), the packet regenerated from it, the decision note's
terminal evidence and provenance figure, and P-022's queue sentence. Packet
gate and checkpoint gate green.

Refs #262, #250

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfeUtLRm6KFRWcTGQZxQQb
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 949ee5f9-7dda-45b4-a81f-81ec95174209


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants