Skip to content

P-022 step 7b: the production Rust OwnIR executable (own-cli ownir) — command, output and exit-code parity behind the existing launcher #261

Description

@PhysShell

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 ownlangcfg, 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 ownir is already declared a defect: the reference's reaction to --help (check --help reads the argument as a file and dies with exit 70; ownir --help exits 2 with "cannot read"). It is not ported; own-cli ownir --help answers with the ownir usage on stdout, exit 0 — the shell convention extended to the subcommand. The Python-first repair of the reference's --help is 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, report included. 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 flags ownir accepts today (--format, --severity, --verbosity) and the reference's value sets, both the --flag V and the --flag=V spelling; exactly one positional argument; every usage error after ownir answered 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 by tests/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 and ok lines, the --verbosity variants), 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-level catch_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 the owen convention, the empty invocation and the unknown command as distinct cases; and ownir --help as 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/--verbosity value; --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-cliown-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 .own commands, no emit here.

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 ownir executable 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_unwind under panic = "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-default fault-injection feature.
  • 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; 130 not invented.)
  • The shell cases follow the C-1 convention and are fixtures, the help text a parity surface of its own; every case after ownir is 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-cliown-ir/own-bridge (and own-lowered/own-diagnostics only if the build needed them). (Exactly own-cliown-ir and own-cliown-bridge, registered in own-diagnostics/tests/dag.rs.)
  • owen, own-check.*, the Action, own-shadow-engine and 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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions