Skip to content

feat(own-cli): the production Rust OwnIR executable, behind the unchanged launcher (#261 261.B) - #347

Merged
PhysShell merged 20 commits into
mainfrom
claude/rust-ownir-executable-6h1zrp
Sep 8, 2026
Merged

feat(own-cli): the production Rust OwnIR executable, behind the unchanged launcher (#261 261.B)#347
PhysShell merged 20 commits into
mainfrom
claude/rust-ownir-executable-6h1zrp

Conversation

@PhysShell

@PhysShell PhysShell commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Что и зачем

#261 (261.B): the production Rust OwnIR executable own-cli ownir — the one core invocation the product seam makes today, as a native binary behind the unchanged owen launcher. It reproduces the reference's ownir contract over a frozen CLI fixture replayed with zero Python on Linux and Windows CI. Nothing is wired, published or defaulted; Python remains the public engine.

Repair passes applied on top of 1a32185 (new commits, no history rewrite). The first (4db644f) applied the five owner rulings of 2026-09-08 plus two review amendments. The second (8b5cfb1..51325a4) repairs what a deeper review of that one found: the Version family's parity was measured on four hand-picked values when the oracle is about value classes, and the CLI-B1 negative control compared two different cases at two different paths. The third (bc8176b) applies the owner's ratification of V4 in its narrower form — wording only, no logic. Any PASS or merge authority bound to 1a32185, 4db644f or 51325a4 is void; bc8176b is the head to review.

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

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

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

  • python tests/run_tests.py
  • ruff check . и mypy
  • селфтесты затронутых скриптов (python scripts/validate_contrib.py --selftest)

Local commands, all green on this head:

python tests/run_tests.py
python tests/test_cli_ownir_fixtures.py            # the Python oracle side
ruff check .
python scripts/render_checkpoint_status.py --check
python scripts/validate_contrib.py --selftest
cd rust && cargo fmt --check
cd rust && cargo clippy --all-targets               # workspace denies are the gate
cd rust && cargo test                               # whole workspace, incl. the DAG gate
cd rust && cargo test -p own-cli                    # the frozen CLI contract, zero Python
cd rust && cargo test -p own-cli --features fault-injection --test faults
cd rust && cargo test -p own-ir --test version_repr_census   # the value-class census
python scripts/mutate_campaign.py --campaign docs/evidence/p022-cli-1.json --run

The campaign, recorded on a5a4312 and unaffected by the two later wording-only commits: 24/24 caught, 0 survived, 0 missed catchers, four layers, clean tree.

Связанные issue

Refs #261, #345, #262, #250


The #250 packet

Scope

The own-cli binary with its single ownir subcommand, its Python-authored CLI fixture, the zero-Python Rust replay, a CI job on both platforms, and a mutation campaign over the display policy, the strict-door families and the process contract.

  • the shell follows the public owen convention (C-1): empty invocation → help on stdout, exit 2; --help/-h → help on stdout, exit 0; --versionown-cli <version>; unknown command → one error line + help on stderr, exit 2. The empty invocation and the unknown command are distinct cases. This text has no Python byte oracle — it is a new cross-implementation parity surface, written once, carried in the fixture manifest and shared with the binary.
  • after ownir, everything is the reference as measured: the usage errors (the module docstring on stdout for a positional-count error or an unknown argument), the strict-door refusals, the display policy, the stream split, the four formats and every exit code. ownir --help is the one case P-022 step 7b: the production Rust OwnIR executable (own-cli ownir) — command, output and exit-code parity behind the existing launcher #261 declares a defect and does not port.
  • the process contract under panic = "unwind": a hook suppresses the default panic output and a top-level catch_unwind produces one actionable stderr diagnostic and exit 70, never 101. A hook alone would only observe a panic. Under OWNLANG_DEBUG the payload and a backtrace print and the exit is still 70.

Explicit non-goals

Python source of truth

ownlang/__main__.pycmd_ownir (lines 338–405) and main/run (795–957), plus ownlang/ownir.py's load, Finding, render_finding and build_sarif. Unchanged: not a character. No file under ownlang/ is in this diff, all repair passes included.

Frozen fixture

tests/fixtures/cli_ownir/ — a manifest plus one <name>.case.json per case, each naming the rule it is the control for and the oracle that authored its bytes (python, python-docstring, owen-convention). Streams live inside JSON strings so a core.autocrlf checkout cannot corrupt an expectation; <OS_ERROR> is the only placeholder, valid only on a line carrying a platform-native OS error text. The repair passes added no second placeholder — CLI-B1 uses structured boundary metadata and an executable guard instead.

No existing fixture moved and no existing golden was regenerated, all repair passes included — git diff 4db644f..HEAD -- ownlang/ tests/fixtures/ is empty.

Fixture regeneration command

python tests/test_cli_ownir_fixtures.py --write     # on Linux, the authoring platform

The writer runs every python-oracle case twice and refuses to write one whose runs disagree; it also refuses a stale, missing or orphan case, and now a case whose declared boundary drifted from the declaration.

Steady-state test command

cd rust && cargo test -p own-cli

Production dependency changes

None outside the new crate, with one deliberate exception the first repair pass required: own-ir gains unicode-properties (already a workspace dependency, zero-dep) so the Version message can reproduce CPython's repr exactly rather than approximately. The second and third passes add nothing: git diff 4db644f..HEAD -- rust/Cargo.toml rust/crates/own-ir/Cargo.toml is empty, and in particular neither serde_json/preserve_order nor arbitrary_precision is enabled. Not a workspace-crate edge — own-ir remains the DAG leaf. own-cli declares own-ir, own-bridge, serde_json and unicode-properties; the DAG gains exactly two workspace edges, own-cli → own-ir and own-cli → own-bridge, registered in own-diagnostics/tests/dag.rs. No own-codegen, no own-shadow, no sha2.

Behavior changes

None in Python. No production behaviour changes anywhere the product calls — the binary is not wired to anything. The repair passes change own-ir's two ownir_version rejection messages to match the reference byte for byte; kind parity is untouched (#259 compares the kind), and what this crate accepts does not moveserde_json remains the only parser whose verdict decides accept/reject, and the second pass's raw re-read decides only how an already-certain rejection is spelled. No consumer of the old text outside own-ir's own implementation and this issue's fixtures exists — the census is in the note §5.2a. The third pass changes prose only; the census assert and its failure message are byte-identical.

The stale panic-per-binary design note in rust/Cargo.toml and its P-022 mirror are corrected as a design note: a cargo profile applies to every target of a build, so "abort for own-cli, unwind for the LSP" was never a plan Cargo could execute.

Acceptance changes

None ticked here — the owner closes #261 by hand after this review. The first repair pass fixed a status-surface self-contradiction on 1a32185 (P-022 said both that 261.B was built and that it was next); the second keeps those same surfaces — row 7b, the Preferred queue, the proposals index and rust/README.md — consistent with what it changed, rather than leaving a flat "byte-parity" claim behind a measurement that now has a named shape; the third narrows V4's statement to the form the owner ratified.

GitHub Actions links

On the current head bc8176b — see the checks on this PR. The 1a32185, 4db644f and 51325a4 runs are superseded.

Rulings, as settled by the repair passes

The 1a32185 draft carried three items as "the owner's to rule on". The owner ruled; this head applies the rulings. Details with exact bytes in the note §5.

(2a) The Version family — FIXED to byte parity, not declared. That text is ours on both sides, so the divergence was a Rust bug. A pre-change consumer/churn census chose the layer: the old text reached own-ir's implementation, two own-ir tests asserting substrings that survive, and this issue's own fixtures — no frozen fixture and no #260 repro/shadow evidence — so the wording is fixed where it is wrong. Measured after: zero own-ir tests moved, no golden regenerated. The corrected taxonomy has two rejection rows, not three: an absent ownir_version is accepted as v0 and carries no message.

(2a, second pass) The oracle is semantic, and the first measurement was the wrong shape. Everything above is still true; it measured four values where the oracle is repr(json.loads(raw)["ownir_version"]) over value classes, and the four scalars chosen are exactly the classes where a serde_json::Value and CPython's json agree. A Value is a lossy rendering of the document for repr, and three defects lived in the loss:

what Value loses Value said CPython says why the first census could not see it
object key order (Map is a BTreeMap; dict is insertion-ordered) {'a': 2, 'b': 1} {'b': 1, 'a': 2} its only object control had one key, and one key is order-invariant
float spelling (Display writes ryū's shortest form; repr pads the exponent) 1e-6 1e-06 its only float was 0.0, which needs no exponent
integer precision (no arbitrary_precision, so an oversized literal arrives as f64) 1e+31, wrong-type arm 100000…000, mismatch arm its integers all fit i64

The third is a branch, not a spelling: a Python int has no width, so an oversized integral version clears the reference's type check and takes its mismatch arm (ruling V3). Fixed by a raw re-read into a PyValue — insertion-ordered dicts with CPython's rebind-in-place semantics, arbitrary-precision integers kept as digits, int/float decided by the literal — scoped to the one contractual value and taken on the rejection path only. Neither serde_json/preserve_order nor arbitrary_precision is enabled: each would fix a row by changing Value for every crate in the workspace, and #260's canonical-domain evidence is measured against the current Value. No #260 evidence is touched.

A 200 000-double sweep then found a fourth: on an exact tie between two candidates of the shortest round-trip length, CPython rounds half to even and Rust's shortest formatter does not (-1128910513108089.2 vs ...3, ≈1 double in 3 000). The digits now come from Rust's exact formatter at the shortest length. Re-swept: 200 000 / 0.

Re-measured in two groups, so the declared divergences can never drift back into the byte denominator. Group 1: 24 value classes 24/24 byte-identical, then 20 000 randomized documents / 0 mismatches. Group 2, declared and excluded: V1 (NaN/Infinity are CPython json extensions this parser refuses — teaching it non-standard JSON to match an error message would widen what the product accepts); V2 (the literal -0, the cross-parser encoding split #260 froze — reporting 0 would name a type we did not read and imply an acceptance we do not grant); and V4, below.

The census is now own-ir/tests/version_repr_census.rs: Python authored those bytes once, the suite defends them with zero Python.

(V4, as the owner ratified it — third pass.) str.isprintable() is answered from the Unicode table each side was built with. CPython 3.11.15 (UCD 14.0.0) and unicode-properties 0.1.4 (UCD 17.0.0) disagree on 15 097 code points by whole-plane sweep — that figure is a specific two-version measurement, not a fixed size of V4, and against a 3.12 (UCD 15.0.0) or 3.13 (UCD 15.1.0) reference it is a different number. The precise claim is that no single static Unicode-property table can be byte-identical to every supported CPython reference version on the code points whose classification differs between those versions; vendoring several UCD releases and dispatching on an explicitly selected reference version is technically possible, and would make the representation version-aware rather than make one static table universal. So a pin is a reference-contract choice, not a fix: the table is a property of the interpreter build, and a pin buys parity with one interpreter while silently losing it against another. Its reach here is measured rather than assumed — no file in the frozen fixture family contains one of those code points, which is why the oracle: "python" cases stay green across 3.11/3.12/3.13.

(2b) The JSON parser detail — CLI-B1, a named typed boundary.

CLI-B1  JSON_PARSER_DETAIL   (applies iff OwnIrErrorKind == Json)
  pinned:   exit 2 · stderr · kind == Json · the FULL CLI-owned wrapper,
            byte-exact: "{path}: error: {path} is not valid JSON: "
  declared: only the bytes AFTER that prefix — the parser library's own text

The wrapper was drifting (the reference carries the path twice; Rust emitted it once), so it is made byte-exact first by an own-cli adapter — touching neither #259 nor #260. The guard has a lock: a Json rejection that loses own-ir's internal prefix fails onto rc 70, never rc 2 and never swallowing its own drift as the tail. The replay proves eligibility (valid UTF-8 → from_json rejects → kind == Json) before relaxing anything, plus a sweep asserting no case can acquire the relaxed matcher by accident.

(2b, second pass) The negative control had a hole, and it is written down rather than quietly repaired. It read a different case at a different path from the positives, and its own comment had relaxed "same path" to "same path shape" — a control whose prose has to widen to stay true is not measuring what it says, and a guard keyed on the path, the extension or the case name would have passed it. The decision now takes the facts bytes as a parameter, and the control runs one case — one argv, one exact path string, one decode route, one fixture — twice:

run bytes strict door eligible
positive the case's own facts, on disk Json yes
negative a valid document with a version mismatch Version no

Everything a guard could be keyed on is held literally identical, and the test asserts both runs resolve the same path from the same case, so eligibility can only turn on the content. The bytes are still frozen fixture bytes — inventing them would move the oracle into the replay. One honest limit is stated in the note: non-vacuity is a property of a control's construction, not something a mutation can prove; M24 shows only that the construction is load-bearing.

(1) Invalid UTF-8 — a declared defect of the Python reference, excluded from the byte contract pending a Python-first normalization before public cutover. Hygiene tail: UnicodeDecodeErrorOwnIRError → rc 2. The earlier reason was wrong and is struck — an oracle exists (Python printed it); we decline to make a CPython exception's wording a cross-language contract. #262 is the tracker of record and carries this under Known differences recorded ahead of the packet (2026-09-08); the #250 roadmap mirror is pending reconciliation.

(3) Windows — three claims. A canonical reference parity: claimed. B Rust portability (Linux bytes == Windows bytes): claimed. C native-Windows Python parity: NOT claimed — the reference emits cp1252/CRLF and dies with UnicodeEncodeError on the non-ASCII cases Linux renders. A behavior change, not folded into a parity claim: #262 is the tracker of record and carries it under Known differences recorded ahead of the packet (2026-09-08); the #250 roadmap mirror is pending reconciliation.

Still measured and deliberately not pinned: SIGINT (Linux signal 2 with a varying traceback; Windows 0xC000013A with empty stderr — no cross-platform contract, 130 not invented); closed stdout (rc 70, deterministic on both, but the case format cannot close a pipe); the uncatchable death's OS number (134/SIGABRT, asserted only as outside {0, 1, 2, 70}). One class still awaits a ruling: the docstring-on-stdout cases, frozen and flagged.

Migration counts

Generated, never typed:

Unexplained difference count: 0. Every pinned case replays byte-for-byte against the built binary on both platforms, and the Version family is byte-identical over 24 value classes and 20 000 randomized documents. The declared differences are each named, measured and controlled rather than tolerated: CLI-B1's parser detail (kind-guarded, negatively controlled), invalid UTF-8 (ruling 1), the reference's non-standard JSON constants (V1), the literal -0 (V2), and the Unicode-table skew (V4 — a representation boundary whose size is a two-version measurement recorded in the note, unreachable by any case in this contract).

Чеклист

  • изменение покрыто тестом/селфтестом: the frozen cases replayed with zero Python on two platforms, the Version family's value-class census, unit tests over the display policy and the boundary guard, structural/determinism/bijection checks, the two forced-failure controls, and a mutation campaign with no survivors and no missed catchers — counts in the generated census and mutation fragments linked above.
  • README/docs обновлены при необходимости: P-022 row 7b and the Preferred queue, the proposals index, rust/README.md's own-cli row, and the note.
  • коммиты в conventional-commit стиле

🤖 Generated with Claude Code

https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv

…H.0)

The record of what `python -m ownlang ownir` actually does, executed rather
than read off the docstring, and the four facts documents whose shape no
existing fixture had.

What the measurement settled, each one a control the fixture will name:

* the exit code is a function of `leaks` alone — `--severity` and
  `--verbosity` never move it, so the docstring's "non-zero if any
  error-level diagnostic" is not the contract and `1 if leaks else 0` is;
* `quiet` hides advisories from the stream and the summary, and nothing else;
* the `ok` line fires on `not shown`, so a document whose only finding is
  suppressed prints BOTH `ok` and the suppressed tally, at exit 0;
* the verbose `by code:` breakdown iterates every finding, suppressed
  included;
* the CLI's SARIF stdout is `json.dumps(indent=2)` with ensure_ascii left at
  its default — pure ASCII, `\uXXXX` escapes, a surrogate pair above the BMP —
  which is NOT the byte shape of the BR-V9 goldens (ensure_ascii=False). Two
  byte shapes, one builder; the builder is not touched.

Two stop conditions are recorded rather than resolved, because neither is
this task's call to make:

* invalid UTF-8 escapes `load()`'s converter and exits 70. Whether a crash on
  malformed input is a contract or a refusal to add is a Python-first
  decision, so nothing about it is pinned and no refusal was invented;
* the strict door's JSON-syntax and version-gate MESSAGES differ between the
  two implementations. #259 deliberately compared the rejection KIND and
  never the message; the exact bytes are in the note.

SIGINT and closed stdout are measured and written down. SIGINT is not pinned:
the reference dies by the signal (KeyboardInterrupt is a BaseException, so
the exit-70 catch-all never sees it) and a plain Rust binary dies the same
way, but the reference's stderr traceback changes between trials, and half a
case is less than what was measured.

Refs #261, #345, #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…#261 H.1)

80 cases under tests/fixtures/cli_ownir/, one file each, listed in a manifest
and never swept. Each names the rule it is the control for, so the census can
be rendered from the fixture instead of typed beside it.

Three oracle classes, tagged so a reader can tell them apart:

* `python` — an executed `python -m ownlang ownir` run. Everything after
  `ownir` is selected, which by C-1 is the reference's own behaviour as
  measured;
* `python-docstring` — the same, where the bytes happen to be the WHOLE
  module docstring on stdout. Frozen as measured and flagged, so the owner
  can declare that exact class a defect without first having to find it;
* `owen-convention` — the top-level shell, which has no Python byte oracle at
  all. The help text is authored once here, carried in the manifest, and the
  binary shares that one source.

The writer earns the word "contract": it runs every python-oracle case TWICE
and refuses to write one whose runs disagree, refuses a stale, missing or
orphan case, and stores streams as JSON strings so a `core.autocrlf` checkout
cannot corrupt an expectation. `<OS_ERROR>` is the single placeholder, valid
only on a line carrying a platform-native OS error text, with the tail it
replaced recorded beside it.

Four inputs are new because no existing document had the shape: every finding
suppressed (the `ok` line and a suppressed tally in one run), a non-ASCII and
astral `file` field, an empty-string ignore_reason, and a facts path through a
directory carrying both a space and non-ASCII.

Refs #261, #345, #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…H.1)

`tests/_preflight.py` refuses a `test_*.py` that can `raise SystemExit` at
import time, and it was right to: the non-determinism guard did exactly that,
so a fixture that had gone unstable would have killed `tests/run_tests.py`
before it reported anything, instead of failing one module.

It is a named `NonDeterministic` now. `--write` refuses to write the case and
returns non-zero; `run()` reports it as one FAIL line and carries on with the
rest — the same information, delivered without taking the suite with it.

The coordinate census moves with it, regenerated rather than typed: it sweeps
`tests/fixtures/**` for OwnIR documents, and the four new facts inputs are
four more of them.

Refs #261, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
`own-cli ownir <facts> [--format F] [--severity S] [--verbosity V]` — the one
core invocation the product seam makes today, as a native binary behind the
unchanged launcher. It reproduces the reference byte for byte over all 80
frozen cases: argument handling, the display policy, the stream split, the
four formats and every exit code.

The two halves have different oracles, and the boundary is the surface (C-1).
The top-level shell follows the public `owen` convention — a parity surface of
its own, with no Python byte oracle, written once and shared with the fixture
manifest. Everything after `ownir` is the reference as measured, including
the parts that surprise: a positional-count error prints the whole module
docstring to STDOUT with exit 2, and an unknown flag is a positional rather
than an error. `ownir --help` is the one case declared a defect and not
ported.

What it reuses and what it owns:

* `own_bridge::check_facts` is the analysis and `render_finding` /
  `build_sarif` are the renders — already byte-pinned by BR-V9, never
  re-derived here;
* the CLI owns the display policy, because it is CLI logic. It is a pure
  function of the findings and the three options, so the traps are unit tests
  rather than process invocations: the exit is `1 if leaks else 0` and no
  option moves it, `quiet` hides advisories and nothing else, the `ok` line
  fires on `not shown` so an all-suppressed document prints ok AND a tally,
  and the verbose breakdown counts every finding;
* the CLI owns the SARIF SERIALIZATION, because `cmd_ownir` writes the same
  document `json.dumps(indent=2)` does — pure ASCII with `\uXXXX` escapes and
  a surrogate pair above the BMP — while the BR-V9 goldens are
  `ensure_ascii=False`. Two byte shapes, one builder; the builder is untouched.

The process contract, under `panic = "unwind"`: a hook suppresses the default
panic output and records the payload, and a top-level `catch_unwind` turns the
unwind into one actionable stderr diagnostic and exit 70, never 101. The hook
alone would only observe the panic. Under OWNLANG_DEBUG the payload and a
captured backtrace print and the exit is still 70 — the asymmetry `run()`
keeps, because exit 1 reads as findings. An off-by-default `fault-injection`
feature gates the two dev-only hooks that force each failure mode, so both are
measured rather than asserted.

The DAG gains exactly two edges, `own-cli -> own-ir` and `own-cli ->
own-bridge`, registered in `dag.rs` where an unregistered member fails the
whole `cargo test`. No `own-codegen`, no `own-shadow`, no `sha2`.

The `panic`-per-binary note above `[profile.release]` is corrected in place as
a design note: Cargo cannot set `panic` per package — a profile applies to
every target of a build — so `abort for own-cli` was never a plan Cargo could
execute. Nothing about the built artifacts moves.

Refs #261, #345, #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…H.3)

`cargo test -p own-cli` builds the binary, runs it against every case in
`tests/fixtures/cli_ownir/` and compares exit code, stdout bytes and stderr
bytes. No Python runs — that separation is the point of the fixture, since a
replay needing the reference would only prove the two agree on a machine that
has both, which is not the machine the cutover is for.

Three structural checks come before any byte comparison, because a fixture
that has quietly stopped covering something passes every byte check it still
has: manifest and case files must be the same set, the format version must
agree on both sides, and every case must reproduce byte-for-byte on a second
run.

`<OS_ERROR>` is the only placeholder and it is held to rules of its own,
asserted rather than trusted: it consumes the rest of ITS line, what it
consumed must be non-empty, the `cannot read <path>: ` prefix stays
byte-exact, and whatever the case expects after that line still has to match.

The owen-convention text is checked through the process rather than the
symbol: an integration test cannot link a `[[bin]]` crate, and comparing what
a user actually sees against what the manifest carries is the stronger form
anyway.

The two failure-mode controls live behind the `fault-injection` feature. A
catchable panic is asserted precisely — exit 70, exactly two stderr lines, the
payload carried through, no findings, no backtrace unless OWNLANG_DEBUG —
because the number is the contract. An uncatchable death is asserted only as a
visible hard failure outside {0, 1, 2, 70} with nothing on stdout: #261
contracts no OS exit number for it, so none is asserted.

Refs #261, #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
`own-cli (ownir parity replay)` builds the binary and replays every frozen
case on both platforms with zero Python, then runs the two failure-mode
controls behind the `fault-injection` feature — which no production build
carries, so they need their own invocation.

Windows is in the matrix because the fixture is authored on Linux, and a byte
contract that has only ever been replayed on its authoring platform has not
been tested: path forms, line endings and the OS error text are exactly where
a CLI port diverges. `rust-core` is deliberately NOT widened to Windows for
every crate — that is a separate cost decision, and this is the crate whose
contract is platform-shaped.

The Python half needs no new job: `tests/run_tests.py` discovers
`test_cli_ownir_fixtures.py` like every other `test_*.py`, so the existing
matrix re-verifies each python-oracle case against the reference on
3.11/3.12/3.13.

Also lands a TEMPORARY workflow and script that measure the reference on
WINDOWS — non-ASCII output through a pipe, closed stdout, and interruption.
#261 rules that SIGINT is measured on both platforms before anything is
written down, and this implementing environment is Linux-only; a measurement
nobody took is not a measurement. It runs the reference rather than the port,
asserts nothing, and gates nothing. Both files are deleted once the note
carries the numbers.

Refs #261, #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
#261 H.5)

The catcher validator special-cased `src/lib.rs` — a LIB target whose unit
tests live wherever the module does, so the whole `src/` tree is searched. A
BIN target's unit tests live exactly the same way, and nothing said so.

`own-cli` is the first binary crate in this workspace whose unit tests a
campaign names as catchers, and without this every one of them read as "names
a test that does not exist" while pointing at a test that plainly did — a
validator failure that looks like an authoring mistake is worse than no
validator.

Refs #261, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
Fifteen mutations over the display policy, the CLI's SARIF serialization, the
usage-error exit codes and the process contract. Each is a plausible
MISREADING of the reference rather than a syntactic accident: every one would
pass a reviewer who had read the module docstring instead of the code.

Three layers rather than `workspace`, because the two failure-mode controls
live behind the off-by-default `fault-injection` feature and a campaign that
cannot run the layer holding a catcher cannot see it catch. `rust-rest` runs
every other member so no-fail-fast holds across layers — it is expected to
catch nothing, and a catcher there would mean a mutation reached past its
target.

M11 and M15 exist as a pair because the first run said so: `usage_error` and
the docstring answer are two different paths to exit 2, and a catcher named on
one cannot see a mutation in the other. The expectation was corrected and the
path it actually named became its own mutation, rather than the expectation
being quietly dropped.

Refs #261, #250

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

15/15 caught, 0 survived, no missed catchers, on a clean tree. The M00
honesty control passed unmutated, so the run measured something.

The two fragments are generated, never typed: the census reads the fixture
manifest for the case count, the oracle classes and the per-rule evidence,
and the mutation fragment derives its counts from the recorded run. Both are
registered in the checkpoint-status gate, so a fixture case added without
regenerating them turns the Python suite red.

Refs #261, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
… resolve (#261 H.6)

The note carries §3-§7: what landed, the ledger by link, what was measured and
deliberately not pinned, the tails, and the commands to reproduce all of it.
The temporary Windows measurement workflow and its script are removed now that
their numbers are in §1.9; the commit that added them is the record that the
measurement was taken.

Three findings are recorded for the owner rather than resolved, because none
of them is this task's call:

* invalid UTF-8 escapes `load()`'s converter and exits 70 on the reference. No
  fixture case freezes it and no refusal was invented; the binary reproduces
  the code and the shape and claims no byte parity, since there is no oracle
  for a Python exception's repr;
* the strict door's JSON-syntax and version-gate MESSAGES differ between the
  implementations, with the exact bytes in §5.2. #259 decided on purpose that
  the rejection KIND is compared and never the message; C-1 asks the CLI for
  the reference's message. Deciding which wins is a #259-surface question;
* the Windows reference is not byte-portable. On a piped stdout it encodes
  with cp1252 and translates line endings, so it emits \r\n where Linux emits
  \n and one 0x97 byte where Linux emits the UTF-8 em dash — which is in the
  `ok` line and most finding messages. On the two non-ASCII `file` cases it
  emits nothing at all and exits 70 with a UnicodeEncodeError, where the Linux
  reference exits 1 with a finding. The fixture stays Linux-generated, as the
  brief requires, and nothing was normalized away.

The useful half of that last one: the Rust binary is byte-identical on both
platforms where the reference is not, so the Linux-authored fixture replays
byte-for-byte on windows-latest — including the cases the Windows reference
cannot produce.

The surfaces move together: P-022 row 7b, the proposals index and
`rust/README.md`'s `own-cli` row. The stale `panic`-per-binary claim is
corrected in P-022's prose and its `[profile.release]` snippet as a design
note: a cargo profile applies to every target of a build, so "abort for
own-cli, unwind for the LSP" was never a plan Cargo could execute.

Refs #261, #345, #262, #250

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

coderabbitai Bot commented Sep 8, 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: 7ac7d143-b204-41d6-a32b-cdfb32d5d43c


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.

…ndary (#261 R2, R3)

Two repairs that share the fixture family and the writer, so they share a
commit; the campaign result is regenerated in the next one.

## R2 — the Version family is FIXED, not declared (ruling 2a)

That text is OURS on both sides, so a divergence there was a Rust bug. Census
first. The taxonomy had to be right before anything moved: `ownir_version`
ABSENT is accepted as v0 and carries no message, so it is not a control to
reproduce — there are exactly two rejection rows, a present value of the wrong
type and a present integer that is not v0.

Measured across their value variants, the wrong-type row diverged three ways
rather than the two the ruling enumerated: `serde_json::Value`'s Display spells
a string with double quotes, a bool lowercase, AND null as `null`, where the
reference interpolates a Python `repr` (`'0'`, `True`, `None`). Containers
diverge too (`["a"]` vs `['a']`). Numbers already agreed. The mismatch row had
dropped the words `Roslyn` and `Python`.

The layer came from a consumer census, not from preference. The old text
reaches `own-ir`'s implementation, two `own-ir` tests that assert substrings
which survive a wording change, and this issue's own CLI fixtures. It reaches
NO frozen fixture and NO #260 repro/shadow evidence: the validation ledger is
Python-authored and carries Python's messages, and the Rust replay compares the
rejection KIND, never the message. So the wording is fixed where it is wrong
and no golden is regenerated — measured after the change, zero `own-ir` tests
needed to move.

`py_repr_value` is a third copy of a helper own-syntax and own-cli already
carry, because `own-ir` is the DAG leaf and may import neither. The alternative
was a message known to be wrong. The sibling `line`-family message still uses
Display: a #259 surface, out of scope, recorded as a tail.

## R3 — CLI-B1, a named boundary with a lock in it (ruling 2b)

  CLI-B1  JSON_PARSER_DETAIL   (applies iff OwnIrErrorKind == Json)
    pinned:   exit 2, stderr, kind == Json, and the FULL CLI-owned wrapper
              byte-exact: "{path}: error: {path} is not valid JSON: "
    declared: only the bytes AFTER that prefix — the parser library's text

The wrapper was drifting before this: the reference bakes the path into the
message inside `load()` and prints it again in `cmd_ownir`, so the line carries
the path TWICE, and Rust emitted it once. Relaxing everything after
`{path}: error: ` would have declared that missing half implementation-defined
too. An `own-cli` adapter guarded by `kind == Json` supplies the CLI-owned half
so only the parser detail is declared — touching neither #259 nor #260.

The guard has a lock: a Json rejection whose message lost `own-ir`'s internal
`not valid JSON: ` prefix is a broken invariant of the Rust implementation,
not a rejection to pass through, so it takes the internal-error path (rc 70)
and CLI-B1 does NOT apply. A guard on the kind that then let the adapter eat
its own structural drift as "the declared tail" would be a door built with the
lock left out.

No second placeholder: one strictly-bounded `<OS_ERROR>` is the whole budget.
The case carries structured `boundary` metadata instead, and the replay PROVES
eligibility before relaxing anything — read the exact facts bytes, decode UTF-8
with no normalization (a decode failure is ruling 1's defect and must never
borrow this boundary), require `OwnIr::from_json` to reject, require the kind
to be Json. The negative control keeps everything constant but the content —
valid UTF-8, same argv shape, a Version rejection instead of a syntax one — so
it proves the guard is on the kind and nothing else.

The three malformed inputs are named `.facts.broken`, not `.facts.json`: the
coordinate census sweeps every `.json` under tests/fixtures/ and would rightly
call an unparseable one a broken fixture. A file whose job is to be invalid
JSON should not claim the extension.

Refs #261, #259, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…R2, R3)

19/19 caught, 0 survived, no missed catchers, on a clean tree; the M00 honesty
control passed unmutated.

Four mutations join for the repairs, each aimed at a way the repair could be
undone without any other test noticing:

* M16 reverts the Version wrong-type message to serde_json Display — the
  string quotes, the lowercase bool and the `null` that ruling 2a called a bug;
* M17 drops `Roslyn` and `Python` from the schema-mismatch wording;
* M18 removes the second path from the CLI-owned JSON wrapper, the `{path} is`
  half the reference bakes in inside `load()` and CLI-B1 pins;
* M19 is the important one: it makes the boundary guard swallow its own
  structural drift, passing a prefix-less Json rejection through as rc 2
  instead of failing onto the internal-error path. That is the lock left out
  of the door, and the unit test catches it by name.

`own-ir` becomes its own campaign layer, because ruling 2a made that crate a
mutable production surface of this contract and a campaign that cannot run the
layer holding a catcher cannot see it catch.

The generated fragments move with it, computed rather than typed. The
coordinate census changed by exactly the file count of the new inputs and no
coordinate slot: the Version documents carry no line or column.

Refs #261, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…settled (#261 R1, R4, R5, R6)

## R1 — the surfaces no longer contradict each other

P-022 said, in one file, that 261.B was "built and replaying on both platforms"
(row 7b) and that "the production OwnIR executable is next" (Preferred queue).
Built and next at once is precisely the derived-status drift this project keeps
surfaces generated to prevent. The queue now reads that #262 is next, with #345
the residual off the cutover path, and row 7b, the queue, the proposals index
and rust/README.md's own-cli row agree.

## R4 — invalid UTF-8 is a declared REFERENCE defect, with an owner

Not "reported, not pinned" — a defect of the Python reference, excluded from
#261's byte contract pending a Python-first normalization before public
cutover, with the hygiene tail opened under #250/#262: UnicodeDecodeError ->
OwnIRError -> rc 2.

The earlier draft's reason was wrong and is struck. It said "there is no oracle
for a Python exception's repr". There is one — Python printed it, and the note
quotes it. The actual reason not to pin is that we DECLINE to make a CPython
exception's wording a cross-language contract.

## R5 — Windows is three claims, and the third is not claimed

A: canonical reference parity, claimed. B: Rust portability, Linux bytes ==
Windows bytes, claimed. C: native-Windows Python parity, NOT CLAIMED — the
reference emits cp1252 and CRLF and dies with UnicodeEncodeError on the
non-ASCII cases the Linux reference renders.

The draft's "the useful half" framing is struck: it read as a parity flavour
over a result that is not parity. C is carried into #262 as a behavior change
— Windows Python today emits cp1252/CRLF, Rust tomorrow emits UTF-8 canonical
bytes on both platforms, which is very likely an improvement AND is still a
change for a Windows user whose tooling reads those bytes.

## R6 — acceptance wording

261.B now reads on every surface: built, with (2a) Version messages
byte-parity, (2b) the JSON parser tail a declared typed boundary CLI-B1, (1)
invalid UTF-8 a declared reference defect with a Python-first tail, (3) Windows
A + B with C not claimed. No count is typed on any surface, and no acceptance
box is ticked here — the owner closes the issue by hand after the new review.

The note's §5 is rewritten around the same split, and §6 records two tails the
repair pass created or made more expensive: py_repr is now carried three times
(own-syntax, own-cli, own-ir), and own-ir's `line`-family message still uses
Display and carries the same divergence the Version message shed — a #259
surface, deliberately untouched.

Refs #261, #345, #262, #250

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…ily (#261 R2b)

R2 claimed byte parity for the `ownir_version` rejection text and measured it
on four hand-picked values. The claim was right and the measurement was too
narrow: `repr()` is a semantic oracle over what CPython's `json` DECODED, and a
`serde_json::Value` is a lossy rendering of the document for that purpose.
Three defects lived in the gap, none of them reachable by a single-key,
integer-valued control:

* object key ORDER. `serde_json::Map` is a `BTreeMap` and a `dict` is
  insertion-ordered, so `{"b": 1, "a": 2}` reprs as `{'a': 2, 'b': 1}` here and
  `{'b': 1, 'a': 2}` there. R2's only object control had one key, which is
  order-invariant — which is exactly why it passed;
* float SPELLING. `Display` writes ryu's shortest form; CPython pads the
  exponent and switches to it on different boundaries. `1e-6` was `1e-6` here
  and `1e-06` there;
* integer PRECISION, which is a branch rather than a spelling. A Python `int`
  has no width, so an integral version too large for `i64` clears the
  reference's type check and lands in its MISMATCH arm carrying its full
  decimal digits. Without `arbitrary_precision` it arrives here as an `f64` and
  took the wrong-type arm as `1e+31` (#261 ruling V3).

Fixed at the narrowest layer that can fix them: a `PyValue` reading of the RAW
document, scoped to the one value whose spelling is contractual and taken only
on the rejection path. Neither `serde_json/preserve_order` nor
`arbitrary_precision` is enabled — both change `Value` for the whole workspace,
and #260's canonical-domain evidence is measured against the current `Value`.
`serde_json` remains the only parser whose verdict decides accept/reject; the
re-read decides only how an already-certain rejection is spelled.

A 200 000-value float sweep then found a fourth: where a double sits exactly
midway between two candidates of the shortest round-trip length, CPython rounds
half to EVEN and Rust's shortest formatter does not (`-1128910513108089.2` vs
`...3`, about one double in 3 000). The digits now come from Rust's exact
formatter asked for the shortest formatter's length.

Measured, in two groups so the declared divergences can never drift back into
the byte denominator:

* 24 value classes, 24/24 byte-identical with `ownlang.ownir.load`;
* 20 000 randomized documents (nested containers, repeated keys, the whole
  string surface, oversized integers), 0 mismatches;
* declared and NOT counted: V1's non-finite constants, V2's literal `-0`, and a
  new V4 — `str.isprintable()` reads the Unicode table each side was BUILT
  with, and CPython 3.11.15 links 14.0.0 where `unicode-properties` 0.1.4 ships
  17.0.0. A whole-plane sweep puts that at 15 097 code points. It is not
  repairable from Rust: pinning this crate's table would buy parity with one
  interpreter and silently lose it against another.

The census is committed as a Rust test, so Python authored those bytes once and
the suite defends them with zero Python from here on.

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

R3 added a negative control for the CLI-B1 boundary and gave it a hole: it read
a DIFFERENT case at a DIFFERENT path from the positives it was contrasted
against, and its own comment had relaxed "same path" to "same path shape". A
guard keyed on the path, the extension or the case name would have passed it,
which is the one thing a negative control exists to rule out.

`cli_b1_eligible` is split so the decision takes the facts bytes as a
PARAMETER, and the control now runs ONE case — one argv, one exact path string,
one decode route, one fixture — twice, against two byte sequences:

| run | bytes | strict door | eligible |
|---|---|---|---|
| positive | the case's own facts, on disk | `Json` | yes |
| negative | a valid document with a version mismatch | `Version` | no |

Everything a guard could accidentally be keyed on is held literally identical
across the two runs, and the test asserts that both runs resolve the same path
from the same case. The bytes are still frozen fixture bytes rather than bytes
invented in the test — inventing them would move the oracle into the replay.

The comment's "same path" is restored, because it is now true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…R2b, R3b)

M16 and M17 are re-anchored: the version gate was rewritten, so the definitions
that named its old text no longer applied to this tree. The recorded result
stays valid for the commit it names; this is the re-anchoring the gate asks for.

Five new mutations, each a plausible misreading rather than a syntactic
accident, and each one reachable only by a control class the first measurement
did not have:

* M20 — a repeated dict key is appended instead of rebound in place, so it
  keeps the wrong position and appears twice;
* M21 — the float exponent loses CPython's zero padding, which is exactly the
  spelling R2 shipped;
* M22 — the gate stops re-reading the raw document, so object keys sort and an
  oversized integer takes the wrong-type arm instead of the mismatch arm;
* M23 — the digits come from the shortest formatter rather than the exact one,
  so a round-half-to-even tie rounds the wrong way;
* M24 — the CLI-B1 eligibility guard judges a file it reaches for instead of
  the bytes it was handed. This mutates the EVIDENCE rather than the product,
  and says so: a control's non-vacuity is a property of its construction and is
  not mutation-provable, so what M24 shows is that the construction is
  load-bearing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…acker of record (#261 R2b, R3b, R3c)

§5.2a — the first Version census was true and the wrong SHAPE. Each of its rows
picked a value; the oracle is about a value CLASS, and the four scalars it chose
are exactly the classes where a `serde_json::Value` and CPython's `json` agree.
The section now says what the loss was (object order, float spelling, integer
precision), why a single-key control could not see it, why the fix is a scoped
raw re-read rather than a workspace-wide feature flag, and what the sweep found
after that: a round-half-to-even tie in the shortest round-trip digits, about
one double in 3 000.

The re-measurement is stated in two groups so the declared divergences can never
drift back into the byte denominator — 24/24 classes and 20 000 randomized
documents in group 1, and V1/V2/V4 in group 2 with the reference's own answer
beside each. `-0` below the top level is explicitly NOT V2, which is why the
stand-down is written as "the value the type check is about".

V4 is new and is a boundary rather than a bug: `str.isprintable()` is answered
from the Unicode table each side was BUILT with, and CPython 3.11.15 links
14.0.0 where `unicode-properties` 0.1.4 ships 17.0.0 — 15 097 code points apart
by whole-plane sweep. Not repairable from Rust, and its reach here is measured
rather than assumed: no file in the frozen fixture family contains one of them.

§5.2b — the CLI-B1 negative control's hole is written down rather than quietly
repaired. It read a different case at a different path, and its own comment had
relaxed "same path" to "same path shape"; a control whose prose has to widen to
stay true is not measuring what it says. Also written down is the limit that
mutation cannot cover: no source mutation distinguishes a control that compares
two documents from one that compares a document with itself, so non-vacuity is a
property of the construction and M24 only shows the construction is load-bearing.

§5.1, §5.3, §6 — tracker state corrected. #262 is the tracker of record and the
reviewer recorded both rulings there under "Known differences recorded ahead of
the packet" (2026-09-08); the #250 roadmap mirror is pending reconciliation. The
note previously said the tail was "opened under #250/#262" and that Windows was
"carried into #262", asserting a tracker state this note is not the authority on
and had not verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…R2b, R3b)

24/24 caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error; every
expected catcher fired. Four layers, clean tree.

M23 is the row worth reading: the round-half-to-even tie is caught by exactly
ONE test, the new value-class census. Nothing chosen by hand reaches it, which
is the same lesson the four defects taught in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
…r pass (#261 R2b, R3b, R3c)

R1 fixed a status-surface self-contradiction; leaving these three stale would
have reintroduced one. P-022 row 7b, the proposals index and `rust/README.md`
all carried the same two claims the second pass changed:

* the (2a) byte-parity claim, stated flat. It is now stated with its shape —
  measured over value CLASSES rather than sample values, 24/24 plus 20 000
  randomized documents, with V1, V2 and V4 named and excluded rather than
  counted. An unqualified "byte-parity" is the same over-claim shape the review
  objected to, one level up;
* the tracker state. "under #250/#262" and "which #262 carries" asserted a
  tracker state these documents are not the authority on. #262 is the tracker of
  record for both rulings; the #250 roadmap mirror is pending reconciliation.

Row 7b and the index also now say what the CLI-B1 negative control actually
does — one case, two byte sequences — rather than "holds everything constant",
which was the claim the old control could not support.

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

The owner ratified V4 in a narrower form than the tree was carrying, and
forbade two phrasings that 51325a4 still used.

(F1) "not repairable from Rust" is false in the absolute. What is true is
narrower and worth saying precisely: no single STATIC Unicode-property table can
be byte-identical to every supported CPython reference version on the code
points whose printability classification differs between those versions.
Vendoring UCD 14.0.0/15.0.0/15.1.0 and dispatching on an explicitly selected
reference version is technically possible — that would make the representation
version-aware rather than make one static table universal. The reason survives
unchanged, because it was the sound half all along: the table is a property of
the interpreter BUILD, so a pin buys parity with one Python and silently loses
it against another. That makes a pin a reference-contract choice, not a fix.

(F2) the mismatch count is a two-version measurement, not a size of V4. It is
what CPython 3.11.15 (UCD 14.0.0) and unicode-properties 0.1.4 (UCD 17.0.0)
disagree on; against a 3.12 or 3.13 reference it is a different number. Every
bare appearance is gone from the status row, the Group-2 class cell and the §6
tail; the two version-labelled measurements — the note's §5.2a sweep and the
census comment above the U+088F assert — are the only places the exact figure
survives, and neither moved.

Wording only: no logic, no test assertion, no fixture, no dependency, no
feature, no generated evidence. The census assert and its failure message are
byte-identical. `git diff 51325a4..HEAD -- ownlang/ tests/fixtures/` is empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv
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.

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

2 participants