Repository navigation
P-022 step 7b: the production Rust OwnIR executable (own-cli ownir) — command, output and exit-code parity behind the existing launcher #261
Description
Parent: #250
Recommended tier: strong
Blocked by: nothing — #259 and #260 are at final acceptance, and this issue's decision packet (261.A) is ratified
Related: #262 (this issue is its CLI prerequisite — C-5), #345 (the residual .own/dev CLI split out of here — C-3), #257 (emit, no longer here), #256 (report, struck), #260 (R-1/R-2: the adapter discipline and the failure rules this executable inherits)
Status — read this first
Closed completed 2026-09-08. 261.A (the decision packet) was ratified 2026-09-08 (C-1..C-5 and the rulings below); 261.B (the implementation) landed the same day via PR #347 (merge commit 206e9c7). The own-cli ownir executable exists behind the unchanged launcher; its Python-authored CLI fixture replays with zero Python on Linux and Windows CI; both failure-mode rulings are measured under an off-by-default fault-injection feature; and the ownir_version Version family is byte-parity with the reference (24 value classes + 20 000 randomized documents), with V1/V2/V4 declared and V3 reproduced — the parser-domain rulings live in #262. The surfaces moved together at the close: this body, #250's #261 entry and Preferred queue, P-022 row 7b, the proposals index and the crate table in rust/README.md (261.A by PR #346; 261.B by PR #347). The residual .own/dev CLI is #345 and the Rust-default cutover is #262 — both separate and still open.
What this issue is now
The production-oriented Rust OwnIR executable — the step 7b gate that admits #262. It reproduces exactly the one core invocation the product seam makes today: own-check.sh, own-check.ps1, the Action and owen check's core call all run
python -m ownlang ownir <facts> --format F --severity S
and nothing else of python -m ownlang. The Rust executable is a binary behind the existing launcher: owen does not change by a line here, no engine becomes default, no package is published. That is the strangler fig at its narrowest seam.
The rest of python -m ownlang — cfg, summaries, explain, .own check, and emit behind #257 — is #345, and it joins this same binary as subcommands later (owner ruling, 2026-09-08): #261 creates the own-cli entry point with ownir; to the cutover the binary may still contain only ownir.
Owner decisions (261.A, ratified 2026-09-08)
C-1 — the oracle is split by surface and behaviour class, and the boundary is the surface, never the reference's internal print branch.
TOP-LEVEL own-cli SHELL — oracle: the public `owen` convention
own-cli help -> stdout, exit 2
own-cli --help | -h help -> stdout, exit 0
own-cli --version version -> stdout, exit 0
own-cli <garbage> one error line + help -> stderr, exit 2
(the empty invocation and the unknown command are distinct cases)
AFTER `ownir` IS SELECTED — oracle: `python -m ownlang ownir`, as measured
positional-count errors, unknown/extra arguments, a missing flag value,
an invalid --format / --severity / --verbosity, the strict OwnIR refusal,
every finding, every summary line, every stream, every exit code
-> stdout / stderr / exit / message exactly as the reference produces them,
unless that exact case is separately declared a defect by the owner.
- The help text of the shell is a new cross-implementation parity surface, not an invented Python contract: no Python source carries a byte oracle for it. This is the one place where the "no new command semantics" guardrail is deliberately relaxed, to the minimum, and it is recorded here as a decision rather than slipped in as a port.
- One case after
owniris already declared a defect: the reference's reaction to--help(check --helpreads the argument as a file and dies with exit 70;ownir --helpexits 2 with "cannot read"). It is not ported;own-cli ownir --helpanswers with theownirusage on stdout, exit 0 — the shell convention extended to the subcommand. The Python-first repair of the reference's--helpis hygiene, not a prerequisite of this issue. - A consequence of this boundary, made visible rather than hidden: for a positional-count error or an unknown argument after
ownir(the reference's parser treats an unknown flag as a second positional), the reference prints its module docstring to stdout with exit 2 — the usage text of the whole PoC CLI,reportincluded. Those bytes are the contract as measured unless the owner declares that exact class a defect; the fixture freezes them and marks them so the owner can see what was frozen.
C-2 — report is struck, not deferred. .ownreport.json was struck at #256: a buffer-storage report needing the AST and the policy resolver, which no cutover step needs and whose shape the project had already refused. #256's decision is not reopened; report is removed from this issue.
C-3 — the production seam and the residual PoC CLI are separated. This issue is the ownir executable only. cfg, summaries, explain, .own check, and emit (which depends on #257) are #345. One issue with four internal stages of different age would have read "#261 half done" forever.
C-4 — engine selection does not belong to the Rust executable. The executable presents one engine and knows nothing of Python. Python/Rust/compare orchestration stays outside: scripts/shadow_compare.py now, the owen launcher at #262's stages later. No silent fallback anywhere — a Rust failure is never a Python success. (This replaces the original "Integration with engine modes" section, which asked the binary to offer Python/reference, Rust and compare modes; only "no silent fallback" survives from it.)
C-5 — the production OwnIR slice is a sufficient CLI prerequisite for #262. #262 depends on this issue's completed product seam, not on #345. #345 remains a migration obligation for as long as P-022 is defined as the full surface migration rather than only the production vertical.
Rulings that are part of the ratification:
stdin NOT part of this issue's production OwnIR contract
catchable panic -> exactly one actionable stderr diagnostic + rc 70
(never 101, Rust's default panic exit: the launcher
maps only 70 onto its internal-error path).
The contract build keeps panic = "unwind": a custom
hook suppresses the default panic output and a
top-level catch_unwind produces the diagnostic and
the exit — a hook alone only observes a panic.
panic = "abort" is NOT set for this binary.
uncatchable death -> visible hard failure, never findings, never clean;
no particular OS exit number is contracted
#262 launcher an unexpected Rust child rc outside the legal set
-> the public internal-error path (5), with the raw
child status retained in evidence/report
(recorded in #262; nothing here changes the launcher)
SIGINT measure the Windows and Linux reference first;
do not invent 130 as a universal contract
emit (#345) ordinary diagnostics or a refusal before codegen -> 1
(the reference refuses to generate on error
diagnostics or a missing module);
a CodegenError escaping generate() -> 70;
any other unexpected exception -> 70
summaries (#345) expected OwnIR refusal -> 2;
unexpected exception -> 70
The contract to reproduce (261.B)
Measured on the reference and frozen as fixtures — the fixture is the contract, this list is its table of contents:
- argv:
ownir <facts>with the flagsowniraccepts today (--format,--severity,--verbosity) and the reference's value sets, both the--flag Vand the--flag=Vspelling; exactly one positional argument; every usage error afterowniranswered exactly as the reference answers it (the docstring on stdout for a positional-count error or an unknown argument; the specific stderr line for a missing or invalid value), exit 2. - exit codes as the reference produces them: a clean run; findings of the tier that fails the run (every non-advisory, non-suppressed finding, independent of
--severity); a usage error or an OwnIR refusal by the strict door, with the reference's message; an internal failure as one actionable stderr diagnostic and exit 70 (the A1 rule pinned bytests/test_cli_contract.py). - stdout/stderr separation exactly as the reference, per format. The per-finding renders (
human,github,msbuild) and the SARIF log builder are already ported and pinned byte for byte by the BR-V9 fixture family (tests/fixtures/verdict_renders/, replayed in Rust since P-022 step 6b: implement Rust own-bridge with layered OwnIR parity #259 cp5.3) and are reused, never re-derived; what this issue adds and freezes is the process-level contract around them — the CLI's display policy (which findings are shown, the summary andoklines, the--verbosityvariants), the stream split, the CLI path's own serialization conventions for SARIF (which differ from the fixture emitter's), measured on the reference and frozen as CLI fixtures. - paths: Windows and Unix forms, spaces and non-ASCII, rendered exactly as the facts document and the reference render them; user-visible paths are contractual.
- determinism: byte-identical stdout and stderr on rerun.
- stdin: not part of the contract (ruling). Whatever the reference does with
-is recorded by the fixture as out of contract, explicitly rather than silently. - panic policy: a catchable panic is exactly one actionable stderr diagnostic and exit 70, under
panic = "unwind"with a top-levelcatch_unwind; an uncatchable death stays a visible hard failure with no OS exit number contracted. Both are measured with a control that forces each, never asserted from the design. - SIGINT/cancellation: the reference is measured on Windows and Linux first, and the measured behaviour is what the fixture pins.
Fixture contract
One Python-authored fixture per case: arguments, working-directory assumptions, input files, stdout, stderr, exit code, created artifacts. Normalize only temp-root prefixes where unavoidable; paths visible to users remain contractual. Rust steady-state replay runs with zero Python.
Required controls:
- the shell: an empty invocation;
--help/-h;--version; an unknown command — each under theowenconvention, the empty invocation and the unknown command as distinct cases; andownir --helpas the one declared-defect case; - after
ownir, as measured: no positional; two positionals; an unknown flag with and without a real path; a flag with no value; each invalid--format/--severity/--verbosityvalue;--format json; - a missing or unreadable facts file → the reference's code and message;
- a facts document the strict door refuses → the reference's code and message;
- clean facts → the reference's exit code and (possibly empty) output, per format;
- findings of the failing tier → the reference's exit code and each format's bytes;
- findings below the failing tier, under each
--severity; - Windows and Unix path forms; spaces and non-ASCII in paths;
- deterministic reruns;
- the forced-panic control and the forced-death control;
- SIGINT, as measured.
Architecture
The binary crate is own-cli — the entry point P-022's crate plan names — created here with the ownir subcommand; #345 adds its subcommands to the same binary later. Argument handlers own parsing, I/O and process exit and hold no analysis or bridge logic. Cargo DAG: own-cli → own-ir, own-bridge, and own-lowered/own-diagnostics only if the build proves the need (their own edges transitively); no own-codegen here (that is #345's emit slice, after #257); the crate-edge gate stays green with no other new edge. The contract build keeps panic = "unwind" (the workspace default): a custom panic hook plus a top-level catch_unwind turn an ordinary panic into exactly one actionable stderr diagnostic and exit 70; panic = "abort" is not set for this binary. The note in rust/Cargo.toml above [profile.release] that calls panic a per-binary choice ("abort for own-cli") is a stale design note — Cargo cannot set panic per package — and is corrected as such, with its mirror in P-022, in the same PR. own-shadow-engine and scripts/shadow_compare.py are untouched — they are #260's dev surface, not this issue's.
Distribution boundary
This issue produces and tests the Rust binary. It does not:
- remove Python from
Owen.Cli; - replace or modify the public launcher;
- publish a new package version;
- choose the default engine.
Guardrails
- No new command semantics beyond the C-1 shell convention.
- No diagnostic behaviour changes.
- No bridge or analysis logic in argument handlers.
- No engine selection and no fallback in the executable; no public cutover; no Python dependency removal.
- No raw byte fixture weakening merely because of a different CLI library.
- No
report, no.owncommands, noemithere.
Execution
The implementing agent works from a written brief (Prompt H) with the fixture contract above. Its merge authority is the standing one: implement, test, open the PR and drive it to a fresh PASS — never merge, auto-merge, squash or rebase without a separate owner merge instruction bound to the exact reviewed head. (Executed: PR #347 was reviewed to PASS and merged as 206e9c7 under an explicit owner merge instruction bound to that exact head.)
Acceptance
- 261.A — command inventory complete and the owner decisions ratified (2026-09-08; this body).
- 261.B — the
own-cli ownirexecutable exists and its fixture is green on Windows and Linux CI with zero Python in the replay. (PR feat(own-cli): the production Rust OwnIR executable, behind the unchanged launcher (#261 261.B) #347,206e9c7.) - Every exit code and the stdout/stderr split reproduce the reference; the four formats reproduce the reference's bytes through the process boundary, reusing the BR-V9 renders.
- Windows and Unix path forms, spaces and Unicode paths are covered.
- A catchable panic is exactly one stderr diagnostic and exit 70 via
catch_unwindunderpanic = "unwind"; an uncatchable death is a visible hard failure asserted without contracting an OS exit number — both measured with forcing controls behind an off-by-defaultfault-injectionfeature. - SIGINT behaviour is measured on both reference platforms and recorded before any Rust behaviour is written down. (Measured — Linux signal 2, Windows
0xC000013A— recorded and deliberately not pinned;130not invented.) - The shell cases follow the C-1 convention and are fixtures, the help text a parity surface of its own; every case after
owniris pinned as measured, the docstring-on-stdout ones marked. - The Cargo DAG is correct: the crate-edge gate is green with no edge beyond
own-cli→own-ir/own-bridge(andown-lowered/own-diagnosticsonly if the build needed them). (Exactlyown-cli→own-irandown-cli→own-bridge, registered inown-diagnostics/tests/dag.rs.) -
owen,own-check.*, the Action,own-shadow-engineand the compare driver are untouched; Python remains the public engine until P-022 step 8: Rust-default cutover, rollback gate, and Python distribution removal #262. - At the close, the status surfaces moved together: this body, Roadmap: ship Owen Alpha and complete the Rust production vertical after #214 #250, P-022 row 7b, the proposals index,
rust/README.md.
Activity
fix(p022): make the oracle probe executable, and unblock the queue line
docs(p022): bring the status surfaces level with what 4b proves (#259 …
[-]P-022 step 7b: implement Rust own-cli with command, output, and exit-code parity[/-][+]P-022 step 7b: the production Rust OwnIR executable (`own-cli ownir`) — command, output and exit-code parity behind the existing launcher[/+]docs(p022): move step 7b surfaces to the ratified #261 decision packet
Merge pull request #346 from PhysShell/claude/p-022-analysis-wiring-m…
fix(campaign): a binary crate's unit tests are addressable as catchers (
evidence(own-cli): the #261 mutation campaign definition (#261 H.5)
evidence(own-cli): run the #261 campaign and render its fragments (#261…
docs(p022): record what #261 built, and the three findings it did not…
fix(strict-door): Version byte parity, and CLI-B1 as a typed JSON bou…
evidence(own-cli): re-run the campaign over the repair surfaces (#261 …
docs(p022): repair the status contradiction and state the rulings as …
fix(strict-door): reproduce repr(json.loads(...)) for the Version fam…
test(cli-b1): flip eligibility on the facts bytes and nothing else (#261
evidence(own-cli): extend the campaign over the R2b/R3b surfaces (#261 …
docs(p022): record what the second measurement found, and name the tr…
evidence(own-cli): re-run the campaign over the R2b/R3b surfaces (#261 …
docs(p022): keep the status surfaces consistent with the second repai…
docs(p022): state V4 as a representation boundary, not an absolute (#261
Merge pull request #347 from PhysShell/claude/rust-ownir-executable-6…
Merge pull request #348 from PhysShell/claude/checkpoint-0-execution-…
feat(launcher): opt-in Rust engine behind the existing launcher (#262 …
docs(P-022): CI gate, generated-counts fragment and status surfaces f…
fix(stage1): make the engine contract hold on Windows, not only on Linux
fix(stage1): five review defects in identity, classification and PS s…
P-022 Stage 3: re-qualify the differential evidence on the candidate …