feat(launcher): opt-in Rust engine behind the existing launcher (#262 Stage 1) - #349
Merged
Conversation
…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
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Что и зачем
#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:
--engine python|rust|compareon all four production launcher surfaces (owen check,scripts/own-check.sh,scripts/own-check.ps1,action.yml). The Action exposes it as anengineinput and delegates the semantics toown-check.shrather than re-implementing them, so the real fan-out is smaller than four and there is one contract to keep true.own-cligets no selector (C-4).OWEN_RUST_COREand nothing else: no PATH discovery, norust/targetprobing, 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.child_exit_code; report schema bumped to 2.70is the engines' shared internal-error code: known, still a failure, never read as findings or clean.The ordering finding this stage repairs.
CheckCommand.csresolved Python and unpackedCoreVendorunconditionally, 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.
Тип изменения
Как проверено
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:
The Rust run additionally succeeded on a build with no vendored
ownlang-coreon 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 isbad-locator-is-2/shell-non-executable, explained below), plus four PowerShell controls (tests/test_stage1_ps1.py) that putown-check.ps1in the adversarial set for the first time.The Windows N/A count fell from four to one.
tests/helpers/stage1_stub.rsis a committed, controllable native candidate compiled with plainrustc— 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 isbad-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-injectionfeature against the real production binary, not a mock.compare-extracts-onceis behavioural: adotnetshim 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 controlM00surviving the unmutated tree:p022-stage1-1p022-stage1-windowsNothing 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 ofown-check.sh, which no Linux catcher can reach. Counts are generated intodocs/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.shbut never theowenlauncher, so every C#-side misreading was invisible. Fixing that also exposed a real production inconsistency, whereown-check.shnamed the input digest on stderr while the launcher printed only the evidence file's path. A second round showedcompare-no-substitutionwas 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 Windowspython tests/test_stage1_ps1.py— 4/4, 0 skipped, 0 N/A on both platformsruff check .— All checks passed;mypy --strict ownlang— Successpython scripts/render_checkpoint_status.py --check— fragments in syncpython scripts/mutate_campaign.py --campaign docs/evidence/p022-stage1-1.json --run— 21/21docs/evidence/p022-stage1-windows.json, on a Windows runner — 9/9python tests/run_tests.py— cleanbash -non both shell scripts; PowerShell AST parse onown-check.ps1; YAML parse onci.ymlandaction.ymlWhat 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
pwshleg under a strict caller could in principle have caught. No such leg existed..gitattributes, and this is the first job that has ever runown-check.shon 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=autowould 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.gitattributestail stays untouched.own-check.ps1emitted PowerShellErrorRecords viaWrite-Error. A caller running$ErrorActionPreference = 'Stop'— exactly how GitHub'spwshshell runs — turns those into terminating errors in the caller, despite the script having handled the condition and exited deliberately. It now writes stderr text, asown-check.shalways did. Verified locally under a strict caller.One documentation error of mine, corrected by measurement. I had written that Windows
comparediverges "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 theCompareModecontract 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'sbash.exestub in System32; a child status is spelled three different ways across runtimes (-6/134/3221226505/-1073740791); and GitHub'spwshwrapper appendsexit $LASTEXITCODE, so a step inherited the exit code of a case it was written to require.CI gains a
stage1-enginejob 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 drivesown-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 -RedirectStandardOutputis 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.ps1be 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: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 neitherC:/norC:\. 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-shapesnow asserts both directions across every implementation on the host, and fails when two of them disagree about a shape.own-check.ps1asked 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 toxdg-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 withUseShellExecute = $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.A campaign definition hashed differently on the two platforms.
core.autocrlf=trueis the Git for Windows default, so the runner checked the definition out with CRLF and the recordedsha256read 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=lfpins 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.
The stderr half of "both streams" was never compared. The agreement control guarded its stderr assertion with
if ref.stderr, and a healthypython -m ownlang ownirwrites none — CI printed184 out, 0 erron Windows and160 out, 0 erron 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.ps1resolves the reference as a barepython, so a shim directory whosepythonis 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.P03is now scoped to stdout andP08removes the stderr replay alone; measured separately, they die for different reasons under the same catcher.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.cmdon Windows, sinceown-check.ps1reaches dotnet through the call operator and that honours PATHEXT), andP09is the mutant it exists to kill. It counts extractions, not every dotnet invocation, and that distinction was measured rather than assumed: wherepwshis 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 thexdg-opennear-miss above.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.
RustCoreLocatordocumented the D3.1 seam backwards.IsExecutablesaid a Windows broken image "already maps to the internal-error path"; it maps to exit 2, becauseEngineRunnerraisesRustCoreNotStartedExceptionandCheckCommandreturnsRustCoreLocator.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.jsonfiles and the generated fragment, so production did not move after it was measured. The Windows record is the push run's — thepull_requestjob 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.
Чеклист
rust/README.md, and the launcher README (user-facing engine docs + the explicit rollback)Generated by Claude Code