Skip to content

feat(launcher): opt-in Rust engine behind the existing launcher (#262 Stage 1) - #349

Merged
PhysShell merged 22 commits into
mainfrom
claude/checkpoint-0-execution-ggdagx
Sep 9, 2026
Merged

feat(launcher): opt-in Rust engine behind the existing launcher (#262 Stage 1)#349
PhysShell merged 22 commits into
mainfrom
claude/checkpoint-0-execution-ggdagx

Conversation

@PhysShell

@PhysShell PhysShell commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Что и зачем

#262 Stage 1 — the Rust core becomes selectable behind the existing launcher, and nothing else moves. Python remains the default and the reference on every surface; nothing public defaults to Rust; Python distribution is untouched. This is explicitly not the cutover — that stays behind Gate G3.

Base is the Checkpoint-0 baseline 46a0d8a167bdfebd184763eb5589c7643083904c.

What lands, by ratified constraint:

  • D1 — one explicit launcher selector --engine python|rust|compare on all four production launcher surfaces (owen check, scripts/own-check.sh, scripts/own-check.ps1, action.yml). The Action exposes it as an engine input and delegates the semantics to own-check.sh rather than re-implementing them, so the real fan-out is smaller than four and there is one contract to keep true. own-cli gets no selector (C-4).
  • D3/D3.1 — Rust/compare resolve the candidate from OWEN_RUST_CORE and nothing else: no PATH discovery, no rust/target probing, no "first binary found". A missing, empty, nonexistent, non-file or non-executable locator, detected before the selected core starts, is a configuration error — public exit 2, one actionable diagnostic, never a fallback.
  • D5 — an unexpected Rust child status takes Owen's internal-error path (5) with the raw status retained in the diagnostic report's typed nullable child_exit_code; report schema bumped to 2. 70 is the engines' shared internal-error code: known, still a failure, never read as findings or clean.
  • D4/D4.1 — compare extracts once, captures the OwnIR bytes once, materialises each engine's input from that single value and re-hashes both against it before either engine starts. Streams are compared as raw bytes, not decoded text. Agreement exposes the reference's result; divergence and execution failure both exit 5 with an actionable diagnostic plus reproduction evidence; a zero-document compare fails rather than reporting agreement over nothing.

The ordering finding this stage repairs. CheckCommand.cs resolved Python and unpacked CoreVendor unconditionally, because there was only ever one engine. Engine selection now happens before any engine-specific resolution, and a Rust-only run resolves no interpreter and unpacks no Python core.

No merge authority is claimed. Implement, drive the exact head to a fresh PASS, stop. Merge only under a separate owner instruction bound to the exact reviewed head SHA. A green PR is not merge authority, and a green Stage-1 PR carries no Stage-2 authority.

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

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

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

The default did not move, and the Rust path needs no Python — measured, not asserted. The sharpest evidence is a single pair of runs against a deliberately broken interpreter:

$ OWEN_PYTHON=/definitely/not/a/python owen check --engine rust <sample>
<sample>/Leak.cs:8: error: [OWN001] IDisposable local 's' is never disposed (leak)
1 finding.
rc=0                      # Rust ran; no interpreter was resolved, no vendored core unpacked

$ OWEN_PYTHON=/definitely/not/a/python owen check <sample>
owen: OWEN_PYTHON='/definitely/not/a/python' did not resolve to Python >=3.11
rc=3                      # the default is still Python, and still fails on Python

The Rust run additionally succeeded on a build with no vendored ownlang-core on disk at all — the pre-Stage-1 launcher could not have.

All eighteen shared adversarial controls pass on BOTH platforms, 0 skipped; 0 not applicable on Linux and 1 on Windows (tests/test_stage1_engine.py, no fail-fast, OWEN_STAGE1_REQUIRE=1; the one N/A is bad-locator-is-2/shell-non-executable, explained below), plus four PowerShell controls (tests/test_stage1_ps1.py) that put own-check.ps1 in the adversarial set for the first time.

ok[bad-locator-is-2]           invalid-locator cases all exit 2, no fallback: 20 on Linux, 18 on
                               Windows, where the shell's non-executable subcase is N/A
ok[absolute-locator-only]      a relative but EXISTING locator is refused, before any extraction
ok[locator-shapes]             8 locator shapes classified as this platform defines them, and
                               every implementation available here agrees
ok[compare-failure-classified] a Python-only failure is execution-failure, not divergence
ok[no-selector-in-own-cli]     own-cli accepts no engine selector, reads no engine environment
ok[default-stays-python]       the default still resolves Python and fails on it (exit 3)
ok[rust-actually-runs-rust]    Rust ran and produced the verdict with Python unusable
ok[rust-failure-no-fallback]   a forced Rust failure produces no verdict on either surface
ok[rc70-is-not-a-verdict]      a Rust child rc 70 takes the public internal-error path (exit 5)
ok[unexpected-rc-maps-to-5]    an unexpected child rc maps to public exit 5
ok[raw-rc-retained]            the raw status is retained as a typed child_exit_code, schema 2
ok[compare-extracts-once]      compare extracted exactly once
ok[compare-same-input]         the candidate received exactly the attested capture
ok[compare-zero-document]      a zero-document compare fails (exit 5) and says so, every surface
ok[divergence-is-5]            a divergence in the bytes alone is exit 5 + reproduction evidence
ok[exec-failure-is-5]          a compare execution failure is exit 5 + failure evidence
ok[compare-no-substitution]    no engine's answer was exposed on divergence or on failure
ok[candidate-identity]         the evidence records the candidate's sha256 and byte length

ok[ps1-absolute-locator]       a relative locator is refused, and the absolute shapes this
                               platform defines are accepted
ok[ps1-not-started-is-2]       an existing-but-unstartable candidate is exit 2 on both paths
ok[ps1-agreement-replays]      agreement replays the reference's RAW BYTES on both streams
ok[ps1-failure-evidence]       the advertised reproduction evidence survives the process

The Windows N/A count fell from four to one. tests/helpers/stage1_stub.rs is a committed, controllable native candidate compiled with plain rustc — no cargo crate, so #261's crate-edge DAG gate is untouched — and it replaces the shebang stubs that had made four compare controls unaskable on Windows. That gap is precisely why three PowerShell defects survived a 16/16 campaign. The one remaining N/A is bad-locator-is-2/shell-non-executable: git-bash runs a mode-644 file with a shebang, so a non-executable candidate cannot be built for the shell surface there; the launcher half of that case does run.

Failures are forced through #261's own off-by-default fault-injection feature against the real production binary, not a mock. compare-extracts-once is behavioural: a dotnet shim on PATH counts extractor invocations, and the count must be exactly 1.

Two mutation campaigns, recorded separately because they are not the same measurement. Both were taken on a clean tree at da897b42fcd7, with every expected catcher hit and the honesty control M00 surviving the unmutated tree:

campaign where it ran mutations result
p022-stage1-1 ubuntu 21 21/21 caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error
p022-stage1-windows windows-latest 9 9/9 caught, same

Nothing is added across those rows. A mutant evaluated where its contract cannot be posed is decorative, so the Windows campaign runs the mutants whose contract only Windows can answer: eight target own-check.ps1, and the ninth targets the drive-rooted arm of own-check.sh, which no Linux catcher can reach. Counts are generated into docs/generated/p022-stage1-mutations.md, never typed.

The campaign earned its keep rather than rubber-stamping. Its first run caught only 11/16 and named the hole precisely: the compare and locator controls drove own-check.sh but never the owen launcher, so every C#-side misreading was invisible. Fixing that also exposed a real production inconsistency, where own-check.sh named the input digest on stderr while the launcher printed only the evidence file's path. A second round showed compare-no-substitution was too weak — D4.1(b) exposes neither engine's verdict on divergence, so "Python is the reference, trust it" is the same failure as trusting the candidate; the control was strengthened rather than the expectation lowered.

  • python tests/test_stage1_engine.py — 18/18, 0 skipped, 0 N/A on Linux; 18/18 with 1 N/A on Windows
  • python tests/test_stage1_ps1.py — 4/4, 0 skipped, 0 N/A on both platforms
  • ruff check . — All checks passed; mypy --strict ownlang — Success
  • python scripts/render_checkpoint_status.py --check — fragments in sync
  • python scripts/mutate_campaign.py --campaign docs/evidence/p022-stage1-1.json --run — 21/21
  • the same runner on docs/evidence/p022-stage1-windows.json, on a Windows runner — 9/9
  • python tests/run_tests.py — clean
  • bash -n on both shell scripts; PowerShell AST parse on own-check.ps1; YAML parse on ci.yml and action.yml

What the Windows leg found

Getting the Windows leg green took six rounds, and it earned its place. Three genuine product defects, none of them reached by the Linux evidence and gates that existed before it. That is the claim, and it is narrower than "Linux could not have found them": defect 3 below is PowerShell semantics, which a Linux pwsh leg under a strict caller could in principle have caught. No such leg existed.

  1. A latent pre-existing defect. The repository had no .gitattributes, and this is the first job that has ever run own-check.sh on a Windows runner (the three existing users are ubuntu-only). A Windows checkout rewrote it to CRLF and git-bash could not run it. Fixed with a deliberately narrow *.sh text eol=lf — a blanket * text=auto would put this repository's byte-sensitive evidence at the mercy of a checkout setting, and P-022 step 7a: add dual-engine shadow mode and zero-diff reproduction artifacts #260's wider .gitattributes tail stays untouched.
  2. A real gap in D3.1's seam. Windows has no execute bit, so a non-runnable candidate passed the locator, was spawned, and died as exit 5. D3.1 draws its line at starting: a candidate that never started is a configuration error. Spawn failure now maps to exit 2 on the launcher (a typed exception) and in the shell (bash's 126/127).
  3. own-check.ps1 emitted PowerShell ErrorRecords via Write-Error. A caller running $ErrorActionPreference = 'Stop' — exactly how GitHub's pwsh shell runs — turns those into terminating errors in the caller, despite the script having handled the condition and exited deliberately. It now writes stderr text, as own-check.sh always did. Verified locally under a strict caller.

One documentation error of mine, corrected by measurement. I had written that Windows compare diverges "for non-ASCII output". It diverges on every run: the Python reference writes CRLF where Rust writes LF, so pure-ASCII findings already differ byte for byte. Both the launcher README and the CompareMode contract comment now say so, and separate line endings (all output) from encoding (non-ASCII only). This was only visible because compare captures raw bytes — a string comparison through one .NET encoding would have normalised CRLF away and reported agreement, hiding a declared difference behind a green control.

Three further causes were harness bugs, fixed in the controls: subprocess.run(["bash", ...]) on a Windows runner resolves to WSL's bash.exe stub in System32; a child status is spelled three different ways across runtimes (-6 / 134 / 3221226505 / -1073740791); and GitHub's pwsh wrapper appends exit $LASTEXITCODE, so a step inherited the exit code of a case it was written to require.

CI gains a stage1-engine job on ubuntu and windows: both platforms because the ratified evidence is explicit Rust-selected runs on each, and they differ in exactly the mechanics (process launch, executable bits, path forms) a Linux-only job leaves unproven. The Windows leg additionally drives own-check.ps1's engine contract, which no Linux leg reaches.

The REQUEST CHANGES repair

Five production defects were reported, all five verified against primary source before anything was touched, all five real. A sixth was found while proving the fourth: Start-Process -RedirectStandardOutput is not byte-faithful — measured on one input, the Python reference wrote 211 bytes and the redirected file held 210, a blank line silently dropped. Compare claims the two engines' public bytes are identical, so a lossy capture of the reference can manufacture a divergence that does not exist or hide one that does.

The review also required that a mutant targeting own-check.ps1 be killed by a Windows catcher, since a Linux leg cannot pose the question. Honouring that turned up three more defects that no Linux control could have seen, and each one is an argument for the rule rather than a compliance step:

  1. The shell's absoluteness test matched nothing. My own fix for defect 1 wrote the drive-rooted arm as [A-Za-z]:[/\]*. Inside a bracket expression the shell's matcher treats the backslash as an escape, so \] escaped the closing bracket, the set was never terminated, and the arm matched neither C:/ nor C:\. Every correct Windows configuration was refused as "not an absolute path". Nothing caught it because every assertion asked one direction — is a relative locator refused? — and a validator that refuses everything answers that perfectly. locator-shapes now asserts both directions across every implementation on the host, and fails when two of them disagree about a shape.

  2. own-check.ps1 asked the platform to OPEN its candidate, never to run it. PowerShell's call operator does not spawn a program. On Linux a non-runnable candidate went to xdg-open; on the Windows runner it opened in notepad, and the job's own cleanup line terminated it. Either way the run exited 0 with empty streams: a clean, finding-free analysis of nothing, which is worse than any wrong exit code. D3.1's seam was unreachable on both platforms, so the try/catch added for defect 3 could never fire. The candidate is now started with UseShellExecute = $false, and the control that had been declared Linux-N/A is required on both platforms again — that N/A was describing a bug in this script, not a property of Linux.

  3. A campaign definition hashed differently on the two platforms. core.autocrlf=true is the Git for Windows default, so the runner checked the definition out with CRLF and the recorded sha256 read the checkout setting rather than the campaign. A run that measured exactly the right tree was filed as evidence for a different one, and the gate was right to refuse it. docs/evidence/*.json text eol=lf pins the bytes that are the contract.

Two harness rules changed with them. A control that raises is now that check's failure rather than the suite's: an unhandled exception had aborted the PowerShell suite mid-run, so the two controls after it never ran and the campaign saw one nameless failure where it needed a named catcher. And a voided campaign now names the checks that voided it, which took two CI rounds to establish by hand while the runner had the names all along.

The second REQUEST CHANGES round

Two proof gaps and two truth-surface contradictions, all four found by review against primary source rather than by anything here.

  1. The stderr half of "both streams" was never compared. The agreement control guarded its stderr assertion with if ref.stderr, and a healthy python -m ownlang ownir writes none — CI printed 184 out, 0 err on Windows and 160 out, 0 err on Linux while the control reported it had checked both. A guard that switches an assertion off exactly when the data is ordinary is a hole with a denominator of zero. The fixture is now built rather than found: own-check.ps1 resolves the reference as a bare python, so a shim directory whose python is the committed native stub makes the reference's bytes choosable, and both engines then agree over a fixture whose stderr is deliberately non-empty (CRLF a text pipeline would rewrite, multi-byte UTF-8 a re-encode would normalise). The old real-reference fixture is kept beside it — only that one proves the replay is faithful to the actual implementation. P03 is now scoped to stdout and P08 removes the stderr replay alone; measured separately, they die for different reasons under the same catcher.

  2. The PowerShell surface never proved D3's preflight POSITION. The shared harness counts extractor invocations, but its counter is a #! script installed only off Windows — so on the one platform whose campaign is the record there was no counter at all, and a validator that drifted to after extraction would still exit 2, still name the absolute requirement, still emit no verdict. There is a counter on both platforms now (a .cmd on Windows, since own-check.ps1 reaches dotnet through the call operator and that honours PATHEXT), and P09 is the mutant it exists to kill. It counts extractions, not every dotnet invocation, and that distinction was measured rather than assumed: where pwsh is installed as a dotnet global tool, merely starting the shell invokes dotnet twice, so a count-everything assertion fails there and passes on the Windows runner — the same shape of mistake as the xdg-open near-miss above.

  3. This body claimed two different N/A denominators. It said "0 not applicable" on both platforms and, twelve lines later, "1 N/A on Windows". CI says the second. Corrected above.

  4. RustCoreLocator documented the D3.1 seam backwards. IsExecutable said a Windows broken image "already maps to the internal-error path"; it maps to exit 2, because EngineRunner raises RustCoreNotStartedException and CheckCommand returns RustCoreLocator.ExitCode. A candidate that never started never ran and cannot have misbehaved. The comment below it was wrong for the same reason and is fixed with it.

The recorded provenance names da897b42fcd7; the head is one commit later and changes only the two .result.json files and the generated fragment, so production did not move after it was measured. The Windows record is the push run's — the pull_request job recorded GitHub's synthetic merge commit, which exists in no branch.

Связанные issue

Refs #262 (Stage 1 of four; #262 stays open through Stage 4), #261, #250, #260. Closes nothing.

Чеклист

  • изменение покрыто тестом/селфтестом — twenty-two adversarial controls across two harnesses, plus two mutation campaigns (21 on Linux, 9 Windows-native) proving they are load-bearing
  • README/docs обновлены — P-022 row 8 and the preferred queue, the proposals index, rust/README.md, and the launcher README (user-facing engine docs + the explicit rollback)
  • коммиты в conventional-commit стиле

Generated by Claude Code

…Stage 1)

Python remains the default and the reference on every surface. Rust becomes
explicitly selectable, and a compare mode runs both engines over one captured
input. Nothing public defaults to Rust and Python distribution is untouched.

D1 — one explicit launcher selector, `--engine python|rust|compare`, on all
four production launcher surfaces: `owen check`, scripts/own-check.sh,
scripts/own-check.ps1 and action.yml (which exposes it as an `engine` input
and delegates the semantics to own-check.sh rather than re-implementing them).
`own-cli` gets no selector: it presents one engine and knows nothing of Python.

D3/D3.1 — Rust/compare resolve the candidate binary from OWEN_RUST_CORE and
from nothing else: no PATH discovery, no rust/target probing, no "first binary
found", because discovery is how a stale binary silently stands in for the one
under test. A missing, empty, nonexistent, non-file or non-executable locator,
detected before the selected core has started, is a configuration error —
public exit 2, one actionable diagnostic, and never a fallback to Python.

The ordering finding this stage repairs: the launcher used to resolve Python
and unpack the vendored core unconditionally, because there was only ever one
engine. Engine selection now happens before any engine-specific resolution, and
a Rust-only run resolves no interpreter and unpacks no Python core — measured
by running it with OWEN_PYTHON pointed at a path that is not a Python.

D5 — an unexpected Rust child status takes Owen's public internal-error path
(5) with the raw status retained in the diagnostic report's typed, nullable
`child_exit_code`; the report schema is bumped to 2. 70 is the engines' shared
internal-error code: known, still a failure, never read as findings or clean.

D4/D4.1 — compare extracts once, captures the OwnIR bytes once, materialises
each engine's input from that single value and re-hashes both against it before
either engine starts. Streams are compared as raw bytes, not decoded text: a
claim about bytes cannot be measured on strings. Agreement exposes the
reference's result; divergence and execution failure both exit 5 with an
actionable diagnostic and reproduction evidence, and neither ever substitutes
one engine's answer for the other's. A zero-document compare fails rather than
reporting agreement over nothing.

Controls: tests/test_stage1_engine.py drives real launchers against a real
candidate and covers the fifteen ratified adversarial controls, without
fail-fast, forcing failures through #261's own fault-injection feature rather
than through a mock. OWEN_STAGE1_REQUIRE=1 turns a skipped control into a
failure so the denominator cannot quietly shrink. docs/evidence/p022-stage1-1
is the mutation campaign over these surfaces.

The `--config` carrier stays Python under every engine: it is a separately
documented non-core Python duty and #262's D9 tail, still an open owner
decision, so neither skipping nor reimplementing it here would be Stage-1 work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
…or Stage 1

CI: a `stage1-engine` job on ubuntu AND windows. Both platforms matter here
rather than being extra coverage — the ratified evidence is explicit
Rust-selected runs on each, and the two differ in exactly the mechanics
(process launch, executable bits, path forms) a Linux-only job leaves
unproven. It builds the production own-cli and a second own-cli with #261's
off-by-default fault-injection feature, so the failure-mode controls force
failures through the real binary rather than a mock, and runs the controls
under OWEN_STAGE1_REQUIRE=1 so a control that cannot run is a failure rather
than a silently shrinking denominator. The Windows leg additionally drives
own-check.ps1's engine contract, which no Linux leg can reach.

Evidence: the Stage-1 campaign is rendered into
docs/generated/p022-stage1-mutations.md by render_checkpoint_status.py, so its
counts are generated from the recorded run and never typed, and
tests/test_checkpoint_status.py fails while the fragment is stale.

Status surfaces move together, describing what becomes true if this exact PR
merges: P-022 row 8 and the preferred queue, the proposals index row,
rust/README.md's own-cli row, and the launcher README (which gains the
user-facing engine documentation and the explicit rollback). Each says plainly
what Stage 1 is NOT: not the cutover, Python still default and reference,
nothing public defaulting to Rust, Python distribution untouched, compare not
yet a promised public feature. The Windows A/B/C behaviour change is named
where a user meets it — compare will report a real divergence there for
non-ASCII output, and that is the declared difference being visible.

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

The first mutation campaign over these surfaces caught 11 of 16 and named the
gap precisely: five mutations survived, and one was caught by a control that
was not supposed to be its catcher. The cause was one hole, not five — the
compare and locator controls drove own-check.sh but never the `owen` launcher,
so every C#-side misreading was invisible to them:

  M06/M07  the launcher's own locator exit code moved to 5 / to 3, unobserved
  M11      the launcher's compare skipped the execution-failure check
  M12      the launcher's compare judged a zero-document input anyway
  M10      the launcher's compare exposed the reference's result on divergence
           (caught only incidentally, by the same-input control)

Every affected control now drives BOTH surfaces, which is also what D2
actually asks for: the engine-selection semantics are one contract, so a
control that only ever exercises one surface is not measuring the contract.

M14 survived for a different reason worth recording: the diverging stub
differed from the reference in its EXIT CODE as well as its bytes, so a
compare that had stopped comparing stdout still looked correct — the exit
check alone flagged it. The stub now returns the same exit code the reference
does and differs only in the bytes, so the stdout comparison is load-bearing.

Strengthening the controls then found a real production inconsistency rather
than a test bug: own-check.sh names the input digest and the candidate on
stderr, but the launcher printed only the evidence file's path. A reader who
must open a JSON file to learn the two identities that make a compare
reproducible has been handed a filename, not a reproduction — so the launcher
now prints the same two facts, and the surfaces say one thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
The second campaign caught 16/16 but still reported M10's expected catchers as
missed: the mutation that exposes the REFERENCE's result on a divergence
tripped `divergence-is-5` but not `compare-no-substitution`.

The tempting fix was to drop that expectation, since "substitution" reads
naturally as one engine's answer standing in for the other's FAILURE. Reading
the ruling instead of the word settles it the other way: D4.1(b) says no
engine's verdict is exposed as the authoritative result. Owen cannot honestly
emit one answer while its reference and its candidate contradict each other,
so falling back on "Python is the reference, trust it" is the same failure as
trusting the candidate — it merely feels safer.

So the control is strengthened rather than the expectation lowered: on
divergence it now also fails if the reference's verdict reaches stdout. The
campaign expectation stands as authored.

Also records the second campaign's result, superseded by the run this commit's
tree produces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
Pure formatting — no control changed meaning. Caught by running the repo's own
lint gate locally rather than by discovering it in CI.

The campaign is re-run on top of this commit so the recorded provenance names
the tree that actually ships: the previous run measured a control file two
line-wraps different from this one, and evidence that names a tree nobody can
check out is weaker than evidence that names one they can.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
16/16 mutations caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error,
every expected catcher hit, and the M00 honesty control survived the unmutated
tree — recorded on a clean tree at 063ac62, the commit this PR ships.

The counts in docs/generated/p022-stage1-mutations.md are projected from the
recorded run by scripts/render_checkpoint_status.py and never typed;
tests/test_checkpoint_status.py fails while the fragment is stale.

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

coderabbitai Bot commented Sep 9, 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: 7bac9c05-2ccd-437b-ab72-3fa8922cd9ab


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.

The Windows leg of the new job failed on its first run. Four distinct causes,
three of them real defects rather than test noise.

1. A LATENT PRE-EXISTING DEFECT, newly exposed. Every own-check.sh invocation
   on Windows exited 1 before reaching any of its own logic. The repository has
   no .gitattributes, so a Windows checkout (core.autocrlf=true by default)
   rewrites scripts/own-check.sh to CRLF, and bash cannot run a script whose
   every line ends in a stray carriage return. No job had ever run own-check.sh
   on a Windows runner — the three that use it are ubuntu-only — so this sat
   latent until this stage asked for the shell surface on both platforms.
   Fixed with a deliberately NARROW `*.sh text eol=lf`: this repository has
   byte-sensitive evidence whose exact bytes are the contract, and a blanket
   `* text=auto` would put those at the mercy of a checkout setting. #260's
   wider .gitattributes tail is untouched.

2. A REAL GAP IN D3.1's SEAM. Windows has no execute bit, so a non-executable
   candidate passed the locator's checks, was spawned, and died as exit 5.
   D3.1 draws the line at STARTING: "bad locator / cannot select the candidate
   -> rc 2; candidate spawned -> rc 5". A candidate that never started is on
   the rc-2 side, so a spawn failure is now caught and reported as the
   configuration error it is — on the launcher (a typed
   RustCoreNotStartedException) and in the shell (bash's 126/127). On Windows
   this is the ONLY point at which a non-runnable candidate can be detected.

3. MY OWN DESIGN COLLIDING WITH ITSELF. The stub-driven controls were
   Unix-only, and OWEN_STAGE1_REQUIRE=1 turns every skip into a failure — so
   the flag that exists to stop a denominator shrinking was failing controls
   that could not exist on the platform. The two most important of them no
   longer need a stub at all: the unexpected-child-status and raw-status
   controls now force an uncatchable death in the REAL candidate through
   #261's existing OWN_CLI_FAULT_ABORT, which runs on both platforms and
   proves a property of the binary the launcher actually spawns. The remainder
   are declared `not applicable` on Windows — printed and counted separately,
   never silently skipped — because a synthetic candidate needs a shebang.

4. A CROSS-RUNTIME CONVENTION, not a defect. Python's subprocess reports a
   signal-killed child as the negative signal number (-6); .NET and every
   shell report 128+signal (134). The launcher records what .NET observed, so
   the control translates into that convention rather than asking the launcher
   to adopt Python's.

Also: the extraction-count control now separates an INSTRUMENT failure from a
contract failure. If its PATH shim never intercepts `dotnet` while the compare
itself succeeds, it reports "not applicable" instead of accusing the launcher
of an extraction count the measurement could not see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
Two follow-ups from the second Windows run, which improved to 6 passing.

The raw-status control compared 3221226505 with -1073740791 and called them
different. They are the same 32-bit value: Python reports a Windows status
unsigned, .NET reports the same bits as a signed int32. The earlier
translation covered Unix signal numbers only. All three conventions are now
mapped into the one the launcher actually records:

  Unix, Python subprocess   -6            the negative signal number
  Unix, .NET / any shell    134           128 + signal
  Windows, Python           3221226505    0xC0000409, unsigned
  Windows, .NET             -1073740791   the same bits, signed int32

These are measured facts about the runtimes, not fudge factors — the launcher
is not being asked to adopt Python's spelling of a status.

The second failure I cannot yet explain: own-check.sh exits 1 on Windows after
passing its locator check, producing no verdict and no findings. It is not the
CRLF defect (that is fixed — the locator control now passes there through this
same script), and I cannot reproduce it on this machine. Rather than guess at
it across CI cycles, the two controls that hit it now include the script's own
stderr and stdout in their failure message. A control that reports only
"expected 5, got 1" keeps the reason to itself, and on a platform the author
cannot reproduce that reason is the whole diagnosis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
The instrumentation added in the previous commit paid for itself immediately.
The Windows failure was never in the launcher: the controls' own stderr, once
printed, turned out to be UTF-16 text reading "You can resolve this by
installing a distribution... Use 'wsl.exe --list --online'".

`subprocess.run(["bash", ...])` on a Windows runner resolves `bash` to
C:\Windows\System32\bash.exe — the WSL LAUNCHER, not a shell. With no
distribution installed it prints that notice and exits 1, which arrives at a
control as a plausible-looking "own-check.sh exited 1" and is nothing of the
kind. Every own-check.sh-driven control on Windows was measuring WSL's absence.

The harness now names Git for Windows' bash explicitly and refuses anything
under System32. This is a harness concern rather than a product one: a Windows
user runs own-check.sh from a git-bash prompt, where `bash` already is the
right one, and the script itself needed no change.

Worth recording as method, not just as a fix: three CI rounds of guessing
would not have found this, and one round of printing the child's own words
did. A control that reports "expected 5, got 1" and keeps the reason to itself
is a control that cannot be debugged on a platform its author cannot run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
The Windows leg reached 9 passing and then failed on something that is not a
defect at all: `--engine compare` diverges there, so two controls that
required agreement failed.

Measured rather than reasoned: the divergence appears on PURE-ASCII output.
The Python reference writes CRLF where the Rust core writes LF, so the two
engines' bytes differ on every Windows run — before encoding enters the
picture at all. That is #262's Windows A/B/C behaviour change, and requiring
agreement on that platform would be requiring exactly the parity #262
explicitly does not claim.

The controls now treat a divergence as a legitimate compare outcome: the
extraction count is measurable whether the compare agreed, diverged, or failed,
because extraction happens before any of them.

This also corrects my own documentation, which was too narrow. Both the
launcher README and CompareMode's contract comment said Windows compare would
diverge "for non-ASCII output". The measurement says every run. The docs now
say so, and separate the two halves of the difference: line endings on all
output, encoding (cp1252 vs canonical UTF-8, and the reference's occasional
UnicodeEncodeError) on non-ASCII.

Worth noting what caught this: comparing raw BYTES rather than decoded text. A
string comparison through one .NET encoding would have normalised the line
endings away and reported agreement — hiding a declared difference behind a
green control.

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

The last Windows failure was a fixture artifact, not a product defect. The
"non-executable" candidate is a mode-644 file carrying a shebang, and git-bash
runs those quite happily — Windows has no execute bit — so own-check.sh
correctly proceeded, the stub ran, and the control saw exit 0 where it wanted
exit 2. Nothing in the launcher or the script was wrong.

The case is now declared inapplicable to the SHELL surface on Windows, and the
control counts its own denominator instead of reporting a fixed 20, so it
cannot claim coverage it did not have.

The same case remains real and still runs for the LAUNCHER on Windows: there
the candidate is spawned as a Windows process, fails to start, and the
D3.1 spawn-seam backstop added earlier turns that into exit 2 — which is why
that half was already passing in this run while the shell half was not. The
two surfaces genuinely differ here because the platform gives them different
questions to answer, and the controls now say so rather than averaging over it.

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

The Windows leg got all the way to its last step: the controls passed with no
FAIL lines and the step printed "own-check.ps1 engine contract OK", meaning
every assertion held. It failed anyway, for two reasons that have nothing to
do with the engine contract.

A real production wart. own-check.ps1 reported its diagnostics with
Write-Error, which emits an ErrorRecord rather than text. A caller running
with $ErrorActionPreference = 'Stop' — which is exactly how GitHub's pwsh
shell runs — turns that into a TERMINATING error in the caller, even though
the script had handled the condition and exited with a deliberate code. A
command-line tool's diagnostics belong on stderr as text, the way
own-check.sh has always written them. Reproduced locally under a strict
caller before and after: the bad-locator case now prints its line, exits 2,
and the caller carries on.

A harness bug beside it. GitHub's pwsh wrapper appends `exit $LASTEXITCODE`,
and this step's last real command exits 2 deliberately — the case it exists to
require. Without an explicit `exit 0` the step inherited the exit code of its
own passing assertion.

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

16/16 caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error, every
expected catcher hit, and the M00 honesty control survived the unmutated tree
— recorded on a clean tree at 0f51868, which is the commit whose CI run
was fully green on both platforms.

The earlier recording (063ac62) is superseded and was never shippable:
the Windows work changed CheckCommand.cs, CompareMode.cs, EngineRunner.cs,
own-check.sh and own-check.ps1 after it was taken, so it named a tree that no
longer existed. Evidence that describes a tree nobody can check out is not
evidence, whatever its counts say.

Counts in docs/generated/p022-stage1-mutations.md are projected from this run
by scripts/render_checkpoint_status.py and never typed.

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

Verified each against primary source before touching it; all five were real.

D1 — a relative OWEN_RUST_CORE was accepted by all three independent
implementations. `Path.GetFullPath` reads as "it resolves the path for me",
and it does: against whatever directory Owen happened to run in, so the same
configuration would select different binaries from different places. That is
the ambient resolution D3 exists to forbid. Now rejected up front, exit 2,
naming the requirement. The shell test accepts the absolute forms this surface
actually receives — the MSYS `/d/a/...` CI passes, `C:\...`, `C:/...`, UNC —
because a bare `/*` glob would turn a correct Windows configuration into a
usage error; it converts between none of them, as D3 ratified a locator and
not a path-translation policy. action.yml is not a fourth implementation: it
delegates, and the control proves the forwarding.

D2 — the compare verdict was inferred as `childExitCode is null && py is not
null && rs is not null ? "divergence" : "execution-failure"`. `child_exit_code`
is the D5 RUST-child carrier, so a Python-only bad exit left it null with both
outcomes present and stamped "divergence" onto evidence whose own diagnostic
beside it said "execution failure". The classification is now passed by the
call site, which is the only place that knows. The external vocabulary is
unchanged — agreement / divergence / execution-failure — because a new
serialized value is a contract the project would owe support for forever.

D3 — own-check.ps1 never mapped a spawn failure to exit 2, and its comment
asserted the opposite of D3.1 ("maps to the internal-error path"). A candidate
that never started is on the locator's side of the seam. Both engine paths now
catch it, and the comment says what the code does.

D4 — ps1 agreement replayed through `Get-Content -Raw | Write-Output`, a
decode-and-re-encode, and emitted no stderr at all. It now replays the
reference's raw bytes on both streams, as C# and own-check.sh do. Implemented
for the contract, not for today's statistics: CRLF-vs-LF makes agreement rare
on Windows now, but a replay that is wrong only when it finally runs is worse
than none.

D5 — ps1 named $cmpDir as reproduction evidence and then deleted it in
`finally`. Pointing a reader at a path and shredding it on the way out is
worse than naming nothing. The directory now survives an execution failure,
as it already did a divergence.

A SIXTH defect, found while proving D4 and fixed with it: `Start-Process
-RedirectStandardOutput` is not byte-faithful. Measured on one input, the
Python reference wrote 211 bytes and the redirected file held 210 — a blank
line silently dropped. Compare claims the engines' public BYTES are identical,
so a lossy capture of the reference can manufacture a divergence that does not
exist or hide one that does; an agreement reached over a corrupted capture is
not an agreement. ps1 now drains both pipes as byte streams, concurrently, the
way the C# launcher does. Fixing only the replay would have left the mechanism
broken underneath it.

Evidence. `tests/helpers/stage1_stub.rs` is a committed, controllable native
candidate compiled with plain `rustc` — no cargo crate, so #261's crate-edge
DAG gate is untouched. It replaces the shebang stubs, which were Unix-only and
had forced four compare controls to be declared not-applicable on Windows: that
gap is why three PowerShell defects survived a 16/16 campaign. All seventeen
shared controls now run on both platforms with zero N/A, and
`tests/test_stage1_ps1.py` adds the PowerShell surface to the adversarial set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
The Linux leg of `tests (py3.13)` failed on ps1-not-started-is-2, and the
reason is worth recording rather than routing around.

PowerShell on Linux does not refuse a non-executable file: it hands it to the
DESKTOP OPENER. The run exits 0 with `xdg-open: no method available for
opening ...`, so the platform answered a different question — "can something
display this?" — and answered it successfully. "The loader will not start this
image" is not a state Linux can be put in.

The part worth admitting: this control PASSED on my development container, for
the wrong reason. No xdg-open is installed there, so the invocation failed and
looked exactly like a refusal. A control whose verdict turns on whether a
desktop helper happens to be installed is not measuring the contract, and I
would have shipped it believing it green.

That is precisely the case the review's Windows-native rule exists for, now
demonstrated rather than asserted: a PowerShell-targeted mutant evaluated on
Linux can be "killed" by an accident of the host. The control is therefore
declared not-applicable off Windows, with the measured reason, and stays
required on Windows where the ps1 mutation campaign runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
…Windows could say so

Windows CI rejected every correct configuration with "OWEN_RUST_CORE is not an
absolute path", naming the path it had just been handed. The cause is one
character in the fix I shipped for review defect 1:

    [A-Za-z]:[/\]*)

Inside a bracket expression the shell's pattern matcher treats `\` as an
escape, so `\]` escapes the closing bracket, the set is never terminated, and
the arm matches NEITHER `C:/` nor `C:\`. The drive-rooted branch was dead on
arrival. Six controls failed on the Windows leg; all six are the same line.

Why no test saw it. Every assertion asked one direction — "is a RELATIVE
locator refused?" — and a validator that refuses everything answers that
perfectly. The positive direction was never asserted, so on Linux, where the
locator is `/home/...` and the POSIX arm carried it, nothing was wrong to see.

Two things follow, and both are production changes, not test changes.

The bracket is fixed and the shapes are gated on the platform. `C:/rust/own-cli`
is absolute where Windows resolves it and is a directory called `C:` in the
current directory anywhere else, so accepting it everywhere would have left
defect 1 half-fixed on this surface: the Linux reading is exactly the ambient
resolution D3 forbids. The accepted set now matches what .NET's
IsPathFullyQualified accepts on the same host, which is what the other two
implementations call, so all three agree on every shape. Drive-relative
`C:own-cli.exe` and root-relative `\own-cli.exe` are refused by all three:
each still resolves against ambient state.

The control asserts both directions. `locator-shapes` drives eight shapes
through every implementation available on the host and asserts the REASON
rather than the exit code — all eight exit 2 either way, and "refused as not
absolute" versus "absolute, but nothing is there" is the whole question. It
also fails when two implementations disagree about a shape, which is the same
"which binary did we measure?" ambiguity under another name. Every path is
absent, so it needs no candidate, no .NET and no spawn.

Mutations, one per independent implementation, for the direction that was
missing: M20 (own-check.sh POSIX arm), M21 (RustCoreLocator.cs inverted), P06
(own-check.ps1 inverted). And P05, which restores the exact typo above.

P05 is the reason the ps1 campaign is now the WINDOWS campaign. A drive-rooted
path is absolute only where Windows resolves it, so a mutant of that arm
changes nothing a Linux runner can observe — the review's rule about
PowerShell-targeted mutants is not about PowerShell, it is about evaluating a
mutant where its contract lives, and one arm of a shell script turns out to
live there too. The campaign gains a second layer that narrows the shared
harness to `locator-shapes`; it needs no candidate and no .NET build, so the
Windows leg stays a shell-and-PowerShell job.

`OWEN_STAGE1_ONLY` narrows the harness and can never widen it, and an unknown
name is an error rather than an empty run: a filter that silently selects
nothing is a way to report a green campaign that measured nothing. Campaign
layers can now carry `env` in the definition, so a narrowed layer says so
where the evidence is read instead of in a wrapper script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
…never to run it

Two platforms answered the same question wrong, and both times the platform
was not at fault.

On Linux, `& $rustCore` against a file the loader cannot start returned 0 and
printed `xdg-open: no method available for opening ...`. I read that as a
platform limit and declared the spawn-seam control not-applicable off Windows.
Windows CI then returned 0 with both streams empty, and the job's own cleanup
line terminated an orphaned NOTEPAD.

Same defect, two desktop handlers. PowerShell's call operator does not spawn a
program; it asks the platform to OPEN it, and a file that is not a runnable
image goes to whatever is registered for it. So D3.1's seam could not be
reached anywhere: the try/catch added for review defect 3 was unreachable, and
`own-check.ps1 -Engine rust` with an existing but unrunnable OWEN_RUST_CORE
exited 0 with no findings — a clean bill of health for an analysis that never
happened, which is worse than any wrong exit code.

`Invoke-CandidateProcess` starts the image with UseShellExecute = $false and
nothing redirected, so the child inherits this process's streams and its
output still arrives live. The start either succeeds or throws, and the throw
is what the existing catch was written for. Measured on Linux: exit 2 with the
"could not be started" diagnostic, where the same input previously exited 0.

The compare path was already correct — it goes through Invoke-CapturedProcess,
which has always set UseShellExecute = $false. Only the direct Rust path
opened its candidate.

Consequences for the evidence, both of which cut against my earlier reading:

The control is required on BOTH platforms and the not-applicable is gone. The
seam is answerable anywhere now, because it is a real spawn. The N/A I added
one commit ago was describing a bug in this script, not a property of Linux —
and it was reasoned from a measurement I trusted too quickly.

P07 is the mutation, and it is the shipped code: back to the call operator. It
belongs in the Windows campaign with the rest of this surface, where a
PowerShell catcher observes it natively.

Also kept: the near-miss. This control PASSED on a developer container with no
xdg-open installed, because the invocation failed there and looked exactly
like a refusal. A verdict that turns on which desktop helper happens to be
installed is not measuring the contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
The Windows leg aborted mid-suite with `OSError: [WinError 193] %1 is not a
valid Win32 application`, and both faults behind it are mine.

`own-check.sh` was handed straight to CreateProcess. On Linux the shebang
carries that; Windows has no such thing, and a .sh file is not a Win32 image.
The shared harness has had `bash_exe()` for this since the WSL `bash.exe` shim
cost a round; the PowerShell harness was written without it. It now has the
same helper, refusing System32's WSL launcher for the same reason.

The larger fault is what the crash did to the evidence. An unhandled exception
took the whole suite down, so the two controls after it never ran and the
mutation campaign saw one nameless failure where it needed a named catcher.
No-fail-fast has to survive a control that RAISES, not only one that reports.
Both harnesses now record an unexpected exception as that check's failure and
carry on.

The campaign runner names them too. A voided run printed "caught (1 failing
test(s))" and stopped, and finding which check that was took two CI rounds
while the runner had the name in hand the whole time. It now prints every
catcher and names them in the error.

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

The Windows-native campaign passed — 7/7 caught, honesty control survived, run
on the exact head — and the gate rejected the result anyway: "the recorded
result was taken over a different campaign definition".

The definition was identical. Its BYTES were not. `core.autocrlf=true` is the
Git for Windows default, so the runner checked the JSON out with CRLF, and
`mutate_campaign.py` records the sha256 of the file it ran:

    79ad1476f219   the same definition, checked out on Linux
    3dda13ce16d6   the same definition, checked out on Windows

So a run that measured exactly the right tree was filed as evidence for a
different one. The gate was right to refuse it; the hash was reading the
checkout setting, not the campaign.

`docs/evidence/*.json text eol=lf` pins the bytes that are the contract. The
existing .gitattributes comment already named this hazard — "byte-sensitive
evidence whose exact bytes are the contract" — and declined to act on it
because no campaign ran on Windows yet. One does now.

The result files get the same treatment from the other side: Python's text
mode writes CRLF on Windows, so `write_result` now opens with newline="\n". A
result recorded on Windows has to be byte-identical to one recorded on Linux,
or the evidence differs from itself by the platform that happened to take it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
…artitioned by platform

Linux, 21 mutations, recorded at 1d649e9 on a clean tree: 21/21 caught,
0 survived, 0 compile-error, 0 invalid, 0 runner-error, every expected catcher
hit, honesty control M00 survived the unmutated tree.

Windows-native, 7 mutations, recorded at the same commit on a windows-latest
runner and copied from that job's own printed result: 7/7 caught, control
survived, every expected catcher hit. Six target scripts/own-check.ps1 and are
killed by PowerShell catchers running on Windows; the seventh targets the
drive-rooted arm of own-check.sh, which no Linux catcher can reach, and is
killed by the shapes layer there.

The two are recorded and reported separately because they are not the same
measurement. A mutant evaluated where its contract cannot be posed is
decorative, so nothing here is added up across platforms.

The result taken at 0f51868 (16/16) is superseded twice over: it predates five
mutations and describes production files that have since changed. So is every
recording between it and this one — each named a tree that no longer exists,
which is the one thing a campaign result must not do.

Counts in docs/generated/p022-stage1-mutations.md are projected from these two
runs by scripts/render_checkpoint_status.py and never typed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
Two proof gaps and one lie in a comment, all found by review against primary
source rather than by anything in this repository.

1. The agreement control's stderr assertion never ran. It was guarded by `if
ref.stderr`, and a healthy `python -m ownlang ownir` writes none: CI printed
`184 out, 0 err` on Windows and `160 out, 0 err` on Linux while the control
reported that it had compared "both streams". A guard that switches an
assertion off exactly when the data is ordinary is not a guard, it is a hole
with a denominator of zero — and this branch has spent two review rounds on
honest denominators.

The fixture is now built rather than found. `python` is resolved by
own-check.ps1 as a bare command, so a shim directory whose `python` IS the
committed native stub makes the REFERENCE's bytes choosable; both engines then
run the same stub over the same fixture and genuinely agree, with stderr that
is deliberately not empty. The bytes are chosen to survive nothing: CRLF a text
pipeline would rewrite, multi-byte UTF-8 a decode-and-re-encode would
normalise. stdout is compared exactly; stderr as a suffix, because the
extraction ahead of the compare sends dotnet's chatter there and that chatter
is not the launcher's replay.

The old fixture is kept beside it. It is the only one that proves the replay is
faithful to the REAL reference implementation, which a stub cannot show.

P03 no longer breaks both streams at once — it broke stdout AND emptied
$errBytes, so its death proved only that stdout was checked. It is now scoped
to stdout, and P08 removes the stderr replay alone while leaving stdout
byte-faithful. Measured separately: P03 dies on stdout in both fixtures with
the stderr assertion silent; P08 dies only on stderr, against a launcher that
emitted 0 bytes there. "Both streams" is load-bearing now, not grammar.

2. The PowerShell surface never proved D3's preflight POSITION. The shared
harness counts extractor invocations, but its counter is a `#!` script
installed only off Windows, so on the one platform whose campaign is the record
there was no counter at all. A validator that drifted to after extraction would
still exit 2, still name the absolute requirement, still emit no verdict, and
every assertion in the control would stay green while Owen paid for a full
Roslyn pass over a candidate it was about to refuse.

There is a counter now on both platforms — a `.cmd` on Windows, since
own-check.ps1 reaches dotnet through PowerShell's call operator and that
honours PATHEXT — and P09 is the mutant it exists to kill: the toolchain is
probed before the locator is validated, which is a thing CLIs really do, and
nothing else in the control notices.

It counts EXTRACTIONS, not every dotnet invocation, and that distinction was
measured, not assumed. A count-everything assertion failed on this development
container and would have passed on the Windows runner: where pwsh is installed
as a dotnet global tool, merely starting the shell invokes dotnet twice. A
control whose verdict turns on how the shell was packaged is not measuring
Owen — the same shape of mistake as the xdg-open near-miss earlier in this
branch.

3. RustCoreLocator's IsExecutable still documented the opposite of the ratified
seam: a Windows broken image "already maps to the internal-error path". It maps
to exit 2 — EngineRunner raises RustCoreNotStartedException and CheckCommand
returns RustCoreLocator.ExitCode — because a candidate that never started never
ran and cannot have misbehaved. The comment below it was wrong for the same
reason, claiming a false "yes" would produce an internal error; both answers
now reach the same exit code, and what a false "yes" actually costs is the
diagnostic and the moment it arrives. A ratified seam described backwards in
production source is a defect whether or not it executes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
…has two graves

p022-stage1-1     ubuntu, local     21/21 caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error
p022-stage1-windows  windows-latest   9/9 caught, same

Both at source commit da897b4 with dirty:false, every expected catcher
hit, and M00 surviving the unmutated tree on each. Nothing is added across
those rows: they are different measurements on different platforms, and a
mutant evaluated where its contract cannot be posed proves nothing about the
surface it edits.

The Windows record is the PUSH run's, not the pull_request run's. Both were
9/9, but the PR-triggered job checks out GitHub's synthetic merge commit and
recorded `106973f375ce` — a commit that exists in no branch. Provenance that
names a tree nobody can check out is not provenance, whatever its counts say.

The two catchers this round exists for, from the Windows log:

  P03 -> ps1::ps1-agreement-replays     stdout replay, stderr left intact
  P08 -> ps1::ps1-agreement-replays     stderr replay alone, stdout intact

Same catcher, different graves, and that is the whole point: measured
separately before either was recorded, P03 dies on stdout in both fixtures
with the stderr assertion silent, and P08 dies only on stderr against a
launcher that emitted nothing there. Before this round one mutation broke both
streams at once and the stdout assertion killed it on its own, so "both
streams" was a sentence rather than a measurement.

  P09 -> ps1::ps1-absolute-locator      the locator preflight keeps its POSITION

P09 leaves every previously asserted observable correct — exit 2, the absolute
requirement named, no verdict — and is caught only by the extraction counter
this round added to the PowerShell surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
@PhysShell
PhysShell merged commit ba1d4cf into main Sep 9, 2026
64 checks passed
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