feat(own-cli): the production Rust OwnIR executable, behind the unchanged launcher (#261 261.B) - #347
Merged
Merged
Conversation
…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
|
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 |
…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
This was referenced Sep 8, 2026
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.
Что и зачем
#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 unchangedowenlauncher. It reproduces the reference'sownircontract 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;bc8176bis the head to review.Тип изменения
Как проверено
python tests/run_tests.pyruff check .иmypypython scripts/validate_contrib.py --selftest)Local commands, all green on this head:
The campaign, recorded on
a5a4312and 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-clibinary with its singleownirsubcommand, 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.owenconvention (C-1): empty invocation → help on stdout, exit 2;--help/-h→ help on stdout, exit 0;--version→own-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.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 --helpis 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.panic = "unwind": a hook suppresses the default panic output and a top-levelcatch_unwindproduces one actionable stderr diagnostic and exit 70, never 101. A hook alone would only observe a panic. UnderOWNLANG_DEBUGthe payload and a backtrace print and the exit is still 70.Explicit non-goals
.ownand dev surfaces —cfg,summaries,explain,.own check;emitbehind #257 #345's commands (cfg,summaries,explain,.own check,emit) — they join this binary later; the dispatch table and the single help surface are built to be joined, not rewritten.owen,own-check.sh,own-check.ps1,action.yml,own-shadow-engineandscripts/shadow_compare.pyare untouched.report— struck at P-022 step 5b: port the SARIF projection with canonical parity #256, not deferred (C-2).Python source of truth
ownlang/__main__.py—cmd_ownir(lines 338–405) andmain/run(795–957), plusownlang/ownir.py'sload,Finding,render_findingandbuild_sarif. Unchanged: not a character. No file underownlang/is in this diff, all repair passes included.Frozen fixture
tests/fixtures/cli_ownir/— a manifest plus one<name>.case.jsonper 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 acore.autocrlfcheckout 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 structuredboundarymetadata 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 platformThe 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
Production dependency changes
None outside the new crate, with one deliberate exception the first repair pass required:
own-irgainsunicode-properties(already a workspace dependency, zero-dep) so the Version message can reproduce CPython'sreprexactly rather than approximately. The second and third passes add nothing:git diff 4db644f..HEAD -- rust/Cargo.toml rust/crates/own-ir/Cargo.tomlis empty, and in particular neitherserde_json/preserve_ordernorarbitrary_precisionis enabled. Not a workspace-crate edge —own-irremains the DAG leaf.own-clideclaresown-ir,own-bridge,serde_jsonandunicode-properties; the DAG gains exactly two workspace edges,own-cli → own-irandown-cli → own-bridge, registered inown-diagnostics/tests/dag.rs. Noown-codegen, noown-shadow, nosha2.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 twoownir_versionrejection messages to match the reference byte for byte; kind parity is untouched (#259 compares the kind), and what this crate accepts does not move —serde_jsonremains 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 outsideown-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 inrust/Cargo.tomland its P-022 mirror are corrected as a design note: a cargo profile applies to every target of a build, so "abort forown-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, twoown-irtests 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: zeroown-irtests moved, no golden regenerated. The corrected taxonomy has two rejection rows, not three: an absentownir_versionis 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 aserde_json::Valueand CPython'sjsonagree. AValueis a lossy rendering of the document forrepr, and three defects lived in the loss:ValuelosesValuesaidMapis aBTreeMap;dictis insertion-ordered){'a': 2, 'b': 1}{'b': 1, 'a': 2}Displaywrites ryū's shortest form;reprpads the exponent)1e-61e-060.0, which needs no exponentarbitrary_precision, so an oversized literal arrives asf64)1e+31, wrong-type arm100000…000, mismatch armi64The third is a branch, not a spelling: a Python
inthas 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 aPyValue— insertion-ordered dicts with CPython's rebind-in-place semantics, arbitrary-precision integers kept as digits,int/floatdecided by the literal — scoped to the one contractual value and taken on the rejection path only. Neitherserde_json/preserve_ordernorarbitrary_precisionis enabled: each would fix a row by changingValuefor every crate in the workspace, and #260's canonical-domain evidence is measured against the currentValue. 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.2vs...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/Infinityare CPythonjsonextensions 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 — reporting0would 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) andunicode-properties0.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 theoracle: "python"cases stay green across 3.11/3.12/3.13.(2b) The JSON parser detail — CLI-B1, a named typed boundary.
The wrapper was drifting (the reference carries the path twice; Rust emitted it once), so it is made byte-exact first by an
own-cliadapter — touching neither #259 nor #260. The guard has a lock: aJsonrejection that losesown-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_jsonrejects →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:
JsonVersionEverything 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:
UnicodeDecodeError→OwnIRError→ 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
UnicodeEncodeErroron 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
0xC000013Awith empty stderr — no cross-platform contract,130not 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:
docs/generated/p022-cli-census.mddocs/generated/p022-cli-mutations.mdUnexplained 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).Чеклист
rust/README.md'sown-clirow, and the note.🤖 Generated with Claude Code
https://claude.ai/code/session_01DTGMj7edRtjhyeXzkeiSWv