diff --git a/docs/evidence/p022-coord-1.json b/docs/evidence/p022-coord-1.json new file mode 100644 index 00000000..88ce8a3b --- /dev/null +++ b/docs/evidence/p022-coord-1.json @@ -0,0 +1,351 @@ +{ + "schema": 1, + "campaign": "p022-coord-1", + "description": "#259 final acceptance, the STRICT door: the coordinate domain of spec/OwnIR.md §4.2 in both implementations. Every rule is mutated twice — once in the reference (ownlang/ownir.py, ownlang/obligations.py) and once in its Rust mirror (own-ir/src/strict.rs, own-ir/src/protocol.rs) — because a domain that only one side enforces is a divergence the ledger would report as a category mismatch rather than as the missing rule it is. Both ends of both bounds are attacked, and off-by-one at each, since a domain checked only far outside cannot tell a correct limit from a narrow one; the two fields §4.2 used to record as validated NOWHERE are attacked at every nesting shape, because then/else/body are three separate recursion sites. Two mutations attack the CATEGORY and the message rather than the verdict: a domain violation filed as `shape` is the exact defect #326's census found in `column`, and it would leave every accept/reject green.", + "layers": [ + { + "id": "py-limits", + "cwd": ".", + "command": [ + "python3", + "tests/test_ownir_defensive_limits.py" + ], + "parser": "python-fail" + }, + { + "id": "py-ledger", + "cwd": ".", + "command": [ + "python3", + "tests/test_ownir_validation_fixtures.py" + ], + "parser": "python-fail" + }, + { + "id": "rust-door", + "cwd": "rust", + "command": [ + "cargo", + "test", + "-p", + "own-ir", + "--no-fail-fast" + ], + "parser": "cargo" + } + ], + "control": { + "id": "M00", + "description": "harness-honesty control: no mutation at all, which must report zero failing layers" + }, + "mutations": [ + { + "id": "M01", + "rule": "§4.2 line domain", + "description": "the strict door loses the domain's LOWER bound, so a negative line is accepted again", + "target": "ownlang/ownir.py", + "pattern": " if not LINE_MIN <= v <= LINE_MAX:", + "replacement": " if not v <= LINE_MAX:", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M02", + "rule": "§4.2 line domain", + "description": "…and the UPPER bound, so an int64 line is accepted again", + "target": "ownlang/ownir.py", + "pattern": " if not LINE_MIN <= v <= LINE_MAX:\\n raise OwnIRError\\(\\n f\\\"\\{where\\} \\{field!r\\} must be a source line in \\\"", + "replacement": " if not LINE_MIN <= v:\n raise OwnIRError(\n f\"{where} {field!r} must be a source line in \"", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M03", + "rule": "§4.2 line domain", + "description": "off-by-one at the bottom: 0 stops being legal, which would break the reference's own absent-line default", + "target": "ownlang/ownir.py", + "pattern": " if not LINE_MIN <= v <= LINE_MAX:\\n raise", + "replacement": " if not LINE_MIN < v <= LINE_MAX:\n raise", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M04", + "rule": "§4.2 line domain", + "description": "…and at the top: 2147483647 stops being legal", + "target": "ownlang/ownir.py", + "pattern": " if not LINE_MIN <= v <= LINE_MAX:\\n raise OwnIRError", + "replacement": " if not LINE_MIN <= v < LINE_MAX:\n raise OwnIRError", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M05", + "rule": "§4.2 message", + "description": "the domain diagnostic stops naming the domain", + "target": "ownlang/ownir.py", + "pattern": "f\"\\{where\\} \\{field!r\\} must be a source line in \"", + "replacement": "f\"{where} {field!r} must be a bad coordinate \"", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M06", + "rule": "§4.2 / D2", + "description": "the subscriptions walk stops checking its line — the field §4.2 used to record as validated nowhere", + "target": "ownlang/ownir.py", + "pattern": " _check_line_domain\\(sln, \"subscription\"\\)", + "replacement": " _ = sln", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M07", + "rule": "§4.2 / D2", + "description": "…and the flow-op walk stops checking its line", + "target": "ownlang/ownir.py", + "pattern": " _check_line_domain\\(ln, what\\)", + "replacement": " _ = ln", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M08", + "rule": "§4.2 / D2", + "description": "the flow walk stops recursing into one nesting shape, so a `while` body's line escapes", + "target": "ownlang/ownir.py", + "pattern": " for key in \\(\"then\", \"else\", \"body\"\\):", + "replacement": " for key in (\"then\", \"else\"):", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M09", + "rule": "§4.2 / D2", + "description": "the subscriptions line loses its bool guard, so `true` reads as line 1 — the bool-is-int trap the contract calls out by name", + "target": "ownlang/ownir.py", + "pattern": " if not isinstance\\(sln, int\\) or isinstance\\(sln, bool\\):", + "replacement": " if not isinstance(sln, int) or False:", + "expected_catchers": [ + "py-limits::line-type", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M10", + "rule": "§4.2 line domain", + "description": "the root-resolve site array stops checking its coordinate", + "target": "ownlang/ownir.py", + "pattern": " _check_line_domain\\(site\\.get\\(\"line\", 0\\), \"service root_resolve_site\"\\)", + "replacement": " _ = site", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M11", + "rule": "§4.1 / §4.2 column", + "description": "the column loses its domain upper bound", + "target": "ownlang/ownir.py", + "pattern": " if v > COLUMN_MAX:", + "replacement": " if v > INT64_MAX:", + "expected_catchers": [ + "py-limits::column-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M12", + "rule": "§4.1 column", + "description": "…and its 1-based lower bound, so `0` becomes a legal column", + "target": "ownlang/ownir.py", + "pattern": "or v < COLUMN_MIN:", + "replacement": "or v < COLUMN_MIN - 1:", + "expected_catchers": [ + "py-limits::column-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M13", + "rule": "§4.2 two doors", + "description": "the strict door stops refusing an out-of-domain event line and degrades instead", + "target": "ownlang/obligations.py", + "pattern": " if not strict:\\n return 0", + "replacement": " if True:\n return 0", + "expected_catchers": [ + "py-limits::line-domain", + "py-ledger::ledger-stale" + ] + }, + { + "id": "M14", + "rule": "§4.2 line domain", + "description": "the Rust door loses the domain's LOWER bound", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " LINE_MIN <= value && value <= LINE_MAX", + "replacement": " value <= LINE_MAX", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M15", + "rule": "§4.2 line domain", + "description": "…and its UPPER bound", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " LINE_MIN <= value && value <= LINE_MAX\\n\\}", + "replacement": " LINE_MIN <= value\n}", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M16", + "rule": "§4.2 line domain", + "description": "off-by-one at the bottom in the Rust door", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " LINE_MIN <= value && value <= LINE_MAX\\n\\}\\n", + "replacement": " LINE_MIN < value && value <= LINE_MAX\n}\n", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M17", + "rule": "§4.2 line domain", + "description": "…and at the top", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " LINE_MIN <= value && value <= LINE_MAX\\n\\}\\n\\n", + "replacement": " LINE_MIN <= value && value < LINE_MAX\n}\n\n", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M18", + "rule": "cp1 taxonomy", + "description": "the Rust domain rejection is filed as `shape` — the category read off the wrong axis, the exact defect #326's census found in `column`", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " Err\\(location\\(format!\\(\\n \\\"\\{what\\} '\\{key\\}' must be a source line in", + "replacement": " Err(shape(format!(\n \"{what} '{key}' must be a source line in", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M19", + "rule": "§4.2 message", + "description": "the Rust domain diagnostic stops naming the domain", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": "must be a source line in \\[\\{LINE_MIN\\}, \\{LINE_MAX\\}\\]", + "replacement": "must be a bad coordinate [{LINE_MIN}, {LINE_MAX}]", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M20", + "rule": "§4.2 / D2", + "description": "the Rust subscriptions walk stops checking its line", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " defaulted_line\\(sub, \"line\", \"subscription\"\\)\\?;", + "replacement": " let _ = ⊂", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ] + }, + { + "id": "M21", + "rule": "§4.2 / D2", + "description": "…and the Rust flow-op walk stops checking its line", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " defaulted_line\\(op, \"line\", &label\\)\\?;", + "replacement": " let _ = &label;", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M22", + "rule": "§4.2 / D2", + "description": "the Rust flow walk stops recursing into one nesting shape", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " for key in \\[\"then\", \"else\", \"body\"\\] \\{", + "replacement": " for key in [\"then\", \"else\"] {", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ] + }, + { + "id": "M23", + "rule": "§4.2 line domain", + "description": "the Rust site array stops checking its coordinate", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " defaulted_line\\(obj, \\\"line\\\", &singular\\)\\?;", + "replacement": " let _ = &singular;", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ] + }, + { + "id": "M24", + "rule": "§4.1 / §4.2 column", + "description": "the Rust column loses its domain upper bound", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " if n > COLUMN_MAX \\{", + "replacement": " if n > i64::MAX {", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M25", + "rule": "§4.1 column", + "description": "…and its 1-based lower bound", + "target": "rust/crates/own-ir/src/strict.rs", + "pattern": " if n < COLUMN_MIN \\{", + "replacement": " if n < COLUMN_MIN - 1 {", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ] + }, + { + "id": "M26", + "rule": "§4.2 two doors", + "description": "the Rust strict door stops refusing an out-of-domain event line and degrades instead", + "target": "rust/crates/own-ir/src/protocol.rs", + "pattern": " Door::Strict => line_domain\\(value, what, \"line\"\\)\\.map\\(\\|\\(\\)\\| value\\),", + "replacement": " Door::Strict => Ok(0),", + "expected_catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language", + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door" + ] + } + ] +} diff --git a/docs/evidence/p022-coord-1.result.json b/docs/evidence/p022-coord-1.result.json new file mode 100644 index 00000000..394ea39a --- /dev/null +++ b/docs/evidence/p022-coord-1.result.json @@ -0,0 +1,269 @@ +{ + "schema": 1, + "comment": "Recorded mutation-campaign run (scripts/mutate_campaign.py --run). Raw facts only: outcomes, catchers, provenance. Counts are derived by scripts/render_checkpoint_status.py; regenerate this file by re-running the campaign, never by hand.", + "campaign": "p022-coord-1", + "definition": "docs/evidence/p022-coord-1.json", + "definition_sha256": "5f2587e9f495e5bfe5d702615889e346c733e4b5db3c8a17c734eea99ede3c1f", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", + "dirty": false, + "recorded_at": "2026-09-07T10:31:54Z", + "layers": [ + "py-limits", + "py-ledger", + "rust-door" + ], + "command": "every layer the definition declares, for every mutation", + "control": { + "id": "M00", + "outcome": "survived", + "catchers": [], + "elapsed_seconds": 0.6 + }, + "mutations": [ + { + "id": "M01", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain" + ], + "elapsed_seconds": 0.6 + }, + { + "id": "M02", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M03", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::column-domain", + "py-limits::line-domain", + "py-limits::nesting", + "py-limits::tolerances" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M04", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M05", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M06", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M07", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M08", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain", + "py-limits::line-type", + "py-limits::nesting" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M09", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-type" + ], + "elapsed_seconds": 1.1 + }, + { + "id": "M10", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M11", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::column-domain" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M12", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::column-domain" + ], + "elapsed_seconds": 1.0 + }, + { + "id": "M13", + "outcome": "caught", + "catchers": [ + "py-ledger::ledger-stale", + "py-limits::line-domain" + ], + "elapsed_seconds": 1.1 + }, + { + "id": "M14", + "outcome": "caught", + "catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.3 + }, + { + "id": "M15", + "outcome": "caught", + "catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.3 + }, + { + "id": "M16", + "outcome": "caught", + "catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door", + "rust-door/tests/roundtrip.rs::additive_unknown_fields_are_preserved", + "rust-door/tests/roundtrip.rs::explicit_null_is_accepted_and_preserved_where_python_accepts_it", + "rust-door/tests/roundtrip.rs::param_effect_vocabulary_is_closed", + "rust-door/tests/roundtrip.rs::round_trips_every_python_fixture", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.4 + }, + { + "id": "M17", + "outcome": "caught", + "catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.4 + }, + { + "id": "M18", + "outcome": "caught", + "catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.5 + }, + { + "id": "M19", + "outcome": "caught", + "catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door", + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on" + ], + "elapsed_seconds": 1.3 + }, + { + "id": "M20", + "outcome": "caught", + "catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.2 + }, + { + "id": "M21", + "outcome": "caught", + "catchers": [ + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.2 + }, + { + "id": "M22", + "outcome": "caught", + "catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.2 + }, + { + "id": "M23", + "outcome": "caught", + "catchers": [ + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.3 + }, + { + "id": "M24", + "outcome": "caught", + "catchers": [ + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.2 + }, + { + "id": "M25", + "outcome": "caught", + "catchers": [ + "rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.2 + }, + { + "id": "M26", + "outcome": "caught", + "catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door", + "rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" + ], + "elapsed_seconds": 1.4 + } + ] +} diff --git a/docs/evidence/p022-coord-2.json b/docs/evidence/p022-coord-2.json new file mode 100644 index 00000000..668cb540 --- /dev/null +++ b/docs/evidence/p022-coord-2.json @@ -0,0 +1,225 @@ +{ + "schema": 1, + "campaign": "p022-coord-2", + "description": "#259 final acceptance, the TOLERANT door: `check_facts()` degrades an out-of-domain coordinate to 0 (absent) rather than raising, clamping or inventing, and the Rust bridge does the same. Three failure modes are attacked at every reader on both sides — degrade -> clamp, degrade -> pass-through, degrade -> raise — because they are three different wrong answers and only the clamp looks harmless: it moves a finding onto a REAL line the producer never named, which no golden comparing anchors can forgive but a reader might. The four promoted verdict_boundary_* goldens are the controls that make this measurable at all; before the promotion the Rust half of every one of these mutations was unreachable behind a refusal. One mutation attacks the LOWERING rather than the verdict, because a Layer 2 coordinate that disagrees with the reference is invisible at Layer 3 — the AST build degrades it a second time — and needed a control of its own.", + "layers": [ + { + "id": "py-limits", + "cwd": ".", + "command": [ + "python3", + "tests/test_ownir_defensive_limits.py" + ], + "parser": "python-fail" + }, + { + "id": "py-verdicts", + "cwd": ".", + "command": [ + "python3", + "tests/test_verdict_fixtures.py" + ], + "parser": "python-fail" + }, + { + "id": "rust-door", + "cwd": "rust", + "command": [ + "cargo", + "test", + "-p", + "own-ir", + "--no-fail-fast" + ], + "parser": "cargo" + }, + { + "id": "rust-bridge", + "cwd": "rust", + "command": [ + "cargo", + "test", + "-p", + "own-bridge", + "--no-fail-fast" + ], + "parser": "cargo" + } + ], + "control": { + "id": "M00", + "description": "harness-honesty control: no mutation at all, which must report zero failing layers" + }, + "mutations": [ + { + "id": "M01", + "rule": "§4.2 degrade, never clamp", + "description": "the reference's tolerant line reader CLAMPS to the domain instead of degrading, so a finding moves to a real line the producer never named", + "target": "ownlang/ownir.py", + "pattern": " return \\(v if isinstance\\(v, int\\) and not isinstance\\(v, bool\\)\\n and LINE_MIN <= v <= LINE_MAX else 0\\)", + "replacement": " return (max(LINE_MIN, min(v, LINE_MAX)) if isinstance(v, int)\n and not isinstance(v, bool) else 0)", + "expected_catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ] + }, + { + "id": "M02", + "rule": "§4.2 two doors", + "description": "…or passes the out-of-domain value straight through, which is the behaviour the domain replaced", + "target": "ownlang/ownir.py", + "pattern": " return \\(v if isinstance\\(v, int\\) and not isinstance\\(v, bool\\)\\n and LINE_MIN <= v <= LINE_MAX else 0\\)", + "replacement": " return (v if isinstance(v, int) and not isinstance(v, bool) else 0)", + "expected_catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ] + }, + { + "id": "M03", + "rule": "§4.2 two doors", + "description": "…or REFUSES the record instead of degrading, dropping the finding entirely — which is what a raise would amount to for an embedder that catches OwnIRError around check_facts", + "target": "ownlang/ownir.py", + "pattern": " return \\(v if isinstance\\(v, int\\) and not isinstance\\(v, bool\\)\\n and LINE_MIN <= v <= LINE_MAX else 0\\)", + "replacement": " if not (isinstance(v, int) and not isinstance(v, bool)\n and LINE_MIN <= v <= LINE_MAX):\n return -1\n return v", + "expected_catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ] + }, + { + "id": "M04", + "rule": "§4.2 degrade to ABSENT", + "description": "the degrade lands on 1 instead of 0 — a fabricated coordinate rather than 'unknown'", + "target": "ownlang/ownir.py", + "pattern": " return \\(v if isinstance\\(v, int\\) and not isinstance\\(v, bool\\)\\n and LINE_MIN <= v <= LINE_MAX else 0\\)", + "replacement": " return (v if isinstance(v, int) and not isinstance(v, bool)\n and LINE_MIN <= v <= LINE_MAX else 1)", + "expected_catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ] + }, + { + "id": "M05", + "rule": "§4.1 / §4.2 column", + "description": "the reference's tolerant column reader loses the domain bound and emits a column the strict door would refuse", + "target": "ownlang/ownir.py", + "pattern": " return \\(v if isinstance\\(v, int\\) and not isinstance\\(v, bool\\)\\n and COLUMN_MIN <= v <= COLUMN_MAX else None\\)", + "replacement": " return (v if isinstance(v, int) and not isinstance(v, bool)\n and COLUMN_MIN <= v else None)", + "expected_catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ] + }, + { + "id": "M06", + "rule": "§4.2 two doors", + "description": "the reference's tolerant PROTOCOL door refuses instead of degrading, which drops the whole method's violations for one impossible coordinate", + "target": "ownlang/obligations.py", + "pattern": " if not strict:\\n return 0", + "replacement": " if False:\n return 0", + "expected_catchers": [ + "py-limits::tolerant-degrade" + ] + }, + { + "id": "M07", + "rule": "§4.2 degrade, never clamp", + "description": "core_line CLAMPS to the domain's top instead of degrading to absent", + "target": "rust/crates/own-bridge/src/ast.rs", + "pattern": " Ok\\(n\\) if n <= LINE_MAX => n,\\n _ => 0,", + "replacement": " Ok(n) if n <= LINE_MAX => n,\n _ => LINE_MAX,", + "expected_catchers": [ + "rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps" + ] + }, + { + "id": "M08", + "rule": "§4.2 two doors", + "description": "…or accepts the whole u32 range again, which is the core's representation deciding the contract — the reading this change exists to refuse", + "target": "rust/crates/own-bridge/src/ast.rs", + "pattern": " Ok\\(n\\) if n <= LINE_MAX => n,\\n _ => 0,", + "replacement": " Ok(n) => n,\n _ => 0,", + "expected_catchers": [ + "rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps" + ] + }, + { + "id": "M09", + "rule": "§4.2 degrade to ABSENT", + "description": "…or degrades to 1, inventing a coordinate instead of admitting it has none", + "target": "rust/crates/own-bridge/src/ast.rs", + "pattern": " Ok\\(n\\) if n <= LINE_MAX => n,\\n _ => 0,", + "replacement": " Ok(n) if n <= LINE_MAX => n,\n _ => 1,", + "expected_catchers": [ + "rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps" + ] + }, + { + "id": "M10", + "rule": "§4.2 two doors", + "description": "the bridge's tolerant line reader passes an out-of-domain fact coordinate through, so a slice the reference drops is kept and anchored where nothing is", + "target": "rust/crates/own-bridge/src/verdict.rs", + "pattern": " match v\\.and_then\\(Value::as_i64\\) \\{\\n Some\\(n\\) if \\(0\\.\\.=2_147_483_647\\)\\.contains\\(&n\\) => n,\\n _ => 0,\\n \\}", + "replacement": " match v.and_then(Value::as_i64) {\n Some(n) => n,\n _ => 0,\n }", + "expected_catchers": [ + "rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + ] + }, + { + "id": "M11", + "rule": "§4.2 degrade, never clamp", + "description": "…or clamps it, moving a DI site anchor onto a real line the registration never named", + "target": "rust/crates/own-bridge/src/verdict.rs", + "pattern": " match v\\.and_then\\(Value::as_i64\\) \\{\\n Some\\(n\\) if \\(0\\.\\.=2_147_483_647\\)\\.contains\\(&n\\) => n,\\n _ => 0,\\n \\}", + "replacement": " match v.and_then(Value::as_i64) {\n Some(n) => n.clamp(0, 2_147_483_647),\n _ => 0,\n }", + "expected_catchers": [ + "rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + ] + }, + { + "id": "M12", + "rule": "§4.2 at Layer 2", + "description": "the LOWERING's line reader passes an out-of-domain coordinate through, so the Layer 2 document disagrees with the reference at the seam the cp2 evidence is taken from — invisible at Layer 3, because the AST build degrades it again on the way in", + "target": "rust/crates/own-bridge/src/lower.rs", + "pattern": " match v\\.and_then\\(Value::as_i64\\) \\{\\n Some\\(n\\) if \\(0\\.\\.=2_147_483_647\\)\\.contains\\(&n\\) => n,\\n _ => 0,\\n \\}", + "replacement": " match v.and_then(Value::as_i64) {\n Some(n) => n,\n _ => 0,\n }", + "expected_catchers": [ + "rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + ] + }, + { + "id": "M13", + "rule": "§4.2 two doors", + "description": "the Rust tolerant PROTOCOL door refuses instead of degrading", + "target": "rust/crates/own-ir/src/protocol.rs", + "pattern": " Door::Tolerant => Ok\\(0\\),", + "replacement": " Door::Tolerant => line_domain(value, what, \"line\").map(|()| value),", + "expected_catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door" + ] + }, + { + "id": "M14", + "rule": "BR-V9 / §4.2", + "description": "the rendered code-flow shifts every step line by one. The DROP this conversion replaced is an EQUIVALENT mutant now — §4.2 guarantees every flow-step line converts, so a filter_map that drops the unconvertible drops nothing — so the mutation is re-anchored onto the conversion itself, which is observable", + "target": "rust/crates/own-bridge/src/render.rs", + "pattern": " line: u32::try_from\\(\\*line\\)\\.unwrap_or\\(0\\),", + "replacement": " line: u32::try_from(*line).unwrap_or(0) + 1,", + "expected_catchers": [ + "rust-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" + ] + }, + { + "id": "M15", + "rule": "§4.1 / §4.2 column", + "description": "the bridge's tolerant COLUMN reader loses the domain bound, so the port emits a column the strict door would refuse — the mirror of M05, and the one the four promoted goldens cannot reach either", + "target": "rust/crates/own-bridge/src/lower.rs", + "pattern": " v\\.and_then\\(Value::as_i64\\)\\n \\.filter\\(\\|c\\| \\(1\\.\\.=2_147_483_647\\)\\.contains\\(c\\)\\)", + "replacement": " v.and_then(Value::as_i64).filter(|c| *c >= 1)", + "expected_catchers": [ + "rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + ] + } + ] +} diff --git a/docs/evidence/p022-coord-2.result.json b/docs/evidence/p022-coord-2.result.json new file mode 100644 index 00000000..faad34b1 --- /dev/null +++ b/docs/evidence/p022-coord-2.result.json @@ -0,0 +1,150 @@ +{ + "schema": 1, + "comment": "Recorded mutation-campaign run (scripts/mutate_campaign.py --run). Raw facts only: outcomes, catchers, provenance. Counts are derived by scripts/render_checkpoint_status.py; regenerate this file by re-running the campaign, never by hand.", + "campaign": "p022-coord-2", + "definition": "docs/evidence/p022-coord-2.json", + "definition_sha256": "f033608e513b098baed9490305baa4f77c6a47728ddcb530097af8745416182b", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", + "dirty": false, + "recorded_at": "2026-09-07T10:32:30Z", + "layers": [ + "py-limits", + "py-verdicts", + "rust-door", + "rust-bridge" + ], + "command": "every layer the definition declares, for every mutation", + "control": { + "id": "M00", + "outcome": "survived", + "catchers": [], + "elapsed_seconds": 2.5 + }, + "mutations": [ + { + "id": "M01", + "outcome": "caught", + "catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ], + "elapsed_seconds": 0.9 + }, + { + "id": "M02", + "outcome": "caught", + "catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ], + "elapsed_seconds": 2.2 + }, + { + "id": "M03", + "outcome": "caught", + "catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ], + "elapsed_seconds": 2.3 + }, + { + "id": "M04", + "outcome": "caught", + "catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ], + "elapsed_seconds": 2.2 + }, + { + "id": "M05", + "outcome": "caught", + "catchers": [ + "py-limits::tolerant-degrade", + "py-verdicts::verdict-fixture" + ], + "elapsed_seconds": 2.1 + }, + { + "id": "M06", + "outcome": "caught", + "catchers": [ + "py-limits::tolerant-degrade" + ], + "elapsed_seconds": 2.2 + }, + { + "id": "M07", + "outcome": "caught", + "catchers": [ + "rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps" + ], + "elapsed_seconds": 2.5 + }, + { + "id": "M08", + "outcome": "caught", + "catchers": [ + "rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps" + ], + "elapsed_seconds": 2.4 + }, + { + "id": "M09", + "outcome": "caught", + "catchers": [ + "rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps" + ], + "elapsed_seconds": 2.5 + }, + { + "id": "M10", + "outcome": "caught", + "catchers": [ + "rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + ], + "elapsed_seconds": 2.3 + }, + { + "id": "M11", + "outcome": "caught", + "catchers": [ + "rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + ], + "elapsed_seconds": 2.3 + }, + { + "id": "M12", + "outcome": "caught", + "catchers": [ + "rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + ], + "elapsed_seconds": 2.3 + }, + { + "id": "M13", + "outcome": "caught", + "catchers": [ + "rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door" + ], + "elapsed_seconds": 2.5 + }, + { + "id": "M14", + "outcome": "caught", + "catchers": [ + "rust-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" + ], + "elapsed_seconds": 2.4 + }, + { + "id": "M15", + "outcome": "caught", + "catchers": [ + "rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + ], + "elapsed_seconds": 2.4 + } + ] +} diff --git a/docs/evidence/p022-cp4-mutations.json b/docs/evidence/p022-cp4-mutations.json index 04f01da1..899009b9 100644 --- a/docs/evidence/p022-cp4-mutations.json +++ b/docs/evidence/p022-cp4-mutations.json @@ -169,12 +169,12 @@ }, { "id": "M15", - "description": "coordinate boundary: core_line clamps instead of refusing", + "description": "coordinate domain: core_line CLAMPS an out-of-domain line to the domain's top instead of degrading it to absent — re-anchored at #259 final acceptance, and its catcher moved with it: the Layer 3 replay caught this while core_line REFUSED, and the coordinate domain makes it unreachable from outside (`lower` degrades one layer earlier), so the control is now the direct one in ast.rs", "target": "rust/crates/own-bridge/src/ast.rs", - "pattern": "u32::try_from\\(line\\)\\.map_err\\(", - "replacement": "u32::try_from(line.clamp(0, i64::from(u32::MAX))).map_err(", + "pattern": "Ok\\(n\\) if n <= LINE_MAX => n,\\n _ => 0,", + "replacement": "Ok(n) if n <= LINE_MAX => n,\n _ => LINE_MAX,", "expected_catchers": [ - "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + "own-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps" ] }, { @@ -225,18 +225,18 @@ "id": "M20", "description": "_as_col accepts 0 as a coordinate", "target": "rust/crates/own-bridge/src/lower.rs", - "pattern": "v\\.and_then\\(Value::as_i64\\)\\.filter\\(\\|c\\| \\*c >= 1\\)", - "replacement": "v.and_then(Value::as_i64).filter(|c| *c >= 0)", + "pattern": " \\.filter\\(\\|c\\| \\(1\\.\\.=2_147_483_647\\)\\.contains\\(c\\)\\)", + "replacement": " .filter(|c| (0..=2_147_483_647).contains(c))", "expected_catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ] }, { "id": "M21", - "description": "guarded DI site line: a negative site line becomes 1 instead of 0", + "description": "the tolerant line reader degrades an out-of-domain coordinate to 1 instead of 0, so a DI site line below the domain passes the >= 1 guard and anchors where nothing is", "target": "rust/crates/own-bridge/src/verdict.rs", - "pattern": "if line < 0 \\{\\n(\\s+)Ok\\(0\\)", - "replacement": "if line < 0 {\\n\\1Ok(1)", + "pattern": "Some\\(n\\) if \\(0..=2_147_483_647\\)\\.contains\\(&n\\) => n,\\n _ => 0,", + "replacement": "Some(n) if (0..=2_147_483_647).contains(&n) => n,\n _ => 1,", "expected_catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ] diff --git a/docs/evidence/p022-cp4-mutations.result.json b/docs/evidence/p022-cp4-mutations.result.json index b5bf3fac..989038fe 100644 --- a/docs/evidence/p022-cp4-mutations.result.json +++ b/docs/evidence/p022-cp4-mutations.result.json @@ -3,10 +3,10 @@ "comment": "Recorded mutation-campaign run (scripts/mutate_campaign.py --run). Raw facts only: outcomes, catchers, provenance. Counts are derived by scripts/render_checkpoint_status.py; regenerate this file by re-running the campaign, never by hand.", "campaign": "p022-cp4", "definition": "docs/evidence/p022-cp4-mutations.json", - "definition_sha256": "c4ca44f6bbc92cdd146931107e1e4959e1bdeb79eed9a717a874b31eba2ae42e", - "source_commit": "5085a5152916b1dae260781cf850733095720fe6", + "definition_sha256": "1556a638a826e5e1938b6a420e1dfe6cdf91582ba21dce4a75db2b4a0ddde6a5", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T20:51:00Z", + "recorded_at": "2026-09-07T10:34:42Z", "packages": [ "own-analysis", "own-bridge", @@ -22,7 +22,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 3.0 + "elapsed_seconds": 4.6 }, "mutations": [ { @@ -32,7 +32,7 @@ "own-bridge/src/lib.rs::verdict::tests::only_error_severity_core_verdicts_are_mapped", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.3 + "elapsed_seconds": 3.4 }, { "id": "M02", @@ -40,7 +40,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.3 }, { "id": "M03", @@ -48,7 +48,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.6 }, { "id": "M04", @@ -56,7 +56,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.9 + "elapsed_seconds": 4.2 }, { "id": "M05", @@ -64,7 +64,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.9 + "elapsed_seconds": 4.0 }, { "id": "M06", @@ -72,7 +72,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::dedup_keeps_findings_that_differ_only_in_an_unobservable_key_member" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 3.9 }, { "id": "M07", @@ -81,7 +81,7 @@ "own-analysis/tests/fact_parity.rs::di_fact_parity", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.2 + "elapsed_seconds": 4.1 }, { "id": "M08", @@ -91,7 +91,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.4 }, { "id": "M09", @@ -99,7 +99,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::malformed_effect_entries_are_skipped_not_coerced" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 4.1 }, { "id": "M10", @@ -107,7 +107,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 4.4 }, { "id": "M11", @@ -117,7 +117,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 4.8 }, { "id": "M12", @@ -130,7 +130,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 5.2 }, { "id": "M13", @@ -139,7 +139,7 @@ "own-analysis/tests/subject.rs::region_escape_carries_the_source_identity", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 4.2 }, { "id": "M14", @@ -149,15 +149,15 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.5 }, { "id": "M15", "outcome": "caught", "catchers": [ - "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" + "own-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps" ], - "elapsed_seconds": 3.9 + "elapsed_seconds": 4.2 }, { "id": "M16", @@ -167,7 +167,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.5 }, { "id": "M17", @@ -175,7 +175,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.2 }, { "id": "M18", @@ -184,7 +184,7 @@ "own-bridge/src/lib.rs::verdict::tests::dedup_keeps_findings_that_differ_only_in_an_unobservable_key_member", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.2 + "elapsed_seconds": 4.3 }, { "id": "M19", @@ -192,7 +192,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::only_error_severity_core_verdicts_are_mapped" ], - "elapsed_seconds": 3.9 + "elapsed_seconds": 4.1 }, { "id": "M20", @@ -217,7 +217,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.5 }, { "id": "M23", @@ -231,7 +231,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.6 }, { "id": "M24", @@ -239,7 +239,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 4.2 }, { "id": "M25", @@ -255,7 +255,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.2 }, { "id": "M27", @@ -263,7 +263,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::dedup_keeps_findings_that_differ_only_in_an_unobservable_key_member" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 3.9 }, { "id": "M28", @@ -271,7 +271,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::dedup_keeps_findings_that_differ_only_in_an_unobservable_key_member" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 3.9 }, { "id": "M29", @@ -279,7 +279,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 3.9 }, { "id": "M30", diff --git a/docs/evidence/p022-cp4b-1.result.json b/docs/evidence/p022-cp4b-1.result.json index 9260d006..04c0b79b 100644 --- a/docs/evidence/p022-cp4b-1.result.json +++ b/docs/evidence/p022-cp4b-1.result.json @@ -4,9 +4,9 @@ "campaign": "p022-cp4b-1", "definition": "docs/evidence/p022-cp4b-1.json", "definition_sha256": "ca44ead1682214048b738ec0c42ccb0cf844c302eb351b050a4c503ae55270f9", - "source_commit": "c7836138bd2fe3b9cfea458a040ac5dda05e890e", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T20:30:43Z", + "recorded_at": "2026-09-07T10:39:09Z", "packages": [ "own-analysis", "own-bridge", @@ -22,16 +22,17 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 3.5 + "elapsed_seconds": 3.1 }, "mutations": [ { "id": "M01", "outcome": "caught", "catchers": [ - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 4.3 }, { "id": "M02", @@ -39,33 +40,36 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 5.1 }, { "id": "M03", "outcome": "caught", "catchers": [ "own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens", - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 5.1 }, { "id": "M04", "outcome": "caught", "catchers": [ "own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens", - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.3 + "elapsed_seconds": 5.0 }, { "id": "M05", "outcome": "caught", "catchers": [ - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 5.2 }, { "id": "M06", @@ -73,7 +77,7 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.8 + "elapsed_seconds": 5.2 }, { "id": "M07", @@ -82,7 +86,7 @@ "own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens", "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.8 }, { "id": "M08", @@ -90,7 +94,7 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.5 + "elapsed_seconds": 4.7 }, { "id": "M09", @@ -98,16 +102,18 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.5 + "elapsed_seconds": 4.8 }, { "id": "M10", "outcome": "caught", "catchers": [ "own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open", - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.8 }, { "id": "M11", @@ -115,23 +121,26 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.5 + "elapsed_seconds": 4.6 }, { "id": "M12", "outcome": "caught", "catchers": [ - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 5.0 }, { "id": "M13", "outcome": "caught", "catchers": [ - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 5.0 }, { "id": "M14", @@ -148,7 +157,7 @@ "own-analysis/src/lib.rs::obligation::tests::a_barrier_in_a_loop_reports_exactly_once", "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.6 }, { "id": "M16", @@ -157,7 +166,7 @@ "own-analysis/src/lib.rs::obligation::tests::a_barrier_in_a_loop_reports_exactly_once", "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.6 }, { "id": "M17", @@ -165,7 +174,7 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.6 }, { "id": "M18", @@ -173,7 +182,7 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.5 }, { "id": "M19", @@ -181,9 +190,12 @@ "catchers": [ "own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open", "own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing", - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", + "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.8 + "elapsed_seconds": 5.1 }, { "id": "M20", @@ -199,7 +211,7 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.5 + "elapsed_seconds": 4.6 }, { "id": "M22", @@ -207,16 +219,17 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.6 }, { "id": "M23", "outcome": "caught", "catchers": [ "own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not", - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/src/lib.rs::verdict::tests::protocol_blocks_that_are_not_lists_yield_nothing" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 4.9 }, { "id": "M24", @@ -225,7 +238,7 @@ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", "own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value" ], - "elapsed_seconds": 5.8 + "elapsed_seconds": 6.2 }, { "id": "M25", @@ -237,6 +250,9 @@ "own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens", "own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing", "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/src/lib.rs::verdict::tests::protocol_blocks_that_are_not_lists_yield_nothing", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value" ], "elapsed_seconds": 6.2 @@ -248,7 +264,7 @@ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", "own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 5.0 }, { "id": "M27", @@ -257,7 +273,7 @@ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", "own-ir/src/lib.rs::protocol::tests::a_narrowed_call_matcher_never_matches_an_unknown_argument" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 5.0 }, { "id": "M28", @@ -268,9 +284,12 @@ "own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens", "own-analysis/src/lib.rs::obligation::tests::close_before_the_barrier_is_clean", "own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing", - "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" + "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", + "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.3 + "elapsed_seconds": 5.1 }, { "id": "M29", @@ -279,10 +298,13 @@ "own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not", "own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open", "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/src/lib.rs::verdict::tests::protocol_blocks_that_are_not_lists_yield_nothing", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value", "own-ir/tests/validation_replay.rs::the_two_loaders_accept_the_same_language" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 5.2 }, { "id": "M30", @@ -290,9 +312,12 @@ "catchers": [ "own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not", "own-analysis/tests/obligation_parity.rs::obligation_fact_parity", + "own-bridge/src/lib.rs::verdict::tests::protocol_blocks_that_are_not_lists_yield_nothing", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", + "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 5.0 }, { "id": "M31", @@ -300,7 +325,7 @@ "catchers": [ "own-ir/src/lib.rs::protocol::tests::a_method_record_becomes_its_event_tree" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 4.8 }, { "id": "M32", @@ -316,7 +341,7 @@ "catchers": [ "own-analysis/tests/obligation_parity.rs::obligation_fact_parity" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.3 } ] } diff --git a/docs/evidence/p022-cp4b-2.json b/docs/evidence/p022-cp4b-2.json index 8a710ac1..5f4f1a44 100644 --- a/docs/evidence/p022-cp4b-2.json +++ b/docs/evidence/p022-cp4b-2.json @@ -263,8 +263,8 @@ "rule": "BR-D2", "description": "a malformed protocol_functions[] entry is dropped from the method list AND takes the rest of the list with it", "target": "rust/crates/own-bridge/src/verdict.rs", - "pattern": " \\.filter_map\\(\\|raw\\| protocol::parse_method\\(raw\\)\\.ok\\(\\)\\)\\n \\.collect\\(\\);", - "replacement": " .map_while(|raw| protocol::parse_method(raw).ok())\n .collect();", + "pattern": " \\.filter_map\\(\\|raw\\| protocol::parse_method\\(raw, protocol::Door::Tolerant\\)\\.ok\\(\\)\\)\\n \\.collect\\(\\);", + "replacement": " .map_while(|raw| protocol::parse_method(raw, protocol::Door::Tolerant).ok())\n .collect();", "expected_catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ] @@ -319,8 +319,8 @@ "rule": "#259 cp4b promotion", "description": "re-declare the promoted protocol exclusion. The replay must fail BOTH because the pinned exclusion set moved and because the exclusion no longer holds — a promotion that is only announced in prose would survive this", "target": "tests/fixtures/verdicts/manifest.json", - "pattern": " \"rust_replay_excluded\": \\[\\n \\{\\n \"name\": \"verdict_boundary_line_negative\",", - "replacement": " \"rust_replay_excluded\": [\n {\n \"name\": \"protocol_isloaded_violation\",\n \"reason\": \"re-declared by a mutation campaign\",\n \"rust_refusal\": \"bridge\",\n \"rust_error_contains\": \"obligation protocol\"\n },\n {\n \"name\": \"verdict_boundary_line_negative\",", + "pattern": " \"rust_replay_excluded\": \\[\\n \\{\\n \"name\": \"verdict_door_effect_deps_not_strings\",", + "replacement": " \"rust_replay_excluded\": [\n {\n \"name\": \"protocol_isloaded_violation\",\n \"reason\": \"re-declared by a mutation campaign\",\n \"rust_refusal\": \"bridge\",\n \"rust_error_contains\": \"obligation protocol\"\n },\n {\n \"name\": \"verdict_door_effect_deps_not_strings\",", "expected_catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ] diff --git a/docs/evidence/p022-cp4b-2.result.json b/docs/evidence/p022-cp4b-2.result.json index 616336c5..aef91125 100644 --- a/docs/evidence/p022-cp4b-2.result.json +++ b/docs/evidence/p022-cp4b-2.result.json @@ -3,10 +3,10 @@ "comment": "Recorded mutation-campaign run (scripts/mutate_campaign.py --run). Raw facts only: outcomes, catchers, provenance. Counts are derived by scripts/render_checkpoint_status.py; regenerate this file by re-running the campaign, never by hand.", "campaign": "p022-cp4b-2", "definition": "docs/evidence/p022-cp4b-2.json", - "definition_sha256": "602633faf9b1ddaa88f102b30a14d79dd17f6f5cfea4ea4188071a1282de4128", - "source_commit": "8c9a2f6b2d63ba4ccb6a144b91dc9a5bdc817feb", + "definition_sha256": "41f919af130e14718c4d8d8fb2dfd8bdaa83c7c728ec92ec7f5b24a625488577", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T20:57:03Z", + "recorded_at": "2026-09-07T10:36:23Z", "packages": [ "own-analysis", "own-bridge", @@ -22,7 +22,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 1.5 + "elapsed_seconds": 4.1 }, "mutations": [ { @@ -43,7 +43,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.5 }, { "id": "M03", @@ -51,7 +51,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.5 }, { "id": "M04", @@ -61,7 +61,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 3.3 + "elapsed_seconds": 3.7 }, { "id": "M05", @@ -69,7 +69,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.1 + "elapsed_seconds": 3.6 }, { "id": "M06", @@ -78,7 +78,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.2 + "elapsed_seconds": 3.6 }, { "id": "M07", @@ -87,7 +87,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.5 }, { "id": "M08", @@ -97,7 +97,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.7 }, { "id": "M09", @@ -117,7 +117,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 3.6 + "elapsed_seconds": 3.5 }, { "id": "M11", @@ -127,7 +127,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 3.6 + "elapsed_seconds": 3.5 }, { "id": "M12", @@ -136,7 +136,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.5 + "elapsed_seconds": 3.6 }, { "id": "M13", @@ -146,7 +146,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.5 }, { "id": "M14", @@ -164,7 +164,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 3.5 + "elapsed_seconds": 3.3 }, { "id": "M16", @@ -173,7 +173,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.5 + "elapsed_seconds": 3.4 }, { "id": "M17", @@ -200,7 +200,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.3 }, { "id": "M20", @@ -208,7 +208,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.2 + "elapsed_seconds": 3.3 }, { "id": "M21", @@ -224,7 +224,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 3.7 }, { "id": "M23", @@ -232,7 +232,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.5 + "elapsed_seconds": 3.6 }, { "id": "M24", @@ -250,7 +250,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 3.6 + "elapsed_seconds": 3.5 }, { "id": "M26", @@ -258,7 +258,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.2 + "elapsed_seconds": 3.3 }, { "id": "M27", @@ -274,7 +274,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.0 + "elapsed_seconds": 2.9 } ] } diff --git a/docs/evidence/p022-cp5-1.result.json b/docs/evidence/p022-cp5-1.result.json index b0702d3a..a2b78257 100644 --- a/docs/evidence/p022-cp5-1.result.json +++ b/docs/evidence/p022-cp5-1.result.json @@ -4,9 +4,9 @@ "campaign": "p022-cp5-1", "definition": "docs/evidence/p022-cp5-1.json", "definition_sha256": "2b4651e54f2862f5b7bdce8f2f682a2bdbd6826677f383736ed7f359ba334d50", - "source_commit": "cdff105ca43ec5b1ced2bd25a8216f4f7b65deb3", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T17:30:57Z", + "recorded_at": "2026-09-07T10:42:17Z", "packages": [ "own-analysis", "own-bridge", @@ -22,7 +22,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 1.6 + "elapsed_seconds": 4.3 }, "mutations": [ { @@ -31,7 +31,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.9 + "elapsed_seconds": 3.4 }, { "id": "M02", @@ -39,7 +39,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.2 }, { "id": "M03", @@ -47,7 +47,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 3.9 + "elapsed_seconds": 4.1 }, { "id": "M04", @@ -71,7 +71,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::a_flow_local_code_without_a_wording_keeps_the_core_message" ], - "elapsed_seconds": 4.5 + "elapsed_seconds": 4.7 }, { "id": "M07", @@ -87,7 +87,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::every_di_lifetime_phrase_is_pinned_including_the_unreachable_two" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 4.3 }, { "id": "M09", @@ -98,7 +98,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.4 }, { "id": "M10", @@ -106,7 +106,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.2 + "elapsed_seconds": 4.3 }, { "id": "M11", @@ -114,7 +114,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::the_capture_route_names_a_non_static_source_it_can_never_be_handed" ], - "elapsed_seconds": 4.8 + "elapsed_seconds": 5.1 }, { "id": "M12", @@ -122,7 +122,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 4.7 }, { "id": "M13", @@ -130,7 +130,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.5 + "elapsed_seconds": 4.4 }, { "id": "M14", @@ -138,7 +138,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.2 }, { "id": "M15", @@ -146,7 +146,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 4.6 }, { "id": "M16", @@ -163,7 +163,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 4.8 }, { "id": "M18", @@ -172,7 +172,7 @@ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.6 }, { "id": "M19", @@ -180,7 +180,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.3 + "elapsed_seconds": 4.5 }, { "id": "M20", @@ -190,7 +190,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.8 + "elapsed_seconds": 4.7 }, { "id": "M21", @@ -200,7 +200,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.8 + "elapsed_seconds": 5.0 }, { "id": "M22", @@ -216,7 +216,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 4.2 }, { "id": "M24", @@ -224,7 +224,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 4.1 }, { "id": "M25", @@ -232,7 +232,7 @@ "catchers": [ "own-analysis/src/lib.rs::effect::tests::the_via_clause_is_omitted_for_a_single_hop_chain" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 4.9 }, { "id": "M26", @@ -240,7 +240,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 4.9 }, { "id": "M27", @@ -248,7 +248,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 4.5 }, { "id": "M28", @@ -256,7 +256,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.7 }, { "id": "M29", @@ -282,7 +282,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 4.7 }, { "id": "M32", @@ -290,7 +290,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 4.5 }, { "id": "M33", @@ -298,7 +298,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.2 + "elapsed_seconds": 4.1 }, { "id": "M34", @@ -306,7 +306,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.2 }, { "id": "M35", @@ -330,7 +330,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.0 }, { "id": "M38", @@ -338,7 +338,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.2 + "elapsed_seconds": 4.1 }, { "id": "M39", @@ -354,7 +354,7 @@ "catchers": [ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.0 }, { "id": "M41", @@ -362,7 +362,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::every_di_lifetime_phrase_is_pinned_including_the_unreachable_two" ], - "elapsed_seconds": 4.2 + "elapsed_seconds": 4.0 }, { "id": "M42", @@ -370,7 +370,7 @@ "catchers": [ "own-analysis/src/lib.rs::effect::tests::the_via_clause_is_omitted_for_a_single_hop_chain" ], - "elapsed_seconds": 3.7 + "elapsed_seconds": 3.9 } ] } diff --git a/docs/evidence/p022-cp5-2.result.json b/docs/evidence/p022-cp5-2.result.json index e8551ec6..3abe8847 100644 --- a/docs/evidence/p022-cp5-2.result.json +++ b/docs/evidence/p022-cp5-2.result.json @@ -4,9 +4,9 @@ "campaign": "p022-cp5-2", "definition": "docs/evidence/p022-cp5-2.json", "definition_sha256": "8641e3ff34598123a194ab64111f033c345f6bb1846544c76c62a59230d39c08", - "source_commit": "2a7ff6d4ab2877b930f79f09bc6f55bfbffd094e", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T15:54:56Z", + "recorded_at": "2026-09-07T10:43:08Z", "packages": [ "own-analysis", "own-bridge", @@ -22,7 +22,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 8.4 + "elapsed_seconds": 3.7 }, "mutations": [ { @@ -32,7 +32,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 5.0 + "elapsed_seconds": 4.7 }, { "id": "M02", @@ -41,7 +41,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.5 + "elapsed_seconds": 4.7 }, { "id": "M03", @@ -50,7 +50,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.7 + "elapsed_seconds": 4.6 }, { "id": "M04", @@ -60,17 +60,18 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 4.7 }, { "id": "M05", "outcome": "caught", "catchers": [ "own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch", + "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte", "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 5.1 }, { "id": "M06", @@ -78,7 +79,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch" ], - "elapsed_seconds": 4.3 + "elapsed_seconds": 4.7 }, { "id": "M07", @@ -86,7 +87,7 @@ "catchers": [ "own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch" ], - "elapsed_seconds": 4.6 + "elapsed_seconds": 5.0 }, { "id": "M08", @@ -96,7 +97,7 @@ "own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.9 + "elapsed_seconds": 5.0 }, { "id": "M09", @@ -104,7 +105,7 @@ "catchers": [ "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.3 + "elapsed_seconds": 4.8 }, { "id": "M10", @@ -113,7 +114,7 @@ "own-shadow/tests/engine.rs::a_projection_names_exactly_the_members_it_carries", "own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 4.1 + "elapsed_seconds": 4.5 } ] } diff --git a/docs/evidence/p022-cp5-3.result.json b/docs/evidence/p022-cp5-3.result.json index 2a063acf..efeb271e 100644 --- a/docs/evidence/p022-cp5-3.result.json +++ b/docs/evidence/p022-cp5-3.result.json @@ -4,9 +4,9 @@ "campaign": "p022-cp5-3", "definition": "docs/evidence/p022-cp5-3.json", "definition_sha256": "6b064c3d76eae07a0ec4a61bf5684f9a21b36213c7ecda694e3617c7e228ba5d", - "source_commit": "ceb8521557128bddca96ea6addb817231fde4f56", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T16:39:22Z", + "recorded_at": "2026-09-07T10:44:55Z", "packages": [ "own-analysis", "own-bridge", @@ -22,7 +22,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 1.7 + "elapsed_seconds": 4.3 }, "mutations": [ { @@ -31,7 +31,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 4.4 + "elapsed_seconds": 3.9 }, { "id": "M02", @@ -39,7 +39,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.6 + "elapsed_seconds": 3.7 }, { "id": "M03", @@ -47,7 +47,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.7 + "elapsed_seconds": 3.8 }, { "id": "M04", @@ -55,7 +55,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 3.7 }, { "id": "M05", @@ -63,7 +63,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.7 + "elapsed_seconds": 3.6 }, { "id": "M06", @@ -71,7 +71,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.5 + "elapsed_seconds": 3.1 }, { "id": "M07", @@ -87,7 +87,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.7 + "elapsed_seconds": 3.6 }, { "id": "M09", @@ -95,7 +95,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.5 + "elapsed_seconds": 3.4 }, { "id": "M10", @@ -103,7 +103,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.3 + "elapsed_seconds": 3.4 }, { "id": "M11", @@ -119,7 +119,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 3.7 }, { "id": "M13", @@ -127,7 +127,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 3.6 }, { "id": "M14", @@ -135,7 +135,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.7 + "elapsed_seconds": 3.9 }, { "id": "M15", @@ -143,7 +143,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.2 + "elapsed_seconds": 3.3 }, { "id": "M16", @@ -151,7 +151,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.3 }, { "id": "M17", @@ -159,7 +159,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 3.2 }, { "id": "M18", @@ -167,7 +167,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.7 + "elapsed_seconds": 3.8 }, { "id": "M19", @@ -175,7 +175,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.6 + "elapsed_seconds": 3.9 }, { "id": "M20", @@ -183,7 +183,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.2 + "elapsed_seconds": 3.5 }, { "id": "M21", @@ -191,7 +191,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.3 + "elapsed_seconds": 3.5 }, { "id": "M22", @@ -199,7 +199,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.2 + "elapsed_seconds": 3.3 }, { "id": "M23", @@ -215,7 +215,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.3 + "elapsed_seconds": 3.4 }, { "id": "M25", @@ -223,7 +223,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.2 }, { "id": "M26", @@ -231,7 +231,7 @@ "catchers": [ "own-bridge/src/lib.rs::render::tests::a_related_step_with_no_line_is_dropped" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 3.7 }, { "id": "M27", @@ -239,7 +239,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.8 + "elapsed_seconds": 3.7 }, { "id": "M28", @@ -247,7 +247,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 3.4 + "elapsed_seconds": 3.3 }, { "id": "M29", @@ -255,7 +255,7 @@ "catchers": [ "own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte" ], - "elapsed_seconds": 4.0 + "elapsed_seconds": 3.9 } ] } diff --git a/docs/evidence/p022-shadow-cp1.result.json b/docs/evidence/p022-shadow-cp1.result.json index 521f51e3..2463b91f 100644 --- a/docs/evidence/p022-shadow-cp1.result.json +++ b/docs/evidence/p022-shadow-cp1.result.json @@ -4,9 +4,9 @@ "campaign": "p022-shadow-cp1", "definition": "docs/evidence/p022-shadow-cp1.json", "definition_sha256": "a5df997a1d84effc16dbbb3a37f0b8cb73b5ee810faa9eae042e969b591b79b0", - "source_commit": "0cdbd0f4410bb5ee4a418337f567e515f4146b3b", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T13:21:42Z", + "recorded_at": "2026-09-07T10:45:41Z", "layers": [ "python", "rust", @@ -17,7 +17,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 0.5 + "elapsed_seconds": 2.0 }, "mutations": [ { @@ -27,7 +27,7 @@ "python::artifact-golden", "python::digest-ledger" ], - "elapsed_seconds": 0.5 + "elapsed_seconds": 0.7 }, { "id": "M02", @@ -36,7 +36,7 @@ "python::artifact-golden", "python::digest-ledger" ], - "elapsed_seconds": 9.8 + "elapsed_seconds": 1.3 }, { "id": "M03", @@ -45,7 +45,7 @@ "python::artifact-golden", "python::digest-ledger" ], - "elapsed_seconds": 1.2 + "elapsed_seconds": 1.3 }, { "id": "M04", @@ -53,7 +53,7 @@ "catchers": [ "python::domain-refusal" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.3 }, { "id": "M05", @@ -61,7 +61,7 @@ "catchers": [ "python::domain-refusal-reason" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.3 }, { "id": "M06", @@ -69,7 +69,7 @@ "catchers": [ "python::domain-refusal-reason" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.2 }, { "id": "M07", @@ -77,7 +77,7 @@ "catchers": [ "python::domain-refusal-reason" ], - "elapsed_seconds": 1.2 + "elapsed_seconds": 1.3 }, { "id": "M08", @@ -85,7 +85,7 @@ "catchers": [ "python::domain-backstop" ], - "elapsed_seconds": 1.0 + "elapsed_seconds": 1.3 }, { "id": "M09", @@ -93,7 +93,7 @@ "catchers": [ "python::tamper-refusal" ], - "elapsed_seconds": 1.2 + "elapsed_seconds": 1.4 }, { "id": "M10", @@ -101,7 +101,7 @@ "catchers": [ "python::artifact-golden" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.4 }, { "id": "M11", @@ -109,7 +109,7 @@ "catchers": [ "python::artifact-golden" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.4 }, { "id": "M12", @@ -117,7 +117,7 @@ "catchers": [ "python::artifact-golden" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.3 }, { "id": "M13", @@ -127,7 +127,7 @@ "python::capture-verify", "python::reduction-golden" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.4 }, { "id": "M14", @@ -135,7 +135,7 @@ "catchers": [ "python::structural-control" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.4 }, { "id": "M15", @@ -143,7 +143,7 @@ "catchers": [ "python::artifact-golden" ], - "elapsed_seconds": 1.1 + "elapsed_seconds": 1.3 }, { "id": "M16", @@ -164,7 +164,7 @@ "rust/tests/repro.rs::every_committed_artifact_round_trips_and_verifies", "rust/tests/repro.rs::every_shared_document_hashes_to_the_recorded_digest" ], - "elapsed_seconds": 1.4 + "elapsed_seconds": 1.5 }, { "id": "M18", @@ -174,7 +174,7 @@ "rust/tests/repro.rs::every_committed_artifact_round_trips_and_verifies", "rust/tests/repro.rs::every_shared_document_hashes_to_the_recorded_digest" ], - "elapsed_seconds": 1.4 + "elapsed_seconds": 1.5 }, { "id": "M19", @@ -185,7 +185,7 @@ "rust/tests/repro.rs::the_canonical_form_ignores_only_insignificant_text_formatting", "rust/tests/trace.rs::every_trace_golden_is_reproduced_byte_for_byte" ], - "elapsed_seconds": 1.4 + "elapsed_seconds": 1.5 }, { "id": "M20", @@ -196,7 +196,7 @@ "rust/tests/repro.rs::the_canonical_form_ignores_only_insignificant_text_formatting", "rust/tests/trace.rs::every_trace_golden_is_reproduced_byte_for_byte" ], - "elapsed_seconds": 1.6 + "elapsed_seconds": 1.7 }, { "id": "M21", @@ -204,7 +204,7 @@ "catchers": [ "rust/tests/repro.rs::the_canonical_form_ignores_only_insignificant_text_formatting" ], - "elapsed_seconds": 1.4 + "elapsed_seconds": 1.8 }, { "id": "M22", @@ -213,7 +213,7 @@ "rust/tests/repro.rs::every_declared_unnameable_document_is_refused", "rust/tests/repro.rs::values_outside_the_canonical_domain_are_refused_at_parse" ], - "elapsed_seconds": 1.3 + "elapsed_seconds": 1.6 }, { "id": "M23", @@ -222,7 +222,7 @@ "rust/tests/repro.rs::every_declared_unnameable_document_is_refused", "rust/tests/repro.rs::values_outside_the_canonical_domain_are_refused_at_parse" ], - "elapsed_seconds": 1.3 + "elapsed_seconds": 1.6 }, { "id": "M24", @@ -230,7 +230,7 @@ "catchers": [ "rust/tests/repro.rs::a_changed_byte_in_the_embedded_document_is_refused" ], - "elapsed_seconds": 1.5 + "elapsed_seconds": 1.7 }, { "id": "M25", @@ -238,7 +238,7 @@ "catchers": [ "rust/tests/repro.rs::verify_refuses_each_structural_violation" ], - "elapsed_seconds": 1.6 + "elapsed_seconds": 1.8 }, { "id": "M26", @@ -246,7 +246,7 @@ "catchers": [ "rust/tests/repro.rs::verify_refuses_each_structural_violation" ], - "elapsed_seconds": 1.6 + "elapsed_seconds": 1.8 }, { "id": "M27", @@ -257,7 +257,7 @@ "rust/tests/repro.rs::every_shared_document_hashes_to_the_recorded_digest", "rust/tests/repro.rs::verify_refuses_each_structural_violation" ], - "elapsed_seconds": 1.4 + "elapsed_seconds": 1.7 }, { "id": "M28", @@ -269,7 +269,7 @@ "rust/tests/repro.rs::the_canonical_form_ignores_only_insignificant_text_formatting", "rust/tests/repro.rs::verify_refuses_each_structural_violation" ], - "elapsed_seconds": 1.4 + "elapsed_seconds": 1.5 }, { "id": "M29", @@ -281,7 +281,7 @@ "rust/tests/repro.rs::every_committed_artifact_round_trips_and_verifies", "rust/tests/repro.rs::verify_refuses_each_structural_violation" ], - "elapsed_seconds": 1.6 + "elapsed_seconds": 1.7 }, { "id": "M30", @@ -292,7 +292,7 @@ "rust/tests/repro.rs::every_committed_artifact_round_trips_and_verifies", "rust/tests/repro.rs::verify_refuses_each_structural_violation" ], - "elapsed_seconds": 1.5 + "elapsed_seconds": 1.6 } ] } diff --git a/docs/evidence/p022-shadow-cp2.result.json b/docs/evidence/p022-shadow-cp2.result.json index 0a30b370..4ac9f4a6 100644 --- a/docs/evidence/p022-shadow-cp2.result.json +++ b/docs/evidence/p022-shadow-cp2.result.json @@ -4,9 +4,9 @@ "campaign": "p022-shadow-cp2", "definition": "docs/evidence/p022-shadow-cp2.json", "definition_sha256": "bd3fd24d65f539972cd035b45ea55462c617b34e3b82c826a5c10d9dc801a842", - "source_commit": "d726b06192c5fd76c94b02fab1011f55a453c14c", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T16:04:49Z", + "recorded_at": "2026-09-07T10:46:02Z", "layers": [ "python", "rust-repro", @@ -18,7 +18,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 5.2 + "elapsed_seconds": 1.8 }, "mutations": [ { @@ -36,7 +36,7 @@ "catchers": [ "python::artifact-golden" ], - "elapsed_seconds": 1.9 + "elapsed_seconds": 1.5 }, { "id": "M33", @@ -44,7 +44,7 @@ "catchers": [ "python::structural-control" ], - "elapsed_seconds": 2.0 + "elapsed_seconds": 1.6 }, { "id": "M34", @@ -61,7 +61,7 @@ "rust-engine/tests/engine.rs::a_projection_names_exactly_the_members_it_carries", "rust-engine/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 2.1 + "elapsed_seconds": 2.0 }, { "id": "M36", @@ -70,7 +70,7 @@ "rust-engine/tests/engine.rs::a_projection_names_exactly_the_members_it_carries", "rust-engine/tests/engine.rs::this_engine_reproduces_its_committed_capture" ], - "elapsed_seconds": 1.8 + "elapsed_seconds": 1.9 }, { "id": "M37", diff --git a/docs/evidence/p022-shadow-cp3.result.json b/docs/evidence/p022-shadow-cp3.result.json index 3bbed467..e17e6d1d 100644 --- a/docs/evidence/p022-shadow-cp3.result.json +++ b/docs/evidence/p022-shadow-cp3.result.json @@ -4,9 +4,9 @@ "campaign": "p022-shadow-cp3", "definition": "docs/evidence/p022-shadow-cp3.json", "definition_sha256": "99e99aacfe3f4ac4e5dff92bc7a5cf07bd3826c843657c28b6f0d490e2534ffa", - "source_commit": "001f6fd3be4e73f4d3878dc430dd4507a90ca273", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T13:24:40Z", + "recorded_at": "2026-09-07T10:46:26Z", "layers": [ "python", "rust-repro", @@ -19,7 +19,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 2.0 + "elapsed_seconds": 2.1 }, "mutations": [ { @@ -28,7 +28,7 @@ "catchers": [ "python::" ], - "elapsed_seconds": 0.5 + "elapsed_seconds": 0.6 }, { "id": "M43", @@ -36,7 +36,7 @@ "catchers": [ "python::trace-golden" ], - "elapsed_seconds": 1.7 + "elapsed_seconds": 1.9 }, { "id": "M44", @@ -45,7 +45,7 @@ "python::trace-golden", "python::trace-shape" ], - "elapsed_seconds": 1.8 + "elapsed_seconds": 2.0 }, { "id": "M45", @@ -53,7 +53,7 @@ "catchers": [ "python::trace-golden" ], - "elapsed_seconds": 1.7 + "elapsed_seconds": 2.0 }, { "id": "M46", @@ -61,7 +61,7 @@ "catchers": [ "python::trace-golden" ], - "elapsed_seconds": 1.7 + "elapsed_seconds": 1.9 }, { "id": "M47", @@ -70,7 +70,7 @@ "python::trace-golden", "python::trace-normalization" ], - "elapsed_seconds": 1.8 + "elapsed_seconds": 1.9 }, { "id": "M48", @@ -78,7 +78,7 @@ "catchers": [ "python::trace-golden" ], - "elapsed_seconds": 1.7 + "elapsed_seconds": 1.9 }, { "id": "M49", @@ -88,7 +88,7 @@ "rust-trace/tests/trace.rs::every_trace_golden_is_reproduced_byte_for_byte", "rust-unit/src/lib.rs::trace::tests::a_fully_listed_document_normalizes" ], - "elapsed_seconds": 2.0 + "elapsed_seconds": 2.2 }, { "id": "M50", @@ -96,7 +96,7 @@ "catchers": [ "rust-trace/tests/trace.rs::every_trace_golden_is_reproduced_byte_for_byte" ], - "elapsed_seconds": 1.9 + "elapsed_seconds": 2.1 }, { "id": "M51", @@ -104,7 +104,7 @@ "catchers": [ "rust-unit/src/lib.rs::trace::tests::a_handle_reference_the_rename_cannot_reach_is_refused" ], - "elapsed_seconds": 2.6 + "elapsed_seconds": 2.8 }, { "id": "M52", @@ -113,7 +113,7 @@ "rust-trace/tests/trace.rs::every_trace_golden_is_reproduced_byte_for_byte", "rust-unit/src/lib.rs::trace::tests::a_fully_listed_document_normalizes" ], - "elapsed_seconds": 2.6 + "elapsed_seconds": 2.8 } ] } diff --git a/docs/evidence/p022-shadow-cp4.json b/docs/evidence/p022-shadow-cp4.json index acdd3c77..8b4afcf2 100644 --- a/docs/evidence/p022-shadow-cp4.json +++ b/docs/evidence/p022-shadow-cp4.json @@ -164,16 +164,14 @@ }, { "id": "M61", - "description": "the port widens the reduction scope to the verdict layer", + "description": "the port widens the reduction scope to the verdict layer. Two further catchers were declared when this campaign was recorded and stopped being catchers on main, before #259's final acceptance: `the_reducer_is_silent_on_unchanged_data...` and `two_engines_that_both_refused_a_layer_agree` only failed while some committed artifact still had ASYMMETRIC verdict-layer statuses, and checkpoint 4b's promotion removed the last of them. Measured at 834f295 with the mutation applied, not inferred. The two that remain are the load-bearing ones — the scope is still pinned by a golden and by the refusal control", "rule": "reduction: the scope is a contract, not a parameter", "target": "rust/crates/own-shadow/src/reduce.rs", "pattern": "pub\\ const\\ REDUCTION_SCOPE:\\ \\[\\&str;\\ 2\\]\\ =\\ \\[\"lowered\",\\ \"summaries\"\\];", "replacement": "pub const REDUCTION_SCOPE: [&str; 3] = [\"lowered\", \"summaries\", \"verdicts\"];", "expected_catchers": [ "rust-reduce/tests/reduce.rs::every_reduction_golden_is_reproduced_byte_for_byte", - "rust-reduce/tests/reduce.rs::the_reducer_is_silent_on_unchanged_data_and_names_a_synthetic_divergence", - "rust-reduce/tests/reduce.rs::the_verdict_layer_is_refused_not_silently_skipped", - "rust-reduce/tests/reduce.rs::two_engines_that_both_refused_a_layer_agree" + "rust-reduce/tests/reduce.rs::the_verdict_layer_is_refused_not_silently_skipped" ] }, { diff --git a/docs/evidence/p022-shadow-cp4.result.json b/docs/evidence/p022-shadow-cp4.result.json index 29578f0e..452ac6a8 100644 --- a/docs/evidence/p022-shadow-cp4.result.json +++ b/docs/evidence/p022-shadow-cp4.result.json @@ -3,10 +3,10 @@ "comment": "Recorded mutation-campaign run (scripts/mutate_campaign.py --run). Raw facts only: outcomes, catchers, provenance. Counts are derived by scripts/render_checkpoint_status.py; regenerate this file by re-running the campaign, never by hand.", "campaign": "p022-shadow-cp4", "definition": "docs/evidence/p022-shadow-cp4.json", - "definition_sha256": "ad87275b58dc706535d6ed25a31b8b735725a1f0ce217975563100379d5fb5d6", - "source_commit": "001f6fd3be4e73f4d3878dc430dd4507a90ca273", + "definition_sha256": "73cdde6a3c5ad5208f89d44e435a6780337d1c5a0bffbb757d3d520d6fedde4e", + "source_commit": "3af8ead18d423d9de760ecbf180ed5ca0ef68dad", "dirty": false, - "recorded_at": "2026-09-06T13:25:01Z", + "recorded_at": "2026-09-07T10:46:49Z", "layers": [ "python", "rust-repro", @@ -19,7 +19,7 @@ "id": "M00", "outcome": "survived", "catchers": [], - "elapsed_seconds": 1.9 + "elapsed_seconds": 2.1 }, "mutations": [ { @@ -29,7 +29,7 @@ "python::reduction-control", "python::reduction-golden" ], - "elapsed_seconds": 0.6 + "elapsed_seconds": 0.7 }, { "id": "M54", @@ -37,7 +37,7 @@ "catchers": [ "python::reduction-control" ], - "elapsed_seconds": 1.7 + "elapsed_seconds": 2.0 }, { "id": "M55", @@ -77,7 +77,7 @@ "catchers": [ "python::reduction-control" ], - "elapsed_seconds": 1.8 + "elapsed_seconds": 1.9 }, { "id": "M60", @@ -85,18 +85,16 @@ "catchers": [ "python::artifact-golden" ], - "elapsed_seconds": 1.7 + "elapsed_seconds": 1.9 }, { "id": "M61", "outcome": "caught", "catchers": [ "rust-reduce/tests/reduce.rs::every_reduction_golden_is_reproduced_byte_for_byte", - "rust-reduce/tests/reduce.rs::the_reducer_is_silent_on_unchanged_data_and_names_a_synthetic_divergence", - "rust-reduce/tests/reduce.rs::the_verdict_layer_is_refused_not_silently_skipped", - "rust-reduce/tests/reduce.rs::two_engines_that_both_refused_a_layer_agree" + "rust-reduce/tests/reduce.rs::the_verdict_layer_is_refused_not_silently_skipped" ], - "elapsed_seconds": 1.9 + "elapsed_seconds": 2.3 }, { "id": "M62", @@ -104,7 +102,7 @@ "catchers": [ "rust-reduce/tests/reduce.rs::two_engines_that_both_refused_a_layer_agree" ], - "elapsed_seconds": 1.9 + "elapsed_seconds": 2.2 }, { "id": "M63", @@ -112,7 +110,7 @@ "catchers": [ "rust-reduce/tests/reduce.rs::every_reduction_golden_is_reproduced_byte_for_byte" ], - "elapsed_seconds": 2.1 + "elapsed_seconds": 2.3 } ] } diff --git a/docs/generated/p022-coord-census.md b/docs/generated/p022-coord-census.md new file mode 100644 index 00000000..78010b47 --- /dev/null +++ b/docs/generated/p022-coord-census.md @@ -0,0 +1,227 @@ + + +# P-022 #259 final acceptance — the source-coordinate census + +The measurement the coordinate-domain decision (`spec/OwnIR.md` §4.2) was taken against: every `line`, `ctor_line` and `column` slot in every JSON file under `tests/fixtures/`, at any depth and under any key. + +It is wider than the door on purpose. A **door slot** sits on an OwnIR *document* and `load()` rules on it; every other row is an **observation** — a golden, a ledger, a captured trace — which the door never sees and which this change must therefore leave alone. `0` stays a legal line (the reference's own default for an absent one), so the observation rows anchored at zero are the records the decision must go on accepting, and a census that read only the inputs could not see them at all. + +Value classes follow the cp1 taxonomy's axis rather than blurring it: `outside-int64` has no representable integer form (`Shape`), while `negative` and `above-int32` are representable coordinates violating the domain rule (`Location`). `below-1` is the column's own 1-based rule. + +| measure | value | +|------------------------------------|------:| +| JSON files scanned | 327 | +| coordinate slots found | 2186 | + +## By value class + +| value class | all slots | door slots | +|---|---:|---:| +| `above-int32` | 21 | 21 | +| `below-1` | 19 | 19 | +| `bool` | 14 | 14 | +| `float` | 2 | 2 | +| `in-domain` | 1743 | 950 | +| `negative` | 23 | 23 | +| `null` | 225 | 7 | +| `outside-int64` | 14 | 14 | +| `string` | 17 | 17 | +| `zero` | 108 | 25 | + +## By family and slot + +`door` marks a slot the strict door rules on. Examples are shown for every class outside `in-domain` / `null` / `zero`, because those are the values a reader needs to see rather than count. + +| family | slot | class | door | count | files | values | +|---|---|---|:--:|---:|---:|---| +| `(root)` | `cases[].diagnostic.evidence[].line` | `in-domain` | — | 40 | 1 | — | +| `(root)` | `cases[].diagnostic.line` | `in-domain` | — | 47 | 1 | — | +| `(root)` | `cases[].diagnostics[].evidence[].line` | `in-domain` | — | 9 | 1 | — | +| `(root)` | `cases[].diagnostics[].evidence[].line` | `zero` | — | 1 | 1 | — | +| `(root)` | `cases[].diagnostics[].line` | `in-domain` | — | 20 | 1 | — | +| `(root)` | `cases[].diagnostics[].line` | `zero` | — | 1 | 1 | — | +| `(root)` | `cases[].document.components[].subscriptions[].column` | `above-int32` | yes | 2 | 1 | `2147483648`, `9223372036854775807` | +| `(root)` | `cases[].document.components[].subscriptions[].column` | `below-1` | yes | 6 | 1 | `-1`, `0` | +| `(root)` | `cases[].document.components[].subscriptions[].column` | `bool` | yes | 2 | 1 | `True` | +| `(root)` | `cases[].document.components[].subscriptions[].column` | `float` | yes | 1 | 1 | `1.0` | +| `(root)` | `cases[].document.components[].subscriptions[].column` | `in-domain` | yes | 3 | 1 | — | +| `(root)` | `cases[].document.components[].subscriptions[].column` | `null` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.components[].subscriptions[].column` | `outside-int64` | yes | 3 | 1 | `-9223372036854775809`, `18446744073709551616`, `9223372036854775808` | +| `(root)` | `cases[].document.components[].subscriptions[].column` | `string` | yes | 1 | 1 | `'3'` | +| `(root)` | `cases[].document.components[].subscriptions[].line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.components[].subscriptions[].line` | `bool` | yes | 1 | 1 | `True` | +| `(root)` | `cases[].document.components[].subscriptions[].line` | `in-domain` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.components[].subscriptions[].line` | `negative` | yes | 3 | 1 | `-1` | +| `(root)` | `cases[].document.components[].subscriptions[].line` | `null` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.components[].subscriptions[].line` | `string` | yes | 1 | 1 | `'x'` | +| `(root)` | `cases[].document.components[].subscriptions[].line` | `zero` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.effects[].bindings[].line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.effects[].bindings[].line` | `in-domain` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.effects[].bindings[].line` | `negative` | yes | 1 | 1 | `-1` | +| `(root)` | `cases[].document.effects[].bindings[].line` | `outside-int64` | yes | 1 | 1 | `9223372036854775808` | +| `(root)` | `cases[].document.effects[].bindings[].line` | `string` | yes | 1 | 1 | `'3'` | +| `(root)` | `cases[].document.effects[].bindings[].line` | `zero` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.effects[].line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.effects[].line` | `in-domain` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.effects[].line` | `negative` | yes | 1 | 1 | `-1` | +| `(root)` | `cases[].document.effects[].line` | `outside-int64` | yes | 1 | 1 | `9223372036854775808` | +| `(root)` | `cases[].document.effects[].line` | `string` | yes | 1 | 1 | `'3'` | +| `(root)` | `cases[].document.effects[].line` | `zero` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.functions[].[].column` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.functions[].[].column` | `below-1` | yes | 8 | 1 | `-2`, `0` | +| `(root)` | `cases[].document.functions[].[].column` | `bool` | yes | 2 | 1 | `True` | +| `(root)` | `cases[].document.functions[].[].column` | `in-domain` | yes | 9 | 1 | — | +| `(root)` | `cases[].document.functions[].[].line` | `above-int32` | yes | 4 | 1 | `2147483648` | +| `(root)` | `cases[].document.functions[].[].line` | `bool` | yes | 4 | 1 | `True` | +| `(root)` | `cases[].document.functions[].[].line` | `in-domain` | yes | 4 | 1 | — | +| `(root)` | `cases[].document.functions[].[].line` | `negative` | yes | 6 | 1 | `-1` | +| `(root)` | `cases[].document.functions[].[].line` | `null` | yes | 4 | 1 | — | +| `(root)` | `cases[].document.functions[].[].line` | `string` | yes | 4 | 1 | `'x'` | +| `(root)` | `cases[].document.functions[].[].line` | `zero` | yes | 4 | 1 | — | +| `(root)` | `cases[].document.functions[].params[].column` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.functions[].params[].column` | `below-1` | yes | 4 | 1 | `-1`, `0` | +| `(root)` | `cases[].document.functions[].params[].column` | `bool` | yes | 1 | 1 | `True` | +| `(root)` | `cases[].document.functions[].params[].column` | `in-domain` | yes | 2 | 1 | — | +| `(root)` | `cases[].document.functions[].params[].column` | `string` | yes | 1 | 1 | `'3'` | +| `(root)` | `cases[].document.functions[].params[].line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.functions[].params[].line` | `in-domain` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.functions[].params[].line` | `negative` | yes | 1 | 1 | `-1` | +| `(root)` | `cases[].document.functions[].params[].line` | `outside-int64` | yes | 1 | 1 | `9223372036854775808` | +| `(root)` | `cases[].document.functions[].params[].line` | `string` | yes | 3 | 1 | `'3'` | +| `(root)` | `cases[].document.functions[].params[].line` | `zero` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.protocol_functions[].events[].[].line` | `in-domain` | yes | 134 | 1 | — | +| `(root)` | `cases[].document.protocol_functions[].events[].line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.protocol_functions[].events[].line` | `bool` | yes | 1 | 1 | `True` | +| `(root)` | `cases[].document.protocol_functions[].events[].line` | `in-domain` | yes | 14 | 1 | — | +| `(root)` | `cases[].document.protocol_functions[].events[].line` | `negative` | yes | 1 | 1 | `-1` | +| `(root)` | `cases[].document.protocol_functions[].events[].line` | `outside-int64` | yes | 1 | 1 | `9223372036854775808` | +| `(root)` | `cases[].document.protocol_functions[].events[].line` | `string` | yes | 3 | 1 | `'3'`, `'x'` | +| `(root)` | `cases[].document.protocol_functions[].events[].line` | `zero` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.services[].ctor_line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.services[].ctor_line` | `in-domain` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.services[].ctor_line` | `negative` | yes | 1 | 1 | `-1` | +| `(root)` | `cases[].document.services[].ctor_line` | `outside-int64` | yes | 1 | 1 | `9223372036854775808` | +| `(root)` | `cases[].document.services[].ctor_line` | `string` | yes | 1 | 1 | `'3'` | +| `(root)` | `cases[].document.services[].ctor_line` | `zero` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.services[].line` | `above-int32` | yes | 2 | 1 | `2147483648`, `9223372036854775807` | +| `(root)` | `cases[].document.services[].line` | `bool` | yes | 1 | 1 | `True` | +| `(root)` | `cases[].document.services[].line` | `float` | yes | 1 | 1 | `1.0` | +| `(root)` | `cases[].document.services[].line` | `in-domain` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.services[].line` | `negative` | yes | 3 | 1 | `-1`, `-5`, `-9223372036854775808` | +| `(root)` | `cases[].document.services[].line` | `null` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.services[].line` | `outside-int64` | yes | 4 | 1 | `-9223372036854775809`, `18446744073709551615`, `18446744073709551616`, `9223372036854775808` | +| `(root)` | `cases[].document.services[].line` | `string` | yes | 1 | 1 | `'3'` | +| `(root)` | `cases[].document.services[].line` | `zero` | yes | 2 | 1 | — | +| `(root)` | `cases[].document.services[].root_resolve_sites[].line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.services[].root_resolve_sites[].line` | `bool` | yes | 1 | 1 | `True` | +| `(root)` | `cases[].document.services[].root_resolve_sites[].line` | `in-domain` | yes | 2 | 1 | — | +| `(root)` | `cases[].document.services[].root_resolve_sites[].line` | `negative` | yes | 1 | 1 | `-1` | +| `(root)` | `cases[].document.services[].root_resolve_sites[].line` | `outside-int64` | yes | 1 | 1 | `9223372036854775808` | +| `(root)` | `cases[].document.services[].root_resolve_sites[].line` | `zero` | yes | 1 | 1 | — | +| `(root)` | `cases[].document.services[].scope_cache_sites[].line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `(root)` | `cases[].document.services[].scope_cache_sites[].line` | `bool` | yes | 1 | 1 | `True` | +| `(root)` | `cases[].document.services[].scope_cache_sites[].line` | `in-domain` | yes | 2 | 1 | — | +| `(root)` | `cases[].document.services[].scope_cache_sites[].line` | `negative` | yes | 1 | 1 | `-1` | +| `(root)` | `cases[].document.services[].scope_cache_sites[].line` | `outside-int64` | yes | 1 | 1 | `9223372036854775808` | +| `(root)` | `cases[].document.services[].scope_cache_sites[].line` | `zero` | yes | 1 | 1 | — | +| `(root)` | `cases[].expected[].line` | `in-domain` | — | 60 | 1 | — | +| `(root)` | `cases[].expected[].line` | `zero` | — | 1 | 1 | — | +| `(root)` | `cases[].methods[].events[].[].line` | `in-domain` | — | 26 | 1 | — | +| `(root)` | `cases[].methods[].events[].line` | `in-domain` | — | 196 | 1 | — | +| `(root)` | `cases[].records[].column` | `in-domain` | — | 4 | 1 | — | +| `(root)` | `cases[].records[].column` | `null` | — | 24 | 1 | — | +| `(root)` | `cases[].records[].diagnostic.evidence[].line` | `in-domain` | — | 14 | 1 | — | +| `(root)` | `cases[].records[].diagnostic.line` | `in-domain` | — | 28 | 1 | — | +| `(root)` | `di_cases[].services[].line` | `in-domain` | yes | 40 | 1 | — | +| `(root)` | `effect_cases[].effects[].bindings[].line` | `in-domain` | yes | 11 | 1 | — | +| `(root)` | `effect_cases[].effects[].line` | `in-domain` | yes | 9 | 1 | — | +| `(root)` | `order_cases[].emitted[].diagnostic.evidence[].line` | `in-domain` | — | 1 | 1 | — | +| `(root)` | `order_cases[].emitted[].diagnostic.line` | `in-domain` | — | 15 | 1 | — | +| `(root)` | `render_cases[].diagnostic.evidence[].line` | `in-domain` | — | 9 | 1 | — | +| `(root)` | `render_cases[].diagnostic.line` | `in-domain` | — | 26 | 1 | — | +| `lowered` | `components[].subscriptions[].line` | `in-domain` | yes | 24 | 10 | — | +| `lowered` | `functions[].[].line` | `in-domain` | yes | 150 | 41 | — | +| `lowered` | `functions[].params[].line` | `in-domain` | yes | 20 | 10 | — | +| `lowered` | `functions[].params[].line` | `zero` | yes | 4 | 3 | — | +| `lowered` | `handles[].line` | `in-domain` | — | 48 | 22 | — | +| `lowered` | `services[].line` | `in-domain` | yes | 2 | 1 | — | +| `ownir` | `components[].subscriptions[].column` | `in-domain` | yes | 2 | 1 | — | +| `ownir` | `components[].subscriptions[].line` | `in-domain` | yes | 20 | 10 | — | +| `ownir` | `functions[].[].column` | `in-domain` | yes | 5 | 1 | — | +| `ownir` | `functions[].[].line` | `in-domain` | yes | 90 | 10 | — | +| `ownir` | `functions[].params[].column` | `in-domain` | yes | 1 | 1 | — | +| `ownir` | `functions[].params[].line` | `in-domain` | yes | 4 | 3 | — | +| `ownir` | `protocol_functions[].events[].[].line` | `in-domain` | yes | 2 | 2 | — | +| `ownir` | `protocol_functions[].events[].line` | `in-domain` | yes | 12 | 2 | — | +| `ownir` | `services[].ctor_line` | `in-domain` | yes | 2 | 1 | — | +| `ownir` | `services[].line` | `in-domain` | yes | 8 | 2 | — | +| `repro` | `components[].subscriptions[].line` | `in-domain` | yes | 3 | 2 | — | +| `repro` | `engines[].layers[].document.findings[].column` | `null` | — | 13 | 6 | — | +| `repro` | `engines[].layers[].document.findings[].line` | `in-domain` | — | 11 | 5 | — | +| `repro` | `engines[].layers[].document.findings[].line` | `zero` | — | 2 | 1 | — | +| `repro` | `engines[].layers[].document.functions[].[].line` | `in-domain` | yes | 18 | 4 | — | +| `repro` | `engines[].layers[].document.functions[].params[].line` | `in-domain` | yes | 4 | 1 | — | +| `repro` | `engines[].layers[].document.handles[].line` | `in-domain` | — | 12 | 4 | — | +| `repro` | `engines[].layers[].document.summaries[].line` | `zero` | — | 4 | 2 | — | +| `repro` | `input.document.components[].subscriptions[].line` | `in-domain` | yes | 3 | 2 | — | +| `repro` | `input.document.effects[].bindings[].line` | `in-domain` | yes | 2 | 1 | — | +| `repro` | `input.document.effects[].line` | `in-domain` | yes | 2 | 1 | — | +| `repro` | `input.document.functions[].[].line` | `in-domain` | yes | 6 | 3 | — | +| `repro` | `input.document.functions[].params[].line` | `in-domain` | yes | 2 | 1 | — | +| `repro` | `input.document.protocol_functions[].events[].[].line` | `in-domain` | yes | 1 | 1 | — | +| `repro` | `input.document.protocol_functions[].events[].line` | `in-domain` | yes | 6 | 1 | — | +| `repro` | `input.document.services[].ctor_line` | `in-domain` | yes | 2 | 1 | — | +| `repro` | `input.document.services[].line` | `in-domain` | yes | 6 | 1 | — | +| `repro` | `traces[].layers[].steps[].value.[].line` | `in-domain` | — | 2 | 1 | — | +| `repro` | `traces[].layers[].steps[].value.column` | `null` | — | 13 | 6 | — | +| `repro` | `traces[].layers[].steps[].value.line` | `in-domain` | — | 39 | 7 | — | +| `repro` | `traces[].layers[].steps[].value.line` | `zero` | — | 6 | 3 | — | +| `repro` | `traces[].layers[].steps[].value.params[].line` | `in-domain` | — | 4 | 1 | — | +| `summaries` | `functions[].[].line` | `in-domain` | yes | 37 | 9 | — | +| `summaries` | `functions[].params[].line` | `in-domain` | yes | 25 | 8 | — | +| `summaries` | `summaries[].line` | `zero` | — | 54 | 26 | — | +| `verdict_renders` | `components[].subscriptions[].column` | `in-domain` | yes | 1 | 1 | — | +| `verdict_renders` | `components[].subscriptions[].line` | `in-domain` | yes | 11 | 5 | — | +| `verdict_renders` | `functions[].[].line` | `in-domain` | yes | 3 | 2 | — | +| `verdict_renders` | `functions[].params[].line` | `in-domain` | yes | 2 | 1 | — | +| `verdict_renders` | `protocol_functions[].events[].[].line` | `in-domain` | yes | 1 | 1 | — | +| `verdict_renders` | `protocol_functions[].events[].line` | `in-domain` | yes | 6 | 1 | — | +| `verdict_renders` | `services[].ctor_line` | `in-domain` | yes | 2 | 1 | — | +| `verdict_renders` | `services[].line` | `in-domain` | yes | 3 | 1 | — | +| `verdicts` | `components[].subscriptions[].column` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `verdicts` | `components[].subscriptions[].column` | `in-domain` | yes | 2 | 2 | — | +| `verdicts` | `components[].subscriptions[].line` | `in-domain` | yes | 38 | 12 | — | +| `verdicts` | `components[].subscriptions[].line` | `zero` | yes | 1 | 1 | — | +| `verdicts` | `effects[].bindings[].line` | `in-domain` | yes | 12 | 6 | — | +| `verdicts` | `effects[].bindings[].line` | `zero` | yes | 1 | 1 | — | +| `verdicts` | `effects[].line` | `in-domain` | yes | 9 | 5 | — | +| `verdicts` | `effects[].line` | `negative` | yes | 1 | 1 | `-3` | +| `verdicts` | `effects[].line` | `zero` | yes | 1 | 1 | — | +| `verdicts` | `findings[].column` | `in-domain` | — | 14 | 5 | — | +| `verdicts` | `findings[].column` | `null` | — | 168 | 74 | — | +| `verdicts` | `findings[].line` | `in-domain` | — | 168 | 69 | — | +| `verdicts` | `findings[].line` | `zero` | — | 14 | 11 | — | +| `verdicts` | `functions[].[].column` | `below-1` | yes | 1 | 1 | `0` | +| `verdicts` | `functions[].[].column` | `in-domain` | yes | 7 | 2 | — | +| `verdicts` | `functions[].[].line` | `above-int32` | yes | 1 | 1 | `4294967296` | +| `verdicts` | `functions[].[].line` | `in-domain` | yes | 51 | 8 | — | +| `verdicts` | `functions[].[].line` | `zero` | yes | 1 | 1 | — | +| `verdicts` | `functions[].params[].line` | `in-domain` | yes | 3 | 2 | — | +| `verdicts` | `functions[].params[].line` | `negative` | yes | 1 | 1 | `-1` | +| `verdicts` | `protocol_functions[].events[].[].line` | `in-domain` | yes | 2 | 2 | — | +| `verdicts` | `protocol_functions[].events[].line` | `in-domain` | yes | 34 | 7 | — | +| `verdicts` | `services[].ctor_line` | `in-domain` | yes | 3 | 3 | — | +| `verdicts` | `services[].line` | `above-int32` | yes | 1 | 1 | `2147483648` | +| `verdicts` | `services[].line` | `in-domain` | yes | 52 | 15 | — | +| `verdicts` | `services[].line` | `negative` | yes | 1 | 1 | `-5` | +| `verdicts` | `services[].line` | `zero` | yes | 2 | 2 | — | +| `verdicts` | `services[].root_resolve_sites[].line` | `in-domain` | yes | 7 | 3 | — | +| `verdicts` | `services[].root_resolve_sites[].line` | `negative` | yes | 1 | 1 | `-2` | +| `verdicts` | `services[].root_resolve_sites[].line` | `zero` | yes | 1 | 1 | — | +| `verdicts` | `services[].scope_cache_sites[].line` | `in-domain` | yes | 6 | 2 | — | + +## Slot inventory reachability + +`tests/coordinate_census.SLOTS` is the door inventory, and it is asserted as a set rather than spot-checked: a declared slot no fixture reaches is a phantom claiming coverage it does not have. + +Every declared slot is reached by at least one fixture. diff --git a/docs/generated/p022-coord-mutations.md b/docs/generated/p022-coord-mutations.md new file mode 100644 index 00000000..efc6841f --- /dev/null +++ b/docs/generated/p022-coord-mutations.md @@ -0,0 +1,90 @@ + + +# P-022 #259 final acceptance — mutation campaigns + +The coordinate-domain contract, measured in two halves because the two doors fail differently: the STRICT door refuses an out-of-domain coordinate and the TOLERANT one degrades it to absent. Every rule is mutated on BOTH sides — the reference and its Rust mirror — since a domain only one implementation enforces is a divergence, not a rule. Every mutation edits a **production** surface (P-022 discipline 2) and every declared layer runs for every mutation (discipline 3: no fail-fast); the counts are derived from the recorded runs by `scripts/mutate_campaign.summarize()`, never typed. + +## the strict door — the coordinate domain, both implementations + +Campaign `p022-coord-1` — #259 final acceptance, the STRICT door: the coordinate domain of spec/OwnIR.md §4.2 in both implementations. Every rule is mutated twice — once in the reference (ownlang/ownir.py, ownlang/obligations.py) and once in its Rust mirror (own-ir/src/strict.rs, own-ir/src/protocol.rs) — because a domain that only one side enforces is a divergence the ledger would report as a category mismatch rather than as the missing rule it is. Both ends of both bounds are attacked, and off-by-one at each, since a domain checked only far outside cannot tell a correct limit from a narrow one; the two fields §4.2 used to record as validated NOWHERE are attacked at every nesting shape, because then/else/body are three separate recursion sites. Two mutations attack the CATEGORY and the message rather than the verdict: a domain violation filed as `shape` is the exact defect #326's census found in `column`, and it would leave every accept/reject green. + +Definition: `docs/evidence/p022-coord-1.json` (sha256 `5f2587e9f495e5bf…`, 26 mutations). Replay on a clean tree with `python scripts/mutate_campaign.py --campaign docs/evidence/p022-coord-1.json --run`; the recorded run is raw outcomes and provenance, the counts below are derived from it. + +| measure | value | +|--------------------------------------------------|---| +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | +| layers run (every one, for every mutation) | `py-limits`, `py-ledger`, `rust-door` | +| mutations | 26 | +| caught | 26 | +| survived | 0 | +| compile-error (no evidence either way) | 0 | +| invalid-mutation | 0 | +| runner-error | 0 | +| caught without every expected catcher | none | +| honesty control `M00` (unmutated tree must pass) | survived — as required | + +| id | rule | mutation | outcome | caught by | +|---|---|---|---|---| +| M01 | §4.2 line domain | the strict door loses the domain's LOWER bound, so a negative line is accepted again | caught | `py-ledger::ledger-stale`
`py-limits::line-domain` | +| M02 | §4.2 line domain | …and the UPPER bound, so an int64 line is accepted again | caught | `py-ledger::ledger-stale`
`py-limits::line-domain` | +| M03 | §4.2 line domain | off-by-one at the bottom: 0 stops being legal, which would break the reference's own absent-line default | caught | `py-ledger::ledger-stale`
`py-limits::column-domain`
`py-limits::line-domain`
`py-limits::nesting`
`py-limits::tolerances` | +| M04 | §4.2 line domain | …and at the top: 2147483647 stops being legal | caught | `py-ledger::ledger-stale`
`py-limits::line-domain` | +| M05 | §4.2 message | the domain diagnostic stops naming the domain | caught | `py-ledger::ledger-stale`
`py-limits::line-domain` | +| M06 | §4.2 / D2 | the subscriptions walk stops checking its line — the field §4.2 used to record as validated nowhere | caught | `py-ledger::ledger-stale`
`py-limits::line-domain` | +| M07 | §4.2 / D2 | …and the flow-op walk stops checking its line | caught | `py-ledger::ledger-stale`
`py-limits::line-domain` | +| M08 | §4.2 / D2 | the flow walk stops recursing into one nesting shape, so a `while` body's line escapes | caught | `py-ledger::ledger-stale`
`py-limits::line-domain`
`py-limits::line-type`
`py-limits::nesting` | +| M09 | §4.2 / D2 | the subscriptions line loses its bool guard, so `true` reads as line 1 — the bool-is-int trap the contract calls out by name | caught | `py-ledger::ledger-stale`
`py-limits::line-type` | +| M10 | §4.2 line domain | the root-resolve site array stops checking its coordinate | caught | `py-ledger::ledger-stale`
`py-limits::line-domain` | +| M11 | §4.1 / §4.2 column | the column loses its domain upper bound | caught | `py-ledger::ledger-stale`
`py-limits::column-domain` | +| M12 | §4.1 column | …and its 1-based lower bound, so `0` becomes a legal column | caught | `py-ledger::ledger-stale`
`py-limits::column-domain` | +| M13 | §4.2 two doors | the strict door stops refusing an out-of-domain event line and degrades instead | caught | `py-ledger::ledger-stale`
`py-limits::line-domain` | +| M14 | §4.2 line domain | the Rust door loses the domain's LOWER bound | caught | `rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door`
`rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M15 | §4.2 line domain | …and its UPPER bound | caught | `rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door`
`rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M16 | §4.2 line domain | off-by-one at the bottom in the Rust door | caught | `rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door`
`rust-door/tests/roundtrip.rs::additive_unknown_fields_are_preserved`
`rust-door/tests/roundtrip.rs::explicit_null_is_accepted_and_preserved_where_python_accepts_it`
`rust-door/tests/roundtrip.rs::param_effect_vocabulary_is_closed`
`rust-door/tests/roundtrip.rs::round_trips_every_python_fixture`
`rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M17 | §4.2 line domain | …and at the top | caught | `rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door`
`rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M18 | cp1 taxonomy | the Rust domain rejection is filed as `shape` — the category read off the wrong axis, the exact defect #326's census found in `column` | caught | `rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door`
`rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M19 | §4.2 message | the Rust domain diagnostic stops naming the domain | caught | `rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door`
`rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on` | +| M20 | §4.2 / D2 | the Rust subscriptions walk stops checking its line | caught | `rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M21 | §4.2 / D2 | …and the Rust flow-op walk stops checking its line | caught | `rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M22 | §4.2 / D2 | the Rust flow walk stops recursing into one nesting shape | caught | `rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M23 | §4.2 line domain | the Rust site array stops checking its coordinate | caught | `rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M24 | §4.1 / §4.2 column | the Rust column loses its domain upper bound | caught | `rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M25 | §4.1 column | …and its 1-based lower bound | caught | `rust-door/tests/validation_replay.rs::the_coordinate_rules_report_the_axis_they_are_on`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M26 | §4.2 two doors | the Rust strict door stops refusing an out-of-domain event line and degrades instead | caught | `rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door`
`rust-door/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | + +## the tolerant door — the degrade, both implementations + +Campaign `p022-coord-2` — #259 final acceptance, the TOLERANT door: `check_facts()` degrades an out-of-domain coordinate to 0 (absent) rather than raising, clamping or inventing, and the Rust bridge does the same. Three failure modes are attacked at every reader on both sides — degrade -> clamp, degrade -> pass-through, degrade -> raise — because they are three different wrong answers and only the clamp looks harmless: it moves a finding onto a REAL line the producer never named, which no golden comparing anchors can forgive but a reader might. The four promoted verdict_boundary_* goldens are the controls that make this measurable at all; before the promotion the Rust half of every one of these mutations was unreachable behind a refusal. One mutation attacks the LOWERING rather than the verdict, because a Layer 2 coordinate that disagrees with the reference is invisible at Layer 3 — the AST build degrades it a second time — and needed a control of its own. + +Definition: `docs/evidence/p022-coord-2.json` (sha256 `f033608e513b098b…`, 15 mutations). Replay on a clean tree with `python scripts/mutate_campaign.py --campaign docs/evidence/p022-coord-2.json --run`; the recorded run is raw outcomes and provenance, the counts below are derived from it. + +| measure | value | +|--------------------------------------------------|---| +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | +| layers run (every one, for every mutation) | `py-limits`, `py-verdicts`, `rust-door`, `rust-bridge` | +| mutations | 15 | +| caught | 15 | +| survived | 0 | +| compile-error (no evidence either way) | 0 | +| invalid-mutation | 0 | +| runner-error | 0 | +| caught without every expected catcher | none | +| honesty control `M00` (unmutated tree must pass) | survived — as required | + +| id | rule | mutation | outcome | caught by | +|---|---|---|---|---| +| M01 | §4.2 degrade, never clamp | the reference's tolerant line reader CLAMPS to the domain instead of degrading, so a finding moves to a real line the producer never named | caught | `py-limits::tolerant-degrade`
`py-verdicts::verdict-fixture` | +| M02 | §4.2 two doors | …or passes the out-of-domain value straight through, which is the behaviour the domain replaced | caught | `py-limits::tolerant-degrade`
`py-verdicts::verdict-fixture` | +| M03 | §4.2 two doors | …or REFUSES the record instead of degrading, dropping the finding entirely — which is what a raise would amount to for an embedder that catches OwnIRError around check_facts | caught | `py-limits::tolerant-degrade`
`py-verdicts::verdict-fixture` | +| M04 | §4.2 degrade to ABSENT | the degrade lands on 1 instead of 0 — a fabricated coordinate rather than 'unknown' | caught | `py-limits::tolerant-degrade`
`py-verdicts::verdict-fixture` | +| M05 | §4.1 / §4.2 column | the reference's tolerant column reader loses the domain bound and emits a column the strict door would refuse | caught | `py-limits::tolerant-degrade`
`py-verdicts::verdict-fixture` | +| M06 | §4.2 two doors | the reference's tolerant PROTOCOL door refuses instead of degrading, which drops the whole method's violations for one impossible coordinate | caught | `py-limits::tolerant-degrade` | +| M07 | §4.2 degrade, never clamp | core_line CLAMPS to the domain's top instead of degrading to absent | caught | `rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps` | +| M08 | §4.2 two doors | …or accepts the whole u32 range again, which is the core's representation deciding the contract — the reading this change exists to refuse | caught | `rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps` | +| M09 | §4.2 degrade to ABSENT | …or degrades to 1, inventing a coordinate instead of admitting it has none | caught | `rust-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps` | +| M10 | §4.2 two doors | the bridge's tolerant line reader passes an out-of-domain fact coordinate through, so a slice the reference drops is kept and anchored where nothing is | caught | `rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | +| M11 | §4.2 degrade, never clamp | …or clamps it, moving a DI site anchor onto a real line the registration never named | caught | `rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | +| M12 | §4.2 at Layer 2 | the LOWERING's line reader passes an out-of-domain coordinate through, so the Layer 2 document disagrees with the reference at the seam the cp2 evidence is taken from — invisible at Layer 3, because the AST build degrades it again on the way in | caught | `rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | +| M13 | §4.2 two doors | the Rust tolerant PROTOCOL door refuses instead of degrading | caught | `rust-door/src/lib.rs::protocol::tests::an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door` | +| M14 | BR-V9 / §4.2 | the rendered code-flow shifts every step line by one. The DROP this conversion replaced is an EQUIVALENT mutant now — §4.2 guarantees every flow-step line converts, so a filter_map that drops the unconvertible drops nothing — so the mutation is re-anchored onto the conversion itself, which is observable | caught | `rust-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte` | +| M15 | §4.1 / §4.2 column | the bridge's tolerant COLUMN reader loses the domain bound, so the port emits a column the strict door would refuse — the mirror of M05, and the one the four promoted goldens cannot reach either | caught | `rust-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | diff --git a/docs/generated/p022-cp1-census.md b/docs/generated/p022-cp1-census.md new file mode 100644 index 00000000..a77a3945 --- /dev/null +++ b/docs/generated/p022-cp1-census.md @@ -0,0 +1,58 @@ + + +# P-022 #259 checkpoint 1 — the strict-door ledger, counted + +`tests/fixtures/ownir_validation.json` is the frozen BR-D1 acceptance language, regenerated from the reference by `python tests/test_ownir_validation_fixtures.py --write` and replayed with zero Python by `own-ir/tests/validation_replay.rs`. + +**What the replay compares is accept/reject and, on a rejection, the CATEGORY — never the message text.** The reference funnels every rejection through one `OwnIRError` whose strings are a human-facing presentation aid, so byte-comparing them across two languages would freeze a debug surface as a contract. The `message` each record carries is Python's own, kept for the record rather than for the comparison. The matrix the replay asserts — agreed accepts, agreed rejects, Rust-only accepts, Rust-only rejects, kind mismatches — is a property of a green `cargo test -p own-ir`, not a number reproduced here: all three failure rows are zero or the build is red. + +| measure | value | +|--------------------------------|------:| +| controls | 294 | +| … accepted | 60 | +| … rejected | 234 | + +## By category + +Seven categories on two axes (`shape` is "no representable primitive or container form"; `location` is "a representable coordinate violating its domain rule"). The split is the one #326's census had to discover, and #259's final acceptance moved a family across it — an `i64::MAX` line was an accept, is now `location`, and `i64::MAX + 1` was and stays `shape`. + +| category | controls | what it means | +|---|---:|---| +| `accepted` | 60 | the document is accepted | +| `identity` | 22 | a name slot — empty, mistyped, or duplicated | +| `json` | 2 | the document is not JSON at all | +| `location` | 50 | a REPRESENTABLE source coordinate violating its coordinate-domain rule — the 1-based column, and the int32 line/column domain of spec/OwnIR.md §4.2 | +| `shape` | 132 | right place, but the value has no representable primitive or container form the contract requires | +| `version` | 7 | the `ownir_version` gate — type or value | +| `vocabulary` | 19 | right JSON type, value outside a closed set | +| `well_formedness` | 2 | right types, legal vocabulary, and the record still cannot mean anything | + +## By section + +The ledger's own grouping, which is BR-D1's check order. A section with acceptances and no rejections is a door nobody probed; one with rejections and no acceptance twin is a rejection nothing discriminates. + +| section | accepted | rejected | by category | +|---|---:|---:|---| +| `components` | 8 | 30 | `location` 7, `shape` 20, `vocabulary` 3 | +| `effects` | 4 | 16 | `location` 4, `shape` 12 | +| `functions` | 21 | 50 | `identity` 2, `location` 22, `shape` 25, `vocabulary` 1 | +| `json` | 0 | 1 | `json` 1 | +| `order` | 0 | 25 | `identity` 5, `json` 1, `location` 4, `shape` 9, `version` 2, `vocabulary` 4 | +| `protocol_functions` | 5 | 25 | `identity` 5, `location` 2, `shape` 13, `vocabulary` 5 | +| `protocols` | 6 | 27 | `identity` 7, `shape` 16, `vocabulary` 2, `well_formedness` 2 | +| `root` | 3 | 9 | `shape` 9 | +| `services` | 12 | 46 | `identity` 3, `location` 11, `shape` 28, `vocabulary` 4 | +| `version` | 1 | 5 | `version` 5 | + +## The coordinate family + +Every control whose document carries a `line`, `ctor_line` or `column` at any depth — the family #259's final acceptance moved, pulled out so a reviewer can find it without reading every record. Each line-bearing field is pinned at four points (`0` and `2147483647` accepted, `-1` and `2147483648` rejected), the two fields §4.2 used to record as validated nowhere carry type controls as well, and the flow-op line is pinned at every nesting shape because `then`/`else`/`body` are three separate recursion sites. + +| measure | value | +|------------------------------------|------:| +| coordinate-bearing controls | 151 | +| … accepted | 39 | +| … rejected `identity` | 1 | +| … rejected `location` | 50 | +| … rejected `shape` | 57 | +| … rejected `vocabulary` | 4 | diff --git a/docs/generated/p022-cp4-census.md b/docs/generated/p022-cp4-census.md index e0e0cf9e..26ced391 100644 --- a/docs/generated/p022-cp4-census.md +++ b/docs/generated/p022-cp4-census.md @@ -6,21 +6,20 @@ Computed by `tests/verdict_census.py` and `tests/verdict_render_census.py` (the **The surface this describes is checkpoint 5's**: the verdict replay compares EVERY `Finding` member (`message`, `related` and `flow` included) and every refusal in full, and the rendered-surface replay compares bytes. At checkpoint 4 the same ledger was compared on identity, anchor, kind and tiering only, and refusals up to their `message=` member; the counts below are the ledger's either way, which is why one fragment serves both and says which surface it means. -| measure | value | -|--------------------------------------------------------------------------------------|------:| -| goldens — Python's complete truth, one per planned case | 94 | -| … swept from `tests/fixtures/ownir` | 22 | -| … swept from `tests/fixtures/lowered` | 27 | -| … swept from `tests/fixtures/summaries` | 9 | -| … synthetic controls (`manifest.json` cases) | 36 | -| reference refusals over all goldens | 5 | -| reference findings over all goldens | 180 | -| declared Rust exclusions — the executable ledger `rust_replay_excluded` | 6 | -| … refused by `check_facts` with an error containing `outside the core's line domain` | 4 | -| … refused at the typed `OwnIr` door (#294 OD-1) | 2 | -| replayed by Rust (goldens minus exclusions) | 88 | -| … reference refusals among them (compared in full) | 5 | -| … findings among them (compared on every `Finding` member) | 174 | +| measure | value | +|-------------------------------------------------------------------------|------:| +| goldens — Python's complete truth, one per planned case | 95 | +| … swept from `tests/fixtures/ownir` | 22 | +| … swept from `tests/fixtures/lowered` | 27 | +| … swept from `tests/fixtures/summaries` | 9 | +| … synthetic controls (`manifest.json` cases) | 37 | +| reference refusals over all goldens | 5 | +| reference findings over all goldens | 182 | +| declared Rust exclusions — the executable ledger `rust_replay_excluded` | 2 | +| … refused at the typed `OwnIr` door (#294 OD-1) | 2 | +| replayed by Rust (goldens minus exclusions) | 93 | +| … reference refusals among them (compared in full) | 5 | +| … findings among them (compared on every `Finding` member) | 180 | The differential counts over the replayed set — Python-only, Rust-only, changed, ordering-only, unexplained — are asserted, not measured here: the Rust replay compares every replayed case's full ordered verdict list (or its refusal text) against the golden on every member, collects every divergence without fail-fast, and fails if one exists. A green `cargo test -p own-bridge --test verdicts` is 0 / 0 / 0 / 0 / 0 by construction; a non-zero count is a red build. diff --git a/docs/generated/p022-cp4-mutations.md b/docs/generated/p022-cp4-mutations.md index 301086e6..51f8f925 100644 --- a/docs/generated/p022-cp4-mutations.md +++ b/docs/generated/p022-cp4-mutations.md @@ -4,11 +4,11 @@ Campaign `p022-cp4` — #259 cp4 analysis wiring: the BR-V1..V8 verdict mapping in own-bridge (skip list, map-or-raise subject, anchors, tiering, dedup key member by member, stable sort), the BR-D2/L8/M1 side paths, the two declared boundaries (protocol refusal, u32 coordinate refusal) and the own-analysis subject stamping the mapping rests on. -Definition: `docs/evidence/p022-cp4-mutations.json` (sha256 `c4ca44f6bbc92cdd…`, 30 mutations). Replay on a clean tree with `python scripts/mutate_campaign.py --campaign docs/evidence/p022-cp4-mutations.json --run`; the recorded run is raw outcomes and provenance, the counts below are derived from it. +Definition: `docs/evidence/p022-cp4-mutations.json` (sha256 `1556a638a826e5e1…`, 30 mutations). Replay on a clean tree with `python scripts/mutate_campaign.py --campaign docs/evidence/p022-cp4-mutations.json --run`; the recorded run is raw outcomes and provenance, the counts below are derived from it. | measure | value | |------------------------------------------------------------|---| -| recorded at commit | `5085a5152916b1dae260781cf850733095720fe6` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | packages tested (every workspace member, `--no-fail-fast`) | `own-analysis`, `own-bridge`, `own-cfg`, `own-diagnostics`, `own-ir`, `own-lowered`, `own-shadow`, `own-syntax` | | mutations | 30 | | caught | 30 | @@ -35,13 +35,13 @@ Definition: `docs/evidence/p022-cp4-mutations.json` (sha256 `c4ca44f6bbc92cdd… | M12 | BR-V3 | BR-V3 OWN001 leak emitted without a subject (own-analysis) | caught | `own-analysis/tests/subject.rs::leak_carries_the_acquire_origin`
`own-analysis/tests/subject.rs::origin_is_inherited_across_a_move`
`own-analysis/tests/subject.rs::param_leak_carries_the_param_origin`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | | M13 | BR-V3 | BR-V3 OWN014 emitted without a subject (own-analysis) | caught | `own-analysis/tests/subject.rs::region_escape_carries_the_source_identity`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | | M14 | — | protocol family silently missing: the OBL findings are computed and then dropped. Re-anchored at #259 cp4b — the rule cp4 stated as a REFUSAL (a protocol-bearing document must never get a verdict list with a family missing) is now stated as a WIRING, and this is the same rule attacked on today's tree | caught | `own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | -| M15 | — | coordinate boundary: core_line clamps instead of refusing | caught | `own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | +| M15 | — | coordinate domain: core_line CLAMPS an out-of-domain line to the domain's top instead of degrading it to absent — re-anchored at #259 final acceptance, and its catcher moved with it: the Layer 3 replay caught this while core_line REFUSED, and the coordinate domain makes it unreachable from outside (`lower` degrades one layer earlier), so the control is now the direct one in ast.rs | caught | `own-bridge/src/lib.rs::ast::tests::core_line_degrades_the_domain_and_never_clamps` | | M16 | BR-V6 | BR-V6 source tiering inverted (static warns, injected does not) | caught | `own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | | M17 | BR-V6 | BR-V6 an empty ignore_reason suppresses | caught | `own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | | M18 | BR-V7 | BR-V7 dedup removed | caught | `own-bridge/src/lib.rs::verdict::tests::dedup_keeps_findings_that_differ_only_in_an_unobservable_key_member`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | | M19 | BR-V1 | BR-V1 ERROR-only half of the mapping predicate removed | caught | `own-bridge/src/lib.rs::verdict::tests::only_error_severity_core_verdicts_are_mapped` | | M20 | — | _as_col accepts 0 as a coordinate | caught | `own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | -| M21 | — | guarded DI site line: a negative site line becomes 1 instead of 0 | caught | `own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | +| M21 | — | the tolerant line reader degrades an out-of-domain coordinate to 1 instead of 0, so a DI site line below the domain passes the >= 1 guard and anchors where nothing is | caught | `own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | | M22 | — | OWN050 advisory never minted | caught | `own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | | M23 | BR-V3 | BR-V3 handle recovered from the wrong subject separator | caught | `own-bridge/src/lib.rs::verdict::tests::a_flow_local_code_without_a_wording_keeps_the_core_message`
`own-bridge/src/lib.rs::verdict::tests::every_di_lifetime_phrase_is_pinned_including_the_unreachable_two`
`own-bridge/src/lib.rs::verdict::tests::only_error_severity_core_verdicts_are_mapped`
`own-bridge/src/lib.rs::verdict::tests::the_capture_route_names_a_non_static_source_it_can_never_be_handed`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | | M24 | — | OWN051 line taken from the callee record instead of the call | caught | `own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | diff --git a/docs/generated/p022-cp4b-mutations.md b/docs/generated/p022-cp4b-mutations.md index ea42a6af..a6bce21c 100644 --- a/docs/generated/p022-cp4b-mutations.md +++ b/docs/generated/p022-cp4b-mutations.md @@ -12,7 +12,7 @@ Definition: `docs/evidence/p022-cp4b-1.json` (sha256 `ca44ead168221404…`, 33 m | measure | value | |------------------------------------------------------------|---| -| recorded at commit | `c7836138bd2fe3b9cfea458a040ac5dda05e890e` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | packages tested (every workspace member, `--no-fail-fast`) | `own-analysis`, `own-bridge`, `own-cfg`, `own-diagnostics`, `own-ir`, `own-lowered`, `own-shadow`, `own-syntax` | | mutations | 33 | | caught | 33 | @@ -25,36 +25,36 @@ Definition: `docs/evidence/p022-cp4b-1.json` (sha256 `ca44ead168221404…`, 33 m | id | rule | mutation | outcome | caught by | |---|---|---|---|---| -| M01 | OwnIR §8 lattice | the merge drops the other path's states: a union becomes 'keep the left' | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | +| M01 | OwnIR §8 lattice | the merge drops the other path's states: a union becomes 'keep the left' | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | | M02 | OwnIR §8 lattice | open provenance joins by the LATEST line instead of the earliest | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M03 | OwnIR §8 lattice | the state union intersects OPEN instead of joining it | caught | `own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M04 | OwnIR §8 definite/maybe | definite reads 'OPEN is possible' instead of 'OPEN on every path' | caught | `own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M05 | obligations._leaf | a re-open moves the provenance forward instead of keeping the earliest site | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | +| M03 | OwnIR §8 lattice | the state union intersects OPEN instead of joining it | caught | `own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | +| M04 | OwnIR §8 definite/maybe | definite reads 'OPEN is possible' instead of 'OPEN on every path' | caught | `own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | +| M05 | obligations._leaf | a re-open moves the provenance forward instead of keeping the earliest site | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte` | | M06 | obligations._leaf (allow beats barrier) | the allow list no longer exempts an event from the barrier check | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M07 | OwnIR §8 precision policy | an opaque write DISCHARGES definitely instead of adding CLOSED to the set | caught | `own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M08 | OwnIR §8 precision policy | an opaque write to an UNTRACKED member discharges the obligation | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M09 | obligations._exit | exit_barriers is ignored: return/throw always cross | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M10 | OwnIR §8 anchor-at-open (the OWN001 precedent) | an end-of-method leak anchors at 0 instead of at the open site | caught | `own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | +| M10 | OwnIR §8 anchor-at-open (the OWN001 precedent) | an end-of-method leak anchors at 0 instead of at the open site | caught | `own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | | M11 | obligations.walk_seq | a sequence keeps walking past an event that left the method | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M12 | obligations.walk (if) | ONE dead arm ends the method instead of both | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M13 | obligations.walk (if) | the merge keeps only the then-arm instead of joining both | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | +| M12 | obligations.walk (if) | ONE dead arm ends the method instead of both | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte` | +| M13 | obligations.walk (if) | the merge keeps only the then-arm instead of joining both | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden` | | M14 | obligations.walk (while) — zero iterations | the fixpoint forgets the header, so a loop can no longer run zero times | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M15 | obligations.walk (while) — two-phase emission | the fixpoint iterations emit, so a barrier in a loop reports more than once | caught | `own-analysis/src/lib.rs::obligation::tests::a_barrier_in_a_loop_reports_exactly_once`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M16 | obligations._emit (the silent phase) | the silent phase does not suppress emission, so a loop's fixpoint iterations report. Attacks `_emit`'s guard rather than the redundant one around the emitting pass: with `_emit` guarded, removing `if !self.silent` around `walk_seq` is an EQUIVALENT mutant (measured — it survived), because a nested pass under an enclosing silent phase emits nothing either way | caught | `own-analysis/src/lib.rs::obligation::tests::a_barrier_in_a_loop_reports_exactly_once`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M17 | obligations._close_lines | close lines inside a loop body are not collected as evidence | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M18 | obligations.check_protocols (late close) | the late close is 'at or after' the barrier instead of strictly after | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M19 | obligations.check_protocols (late close) | the late-close hop is attached to exit leaks instead of barrier crossings | caught | `own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | +| M19 | obligations.check_protocols (late close) | the late-close hop is attached to exit leaks instead of barrier crossings | caught | `own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | | M20 | obligations.check_protocols (sort key) | violations sort by line before file | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M21 | obligations.check_protocols (sort key) | the protocol tiebreak is dropped from the sort key | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M22 | obligations.check_protocols (sort key) | the barrier-description tiebreak is dropped from the sort key | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M23 | obligations.unmatched_scopes | an UNSCOPED protocol is reported as a dead rule | caught | `own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | +| M23 | obligations.unmatched_scopes | an UNSCOPED protocol is reported as a dead rule | caught | `own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/src/lib.rs::verdict::tests::protocol_blocks_that_are_not_lists_yield_nothing` | | M24 | OwnIR §8 scope (suffix match) | a scope entry matches anywhere in the method name instead of as a suffix | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value` | -| M25 | OwnIR §8 scope (empty = every method) | an empty scope is inverted: a scoped protocol applies everywhere | caught | `own-analysis/src/lib.rs::obligation::tests::a_barrier_in_a_loop_reports_exactly_once`
`own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not`
`own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value` | +| M25 | OwnIR §8 scope (empty = every method) | an empty scope is inverted: a scoped protocol applies everywhere | caught | `own-analysis/src/lib.rs::obligation::tests::a_barrier_in_a_loop_reports_exactly_once`
`own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not`
`own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/src/lib.rs::verdict::tests::protocol_blocks_that_are_not_lists_yield_nothing`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value` | | M26 | OwnIR §8 tracked flags | tracks_target ignores the member name, so every assign target is tracked | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value` | | M27 | OwnIR §8 precision policy (unknown argument) | a call with an UNKNOWN argument matches an args-narrowed matcher | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-ir/src/lib.rs::protocol::tests::a_narrowed_call_matcher_never_matches_an_unknown_argument` | -| M28 | OwnIR §8 matcher (assign value narrowing) | an assign matcher ignores the written value, so a close also opens | caught | `own-analysis/src/lib.rs::obligation::tests::a_barrier_in_a_loop_reports_exactly_once`
`own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/src/lib.rs::obligation::tests::close_before_the_barrier_is_clean`
`own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | -| M29 | OwnIR §8 grammar (exit_barriers default) | an absent exit_barriers defaults to false instead of true | caught | `own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not`
`own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value`
`own-ir/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | -| M30 | OwnIR §8 grammar (scope construction) | a validated scope entry is checked and then dropped, so every scope is empty | caught | `own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value` | +| M28 | OwnIR §8 matcher (assign value narrowing) | an assign matcher ignores the written value, so a close also opens | caught | `own-analysis/src/lib.rs::obligation::tests::a_barrier_in_a_loop_reports_exactly_once`
`own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/src/lib.rs::obligation::tests::an_opaque_write_may_discharge_but_never_opens`
`own-analysis/src/lib.rs::obligation::tests::close_before_the_barrier_is_clean`
`own-analysis/src/lib.rs::obligation::tests::open_barrier_close_is_one_definite_crossing`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | +| M29 | OwnIR §8 grammar (exit_barriers default) | an absent exit_barriers defaults to false instead of true | caught | `own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not`
`own-analysis/src/lib.rs::obligation::tests::an_end_of_method_leak_anchors_at_the_open`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/src/lib.rs::verdict::tests::protocol_blocks_that_are_not_lists_yield_nothing`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value`
`own-ir/tests/validation_replay.rs::the_two_loaders_accept_the_same_language` | +| M30 | OwnIR §8 grammar (scope construction) | a validated scope entry is checked and then dropped, so every scope is empty | caught | `own-analysis/src/lib.rs::obligation::tests::a_dead_scope_is_reported_and_an_unscoped_protocol_is_not`
`own-analysis/tests/obligation_parity.rs::obligation_fact_parity`
`own-bridge/src/lib.rs::verdict::tests::protocol_blocks_that_are_not_lists_yield_nothing`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-ir/src/lib.rs::protocol::tests::a_protocol_record_becomes_its_typed_value` | | M31 | OwnIR §8 grammar (file default) | an absent protocol-function 'file' defaults to the empty string, not '?' | caught | `own-ir/src/lib.rs::protocol::tests::a_method_record_becomes_its_event_tree` | | M32 | P-022 discipline 2 (the control is the thing under test) | corrupt the frozen oracle: the anchor of the leak whose open carries no line moves off 0. The replay must fail, or it is not reading the reference's answer | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | | M33 | P-022 discipline 2 (the control is the thing under test) | corrupt the frozen oracle's dead-rule list. The replay must fail, or the unmatched_scopes half is carried without being compared | caught | `own-analysis/tests/obligation_parity.rs::obligation_fact_parity` | @@ -63,11 +63,11 @@ Definition: `docs/evidence/p022-cp4b-1.json` (sha256 `ca44ead168221404…`, 33 m Campaign `p022-cp4b-2` — #259 cp4b.2, the BRIDGE half of the obligation family: the (kind, definite) -> OBL001-004 table, all four line-free wordings and both exit_desc branches, the component/handler derivations, every step of the opened -> barrier (or exit) -> late-close slice with its line guard, the anchorless advisory OBL005 with its sorted CPython-repr scope list, and the three tolerant-door rules (skip a malformed entry whole, first-wins on a duplicate name, a non-list block silences the family). Three mutations attack the CONTROLS rather than the port: two corrupt the frozen Layer 3 and rendered oracles and require their replays to fail, and one re-declares the exclusion this checkpoint promoted — a promotion that were only announced in prose would survive it. -Definition: `docs/evidence/p022-cp4b-2.json` (sha256 `602633faf9b1ddaa…`, 28 mutations). Replay on a clean tree with `python scripts/mutate_campaign.py --campaign docs/evidence/p022-cp4b-2.json --run`; the recorded run is raw outcomes and provenance, the counts below are derived from it. +Definition: `docs/evidence/p022-cp4b-2.json` (sha256 `41f919af130e1471…`, 28 mutations). Replay on a clean tree with `python scripts/mutate_campaign.py --campaign docs/evidence/p022-cp4b-2.json --run`; the recorded run is raw outcomes and provenance, the counts below are derived from it. | measure | value | |------------------------------------------------------------|---| -| recorded at commit | `8c9a2f6b2d63ba4ccb6a144b91dc9a5bdc817feb` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | packages tested (every workspace member, `--no-fail-fast`) | `own-analysis`, `own-bridge`, `own-cfg`, `own-diagnostics`, `own-ir`, `own-lowered`, `own-shadow`, `own-syntax` | | mutations | 28 | | caught | 28 | diff --git a/docs/generated/p022-cp5-inventory.md b/docs/generated/p022-cp5-inventory.md index 9190d85b..4e461122 100644 --- a/docs/generated/p022-cp5-inventory.md +++ b/docs/generated/p022-cp5-inventory.md @@ -12,7 +12,7 @@ Checkpoint 4 proved identity, anchor, kind and tiering over the replayed set ([c | ledger row | surface | what it is | all goldens | replayed | |---|---|---|---:|---:| -| `flowlocal_own001_never` | bridge | flow-local OWN001, never released | 27 | 25 | +| `flowlocal_own001_never` | bridge | flow-local OWN001, never released | 27 | 27 | | `flowlocal_own001_partial` | bridge | flow-local OWN001, released on some path | 10 | 10 | | `flowlocal_own001_pool_partial` | bridge | flow-local OWN001 on a pooled buffer, returned on some path | 2 | 2 | | `flowlocal_own002` | bridge | flow-local use-after-dispose | 6 | 6 | @@ -24,7 +24,7 @@ Checkpoint 4 proved identity, anchor, kind and tiering over the replayed set ([c | `flowlocal_fallback` | core-diagnostic | flow-local fallback: the core message, verbatim — **not replayed**: unreachable from any facts document (the nine-op flow vocabulary raises only codes that HAVE a wording); pinned by a `verdict::tests` control driven through `map_core`, whose expected text is read from the recorded probe `tests/fixtures/unreachable_branches.json`. The core message it interpolates is checkpoint 5.2's | 0 | 0 | | `flowlocal_fallback_pool` | core-diagnostic | flow-local pooled fallback: the core message, verbatim — **not replayed**: unreachable from any facts document (the nine-op flow vocabulary raises only codes that HAVE a wording); pinned by a `verdict::tests` control driven through `map_core`, whose expected text is read from the recorded probe `tests/fixtures/unreachable_branches.json`. The core message it interpolates is checkpoint 5.2's | 0 | 0 | | `own025_view` | bridge | OWN025 pooled-view wording | 2 | 2 | -| `own014_di_singleton` | bridge | OWN014 captive, source registered singleton | 4 | 4 | +| `own014_di_singleton` | bridge | OWN014 captive, source registered singleton | 6 | 6 | | `own014_di_scoped` | bridge | OWN014 captive, source registered scoped | 1 | 1 | | `own014_di_transient` | bridge | OWN014 captive, source registered transient — **not replayed**: unreachable from any facts document: `transient` is the shortest region, so no subscriber it could outlive exists, and an unrecognised lifetime never reaches `di_source_life` (the DI life map admits only the three). Pinned by `verdict::tests::every_di_lifetime_phrase_is_pinned_including_the_unreachable_two`, whose expected text is not written in the test: it is read from the recorded probe `tests/fixtures/unreachable_branches.json` | 0 | 0 | | `own014_di_unknown_life` | bridge | OWN014 captive, source lifetime outside the three known ones — **not replayed**: unreachable from any facts document: `transient` is the shortest region, so no subscriber it could outlive exists, and an unrecognised lifetime never reaches `di_source_life` (the DI life map admits only the three). Pinned by `verdict::tests::every_di_lifetime_phrase_is_pinned_including_the_unreachable_two`, whose expected text is not written in the test: it is read from the recorded probe `tests/fixtures/unreachable_branches.json` | 0 | 0 | @@ -47,12 +47,12 @@ Checkpoint 4 proved identity, anchor, kind and tiering over the replayed set ([c | `advisory_own050` | bridge | OWN050 unresolved-reference note | 5 | 5 | | `advisory_own051` | bridge | OWN051 unverified-transfer note | 6 | 6 | | `advisory_own052` | bridge | OWN052 degraded-inference note | 1 | 1 | -| `di001_message` | core-analysis | DI001 captive message (di.py) | 12 | 10 | +| `di001_message` | core-analysis | DI001 captive message (di.py) | 12 | 11 | | `di002_message` | core-analysis | DI002 weak-captive message (di.py) | 2 | 2 | | `di003_message` | core-analysis | DI003 captured-transient message (di.py) | 1 | 1 | | `di004_message` | core-analysis | DI004 root-resolution message (di.py) | 7 | 7 | | `di005_message` | core-analysis | DI005 scope-cache message (di.py) | 5 | 5 | -| `eff001_message` | core-analysis | EFF001 storm message (effects.py) | 8 | 6 | +| `eff001_message` | core-analysis | EFF001 storm message (effects.py) | 8 | 7 | | `obl001_barrier_definite` | bridge-protocol | OBL001: a barrier fires while the obligation is open on every path | 4 | 4 | | `obl002_barrier_maybe` | bridge-protocol | OBL002: a barrier fires while it is open on some path | 1 | 1 | | `obl003_exit_definite` | bridge-protocol | OBL003: the method leaves while the obligation is open on every path | 8 | 8 | @@ -67,7 +67,7 @@ Each is its own degradation rule inside an analysis message — the tail is drop |---|---|---|---:|---:| | `tail_consumed_typed` | wording tail | ` [consumed by the '' constructor at f:l]` | 4 | 4 | | `tail_consumed_bare` | wording tail | ` [consumed by the constructor at f:l]` (impl type unknown) | 2 | 2 | -| `tail_consumed_absent` | wording tail | no consuming-constructor tail (location unknown) | 9 | 7 | +| `tail_consumed_absent` | wording tail | no consuming-constructor tail (location unknown) | 9 | 8 | | `tail_registered` | wording tail | ` [singleton registered at f:l]` on DI004/DI005 | 7 | 7 | | `tail_registered_absent` | wording tail | no registration tail (the primary IS the registration) | 5 | 5 | | `tail_obl_exit_falls_off` | wording tail | `the method falls off the end` — a leak off the end has no exit site to name | 3 | 3 | @@ -79,14 +79,14 @@ One row per `related`/`flow` family; a slice matching no family (or two) fails t | ledger row | surface | what it is | all goldens | replayed | |---|---|---|---:|---:| -| `di_path_1` | flow | DI retention path, one resolvable hop (the rest dropped) | 2 | 1 | +| `di_path_1` | flow | DI retention path, one resolvable hop (the rest dropped) | 2 | 2 | | `di_path_2` | flow | DI retention path, captor → captured | 20 | 19 | | `di_path_3plus` | flow | DI retention path with `via` hops | 4 | 4 | | `di_consumer_related_typed` | related | DI consuming constructor, impl type known | 4 | 4 | | `di_consumer_related_bare` | related | DI consuming constructor, impl type unknown | 2 | 2 | | `di004_registration_related` | related | DI004 registration beside the call site | 3 | 3 | | `di005_registration_related` | related | DI005 registration beside the store site | 4 | 4 | -| `capture_escape_flow` | flow | OWN014 subscribe site → source registration site | 4 | 4 | +| `capture_escape_flow` | flow | OWN014 subscribe site → source registration site | 5 | 5 | | `effect_flow` | flow | EFF001 re-run site → identity-mint site | 5 | 4 | | `flowlocal_flow_own002` | flow | flow-local OWN002 origin → violation | 4 | 4 | | `flowlocal_flow_own003` | flow | flow-local OWN003 origin → violation | 1 | 1 | @@ -107,13 +107,13 @@ The rules that produce an EMPTY slice: a step whose line is unknown is omitted, | ledger row | surface | what it is | all goldens | replayed | |---|---|---|---:|---:| -| `di_consumer_related_dropped` | related | DI001/2/3 with no consuming-constructor related (line < 1) | 9 | 7 | +| `di_consumer_related_dropped` | related | DI001/2/3 with no consuming-constructor related (line < 1) | 9 | 8 | | `di004_related_dropped` | related | DI004 with no registration related (the primary IS the registration) | 4 | 4 | | `di005_related_dropped` | related | DI005 with no registration related (the primary IS the registration) | 1 | 1 | -| `capture_escape_flow_dropped` | flow | DI-sourced OWN014 with no escape slice (source registration unknown → < 2 steps) | 2 | 2 | +| `capture_escape_flow_dropped` | flow | DI-sourced OWN014 with no escape slice (source registration unknown → < 2 steps) | 3 | 3 | | `capture_flow_absent` | flow | OWN014 from the capture route: no escape slice by design (only the DI-sourced branch builds one) | 6 | 6 | -| `effect_flow_dropped` | flow | EFF001 with no slice (a re-run or mint line < 1) | 3 | 2 | -| `flowlocal_flow_absent` | flow | OWN001 on a local/pooled record: a single-point finding, no slice by design | 45 | 43 | +| `effect_flow_dropped` | flow | EFF001 with no slice (a re-run or mint line < 1) | 3 | 3 | +| `flowlocal_flow_absent` | flow | OWN001 on a local/pooled record: a single-point finding, no slice by design | 45 | 45 | | `protocol_flow_absent` | flow | an OBL finding with no slice at all: the anchorless OBL005 by design, and a leak whose only step has an unknown line | 4 | 4 | ## BR-V9 — rendered surfaces diff --git a/docs/generated/p022-cp5-mutations.md b/docs/generated/p022-cp5-mutations.md index 233b71b9..f8584a3d 100644 --- a/docs/generated/p022-cp5-mutations.md +++ b/docs/generated/p022-cp5-mutations.md @@ -12,7 +12,7 @@ Definition: `docs/evidence/p022-cp5-1.json` (sha256 `2b4651e54f2862f5…`, 42 mu | measure | value | |------------------------------------------------------------|---| -| recorded at commit | `cdff105ca43ec5b1ced2bd25a8216f4f7b65deb3` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | packages tested (every workspace member, `--no-fail-fast`) | `own-analysis`, `own-bridge`, `own-cfg`, `own-diagnostics`, `own-ir`, `own-lowered`, `own-shadow`, `own-syntax` | | mutations | 42 | | caught | 42 | @@ -76,7 +76,7 @@ Definition: `docs/evidence/p022-cp5-2.json` (sha256 `8641e3ff34598123…`, 10 mu | measure | value | |------------------------------------------------------------|---| -| recorded at commit | `2a7ff6d4ab2877b930f79f09bc6f55bfbffd094e` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | packages tested (every workspace member, `--no-fail-fast`) | `own-analysis`, `own-bridge`, `own-cfg`, `own-diagnostics`, `own-ir`, `own-lowered`, `own-shadow`, `own-syntax` | | mutations | 10 | | caught | 10 | @@ -93,7 +93,7 @@ Definition: `docs/evidence/p022-cp5-2.json` (sha256 `8641e3ff34598123…`, 10 mu | M02 | BR-V3 | the resolver emits a message-less diagnostic again (the title stands in) | caught | `own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | | M03 | BR-V3 | own-analysis ignores the message own-cfg carries and substitutes the title | caught | `own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | | M04 | BR-V3 | py_repr always single-quotes (cp4's placeholder, the bug cp5.2 found) | caught | `own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | -| M05 | BR-V3 | py_repr's quote switch inverted (double unless a quote is present) | caught | `own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | +| M05 | BR-V3 | py_repr's quote switch inverted (double unless a quote is present) | caught | `own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch`
`own-bridge/tests/renders.rs::replays_every_rendered_surface_byte_for_byte`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | | M06 | BR-V3 | py_repr leaves a backslash unescaped | caught | `own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch` | | M07 | BR-V3 | py_repr leaves an ASCII control character raw | caught | `own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch` | | M08 | BR-V3 | py_repr renders an absent subject as an empty string, not None | caught | `own-bridge/src/lib.rs::verdict::tests::py_repr_matches_cpython_including_the_quote_switch`
`own-bridge/tests/verdicts.rs::replays_every_case_to_its_golden`
`own-shadow/tests/engine.rs::this_engine_reproduces_its_committed_capture` | @@ -108,7 +108,7 @@ Definition: `docs/evidence/p022-cp5-3.json` (sha256 `6b064c3d76eae07a…`, 29 mu | measure | value | |------------------------------------------------------------|---| -| recorded at commit | `ceb8521557128bddca96ea6addb817231fde4f56` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | packages tested (every workspace member, `--no-fail-fast`) | `own-analysis`, `own-bridge`, `own-cfg`, `own-diagnostics`, `own-ir`, `own-lowered`, `own-shadow`, `own-syntax` | | mutations | 29 | | caught | 29 | diff --git a/docs/generated/p022-shadow-census.md b/docs/generated/p022-shadow-census.md index 3522671e..5d6ecb40 100644 --- a/docs/generated/p022-shadow-census.md +++ b/docs/generated/p022-shadow-census.md @@ -24,8 +24,8 @@ which also states the byte-level boundary repeated in the unmeasured set below. | `tests/fixtures/ownir` | 22 | | `tests/fixtures/repro` | 3 | | `tests/fixtures/summaries` | 9 | -| `tests/fixtures/verdicts` | 36 | -| **total** | **97** | +| `tests/fixtures/verdicts` | 37 | +| **total** | **98** | Every one of those documents is canonicalized and hashed by the reference (`ownlang/repro.py`) and re-hashed from the same file by the port @@ -36,8 +36,8 @@ the difference is named in the unmeasured set below. | surface | count | |---|---| -| documents captured and digest-pinned | 97 | -| tamper controls (one changed character per document, refusal required) | 97 | +| documents captured and digest-pinned | 98 | +| tamper controls (one changed character per document, refusal required) | 98 | | documents both engines must REFUSE to name (`domain_refusals`) | 6 | | reproduction artifacts committed and replayed byte-for-byte | 9 | | structural negative controls on `verify` (each side) | 18 | @@ -102,7 +102,8 @@ hid the second would delete the defect the layer exists to expose. The reducer walks the pair in pipeline order over **['lowered', 'summaries']** and names the first place they part company: the layer, the step address and the *minimal* difference inside it. The `verdicts` layer is **refused, not skipped** — -comparing final diagnostics is #260's acceptance, blocked by #259 — and the +comparing final diagnostics is #260's own acceptance, and crossing that line is +its decision to take (#259's final acceptance is reached) — and the refusal is carried in every reduction, so "not compared" can never be read as "compared and agreed". @@ -164,8 +165,10 @@ non-zero counter there is not representable as a passing build. The gates: Acceptance must therefore prove the byte-level invariant separately; until it does, "same input" here means canonical identity and nothing stronger ([owner decision B-1](../notes/p022-shadow-infra-owner-decisions.md)). -- **End diagnostics compared as an acceptance surface** — #260's acceptance, - blocked by #259 (cp5 and 4b). Not attempted, not approximated. +- **End diagnostics compared as an acceptance surface** — #260's acceptance. + It was blocked by #259 while cp5, 4b and the coordinate-domain contract were + open; all three have landed, so what remains is this step's own decision to + cross the line. Not attempted, not approximated. - **The verdict layer.** Refused by the reducer, and recorded as refused in every reduction. This is the same blocker as the row above, stated where a tool could otherwise have quietly crossed it. diff --git a/docs/generated/p022-shadow-mutations.md b/docs/generated/p022-shadow-mutations.md index 31f4e717..368662c3 100644 --- a/docs/generated/p022-shadow-mutations.md +++ b/docs/generated/p022-shadow-mutations.md @@ -12,7 +12,7 @@ Definition: `docs/evidence/p022-shadow-cp1.json` (sha256 `a5df997a1d84effc…`, | measure | value | |--------------------------------------------------|---| -| recorded at commit | `0cdbd0f4410bb5ee4a418337f567e515f4146b3b` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | layers run (every one, for every mutation) | `python`, `rust`, `rust-unit` | | mutations | 30 | | caught | 30 | @@ -64,7 +64,7 @@ Definition: `docs/evidence/p022-shadow-cp2.json` (sha256 `bd3fd24d65f53997…`, | measure | value | |--------------------------------------------------|---| -| recorded at commit | `d726b06192c5fd76c94b02fab1011f55a453c14c` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | layers run (every one, for every mutation) | `python`, `rust-repro`, `rust-engine`, `rust-unit` | | mutations | 11 | | caught | 11 | @@ -97,7 +97,7 @@ Definition: `docs/evidence/p022-shadow-cp3.json` (sha256 `99e99aacfe3f4ac4…`, | measure | value | |--------------------------------------------------|---| -| recorded at commit | `001f6fd3be4e73f4d3878dc430dd4507a90ca273` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | layers run (every one, for every mutation) | `python`, `rust-repro`, `rust-engine`, `rust-trace`, `rust-unit` | | mutations | 11 | | caught | 11 | @@ -126,11 +126,11 @@ Definition: `docs/evidence/p022-shadow-cp3.json` (sha256 `99e99aacfe3f4ac4…`, Campaign `p022-shadow-cp4` — The mutation campaign for P-022 step 7a checkpoint 4 (first-divergence reduction over the lowered and MOS layers). Separate from cp1/cp2/cp3 on purpose — each checkpoint's evidence stays frozen at what it measured. Every mutation edits a PRODUCTION surface — ownlang/repro.py or rust/crates/own-shadow/src/ — never a test. Five layers run for every mutation (discipline 3: no fail-fast), including the crate's unit tests. M00 is the harness-honesty control. -Definition: `docs/evidence/p022-shadow-cp4.json` (sha256 `ad87275b58dc7065…`, 11 mutations). Replay on a clean tree with `python scripts/mutate_campaign.py --campaign docs/evidence/p022-shadow-cp4.json --run`; the recorded run is raw outcomes and provenance, the counts below are derived from it. +Definition: `docs/evidence/p022-shadow-cp4.json` (sha256 `73cdde6a3c5ad520…`, 11 mutations). Replay on a clean tree with `python scripts/mutate_campaign.py --campaign docs/evidence/p022-shadow-cp4.json --run`; the recorded run is raw outcomes and provenance, the counts below are derived from it. | measure | value | |--------------------------------------------------|---| -| recorded at commit | `001f6fd3be4e73f4d3878dc430dd4507a90ca273` | +| recorded at commit | `3af8ead18d423d9de760ecbf180ed5ca0ef68dad` | | layers run (every one, for every mutation) | `python`, `rust-repro`, `rust-trace`, `rust-reduce`, `rust-unit` | | mutations | 11 | | caught | 11 | @@ -151,6 +151,6 @@ Definition: `docs/evidence/p022-shadow-cp4.json` (sha256 `ad87275b58dc7065…`, | M58 | reduction: the reducer names the FIRST divergence in pipeline order | the reference reports the LAST divergence instead of the first | caught | `python::reduction-golden` | | M59 | reduction: object key order is significant — the surfaces fix field order byte-exactly | the reference stops distinguishing a key-ORDER difference from agreement | caught | `python::reduction-control` | | M60 | capture: each layer document is carried in the key order its OWN surface fixes | the reference carries the MOS document in `dump_summaries`' insertion order rather than its surface's | caught | `python::artifact-golden` | -| M61 | reduction: the scope is a contract, not a parameter | the port widens the reduction scope to the verdict layer | caught | `rust-reduce/tests/reduce.rs::every_reduction_golden_is_reproduced_byte_for_byte`
`rust-reduce/tests/reduce.rs::the_reducer_is_silent_on_unchanged_data_and_names_a_synthetic_divergence`
`rust-reduce/tests/reduce.rs::the_verdict_layer_is_refused_not_silently_skipped`
`rust-reduce/tests/reduce.rs::two_engines_that_both_refused_a_layer_agree` | +| M61 | reduction: the scope is a contract, not a parameter | the port widens the reduction scope to the verdict layer. Two further catchers were declared when this campaign was recorded and stopped being catchers on main, before #259's final acceptance: `the_reducer_is_silent_on_unchanged_data...` and `two_engines_that_both_refused_a_layer_agree` only failed while some committed artifact still had ASYMMETRIC verdict-layer statuses, and checkpoint 4b's promotion removed the last of them. Measured at 834f295 with the mutation applied, not inferred. The two that remain are the load-bearing ones — the scope is still pinned by a golden and by the refusal control | caught | `rust-reduce/tests/reduce.rs::every_reduction_golden_is_reproduced_byte_for_byte`
`rust-reduce/tests/reduce.rs::the_verdict_layer_is_refused_not_silently_skipped` | | M62 | reduction: a refusal's text is each engine's own | the port compares two engines' refusal texts instead of the fact that both refused | caught | `rust-reduce/tests/reduce.rs::two_engines_that_both_refused_a_layer_agree` | | M63 | reduction: the reducer names the FIRST divergence in pipeline order | the port reports the last divergence instead of the first | caught | `rust-reduce/tests/reduce.rs::every_reduction_golden_is_reproduced_byte_for_byte` | diff --git a/docs/notes/p022-bridge-verdict-final-acceptance.md b/docs/notes/p022-bridge-verdict-final-acceptance.md new file mode 100644 index 00000000..5ebcf9f2 --- /dev/null +++ b/docs/notes/p022-bridge-verdict-final-acceptance.md @@ -0,0 +1,441 @@ +# P-022 step 6b (#259) — final acceptance: the coordinate-domain contract + +> Status: **in execution.** final.0 the inventory (this note's §1–§2), final.1 +> the Python-first contract change, final.2 the Rust mirror and the promotion +> of the four boundary controls, final.3 the campaigns and the status surfaces. +> This note is both the plan and the completeness ledger. Counts are not typed +> here — they live in the generated fragments +> ([coordinate census](../generated/p022-coord-census.md), +> [cp1 census](../generated/p022-cp1-census.md), +> [Layer 3 census](../generated/p022-cp4-census.md), +> [campaigns](../generated/p022-coord-mutations.md)) and the prose links. + +Checkpoints 1–5 and 4b took `own_bridge::check_facts` to Layer 3 parity over +the measured set, at the full `Finding` and at the rendered surfaces. What #259 +still owed was one **contract decision**, not one more port: `spec/OwnIR.md` +§4.2 bounds a source coordinate at signed 64 bits, the core's AST holds a line +as `u32`, and the four `verdict_boundary_*` controls sat in the verdict +ledger's `rust_replay_excluded` because of the gap between those two numbers. + +The decision is the owner's and is recorded in §0. This note measures the tree +against it **before** anything moves, because the previous contract change of +this shape (#326) established the order: a change to what the reference accepts +lands Python-first, and the census that judges it is taken against the tree +rather than against the change. + +## 0. The decision (owner), and what it is not + +**D1 — the domain.** Every `line` in an OwnIR document is an integer in +`[0, 2147483647]`. `0` means "unknown / file-level" and stays legal: it is the +reference's own default for an absent line (`s.get("line", 0)` and its +siblings throughout `load()`), and the corpus carries it in the goldens as well +as the inputs (§2). A negative line is rejected. A line above `2^31 - 1` is +rejected. Every `column` is in `[1, 2147483647]`, or absent, or `null`. + +The rationale, in these terms and no others: + +* **int32 is the line type of every consumer this project feeds.** Roslyn's + `LinePosition.Line` is an `int`; LSP's `uinteger` is capped at `2^31 - 1`; + .NET diagnostics carry the same width. A coordinate wider than that cannot + reach the place it points at. +* **Zero is the reference's own absent sentinel**, so bounding the domain below + at `0` rather than at `1` is reading the reference, not tightening it. +* **No producer emits a negative coordinate.** + `frontend/roslyn/OwnSharp.Extractor/Program.cs` writes + `StartLinePosition.Line + 1`, i.e. a 1-based line off a 0-based Roslyn + position; the census in §2 finds negatives only in controls written to be + negative. + +**Never** "Rust holds `u32`, so the reference is wrong". That sentence is +banned from every surface this change touches. The representation of one +consumer is not an argument about a contract; the set of consumers the contract +must serve is. + +**D2 — where it is enforced.** The strict door (`ownlang.ownir.load()`) rejects +an out-of-domain coordinate with category **Location** — the cp1 taxonomy's "a +representable coordinate violating its domain rule" — for **every** +line-bearing field, including the two §4.2 records as unchecked today: +`components[].subscriptions[].line` and the `line` on a flow op inside +`functions[].body` (recursing through `then` / `else` / `body`, exactly as +`_check_column` already does). Those two fields also gain **type** validation: +a string or a bool there is a Shape rejection, as it is everywhere else. §4.2's +deferred question closes in the same move. + +**D3 — the tolerant door.** `check_facts()` on un-validated facts **degrades** +an out-of-domain line to `0` (absent) — exactly as `_as_col` degrades a bad +column to `None`. It never raises, never clamps to a neighbour, never invents. +The Rust bridge does the same where it refuses today +(`own_bridge::ast::core_line` and its callers in `verdict.rs`). + +The consequence is the point of the whole change: the four `verdict_boundary_*` +controls leave `rust_replay_excluded`, and the exclusion ledger is left naming +only the two #294 OD-1 door controls — a declared boundary, measured, not open +work. + +**What this is not.** Not a re-litigation of #326: signed-64 stays the +*representable integer form* an OwnIR coordinate may take, and the domain is a +rule about which of those values mean anything. The two are different axes and +the cp1 taxonomy already separates them (§2.4). Not a change to any severity, +any message matrix, any analysis. Not #294 OD-1, not the DI004/DI005 +rendered-surface case, not the BR-V5 protocol-path sentence, not `own-shadow`'s +Layer 3 boundary. + +## 1. The inventory: every coordinate slot, on both doors, in both languages + +### 1.1 Lines + +| slot | strict door (`load()`) today | tolerant door (`check_facts`) today | Rust typed model | Rust `u32` conversion | +|---|---|---|---|---| +| `services[].line` | type + §4.2 range (`_check_int_range`) | `_as_int` | `Option` | `verdict::anchor_line` → `ast::core_line` | +| `services[].ctor_line` | type + §4.2 range | `_as_int` | `Option` | `verdict::guarded_line` → `ast::core_line` | +| `services[].root_resolve_sites[].line` | type (inside the `all(...)`) + §4.2 range | `_as_int` | `Option` (`Site`) | `verdict::guarded_line` → `ast::core_line` | +| `services[].scope_cache_sites[].line` | type + §4.2 range | `_as_int` | `Option` (`Site`) | `verdict::guarded_line` | +| `effects[].line` | type + §4.2 range | `_as_int` | `Option` | `verdict::anchor_line` | +| `effects[].bindings[].line` | type + §4.2 range | `_as_int` | `Option` | `verdict::guarded_line` | +| `functions[].params[].line` | type + §4.2 range | `_as_int` | `Option` (`Param`) | `ast::param` → `ast::core_line` | +| `protocol_functions[].events[].line` | type + §4.2 range, through `obligations._opt_line` | the same parser, per-entry skip on failure | typed by `own_ir::protocol` | the analysis owns its own anchor | +| `components[].subscriptions[].line` | **nothing** — not range, not type | `_as_int` | untyped: `Subscription.extra` (a raw `Value`) | `verdict` reads it as an `i64` anchor | +| `functions[].body[]…line` (flow op, at every nesting shape) | **nothing** | `_as_int` | untyped: `Function.body` is raw `Value`s | `ast::stmt` → `ast::core_line` per op | + +The last two rows are §4.2's recorded exception, and it is symmetric: **neither +implementation types those fields**, which is why #326 recorded them as an open +contract question rather than as a parity gap. D2 closes the question. + +### 1.2 Columns + +| slot | strict door today | tolerant door today | Rust | +|---|---|---|---| +| `components[].subscriptions[].column` | `_check_column`: representability, then the 1-based rule, then the §4.2 upper bound | `_as_col` → `None` on anything that is not a 1-based int | `Option` on `Subscription`, checked by `strict::column` | +| `functions[].params[].column` | `_check_column` | `_as_col` | raw, checked by `strict::column` | +| `functions[].body[]…column` (every nesting shape) | `_check_flow_columns` → `_check_column`, recursing `then`/`else`/`body` | `_as_col` | raw, checked by `strict::flow_columns` | + +### 1.3 Where a line becomes a `u32`, and where a column does not + +`own_bridge::ast::core_line` is the **only** narrowing on the Rust path. It is +called from `ast::param` and from every `ast::stmt` arm (acquire, release, use, +overspan, return, alias_join, call, subscribe, if, while), and from +`verdict::anchor_line` / `verdict::guarded_line` (services `line` and +`ctor_line`, both site arrays, effect and binding lines). Today it returns a +`BridgeError` naming "outside the core's line domain"; D3 makes it degrade. + +The **column** path has no narrowing at all, and that answers a question this +task expected to go the other way. Measured, not read: a document carrying +`"column": 1099511627776` (`2^40`) is + +* **accepted** by the Rust strict door (`OwnIr::from_json` → `Ok`), because + `strict::column` checks representability and the 1-based rule, and `2^40` is + a representable positive integer; +* carried as `Some(1099511627776)` on `own_bridge::Finding::column` + (`Option`); +* emitted as `"startColumn":1099511627776` by `own_bridge::build_sarif`. + +So there is **no second undeclared boundary** on the column path: it is `i64` +end to end, agreeing with the reference. The finding is recorded here because +the absence of a boundary is only evidence once it has been measured — and +because it changes what this task's column work is. `_as_col` and +`own_bridge::lower::as_col` gain D1's upper bound not to close a divergence but +so that the tolerant door cannot emit a column the strict door would refuse. + +### 1.4 The comparison surface, restated because it is easy to over-claim + +The cp1 ledger compares **accepted / rejected** and, on rejection, the +**category** — never the message text. That is a deliberate, documented +decision (`tests/test_ownir_validation_fixtures.py` § "What is compared, and +what deliberately is not", and the same paragraph in +`own-ir/tests/validation_replay.rs`): the reference funnels every rejection +through one `OwnIRError` whose strings are a human-facing presentation aid, so +byte-comparing them across two languages would freeze a debug surface as a +contract and fail on every rewording. + +This change does **not** move that surface. The two doors' new messages are +written to say the same thing in the same shape, because a reader comparing +them by hand should not have to translate; but the executable claim stays +"same accept/reject, same category", and the ledger's recorded `message` stays +Python's own, for the record rather than for the comparison. Stating otherwise +here would be a claim the tree does not make. + +## 2. The census, the churn budget, and the controls that flip + +### 2.1 The census + +`tests/coordinate_census.py` walks **every** `.json` file under +`tests/fixtures/` and classifies every `line`, `ctor_line` and `column` slot at +any depth, by family, by slot and by value class; the rendered result is +[`p022-coord-census.md`](../generated/p022-coord-census.md), gated by +`scripts/render_checkpoint_status.py --check` like every other fragment. + +Three properties of it are load-bearing: + +* **It is wider than the door.** A coordinate on an OwnIR *document* is + something `load()` rules on; a coordinate in a golden is an *output* and the + door never sees it. The census marks the difference and counts both, because + D1's "`0` stays legal" is a claim about the outputs — the summaries goldens + alone carry dozens of zero-anchored records, and a census that read only the + inputs could not have seen a single one of them. +* **Its value classes follow the taxonomy's axis.** `outside-int64` (no + representable integer form → `Shape`) is a different class from `negative` + and `above-int32` (representable, out of domain → `Location`). Folding them + would have reproduced exactly the defect #326's census had to discover: one + violation classified two ways because the category was read off the + reference's diagnostic instead of off the mechanism. +* **The slot inventory is asserted as a set.** `SLOTS` is the door inventory, + and a declared slot no fixture reaches is reported as a phantom — the same + rule `tests/test_ownir_defensive_limits.py` applies to the schema's binding + map, for the same reason. + +What the census shows, in words (the numbers are in the fragment): every +out-of-domain coordinate in the tree is either a **control written to be one** +— the cp1 ledger's edge cases, the four `verdict_boundary_*` documents, and one +synthetic `root_resolve_sites[].line: -2` in +`verdict_di_duplicate_sites_last_wins.facts.json` — or a **zero**. There is no +corpus document, and no real extractor output, carrying a coordinate this +decision takes away. + +The `-2` site line deserves its own sentence, because it looks like churn and +is not. Its only reader guards on `>= 1` (a site line below 1 falls back to the +registration anchor), so `-2` and `0` behave identically on every path; the +committed golden already anchors that finding at `reg.cs:13`, the registration. +The Rust side already folds it (`verdict::guarded_line` returns `0` for a +negative). Degrading it to `0` changes nothing observable, and the golden is +outside the churn budget below. + +### 2.2 The churn budget, written before the change + +Measured by probe rather than predicted: the tolerant line reader was replaced +in-process with D3's degrade (`ownlang.ownir._as_int` monkeypatched to the +`_as_line` semantics) and every fixture family re-projected against its +committed goldens. + +**Changes:** + +1. `tests/fixtures/verdicts/verdict_boundary_line_negative.verdicts.json` +2. `tests/fixtures/verdicts/verdict_boundary_line_above_u32.verdicts.json` +3. `tests/fixtures/verdicts/verdict_boundary_service_line_negative.verdicts.json` +4. `tests/fixtures/verdicts/verdict_boundary_effect_line_negative.verdicts.json` + +— in each case the finding's `line` alone, moving to `0`. The DI001 evidence +slice in (3) does **not** move: its captor step was already dropped by the +`>= 1` guard at `-5` and is still dropped at `0`. + +Plus the generated fragments, which are projections of the evidence and change +because the evidence does: `p022-coord-census.md` (the goldens' classes move), +`p022-cp4-census.md` (the exclusion ledger shrinks and the replayed set grows), +and the new `p022-cp1-census.md` / `p022-coord-mutations.md`. + +**Does not change, measured:** + +* `tests/fixtures/verdict_renders/` — zero of the rendered-surface goldens; +* `tests/fixtures/summaries/`, `tests/fixtures/lowered/` — zero; +* `tests/fixtures/obligation_fact_parity.json`, + `tests/fixtures/di_eff_fact_parity.json` — zero; +* `tests/fixtures/repro/` — **zero**, including `digests.json`. This one is + worth stating precisely, because the shape of #339's and 4b's change made a + four-record churn the natural expectation. It does not happen, for two + independent reasons: the digest ledger pins the canonical hash of each + **facts** document, and no facts document changes; and the curated + reproduction-artifact set — the only place a layer envelope's + `produced`/`refused` status is committed — does not contain any of the four + boundary documents. The `own-shadow` reducer still refuses Layer 3, which is + #260's boundary and unmoved. + +Any diff outside this list is a defect in the work, not "what the tool +produced". It is checked with `git diff --stat` against the branch base before +every commit, and the confirmed stat is pasted into §3. + +### 2.3 The cp1 ledger controls that flip + +Four accept-controls become Location rejections; one stays accepted. Each is a +**re-measure**, not a weakening: the reference changes Python-first, so +stop-condition 1 ("a Rust/Python divergence is a Rust bug") does not apply, and +the ledger is re-derived from the new reference rather than relaxed to fit a +port. This is the fourth census. + +| control | was | becomes | the `why` the decision replaced | +|---|---|---|---| +| `accept-line-at-i64-max` | accept | reject, `location` | "the largest representable line" | +| `accept-line-at-i64-min` | accept | reject, `location` | "…and the smallest, because the range is closed at BOTH ends and a port that bounded only the top would pass a one-sided test" | +| `accept-negative-line` | accept | reject, `location` | "a NEGATIVE line is accepted: only columns carry the 1-based rule, and conflating the two would tighten the door" | +| `accept-column-at-i64-max` | accept | reject, `location` | "…and the largest column" | +| `accept-zero-line` | accept | **accept** (unchanged) | "…and zero is the line default" | + +`accept-negative-line`'s old `why` is the position D1 replaces, and it is +quoted rather than deleted: it was a correct reading of the contract as it then +stood, and the reason it no longer holds is that the contract now says lines +carry a domain rule of their own — not that columns and lines were conflated +after all. The controls whose values sit *outside* signed 64 bits +(`line-below-i64`, `line-above-i64`, `line-at-u64-max`, `line-above-u64`, +`line-float`, `column-above-i64`, `column-above-u64`, `column-float`) keep +their `shape` category untouched, which is what keeps the two axes apart: those +values have no representable integer form to violate a domain rule with. + +New controls land per section and per field — `-1` and `2147483648` rejected as +`location`, `0` and `2147483647` accepted — for every line-bearing field +including the two D2 admits, plus type controls (`"x"`, `true` → `shape`) for +those two, plus the columns' new upper bound. The full set is generated into +[`p022-cp1-census.md`](../generated/p022-cp1-census.md); no count is typed here. + +## 3. What landed, per checkpoint + +### 3.1 The four commits + +| commit | what | +|---|---| +| `docs(p022): inventory the coordinate domain before changing it (#259 final.0)` | this note's §1–§2, `tests/coordinate_census.py`, the generated coordinate census. No production code — the census that judges a contract change is taken against the tree, not against the change. | +| `feat(ownir): bound source coordinates to int32, validate every line field (Python-first) (#259 final.1)` | §4.2 and §4.1, `spec/ownir.schema.json`, `ownlang/ownir.py`, `ownlang/obligations.py`, the extended defensive-limits controls, the cp1 ledger. | +| `feat(bridge): mirror the coordinate domain and promote the four boundary controls (#259 final.2)` | `own-ir/src/strict.rs` and `protocol.rs`, `own-bridge/src/{ast,lower,verdict,render}.rs`, the exclusion ledger 6 → 2, four controls, the campaign definitions. | +| `docs(p022): record the campaigns and bring the status surfaces level (#259 final.3)` | the recorded runs, the generated fragments, P-022, the index, `spec/Bridge.md`, `spec/BridgeBehaviorMatrix.md`, this note's §3–§7. | + +### 3.2 The churn, confirmed + +`git diff --stat` against the branch base, for the whole change: + +```text +$ git diff --stat 834f295 # the branch base + tests/fixtures/ownir_validation.json | 1831 +++++++++++++++++++- + tests/fixtures/repro/digests.json | 22 +- + tests/fixtures/verdicts/manifest.json | 32 +- + ...erdict_boundary_effect_line_negative.facts.json | 5 +- + ...ict_boundary_effect_line_negative.verdicts.json | 2 +- + .../verdict_boundary_line_above_u32.facts.json | 6 +- + .../verdict_boundary_line_above_u32.verdicts.json | 2 +- + .../verdict_boundary_line_negative.facts.json | 9 +- + .../verdict_boundary_line_negative.verdicts.json | 2 +- + ...rdict_boundary_service_line_negative.facts.json | 7 +- + ...ct_boundary_service_line_negative.verdicts.json | 2 +- + .../verdict_domain_tolerant_readers.facts.json | 48 + + .../verdict_domain_tolerant_readers.verdicts.json | 48 + + ...and 52 more files, none of them a fixture + 65 files changed, 5439 insertions(+), 792 deletions(-) +``` + +Against §2.2's budget, written before anything moved: + +* the four `verdict_boundary_*.verdicts.json` goldens changed, and only in the + finding's `line`. As predicted, the DI001 evidence slice did not move. +* **the budget was wrong about one thing, in the direction of the work rather + than of the measurement.** It predicted zero churn under + `tests/fixtures/repro/`, and that was right about what the *change* forces — + the digest ledger pins each **facts** document and none of them changes for + a contract reason. Five records moved anyway, and both reasons are + deliberate: four because the boundary fixtures' own `_doc` blocks said "the + Rust core refuses the document rather than clamp the coordinate", which + final.2 makes false (a control whose documentation contradicts the behaviour + it pins is worse than four digest records), and one insertion because a + campaign found a blind spot and the fix was a new control (§5). Zero + artifacts, traces or reductions moved; the `own-shadow` reducer still + refuses Layer 3. +* nothing else in `tests/fixtures/` changed: `verdict_renders/`, `summaries/`, + `lowered/` and both fact-parity ledgers are untouched, and the goldens + anchored at line `0` — the summaries family alone carries dozens — came + through exactly as D1 requires. + +## 4. The differential over the measured set + +Not restated here as numbers, because it is not measured here: it is +**asserted** by two replays that run with zero Python. + +* `own-ir/tests/validation_replay.rs` runs every control in the cp1 ledger + through `OwnIr::from_json` and builds the matrix — agreed accepts, agreed + rejects, Rust-only accepts, Rust-only rejects, kind mismatches — collecting + every divergence without fail-fast. All three failure rows must be zero, and + no control may escape into serde. A green `cargo test -p own-ir` is that + statement; a non-zero row is a red build, not a number to copy. +* `own-bridge/tests/verdicts.rs` replays every non-excluded Layer 3 case + against its golden on **every `Finding` member** and every refusal in full, + and now additionally asserts the §4.2 domain at **Layer 2** over every one of + them. The four `verdict_boundary_*` controls are inside that set for the + first time. + +Migration counters over the replayed set, in the #250 packet's vocabulary: +Python-only **0**, Rust-only **0**, changed **0**, ordering-only **0**, +unexplained **0** — asserted by those two replays, not counted by hand. + +The ledger's own counts live in +[`p022-cp1-census.md`](../generated/p022-cp1-census.md) and +[`p022-cp4-census.md`](../generated/p022-cp4-census.md); the corpus census is +[`p022-coord-census.md`](../generated/p022-coord-census.md). + +## 5. What the campaigns found + +Two new campaigns +([`p022-coord-mutations.md`](../generated/p022-coord-mutations.md)), split by +door rather than by sub-checkpoint because the two doors fail differently: the +strict one refuses, the tolerant one degrades. Every rule is mutated on **both** +sides, since a domain only one implementation enforces is a divergence rather +than a rule. Every existing campaign was re-run against the new tree, and five +rotted anchors were re-anchored rather than deleted. + +The first pass of the tolerant campaign is the part worth recording, because +**seven of fifteen mutations survived** and the reason was the same in every +case: the promotion had made a rule reachable *in principle* while the corpus +still could not observe it. + +1. **`ast::core_line` cannot be reached from outside any more.** `lower` reads + every fact coordinate through `as_line`, so the Layer 2 document the AST is + built from is already inside the domain and an out-of-domain value dies one + layer earlier. Clamping in `core_line`, or accepting the whole `u32` range + again, changed no golden. It is a real second line of defence over an `i64` + field wider than the domain, and it is not an end-to-end control — so the + control is a direct one, and its doc comment says so rather than letting a + unit test on the function under test read as end-to-end evidence. +2. **The tolerant readers the four promoted controls do not reach.** Every one + of those four passes through the AST build, which degrades a second time, so + a port that kept the raw fact value still produced the right anchor. Two + readers are not covered by that: a DI registration line an escape slice's + source hop reads (guarded on `>= 1` and never narrowed again) and the + subscription column (read by the column reader alone). One synthetic Layer 3 + case, `verdict_domain_tolerant_readers`, closes both — at `2^31` the hop must + be DROPPED and the column ABSENT, with an in-domain twin beside each so the + control cannot pass by refusing everything. +3. **One equivalent mutant, proven and re-anchored.** `render::code_flows` used + to drop a step it could not convert; §4.2 makes every flow-step line + convertible, so re-introducing the drop drops nothing and mutating the + fallback value is unreachable. The mutation was moved onto the conversion + itself — a one-line shift of every step — which the byte-exact rendered + replay catches. + +The campaign also found a divergence the promotion introduced rather than +exposed: `lower::as_col` had no upper bound, so final.1's Python change would +have left the two tolerant column readers disagreeing. Bounded, with the +control above. + +Two mutations were rewritten rather than kept, because each was *caught* by a +traceback rather than by a named check: a Python mutation that removes a type +guard makes the domain comparison raise a `TypeError`, and a catcher recorded +as "non-zero exit with no reported failure" is evidence of a crash, not of the +rule. Both now attack the same rule where it is legible — the bool-is-int trap +on the strict door, and a refusing tolerant reader on the other. + +## 6. Measured, not claimed + +* **The Rust column path has no `u32` anywhere.** Measured through the crate + rather than read: a document carrying `"column": 2^40` is accepted by the + Rust strict door, carried as `Some(1099511627776)` on `Finding::column`, and + emitted to SARIF as `startColumn 1099511627776`. There was no second + undeclared boundary to find. The tolerant column readers gained D1's upper + bound on both sides so the tolerant door cannot emit a column the strict door + would refuse — not to close a divergence, but so one cannot open. +* **The cp1 ledger compares category, never message text**, and this change + does not move that surface. The two doors' new messages are written to say + the same thing in the same shape, and a Rust-local control now pins the + port's own text against drift; neither makes message text a cross-language + contract, and claiming otherwise would be a claim the tree does not make. +* **The protocol family's tolerant rule is untouched.** Type and + representability are grammar and still skip a malformed entry whole (cp4b's + rule); only the DOMAIN follows the door. That split is a `Door` parameter + rather than a second parser, because two readings of one grammar is exactly + what 4b collapsed into one. +* **`ast::core_line` is unreachable end to end**, per §5.1. Recorded rather + than removed: the type it guards is wider than the domain. +* **The `own-shadow` reducer still refuses Layer 3.** Unmoved, and #260's + boundary. Nothing here crosses it. + +## 7. The wording this earns + +> #259 final acceptance reached: Layer 3 parity over the **full** #258 family +> inventory at the full `Finding` and the rendered surfaces; declared boundary: +> the two OD-1 door controls (#294), measured, not open work. + +Not "verdict parity complete". Not "shadow mode" — that is #260's acceptance, +still blocked on its own two decisions. Not "P-022 done", and not "Rust is the +default": the cutover is #262's, behind #260 and #261. diff --git a/docs/proposals/P-022-rust-core-migration.md b/docs/proposals/P-022-rust-core-migration.md index a31fd749..f28af494 100644 --- a/docs/proposals/P-022-rust-core-migration.md +++ b/docs/proposals/P-022-rust-core-migration.md @@ -56,12 +56,14 @@ was #258 alone, which is satisfied. Per the checkpoints #259 itself defines: | #259 checkpoint | Status | Evidence / what remains | |---|---|---| -| 1 — typed OwnIR validation | **complete — no known strict-door divergence** | Three censuses. The first froze 77 controls and read 0/0/0 — then review found seven divergences the ledger could not express, because the same author wrote the ledger and the port and one gap in reading BR-D1 produced a matching gap in each (`_svc()` always supplied `lifetime`, so no control could omit it). The second is derived from `load()` and `obligations.py` line by line: **193 controls**, opening a further **58** permissive documents and **9** category mismatches. Closing them was architectural — the strict door is a sequential validator over the raw document (`own-ir/src/strict.rs`) reproducing BR-D1's interleaving of shape and semantics *per section, in declaration order*; `serde` is the typed constructor, and a document it rejects after validation is reported as a hole in the validator and asserted against. The obligation **acceptance grammar** is ported (`own-ir/src/protocol.rs`); protocol *analysis* is not, and is not part of what the door accepts. The third census admitted the two families the second had measured and deliberately excluded — source coordinates beyond signed 64 bits, and nesting depth — once #326 closed them Python-first. That opened 7 permissive documents and 8 more category mismatches, and the classification defect underneath them was the ledger reading its category off the reference's *diagnostic* rather than off the mechanism: `_check_column` raises one message for a bool, a string, a float, an out-of-range integer and a zero alike, so a bool column was filed as a 1-based-contract violation. Taxonomy is **seven** categories on **two axes** — `Shape` is now "no representable primitive or container form", `Location` is "a representable coordinate violating its domain rule", and `WellFormedness` covers records that are typed and vocabulary-legal and still cannot mean anything. **216 controls, matrix 35/181, 0/0/0**, no control escaping into serde; 48 mutations across the three rounds, all caught. #294 OD-2 remains a separate tolerant-door concern | +| 1 — typed OwnIR validation | **complete — no known strict-door divergence** | Four censuses. The first froze 77 controls and read 0/0/0 — then review found seven divergences the ledger could not express, because the same author wrote the ledger and the port and one gap in reading BR-D1 produced a matching gap in each (`_svc()` always supplied `lifetime`, so no control could omit it). The second is derived from `load()` and `obligations.py` line by line: **193 controls**, opening a further **58** permissive documents and **9** category mismatches. Closing them was architectural — the strict door is a sequential validator over the raw document (`own-ir/src/strict.rs`) reproducing BR-D1's interleaving of shape and semantics *per section, in declaration order*; `serde` is the typed constructor, and a document it rejects after validation is reported as a hole in the validator and asserted against. The obligation **acceptance grammar** is ported (`own-ir/src/protocol.rs`); protocol *analysis* is not, and is not part of what the door accepts. The third census admitted the two families the second had measured and deliberately excluded — source coordinates beyond signed 64 bits, and nesting depth — once #326 closed them Python-first. That opened 7 permissive documents and 8 more category mismatches, and the classification defect underneath them was the ledger reading its category off the reference's *diagnostic* rather than off the mechanism: `_check_column` raises one message for a bool, a string, a float, an out-of-range integer and a zero alike, so a bool column was filed as a 1-based-contract violation. Taxonomy is **seven** categories on **two axes** — `Shape` is now "no representable primitive or container form", `Location` is "a representable coordinate violating its domain rule", and `WellFormedness` covers records that are typed and vocabulary-legal and still cannot mean anything. The **fourth** census is #259's final acceptance: `spec/OwnIR.md` §4.2 now bounds every `line` to the int32 domain its consumers actually have and validates the two fields it used to record as checked **nowhere**, so the ledger gains a control family per field and **four accept-controls flip to `location` rejections**. That is a re-measure, not a weakening — the reference changed Python-first, so "a divergence is a Rust bug" never applied — and each flipped control keeps its `accept-` name with a `why` that states the position it replaced, so the flip reads as one re-measured control rather than a new one beside a deleted one. The two axes stay apart, and this is what proves they were never one rule wearing two names: `i64::MAX` as a line was accepted and is now `Location`, while `i64::MAX + 1` was and stays `Shape`. No control escapes into serde and all three failure rows of the matrix are zero — a property of a green `cargo test -p own-ir`, not a number. **Every count now lives in the generated census** [`p022-cp1-census.md`](../generated/p022-cp1-census.md); the three that used to be typed here stay in the issue bodies as history. #294 OD-2 remains a separate tolerant-door concern | | 2 — fact lowering | **complete** | `lower()` → `own_lowered`; **27/27** `rust_replay` cases in `tests/fixtures/lowered/manifest.json` byte-exact | | 3 — interprocedural MOS | **complete for the stage-1 domain** | `dump_summaries()` byte-identical to `python -m ownlang summaries` across **35** `*.summaries.json` goldens. Container-valued metadata is **outside** the declared scalar-metadata parity domain — a separate #294-class door decision, not a silent gap | -| 4 — analysis wiring | **complete at the checkpoint-4 surface — parity over the measured set, with the unmeasured set named** | `own_bridge::check_facts` is BR-V1 verbatim: the Layer 2 document rebuilt as the core AST (`ast.rs` — composing the cp2 evidence rather than lowering twice), `own_analysis::check_module` (ownership, lifetime, buffer policy), the `services[]`/`effects[]` blocks through the `own-analysis` DI and effect finders (BR-P1/P2), the OWN050/051/052 side paths, ERROR-only mapping through the verdict's **`subject`** to a fact handle with the reference's map-or-raise refusal (BR-V2/V3), the analysis-selected anchors preserved (DI004 call site, DI005 store site, OWN025 view site — BR-V5), tiering and suppression (BR-V6), dedup and the stable `(file, line, column, code)` sort (BR-V7/V8). Landing it needed one core change: `own-analysis` now stamps `subject` exactly where `analysis.py`/`lifetimes.py` pass `subject=sym.origin` (pinned through `check_module`). **Layer 3 fixture family built** (`ownlang/verdicts.py`, `tests/fixtures/verdicts/`, `tests/test_verdict_fixtures.py`; Rust replay `own-bridge/tests/verdicts.rs`): the goldens are Python's complete truth — the swept `ownir`/`lowered`/`summaries` corpora plus the synthetic controls — carrying every `Finding` member; the cp4 replay compares identity, anchor, kind and tiering (all members but `message`/`related`/`flow`) and **asserts** equality over the replayed set — every divergence collected without fail-fast, any one of them a red build, so the differential is a property of a green replay rather than a number to copy here. The rest is a **named exclusion ledger the replay executes**, not a silent gap — the documents declaring obligation protocols (the OBL analysis was not ported; the bridge *refused* them rather than return a list with a family missing — **promoted at row 4b**), the controls for the one representability boundary (a coordinate outside the core's `u32` line domain, which the strict door admits per `spec/OwnIR.md` §4.2 — refused, never clamped; the contract decision is one #259 still owes, and the stated direction is the Python-first tightening recorded on the cp5 row), and the #294 OD-1 controls where the reference's tolerant-door skip/ignore is unreachable through the typed Rust constructor. One declared comparison boundary on refusals: the map-or-raise text is compared up to its `message=` member, because the core's messages are still titles. **The measured census** (goldens by origin, exclusions by expectation, the replayed set with its refusals and findings) **and the recorded mutation campaign live only in the generated fragments** [`docs/generated/p022-cp4-census.md`](../generated/p022-cp4-census.md) and [`docs/generated/p022-cp4-mutations.md`](../generated/p022-cp4-mutations.md), rendered from the ledger and the campaign evidence by `scripts/render_checkpoint_status.py` and held in sync by the test gate — no count is typed in this table | +| 4 — analysis wiring | **complete at the checkpoint-4 surface — parity over the measured set, with the unmeasured set named** | `own_bridge::check_facts` is BR-V1 verbatim: the Layer 2 document rebuilt as the core AST (`ast.rs` — composing the cp2 evidence rather than lowering twice), `own_analysis::check_module` (ownership, lifetime, buffer policy), the `services[]`/`effects[]` blocks through the `own-analysis` DI and effect finders (BR-P1/P2), the OWN050/051/052 side paths, ERROR-only mapping through the verdict's **`subject`** to a fact handle with the reference's map-or-raise refusal (BR-V2/V3), the analysis-selected anchors preserved (DI004 call site, DI005 store site, OWN025 view site — BR-V5), tiering and suppression (BR-V6), dedup and the stable `(file, line, column, code)` sort (BR-V7/V8). Landing it needed one core change: `own-analysis` now stamps `subject` exactly where `analysis.py`/`lifetimes.py` pass `subject=sym.origin` (pinned through `check_module`). **Layer 3 fixture family built** (`ownlang/verdicts.py`, `tests/fixtures/verdicts/`, `tests/test_verdict_fixtures.py`; Rust replay `own-bridge/tests/verdicts.rs`): the goldens are Python's complete truth — the swept `ownir`/`lowered`/`summaries` corpora plus the synthetic controls — carrying every `Finding` member; the cp4 replay compares identity, anchor, kind and tiering (all members but `message`/`related`/`flow`) and **asserts** equality over the replayed set — every divergence collected without fail-fast, any one of them a red build, so the differential is a property of a green replay rather than a number to copy here. The rest is a **named exclusion ledger the replay executes**, not a silent gap — the documents declaring obligation protocols (the OBL analysis was not ported; the bridge *refused* them rather than return a list with a family missing — **promoted at row 4b**), the four controls for the coordinate boundary — a coordinate outside the core's `u32` line domain, refused rather than clamped while `spec/OwnIR.md` §4.2 still admitted every signed-64 value — **promoted at final acceptance**, once §4.2 bounded a line to the int32 domain its consumers actually have and both tolerant doors degraded an out-of-domain coordinate to `0`, and the #294 OD-1 controls where the reference's tolerant-door skip/ignore is unreachable through the typed Rust constructor. One declared comparison boundary on refusals: the map-or-raise text is compared up to its `message=` member, because the core's messages are still titles. **The measured census** (goldens by origin, exclusions by expectation, the replayed set with its refusals and findings) **and the recorded mutation campaign live only in the generated fragments** [`docs/generated/p022-cp4-census.md`](../generated/p022-cp4-census.md) and [`docs/generated/p022-cp4-mutations.md`](../generated/p022-cp4-mutations.md), rendered from the ledger and the campaign evidence by `scripts/render_checkpoint_status.py` and held in sync by the test gate — no count is typed in this table | | 4b — protocol analysis (OBL001–005) | **complete — Layer 3 parity over the measured set, protocol family included** | The gap in #259's checkpoint list is closed: `ownlang/obligations.py` has a port. The analysis is `own-analysis/src/obligation.rs` (BR-B1: the analysis owns its verdict) — the `{OPEN, CLOSED}` set lattice with min-line provenance, the opens-before-closes-before-barriers leaf order with allow beating barrier, the never-invent asymmetry of an opaque write, the exits with the OWN001 anchor-at-acquire precedent, the loop's silent fixpoint and single emitting pass, the reachability-blind close-line evidence and the four-part sort key. The typed `Protocol`/`MethodEvents` values are built by **one** implementation of the grammar with two consumers — `own-ir/src/protocol.rs` grew from validate-only to validate-and-construct, so the strict door and the analysis cannot drift into two readings of one grammar — and no strict-door error text or category moved (the cp1 ledger's protocol controls stayed green). A third **fact-parity family** at the analysis level mirrors the DI/effect one (`tests/test_obligation_fact_parity.py` → `tests/fixtures/obligation_fact_parity.json` → `own-analysis/tests/obligation_parity.rs`): the reference's own `check_protocols` / `unmatched_scopes` freeze every violation WHOLE, and the replay compares every member with zero Python. Cases carry RAW documents, so each side builds the values with its own half of the grammar. The bridge maps BR-P3 in its BR-V1 place (after effects, before OWN050): the `(kind, definite)` table, the four deliberately line-free wordings, the identity derivations, the anchorless advisory OBL005 and the opened→barrier(→late-close) slice. `refuse_protocols` is **gone** and both reference documents are **promoted** out of `rust_replay_excluded` — the exclusion ledger shrinks by two and its pinned set with it, which is the contract change this checkpoint exists to make. Neither golden was regenerated to reach it. Eight synthetic controls (seven Layer 3, one rendered) close every row the corpus could not reach, insertion-stable. Two things are **measured and recorded rather than claimed**: the BR-V5 "a slice shorter than two steps is dropped" rule is not applied on the protocol path (a leak off the end carries a one-step slice), and the family's append POSITION is unobservable end to end because the sort key's code component decides first. One golden family IS regenerated, for a stated reason: the shadow artifact and trace of the protocol document, whose Rust `verdicts` layer moves from `refused` to `produced` — the same shape of change #339 recorded for `partial` → `full`; the `own-shadow` reducer still refuses Layer 3, which is #260's boundary and unmoved. Counts live only in the generated fragments [`p022-cp4-census.md`](../generated/p022-cp4-census.md), [`p022-cp5-inventory.md`](../generated/p022-cp5-inventory.md) and [`p022-cp4b-mutations.md`](../generated/p022-cp4b-mutations.md) ([note](../notes/p022-bridge-verdict-checkpoint4b.md)) | -| 5 — full fact-to-verdict parity | **complete at the cp5 surface — Layer 3 parity over the measured set at the full `Finding` and the rendered surfaces** | the goldens already carry `message`, `related` and `flow`; cp5 is the message matrix (BR-V4), the evidence slices, the rendered surfaces (`render*`, `build_sarif` on the bridge path — BR-V9) and tightening the cp4 replay to full equality on the **same frozen goldens** — no regeneration beside the implementation. The **surface inventory** is taken and generated, never typed ([`p022-cp5-inventory.md`](../generated/p022-cp5-inventory.md), read by [the cp5 note](../notes/p022-bridge-verdict-checkpoint5.md)): one ledger row per BR-V4 wording branch with **who owns the string** (the bridge's matrix, the DI/effect finders' own `message`, or the core diagnostic's — which this core does not have at all, carrying each code's TITLE instead), one per BR-V5 evidence family and degradation rule, and one per BR-V9 rendered-surface rule; a row at zero over the replayed set is a branch cp5 must add a control for, not one it may declare. **cp5.1** put `message`, `related` and `flow` on `own_bridge::Finding`, ported the BR-V4 matrix and the BR-V5 slice builders, and tightened the replay to EVERY `Finding` member — against the goldens exactly as committed, none regenerated. The analysis-owned halves went where the reference keeps them: the DI finders' own `message` and ctor/registration metadata, and the effect storm's message, kind, mint line and reference chain, all additive in `own-analysis` with no existing core surface changed. Synthetic cases close every reachable branch the inventory named (insertion-stable: zero existing records rewritten, in the verdict ledger and in the shadow digest ledger alike); the branches no facts document can reach are pinned by `verdict::tests` controls driven through `map_core`, and their expected text is not written in those tests: it is read from `tests/fixtures/unreachable_branches.json`, the reference's own output recorded by `tests/test_unreachable_branch_probe.py` running `check_facts` with its lowering substituted — so the oracle's answer is re-runnable rather than a claim, and there is no second copy of the text for a port to drift into agreeing with. **cp5.2** removed the last comparison boundary on a refusal: `own_cfg::Diag` now carries the reference's message for the resolver text the BR-V3 map-or-raise line interpolates (measured to be the only one the corpus reaches), `own-analysis` reads it, and the three `hoist_neg_*` refusals compare byte for byte. The unported remainder is an `Option`, so a code whose text is missing renders as its title and the first golden that refuses on it goes red demanding the message — a tripwire, not a blind spot. Removing the cut immediately exposed the defect it was hiding: cp4's `py_repr` quoted unconditionally with `'`, where CPython switches to `"` for a string containing a single quote — which every core message naming an identifier does. Fixed in Rust and pinned against CPython's own output. One consequence in `own-shadow`: its verdict capture declared a *partial* projection whose reason said cp5 was not ported, so it now emits every member and declares `full`, with the projection check extended to validate a `full` claim too — the reducer still **refuses** the verdict layer, which is #260's boundary and unmoved. **cp5.3** built the surface that had no golden at all: `ownlang/renders.py` (an observer beside `verdicts.py`), the `tests/fixtures/verdict_renders/` family under its own frozen ledger where each case names the BR-V9 rows it is the control for, a verify/`--write` harness, and a Rust replay comparing the **bytes** — SARIF key order is part of this surface, so the port reconstructs the document through typed structs whose field order is the emitter's key order rather than a map type that sorts. `codeFlows` reuses `own_diagnostics::code_flow` verbatim; `relatedLocations` deliberately does **not**, because the core's builder drops a step with an empty file and the bridge's does not — reuse there would have been a port bug, and a golden pins the difference. `own-diagnostics` is unchanged on the core path. The checkpoint-4 `subject` tail is closed: both sides scan the rendered bytes for a `subject` key rather than restating that none can appear. Row **4b** has since landed (the protocol family is ported and replayed), so what remains for #259's final acceptance is the coordinate-domain decision alone. The `u32` coordinate boundary stays excluded until its contract decision lands; the owner's stated direction is a **Python-first tightening** of the coordinate domain (a negative source line is meaningless), as its own contract change with parity evidence — never "Rust is `u32`, so the reference is wrong" | +| 5 — full fact-to-verdict parity | **complete at the cp5 surface — Layer 3 parity over the measured set at the full `Finding` and the rendered surfaces** | the goldens already carry `message`, `related` and `flow`; cp5 is the message matrix (BR-V4), the evidence slices, the rendered surfaces (`render*`, `build_sarif` on the bridge path — BR-V9) and tightening the cp4 replay to full equality on the **same frozen goldens** — no regeneration beside the implementation. The **surface inventory** is taken and generated, never typed ([`p022-cp5-inventory.md`](../generated/p022-cp5-inventory.md), read by [the cp5 note](../notes/p022-bridge-verdict-checkpoint5.md)): one ledger row per BR-V4 wording branch with **who owns the string** (the bridge's matrix, the DI/effect finders' own `message`, or the core diagnostic's — which this core does not have at all, carrying each code's TITLE instead), one per BR-V5 evidence family and degradation rule, and one per BR-V9 rendered-surface rule; a row at zero over the replayed set is a branch cp5 must add a control for, not one it may declare. **cp5.1** put `message`, `related` and `flow` on `own_bridge::Finding`, ported the BR-V4 matrix and the BR-V5 slice builders, and tightened the replay to EVERY `Finding` member — against the goldens exactly as committed, none regenerated. The analysis-owned halves went where the reference keeps them: the DI finders' own `message` and ctor/registration metadata, and the effect storm's message, kind, mint line and reference chain, all additive in `own-analysis` with no existing core surface changed. Synthetic cases close every reachable branch the inventory named (insertion-stable: zero existing records rewritten, in the verdict ledger and in the shadow digest ledger alike); the branches no facts document can reach are pinned by `verdict::tests` controls driven through `map_core`, and their expected text is not written in those tests: it is read from `tests/fixtures/unreachable_branches.json`, the reference's own output recorded by `tests/test_unreachable_branch_probe.py` running `check_facts` with its lowering substituted — so the oracle's answer is re-runnable rather than a claim, and there is no second copy of the text for a port to drift into agreeing with. **cp5.2** removed the last comparison boundary on a refusal: `own_cfg::Diag` now carries the reference's message for the resolver text the BR-V3 map-or-raise line interpolates (measured to be the only one the corpus reaches), `own-analysis` reads it, and the three `hoist_neg_*` refusals compare byte for byte. The unported remainder is an `Option`, so a code whose text is missing renders as its title and the first golden that refuses on it goes red demanding the message — a tripwire, not a blind spot. Removing the cut immediately exposed the defect it was hiding: cp4's `py_repr` quoted unconditionally with `'`, where CPython switches to `"` for a string containing a single quote — which every core message naming an identifier does. Fixed in Rust and pinned against CPython's own output. One consequence in `own-shadow`: its verdict capture declared a *partial* projection whose reason said cp5 was not ported, so it now emits every member and declares `full`, with the projection check extended to validate a `full` claim too — the reducer still **refuses** the verdict layer, which is #260's boundary and unmoved. **cp5.3** built the surface that had no golden at all: `ownlang/renders.py` (an observer beside `verdicts.py`), the `tests/fixtures/verdict_renders/` family under its own frozen ledger where each case names the BR-V9 rows it is the control for, a verify/`--write` harness, and a Rust replay comparing the **bytes** — SARIF key order is part of this surface, so the port reconstructs the document through typed structs whose field order is the emitter's key order rather than a map type that sorts. `codeFlows` reuses `own_diagnostics::code_flow` verbatim; `relatedLocations` deliberately does **not**, because the core's builder drops a step with an empty file and the bridge's does not — reuse there would have been a port bug, and a golden pins the difference. `own-diagnostics` is unchanged on the core path. The checkpoint-4 `subject` tail is closed: both sides scan the rendered bytes for a `subject` key rather than restating that none can appear. Row **4b** has since landed (the protocol family is ported and replayed), and so has the coordinate-domain decision that was the last thing #259's final acceptance owed. That decision landed Python-first and as its own change with its own parity evidence: §4.2 bounds every `line` to `[0, 2147483647]` and every `column` to `[1, 2147483647]`, because int32 is the line type of every consumer this project feeds — never "Rust is `u32`, so the reference is wrong". `0` stays legal as the reference's own absent sentinel, the strict door refuses an out-of-domain coordinate as `Location` and the tolerant door degrades it to `0` (degrade, never clamp), and the four boundary controls are promoted out of the exclusion ledger — see the [final-acceptance note](../notes/p022-bridge-verdict-final-acceptance.md) | + +**#259 final acceptance reached: Layer 3 parity over the *full* #258 family inventory at the full `Finding` and the rendered surfaces; declared boundary: the two OD-1 door controls (#294), measured, not open work.** The coordinate-domain contract landed Python-first, the port mirrors it, and the exclusion ledger the replay executes now names only those two. Not shadow mode — that is #260's acceptance and is blocked on its own two decisions; not "P-022 done"; not "Rust is the default", which is #262's cutover. The measured record is the [final-acceptance note](../notes/p022-bridge-verdict-final-acceptance.md) and the generated fragments it links. **Open steps — each owned by exactly one child issue under #250:** @@ -71,20 +73,29 @@ was #258 alone, which is satisfied. Per the checkpoints #259 itself defines: | 5b | SARIF projection, canonical parity | #256 | **SARIF complete** — `own_diagnostics::sarif` ports `diag_sarif.py` + `evidence.py`, replayed against 16 cases / 21 results with zero volatile fields stripped. `.ownreport.json` is **struck from this step, not deferred**: measured against the tree it is a *buffer* report (`{module, buffers[]}`), carries no diagnostics/Evidence/tool metadata, and porting it needs `ast_nodes` + `buffers.resolve` — which this step's own guardrail forbids `own-diagnostics` from reaching. See [below](#what-256-asked-for-that-the-tree-does-not-have). The OwnIR SARIF path (`DI`/`EFF`/`OBL`) stays with the bridge (#259) | | 5c | `own-codegen` (analysis-independent sibling) | #257 | **ready**, independent of the analysis path — parallelizable | | 6a | OwnIR **bridge semantics formalized** before the port | #258 | **complete** — see above | -| 6b | Rust `own-bridge`, layered OwnIR parity | #259 | **in progress** — checkpoint table above | -| 7a | dual-engine shadow mode + zero-diff reproduction artifacts | #260 (supported by #269) | **infrastructure COMPLETE — checkpoints 1–4 landed**; final acceptance **still blocked by #259**. Every item this row listed as *sliceable now* is built and replayed with zero of the other engine's runtime: cp1 same-input `OwnIR` capture + canonical hash and the reproduction-artifact format; cp2 the **engine protocol** (each engine declares what it could *produce*); cp3 the **`AnalysisTrace`** (#269 — stable-ID normalization, per-layer ordering semantics *declared* rather than normalized away); cp4 **first-divergence reduction** over the *lowered*/MOS layers, naming layer, step and the minimal difference. `ownlang/repro.py` is the reference emitter, `own-shadow` the port's half. Every document digest-pinned and re-hashed, every artifact, trace and reduction reproduced byte-for-byte, with tamper, domain-refusal, structural and backstop controls on both sides and four fully-caught campaigns — all of it counted in the generated census, never here (row 4b removed one of its `status` observations by wiring the family the port used to refuse, which is exactly why this row states no number). Divergence classification, now **computed** by the reducer rather than gate-implied: left-only 0 / right-only 0 / changed 0 / ordering-only 0 / unexplained 0, with the remaining `status` observations boundaries the port declares ([cp1](../notes/p022-shadow-infra-checkpoint1.md), [cp2](../notes/p022-shadow-infra-checkpoint2.md), [cp3](../notes/p022-shadow-infra-checkpoint3.md), [cp4](../notes/p022-shadow-infra-checkpoint4.md), [generated census](../generated/p022-shadow-census.md)). Four **findings**, each recorded and resolved as a contract decision rather than by bending either engine: `-0` reads as an integer in CPython's `json` and a float in `serde_json`; the trace schema was ambiguous about where a duplicate address takes its `~n`; the capture carried the MOS document in a key order its own surface does not fix; and the two reducers disagreed about whether object key order is significant. **Still NOT shadow mode and not parity**: the reducer *refuses* the verdict layer and records the refusal, because acceptance compares end diagnostics; cp5 and 4b have landed, so what that line now waits on is #259's own final acceptance. Crossing that line is a contract decision — the scope, engine and layer vocabularies are frozen constants with tests that fail when they move. **Also not proved: #260's raw-byte same-input invariant.** What cp1 establishes is shared *canonical document identity*; canonical-equivalent input is not byte-identical input, so acceptance must additionally prove that both engines consumed the identical captured byte sequence. Three departures from the slice's brief — capture and the artifact format kept as one checkpoint, the `-0` domain narrowing (an engine divergence the brief said to report rather than decide), and `sha2` for `own-shadow` only — are ratified on the record in the [owner-decision ledger](../notes/p022-shadow-infra-owner-decisions.md) | +| 6b | Rust `own-bridge`, layered OwnIR parity | #259 | **final acceptance reached** — see the checkpoint table and the line above it | +| 7a | dual-engine shadow mode + zero-diff reproduction artifacts | #260 (supported by #269) | **infrastructure COMPLETE — checkpoints 1–4 landed**; final acceptance **no longer blocked by #259**, whose own final acceptance is reached — it now waits on this row's two decisions alone (crossing the verdict-layer line, and the raw-byte same-input invariant). Every item this row listed as *sliceable now* is built and replayed with zero of the other engine's runtime: cp1 same-input `OwnIR` capture + canonical hash and the reproduction-artifact format; cp2 the **engine protocol** (each engine declares what it could *produce*); cp3 the **`AnalysisTrace`** (#269 — stable-ID normalization, per-layer ordering semantics *declared* rather than normalized away); cp4 **first-divergence reduction** over the *lowered*/MOS layers, naming layer, step and the minimal difference. `ownlang/repro.py` is the reference emitter, `own-shadow` the port's half. Every document digest-pinned and re-hashed, every artifact, trace and reduction reproduced byte-for-byte, with tamper, domain-refusal, structural and backstop controls on both sides and four fully-caught campaigns — all of it counted in the generated census, never here (row 4b removed one of its `status` observations by wiring the family the port used to refuse, which is exactly why this row states no number). Divergence classification, now **computed** by the reducer rather than gate-implied: left-only 0 / right-only 0 / changed 0 / ordering-only 0 / unexplained 0, with the remaining `status` observations boundaries the port declares ([cp1](../notes/p022-shadow-infra-checkpoint1.md), [cp2](../notes/p022-shadow-infra-checkpoint2.md), [cp3](../notes/p022-shadow-infra-checkpoint3.md), [cp4](../notes/p022-shadow-infra-checkpoint4.md), [generated census](../generated/p022-shadow-census.md)). Four **findings**, each recorded and resolved as a contract decision rather than by bending either engine: `-0` reads as an integer in CPython's `json` and a float in `serde_json`; the trace schema was ambiguous about where a duplicate address takes its `~n`; the capture carried the MOS document in a key order its own surface does not fix; and the two reducers disagreed about whether object key order is significant. **Still NOT shadow mode and not parity**: the reducer *refuses* the verdict layer and records the refusal, because acceptance compares end diagnostics. cp5, 4b and #259's final acceptance have all landed, so what that line now waits on is this row's own decisions rather than another issue's. Crossing that line is a contract decision — the scope, engine and layer vocabularies are frozen constants with tests that fail when they move. **Also not proved: #260's raw-byte same-input invariant.** What cp1 establishes is shared *canonical document identity*; canonical-equivalent input is not byte-identical input, so acceptance must additionally prove that both engines consumed the identical captured byte sequence. Three departures from the slice's brief — capture and the artifact format kept as one checkpoint, the `-0` domain narrowing (an engine divergence the brief said to report rather than decide), and `sha2` for `own-shadow` only — are ratified on the record in the [owner-decision ledger](../notes/p022-shadow-infra-owner-decisions.md) | | 7b | Rust `own-cli`: command/output/exit-code parity | #261 | blocked — needs the production bridge and the output surfaces | | 8 | Rust-default **cutover**, rollback gate, Python distribution removal | #262 | blocked by #260/#261 and final parity | -**Preferred queue:** the coordinate-domain decision → #259 final acceptance → -#260 acceptance / #261. Both cp5 and 4b are off this queue: each is complete at -its surface, and the one thing #259's final acceptance still needs is the -contract decision on coordinates. The #260/#269 +**Preferred queue:** #259 final acceptance reached → #260 acceptance / #261. +cp5, 4b and the coordinate-domain decision are all off this queue: each is +complete at its surface, and the decision that was the last thing #259's final +acceptance needed has landed Python-first with its own parity evidence. The +#260/#269 *infrastructure* slice ran in parallel by design — it is measured on the landed checkpoints and takes no position on the ones that are open, which is why its first checkpoint could land without waiting on cp5; its **acceptance** is a different thing and waits on both items above. +The coordinate DOMAIN that used to head this queue landed the same way and for +the same reason (see the cp5 row and the +[note](../notes/p022-bridge-verdict-final-acceptance.md)): int32 is the line +type of every consumer this project feeds, so the bound belongs in the +contract, and it had to land Python-first with cp1 re-measured against it +rather than merged beside the port that mirrors it. That is the second time +this order has been load-bearing. + The defensive limits that used to head this queue landed in #326, and the order was load-bearing rather than tidy. cp1 could report 0/0/0 only over a set with two known divergence families removed from it, and closing them by widening Rust diff --git a/docs/proposals/README.md b/docs/proposals/README.md index f3d7453d..12a27da7 100644 --- a/docs/proposals/README.md +++ b/docs/proposals/README.md @@ -41,7 +41,7 @@ proposal is marked `done` with a pointer. | [P-017](P-017-multi-stack-frontends.md) | Multi-stack frontends (OwnTS / OwnJVM: OwnJava + OwnKotlin) | draft | | [P-020](P-020-ownts-react-effects.md) | OwnTS React effects profile (`Own.React`) — the effect-storm angle | draft | | [P-021](P-021-async-audit-pack.md) | Async audit pack (`Own.Async`) | draft | -| [P-022](P-022-rust-core-migration.md) | Rust core migration: crate DAG, patterns, prior art, differential oracle (Python = golden) | in execution — steps 0–4 built (#214/#249); step 5a done (full diagnostic contract, #255 via #319/#320/#321); step 5b SARIF done (#256; `.ownreport.json` struck — a buffer report needing the AST, not a diagnostics surface); step 6a done (`spec/Bridge.md`, #258); step 6b underway (`own-lowered`/`own-bridge`, #259: lowering and MOS parity landed; strict-door validation complete with no known divergence — the first 0/0/0 proved to be the ledger agreeing with its own author, and the second omitted two families that a Python-first defensive-limit change (#326) had to close before the third could measure them; analysis wiring complete at the checkpoint-4 surface — `check_facts` through the real analyses, Layer 3 goldens built, with an executable exclusion ledger naming each declared boundary; **cp5 complete at its surface** — the replay compares EVERY `Finding` member (the BR-V4 wording matrix and the BR-V5 evidence slices included) and every refusal in full, and a second fixture family freezes the BR-V9 rendered surfaces byte for byte, all against goldens none of which was regenerated; **row 4b complete** — the obligation-protocol analysis (OBL001–005) is ported into `own-analysis`, its typed values come from the ONE grammar in `own-ir` that the strict door already delegated to, an analysis-level fact-parity family freezes every violation member with zero Python, the bridge maps BR-P3 in its BR-V1 place, and both protocol documents are promoted out of the exclusion ledger without regenerating either golden; what remains for #259's final acceptance is the coordinate-domain decision. Every count is generated: `docs/generated/p022-cp4-census.md`, `docs/generated/p022-cp5-inventory.md` and `docs/generated/p022-cp4b-mutations.md`); step 7a shadow-mode INFRASTRUCTURE **complete for everything the row listed as sliceable now** — checkpoints 1–4 (`ownlang/repro.py` + `own-shadow`: canonical same-input `OwnIR` identity, the reproduction-artifact format, the engine protocol, the `AnalysisTrace` (#269) with stable-ID normalization, and first-divergence reduction over the lowered/MOS layers); the corpus digest-pinned and re-hashed with zero Python, its artifacts, traces and reductions reproduced byte-for-byte, every campaign fully caught, and the computed divergence classification zero on every axis over the lowered+MOS scope, with the declared-boundary status observations named — all of it counted in `docs/generated/p022-shadow-census.md` and `docs/generated/p022-shadow-mutations.md`, never here; the findings are recorded and closed as contract decisions. NOT shadow mode and not parity: the reducer REFUSES the verdict layer and records the refusal, since acceptance compares end diagnostics and stays blocked by #259, and #260's raw-byte same-input invariant is not proved either — cp1 establishes shared CANONICAL document identity, which is the weaker claim; three departures from the slice's brief (checkpoint grouping, the `-0` domain narrowing, `sha2`) are ratified in [the owner-decision ledger](../notes/p022-shadow-infra-owner-decisions.md); Python authoritative until cutover | +| [P-022](P-022-rust-core-migration.md) | Rust core migration: crate DAG, patterns, prior art, differential oracle (Python = golden) | in execution — steps 0–4 built (#214/#249); step 5a done (full diagnostic contract, #255 via #319/#320/#321); step 5b SARIF done (#256; `.ownreport.json` struck — a buffer report needing the AST, not a diagnostics surface); step 6a done (`spec/Bridge.md`, #258); step 6b complete at final acceptance (`own-lowered`/`own-bridge`, #259: lowering and MOS parity landed; strict-door validation complete with no known divergence — the first 0/0/0 proved to be the ledger agreeing with its own author, and the second omitted two families that a Python-first defensive-limit change (#326) had to close before the third could measure them; analysis wiring complete at the checkpoint-4 surface — `check_facts` through the real analyses, Layer 3 goldens built, with an executable exclusion ledger naming each declared boundary; **cp5 complete at its surface** — the replay compares EVERY `Finding` member (the BR-V4 wording matrix and the BR-V5 evidence slices included) and every refusal in full, and a second fixture family freezes the BR-V9 rendered surfaces byte for byte, all against goldens none of which was regenerated; **row 4b complete** — the obligation-protocol analysis (OBL001–005) is ported into `own-analysis`, its typed values come from the ONE grammar in `own-ir` that the strict door already delegated to, an analysis-level fact-parity family freezes every violation member with zero Python, the bridge maps BR-P3 in its BR-V1 place, and both protocol documents are promoted out of the exclusion ledger without regenerating either golden; **#259 final acceptance reached** — the last thing it owed was the coordinate-domain decision, and that landed Python-first: `spec/OwnIR.md` §4.2 bounds every `line` to `[0, 2147483647]` and every `column` to `[1, 2147483647]` (int32 is the line type of every consumer this project feeds; `0` stays legal as the reference's own absent sentinel), every line-bearing field is validated including the two §4.2 recorded as checked nowhere, the tolerant door degrades an out-of-domain coordinate rather than clamping it, the Rust door and bridge mirror all of it, and the four `verdict_boundary_*` controls are promoted out of the exclusion ledger — which now names only the two #294 OD-1 door controls, a declared boundary rather than open work. Not shadow mode, which is #260's acceptance. Every count is generated: `docs/generated/p022-cp1-census.md`, `docs/generated/p022-cp4-census.md`, `docs/generated/p022-coord-census.md`, `docs/generated/p022-cp5-inventory.md`, `docs/generated/p022-cp4b-mutations.md` and `docs/generated/p022-coord-mutations.md`); step 7a shadow-mode INFRASTRUCTURE **complete for everything the row listed as sliceable now** — checkpoints 1–4 (`ownlang/repro.py` + `own-shadow`: canonical same-input `OwnIR` identity, the reproduction-artifact format, the engine protocol, the `AnalysisTrace` (#269) with stable-ID normalization, and first-divergence reduction over the lowered/MOS layers); the corpus digest-pinned and re-hashed with zero Python, its artifacts, traces and reductions reproduced byte-for-byte, every campaign fully caught, and the computed divergence classification zero on every axis over the lowered+MOS scope, with the declared-boundary status observations named — all of it counted in `docs/generated/p022-shadow-census.md` and `docs/generated/p022-shadow-mutations.md`, never here; the findings are recorded and closed as contract decisions. NOT shadow mode and not parity: the reducer REFUSES the verdict layer and records the refusal, since acceptance compares end diagnostics and crossing that line is this step's own decision (#259's final acceptance is reached), and #260's raw-byte same-input invariant is not proved either — cp1 establishes shared CANONICAL document identity, which is the weaker claim; three departures from the slice's brief (checkpoint grouping, the `-0` domain narrowing, `sha2`) are ratified in [the owner-decision ledger](../notes/p022-shadow-infra-owner-decisions.md); Python authoritative until cutover | | [P-023](P-023-architecture-guard.md) | Architecture guard (`Own.Arch`): rules.yaml intent model + dependency-graph gate + baseline ratchet | draft | | [P-024](P-024-security-audit-profile.md) | Security audit profile (external tools + SARIF adapters; rejects own scanner engine) | draft | | [P-025](P-025-obligation-protocols.md) | Obligation protocols (`Own.Protocols`): barrier-sensitive project invariants (OBL001–005) | first slice built (core + bridge + fixtures; extractor pending) | diff --git a/ownlang/obligations.py b/ownlang/obligations.py index 747a5e5d..14a6f516 100644 --- a/ownlang/obligations.py +++ b/ownlang/obligations.py @@ -74,6 +74,31 @@ INT64_MIN = -(2 ** 63) INT64_MAX = 2 ** 63 - 1 +# The source-coordinate DOMAIN, inside the representable form above. +# +# Representability and domain are two axes, and keeping them apart is what the +# #259 cp1 taxonomy is built on: a value outside the signed-64 range has no +# integer form the contract can hold (a shape failure), while a value inside it +# and outside the domain IS a coordinate and violates the rule about what a +# coordinate may be (a location failure). +# +# int32 is the domain because it is the line type of every consumer this +# project feeds: Roslyn's `LinePosition.Line`, LSP's `uinteger` (capped at +# 2^31-1), .NET diagnostics. A coordinate wider than that cannot reach the +# place it points at. `0` stays legal at the bottom because it is the +# reference's OWN default for an absent line (`raw.get("line", 0)` here and +# `s.get("line", 0)` throughout `load()`) — bounding at 0 rather than at 1 +# reads the reference rather than tightening it. A negative line is refused: no +# producer emits one (the Roslyn extractor writes `StartLinePosition.Line + 1`) +# and nothing downstream can point at it. +LINE_MIN = 0 +LINE_MAX = 2 ** 31 - 1 + +# A column is 1-based (spec/OwnIR.md §4.1), so its domain starts one higher — +# same upper bound, for the same reason. +COLUMN_MIN = 1 +COLUMN_MAX = 2 ** 31 - 1 + # Maximum nesting of `if`/`while` bodies in a flow body or an event tree. # # Chosen by measurement, from both ends: @@ -235,7 +260,21 @@ def _require_str(raw: dict[str, Any], key: str, ctx: str) -> str: return v -def _opt_line(raw: dict[str, Any], ctx: str) -> int: +def _opt_line(raw: dict[str, Any], ctx: str, strict: bool = True) -> int: + """An event's source line, under the two-doors rule (spec/OwnIR.md §4.2). + + Type and representability are the grammar's and always fail loud: a `line` + that is not an integer, or has no signed-64 form, is a malformed event and + the entry is not parseable at all. + + The DOMAIN is the door's. `load()` parses with `strict=True` and a line + outside `[0, 2^31-1]` is refused, exactly as every other validated line is; + the tolerant `check_facts()` path parses with `strict=False` and DEGRADES + it to `0` — absent — the same contract `_as_col` has had for columns since + #317. Degrading rather than refusing matters here more than elsewhere: a + refusal on this path drops the whole method's events, so one impossible + coordinate would silently take a real violation with it. + """ v = raw.get("line", 0) if not isinstance(v, int) or isinstance(v, bool): raise ProtocolFactsError(f"{ctx}: 'line' must be an integer, got {v!r}") @@ -243,7 +282,13 @@ def _opt_line(raw: dict[str, Any], ctx: str) -> int: raise ProtocolFactsError( f"{ctx}: 'line' must fit a signed 64-bit integer, got {v} " f"(spec/OwnIR.md §4.2)") - return v + if LINE_MIN <= v <= LINE_MAX: + return v + if not strict: + return 0 + raise ProtocolFactsError( + f"{ctx}: 'line' must be a source line in [{LINE_MIN}, {LINE_MAX}], " + f"got {v} (spec/OwnIR.md §4.2)") def parse_matcher(raw: Any, ctx: str, require_value: bool = False) -> Matcher: @@ -327,14 +372,18 @@ def parse_protocol(raw: Any) -> Protocol: methods=tuple(methods_raw), description=desc) -def parse_events(raw: Any, ctx: str, depth: int = 0) -> tuple[Event, ...]: +def parse_events(raw: Any, ctx: str, depth: int = 0, strict: bool = True) -> tuple[Event, ...]: """Parse an ordered event list (recursive over `if`/`while`), fail-loud on an unknown `ev` — the same rule as an unknown flow op (OwnIR IR4). `depth` counts enclosing `if`/`while` bodies; the top-level list is 0. The bound is a defensive limit on external input (spec/OwnIR.md §4.2), not a reachable property of real code — the deepest event tree in this - repository's fixtures is 2.""" + repository's fixtures is 2. + + `strict` selects which door's coordinate contract an event `line` is read + under — see `_opt_line`. It travels the recursion so a nested event is + read under the same contract as a top-level one.""" if depth > MAX_NESTING_DEPTH: raise ProtocolFactsError( f"{ctx}: events nested deeper than {MAX_NESTING_DEPTH} levels " @@ -350,7 +399,7 @@ def parse_events(raw: Any, ctx: str, depth: int = 0) -> tuple[Event, ...]: raise ProtocolFactsError( f"{ctx}: unknown protocol event {ev!r} — the vocabulary is " f"{sorted(EVENT_KINDS)} (spec/OwnIR.md §8)") - line = _opt_line(e, ctx) + line = _opt_line(e, ctx, strict) if ev == "assign": target = _require_str(e, "target", f"{ctx} assign") value = e.get("value") @@ -371,24 +420,31 @@ def parse_events(raw: Any, ctx: str, depth: int = 0) -> tuple[Event, ...]: elif ev == "throw": out.append(ThrowEv(line=line)) elif ev == "if": - out.append(IfEv(line=line, - then=parse_events(e.get("then", []), ctx, depth + 1), - orelse=parse_events(e.get("else", []), ctx, depth + 1))) + out.append(IfEv( + line=line, + then=parse_events(e.get("then", []), ctx, depth + 1, strict), + orelse=parse_events(e.get("else", []), ctx, depth + 1, strict))) else: # "while" — EVENT_KINDS is closed, checked above out.append(WhileEv( - line=line, body=parse_events(e.get("body", []), ctx, depth + 1))) + line=line, + body=parse_events(e.get("body", []), ctx, depth + 1, strict))) return tuple(out) -def parse_method(raw: Any) -> MethodEvents: - """Parse one `protocol_functions[]` entry.""" +def parse_method(raw: Any, strict: bool = True) -> MethodEvents: + """Parse one `protocol_functions[]` entry. + + `strict` is the door (see `_opt_line`): `load()` passes the default and an + out-of-domain event line is refused; the tolerant `check_facts()` path + passes `False` and it degrades to `0`.""" if not isinstance(raw, dict): raise ProtocolFactsError(f"a protocol function must be an object, got {raw!r}") name = _require_str(raw, "name", "protocol function") file = raw.get("file", "?") if not isinstance(file, str): raise ProtocolFactsError(f"protocol function '{name}': 'file' must be a string") - events = parse_events(raw.get("events", []), f"protocol function '{name}'") + events = parse_events(raw.get("events", []), f"protocol function '{name}'", + strict=strict) return MethodEvents(name=name, file=file, events=events) diff --git a/ownlang/ownir.py b/ownlang/ownir.py index 02273693..2ed07b28 100644 --- a/ownlang/ownir.py +++ b/ownlang/ownir.py @@ -148,8 +148,12 @@ from .effects import find_effect_storms from .evidence import code_flow, di_path_steps from .obligations import ( + COLUMN_MAX, + COLUMN_MIN, INT64_MAX, INT64_MIN, + LINE_MAX, + LINE_MIN, MAX_NESTING_DEPTH, MethodEvents, Protocol, @@ -539,14 +543,22 @@ def build_sarif(findings: list[Finding], severity: str = "error") -> dict[str, A } -def _check_int_range(v: int, where: str, field: str = "line") -> None: - """The representable range of an OwnIR source coordinate (spec/OwnIR.md §4.2). +def _check_representable(v: int, where: str, field: str = "line") -> None: + """The representable FORM of an OwnIR source coordinate (spec/OwnIR.md §4.2). Python integers are unbounded; a consumer's are not. Without this bound the fact vocabulary is only implementable in a language with bignums, which is a contract accident rather than a decision — and it surfaces downstream as a port rejecting a document the reference accepted. Stated and enforced here - instead, Python-first. + instead, Python-first (#326). + + This is the first of TWO axes and stays exactly what #326 made it: whether + the value has an integer form the contract can hold at all. What that form + is allowed to MEAN is `_check_line_domain` / `_check_column` below. Keeping + them apart is not tidiness — the #259 cp1 taxonomy classifies a failure by + its mechanism, and folding the two would report "no representable form" and + "outside the coordinate domain" as one category again, which is the exact + defect that census had to discover. """ if not INT64_MIN <= v <= INT64_MAX: raise OwnIRError( @@ -554,8 +566,28 @@ def _check_int_range(v: int, where: str, field: str = "line") -> None: f"(spec/OwnIR.md §4.2)") +def _check_line_domain(v: int, where: str, field: str = "line") -> None: + """The DOMAIN of a source line: `[0, 2^31-1]` (spec/OwnIR.md §4.2). + + int32 is the line type of every consumer this project feeds — Roslyn's + `LinePosition.Line`, LSP's `uinteger`, .NET diagnostics — so a line outside + it cannot reach the place it points at. `0` is legal and means "unknown / + file-level": it is this module's own default for an absent line, so the + bottom of the domain reads the reference rather than tightening it. A + negative line is refused; no producer emits one. + + `load()` is fail-loud; `check_facts()` on un-validated facts degrades to `0` + through `_as_line`. Two entry points, two contracts, as with `column`. + """ + _check_representable(v, where, field) + if not LINE_MIN <= v <= LINE_MAX: + raise OwnIRError( + f"{where} {field!r} must be a source line in " + f"[{LINE_MIN}, {LINE_MAX}], got {v} (spec/OwnIR.md §4.2)") + + def _check_column(v: Any, where: str) -> None: - """Fail-loud shape check for an optional source `column` (#317). + """Fail-loud shape check for an optional source `column` (#317, §4.2). A column is a 1-based coordinate or it is absent. `0` is rejected rather than treated as "unknown": SARIF columns start at 1, so a 0 is a producer bug, and @@ -569,21 +601,35 @@ def _check_column(v: Any, where: str) -> None: """ if v is None: return - if isinstance(v, bool) or not isinstance(v, int) or v < 1: + if isinstance(v, bool) or not isinstance(v, int) or v < COLUMN_MIN: raise OwnIRError( f"{where} 'column' must be a 1-based integer or absent, got {v!r}") - # …and bounded above, for the same reason `line` is (spec/OwnIR.md §4.2): - # a column no consumer can represent is not a usable coordinate. - _check_int_range(v, where, "column") + # …then the same two axes a line carries: a representable form, and a + # domain inside it. A column no consumer can hold is not a usable + # coordinate, whichever of the two bounds it passes (spec/OwnIR.md §4.2). + _check_representable(v, where, "column") + if v > COLUMN_MAX: + raise OwnIRError( + f"{where} 'column' must be a source column in " + f"[{COLUMN_MIN}, {COLUMN_MAX}], got {v} (spec/OwnIR.md §4.2)") -def _check_flow_columns(nodes: Any, where: str, depth: int = 0) -> None: - """Validate `column` on every flow op, including inside `if`/`while` bodies, - and bound how deeply those bodies may nest. +def _check_flow_coordinates(nodes: Any, where: str, depth: int = 0) -> None: + """Validate `line` and `column` on every flow op, including inside + `if`/`while` bodies, and bound how deeply those bodies may nest. Recursive because a hoisted branch acquire - the path most likely to be forgotten - lives inside a nested body, not at the top level. + `line` joined `column` here in #259's final acceptance. Until then a flow + op's `line` was checked NOWHERE — not for range and not even for type — and + §4.2 recorded that as an open contract question rather than a parity gap, + because both implementations agreed about it. The coordinate-domain + decision answers the question: a coordinate no rule reads must still be + well-formed, because the tolerant door DOES read it and anchors findings on + it. So the walk that already existed for columns now carries both, in the + record's own order — line, then column, on the same node (§4.1). + `depth` counts enclosing bodies; the top-level list is 0. The bound is a defensive limit on external input (spec/OwnIR.md §4.2), not a property real code reaches - the deepest flow body in this repository's fixtures is 3.""" @@ -601,9 +647,14 @@ def _check_flow_columns(nodes: Any, where: str, depth: int = 0) -> None: for n in nodes: if not isinstance(n, dict): continue - _check_column(n.get("column"), f"{where} op {n.get('op')!r}") + what = f"{where} op {n.get('op')!r}" + ln = n.get("line", 0) + if not isinstance(ln, int) or isinstance(ln, bool): + raise OwnIRError(f"{what} 'line' must be an integer, got {ln!r}") + _check_line_domain(ln, what) + _check_column(n.get("column"), what) for key in ("then", "else", "body"): - _check_flow_columns(n.get(key), where, depth + 1) + _check_flow_coordinates(n.get(key), where, depth + 1) def load(path: str) -> dict[str, Any]: @@ -654,8 +705,17 @@ def load(path: str) -> dict[str, Any]: raise OwnIRError( f"unknown resource kind {r!r} — a new kind is a vocabulary " f"change that must bump OWNIR_VERSION (see spec/OwnIR.md §2)") - # The source COLUMN of the same node `line` anchors on (#317). Optional - # and additive: absent on every fact an older extractor emitted. + # The record's own source coordinate — line first, then the COLUMN + # of the same node it anchors on (#317, §4.1). The line was checked + # NOWHERE until #259's final acceptance, not even for type: §4.2 + # recorded it as an open contract question, and the + # coordinate-domain decision answers it. Both are optional and + # additive: absent on every fact an older extractor emitted. + sln = s.get("line", 0) + if not isinstance(sln, int) or isinstance(sln, bool): + raise OwnIRError( + f"subscription 'line' must be an integer, got {sln!r}") + _check_line_domain(sln, "subscription") _check_column(s.get("column"), "subscription") t = s.get("type") if t is not None and not isinstance(t, str): @@ -714,14 +774,14 @@ def load(path: str) -> dict[str, Any]: ln = s.get("line", 0) if not isinstance(ln, int) or isinstance(ln, bool): raise OwnIRError("service 'line' must be an integer") - _check_int_range(ln, "service") + _check_line_domain(ln, "service") # the consuming-constructor location (optional, P-006 Q#1) is validated like file/line. if not isinstance(s.get("ctor_file", "?"), str): raise OwnIRError("service 'ctor_file' must be a string") cln = s.get("ctor_line", 0) if not isinstance(cln, int) or isinstance(cln, bool): raise OwnIRError("service 'ctor_line' must be an integer") - _check_int_range(cln, "service", "ctor_line") + _check_line_domain(cln, "service", "ctor_line") if not isinstance(s.get("ctor_type", ""), str): raise OwnIRError("service 'ctor_type' must be a string") # DI004 call-site metadata (optional): an array of {type, file, line} objects. @@ -735,7 +795,7 @@ def load(path: str) -> dict[str, Any]: "service 'root_resolve_sites' must be an array of " "{type:str, file:str, line:int} objects") for site in sites: - _check_int_range(site.get("line", 0), "service root_resolve_site") + _check_line_domain(site.get("line", 0), "service root_resolve_site") # DI005 (scope-cached captive): types resolved from a self-created scope and cached # into a field, plus their field-store sites — validated like root_resolves / its sites. scope_cached = s.get("scope_cached", []) @@ -752,7 +812,7 @@ def load(path: str) -> dict[str, Any]: "service 'scope_cache_sites' must be an array of " "{type:str, file:str, line:int} objects") for site in csites: - _check_int_range(site.get("line", 0), "service scope_cache_site") + _check_line_domain(site.get("line", 0), "service scope_cache_site") # Optional reactive-effect graph (EFF001 — effect storm, P-020). Additive and # optional: an older core simply ignores it. Each effect carries its render-scope # binding table; the core (ownlang/effects.py) decides identity stability. @@ -769,7 +829,7 @@ def load(path: str) -> dict[str, Any]: eln = eff.get("line", 0) if not isinstance(eln, int) or isinstance(eln, bool): raise OwnIRError("effect 'line' must be an integer") - _check_int_range(eln, "effect") + _check_line_domain(eln, "effect") binds = eff.get("bindings", []) if not isinstance(binds, list) or not all(isinstance(b, dict) for b in binds): raise OwnIRError("effect 'bindings' must be a JSON array of objects") @@ -784,7 +844,7 @@ def load(path: str) -> dict[str, Any]: bln = b.get("line", 0) if not isinstance(bln, int) or isinstance(bln, bool): raise OwnIRError("binding 'line' must be an integer") - _check_int_range(bln, "binding") + _check_line_domain(bln, "binding") # Optional per-method flow bodies (P-016 B0b/B2 — local IDisposable # acquire/use/release over a CFG). Additive/optional; an older core ignores it. fns = result.get("functions", []) @@ -805,7 +865,7 @@ def load(path: str) -> dict[str, Any]: # and optional — an older core just reads functions without contracts. An # omitted `effect` is INFERRED from the body (v1 contract inference), so the # field is a hint/override, not a requirement. - _check_flow_columns(f.get("body"), "function body") + _check_flow_coordinates(f.get("body"), "function body") ps = f.get("params", []) if not isinstance(ps, list) or not all(isinstance(p, dict) for p in ps): raise OwnIRError("a function's 'params' must be a JSON array of objects") @@ -820,7 +880,7 @@ def load(path: str) -> dict[str, Any]: if not isinstance(pl, int) or isinstance(pl, bool): raise OwnIRError( f"parameter 'line' must be an integer, got {pl!r}") - _check_int_range(pl, "parameter") + _check_line_domain(pl, "parameter") _check_column(p.get("column"), "parameter") peff = p.get("effect") if peff is not None and peff not in _PARAM_EFFECTS: @@ -1076,7 +1136,7 @@ def to_module(facts: dict[str, Any], gid += 1 handles[handle] = {**sub, "component": cname, "file": comp.get("file", "?")} - line = _as_int(sub.get("line", 0)) + line = _as_line(sub.get("line", 0)) params.append(Param(handle, TypeRef("EventSource", False, False, 0), 0, lifetime=region)) body.append(Subscribe(handle, line)) @@ -1119,7 +1179,7 @@ def to_module(facts: dict[str, Any], handles[handle] = {**sub, "component": cname, "file": comp.get("file", "?"), "di_source_life": src_life} - line = _as_int(sub.get("line", 0)) + line = _as_line(sub.get("line", 0)) params.append(Param(handle, TypeRef("EventSource", False, False, 0), 0, lifetime=_DI_REGION[src_life])) @@ -1132,7 +1192,7 @@ def to_module(facts: dict[str, Any], handles[handle] = {**sub, "component": cname, "file": comp.get("file", "?")} rtype, _ = _route_resource(rkind) - line = _as_int(sub.get("line", 0)) + line = _as_line(sub.get("line", 0)) body.append(Let(handle, Acquire(rtype, [], line), line)) if sub.get("released"): body.append(Release(handle, line)) @@ -1331,7 +1391,7 @@ def walk(ns: Any) -> None: if ps is not None and ps.transfer in (Transfer.MAY, Transfer.UNKNOWN): out.append((str(a), callee, ps.transfer.value, - _as_int(n.get("line", 0)))) + _as_line(n.get("line", 0)))) elif op == "if": walk(n.get("then")) walk(n.get("else")) @@ -2221,7 +2281,7 @@ def _lower_fn_params(fn: dict[str, Any], ffile: str, fname: str, tref = TypeRef("int", False, False) # a plain (non-owned) parameter sym = f"parg_{loc[0]}" loc[0] += 1 - line = _as_int(p.get("line", 0)) + line = _as_line(p.get("line", 0)) localmap[cname] = sym handles[sym] = {"file": ffile, "line": line, "column": _as_col(p.get("column")), "event": cname, @@ -2334,7 +2394,7 @@ def walk(ns: Any, depth: int, in_loop: bool) -> None: if not isinstance(n, dict): continue op = n.get("op") - line = _as_int(n.get("line", 0)) + line = _as_line(n.get("line", 0)) acq = (str(n.get("var", "?")) if op == "acquire" else fresh_result(n) if op == "call" else None) if acq is not None: @@ -2421,7 +2481,7 @@ def _lower_flow(nodes: list[Any], ffile: str, fname: str, if not isinstance(n, dict): continue op = n.get("op") - line = _as_int(n.get("line", 0)) + line = _as_line(n.get("line", 0)) if op == "acquire": name = str(n.get("var", "?")) if name in hoisted: @@ -2658,7 +2718,7 @@ def _flow_local_steps(sub: dict[str, Any], code: str, dline: int, point (the acquire itself) and gets no flow. Empty when a line is unknown or the two sites coincide (then the primary location already says it all).""" viol = _FLOW_LOCAL_VIOLATION.get(code) - acq = _as_int(sub.get("line", 0)) + acq = _as_line(sub.get("line", 0)) if viol is None or acq < 1 or dline < 1 or dline == acq: return () f = str(sub.get("file", "?")) @@ -2734,7 +2794,7 @@ def check_facts(facts: dict[str, Any]) -> list[Finding]: # registration site of each DI service, to anchor a subscription-escape slice's # source hop: the injected event SOURCE is a registered service (P-006 + P-004), so # the reachability slice can point at where that longer-lived source was registered. - svc_loc = {str(s.get("name", "")): (str(s.get("file", "?")), _as_int(s.get("line", 0))) + svc_loc = {str(s.get("name", "")): (str(s.get("file", "?")), _as_line(s.get("line", 0))) for s in (facts.get("services") or []) if isinstance(s, dict)} findings: list[Finding] = [] @@ -2839,7 +2899,7 @@ def check_facts(facts: dict[str, Any]) -> list[Finding]: "OWN009": f"IDisposable local '{name}' may be used after disposal on some path", }.get(d.code, f"IDisposable local '{name}': {d.message}") findings.append(Finding( - file=sub["file"], line=_as_int(sub.get("line", 0)), + file=sub["file"], line=_as_line(sub.get("line", 0)), column=_as_col(sub.get("column")), code=d.code, component=component, event=name, handler="", message=msg, kind="pooled buffer" if pool else "disposable", @@ -2872,7 +2932,7 @@ def check_facts(facts: dict[str, Any]) -> list[Finding]: # reachability slice: the subscribe site -> where the longer-lived source was # registered (its lifetime is why this escapes). The source hop is present only # when the registration site is known from the services graph. - sub_ln = _as_int(sub.get("line", 0)) + sub_ln = _as_line(sub.get("line", 0)) esc_flow: tuple[tuple[str, int, str], ...] = () if sub_ln >= 1: steps = [(str(sub["file"]), sub_ln, @@ -2885,7 +2945,7 @@ def check_facts(facts: dict[str, Any]) -> list[Finding]: if len(steps) >= 2: esc_flow = tuple(steps) findings.append(Finding( - file=sub["file"], line=_as_int(sub.get("line", 0)), + file=sub["file"], line=_as_line(sub.get("line", 0)), column=_as_col(sub.get("column")), code=d.code, component=component, event=event, handler=handler, message=message, kind="subscription token", flow=esc_flow, @@ -2915,7 +2975,7 @@ def check_facts(facts: dict[str, Any]) -> list[Finding]: f"lifetime, so it can never be collected — a region escape " f"(leak, no release path{lam})") findings.append(Finding( - file=sub["file"], line=_as_int(sub.get("line", 0)), + file=sub["file"], line=_as_line(sub.get("line", 0)), column=_as_col(sub.get("column")), code=d.code, component=component, event=event, handler=handler, message=message, kind="subscription token", ignore_reason=ir)) @@ -2979,7 +3039,7 @@ def check_facts(facts: dict[str, Any]) -> list[Finding]: f"but never unsubscribed — the source keeps " f"'{component}' alive (leak{lam})") findings.append(Finding( - file=sub["file"], line=_as_int(sub.get("line", 0)), + file=sub["file"], line=_as_line(sub.get("line", 0)), column=_as_col(sub.get("column")), code=d.code, component=component, event=event, handler=handler, message=message, kind=kind, severity=fsev, ignore_reason=ir)) @@ -3054,11 +3114,25 @@ def check_facts(facts: dict[str, Any]) -> list[Finding]: return findings -def _as_int(v: Any) -> int: - """A non-throwing int coercion: load() already validates `line`, but - check_facts may be called directly (tests, embedders) on un-validated facts, - so a bad `line` degrades to 0 rather than raising a bare ValueError.""" - return v if isinstance(v, int) and not isinstance(v, bool) else 0 +def _as_line(v: Any) -> int: + """A non-throwing line coercion: `load()` already validates every `line`, + but `check_facts` may be called directly (tests, embedders) on un-validated + facts, so a line outside the §4.2 domain degrades to 0 — "unknown / + file-level", the same value an absent line reads as — rather than raising a + bare ValueError or anchoring a finding at a coordinate nothing can point at. + + Degrade, never clamp: `2**31` does not become `2**31 - 1`, and `-1` does + not become `1`. A clamp would move the finding to a REAL line that is not + the one the producer meant, which is worse than saying nothing — the same + never-invent rule `_as_col` has carried for columns since #317. + + This is the tolerant half of the two-entry-point contract in §4.2. The + strict door never reaches it: a document `load()` accepts has no + out-of-domain coordinate by construction, which + `tests/test_ownir_defensive_limits.py` asserts over the cp1 ledger rather + than stating in prose.""" + return (v if isinstance(v, int) and not isinstance(v, bool) + and LINE_MIN <= v <= LINE_MAX else 0) def _as_col(v: Any) -> int | None: @@ -3066,8 +3140,10 @@ def _as_col(v: Any) -> int | None: column; `check_facts` may be called directly on un-validated facts, so anything that is not a real 1-based coordinate degrades to None — absent, never invented. `bool` is excluded explicitly: `True` is an `int` in Python and would otherwise - read as column 1.""" - return v if isinstance(v, int) and not isinstance(v, bool) and v >= 1 else None + read as column 1. The upper bound is the §4.2 domain's, so the tolerant door + cannot emit a column the strict door would refuse.""" + return (v if isinstance(v, int) and not isinstance(v, bool) + and COLUMN_MIN <= v <= COLUMN_MAX else None) def _consumer_related(c: Any) -> tuple[tuple[str, int, str], ...]: @@ -3129,7 +3205,7 @@ def _resolve_sites(raw: Any) -> tuple[tuple[str, str, int], ...]: if not isinstance(raw, list): return () return tuple( - (str(x.get("type", "")), str(x.get("file", "?")), _as_int(x.get("line", 0))) + (str(x.get("type", "")), str(x.get("file", "?")), _as_line(x.get("line", 0))) for x in raw if isinstance(x, dict) ) @@ -3155,11 +3231,11 @@ def _di_findings(facts: dict[str, Any]) -> list[Finding]: # type from a non-extractor producer must not coerce to a disposable=True. disposable=s.get("disposable") is True, file=str(s.get("file", "?")), - line=_as_int(s.get("line", 0)), + line=_as_line(s.get("line", 0)), # the consuming constructor's location (where the capture is injected) — a # secondary anchor distinct from the registration site above (P-006 Q#1). ctor_file=str(s.get("ctor_file", "?")), - ctor_line=_as_int(s.get("ctor_line", 0)), + ctor_line=_as_line(s.get("ctor_line", 0)), # the IMPLEMENTATION type owning that ctor — named in the finding instead of the # (possibly interface) service name, which would point at a ctor-less type (Codex). ctor_type=str(s.get("ctor_type", "")), @@ -3275,7 +3351,7 @@ def _effect_findings(facts: dict[str, Any]) -> list[Finding]: break bindings.append(EffectBinding( name=str(b.get("name", "?")), init=str(b.get("init", "unknown")), - refs=tuple(refs), line=_as_int(b.get("line", 0)))) + refs=tuple(refs), line=_as_line(b.get("line", 0)))) if malformed: continue effects.append(ReactEffect( @@ -3284,7 +3360,7 @@ def _effect_findings(facts: dict[str, Any]) -> list[Finding]: io=io, bindings=tuple(bindings), file=str(e.get("file", "?")), - line=_as_int(e.get("line", 0)), + line=_as_line(e.get("line", 0)), )) out: list[Finding] = [] for s in find_effect_storms(effects): @@ -3362,7 +3438,12 @@ def _protocol_findings(facts: dict[str, Any]) -> list[Finding]: methods: list[MethodEvents] = [] for fraw in raw_fns: try: - methods.append(parse_method(fraw)) + # strict=False: this is the tolerant door, so an event `line` + # outside the §4.2 domain degrades to 0 rather than making the + # whole method unparseable and taking its real violations with it. + # Type and representability still fail loud and still skip the + # entry — those are grammar, not coordinate domain. + methods.append(parse_method(fraw, strict=False)) except ProtocolFactsError: continue if not protocols: @@ -3430,7 +3511,7 @@ def _unresolved_findings(facts: dict[str, Any]) -> list[Finding]: f"unresolved reference (build the project or pass " f"references); leakage analysis skipped") out.append(Finding( - file=cfile, line=_as_int(sub.get("line", 0)), + file=cfile, line=_as_line(sub.get("line", 0)), column=_as_col(sub.get("column")), code="OWN050", component=cname, event=event, handler=handler, message=message, kind="unresolved reference", advisory=True)) diff --git a/pyproject.toml b/pyproject.toml index e13bc4ab..38c20d75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,10 @@ select = ["E", "W", "F", "I", "B", "UP", "C4", "RUF"] python_version = "3.11" files = [ "ownlang", + "tests/coordinate_census.py", "tests/verdict_census.py", "tests/verdict_render_census.py", + "tests/validation_census.py", "tests/verdict_surface_inventory.py", "tests/test_obligation_fact_parity.py", "scripts/mutate_campaign.py", diff --git a/rust/crates/own-analysis/src/obligation.rs b/rust/crates/own-analysis/src/obligation.rs index 2b1b2ade..6b59cf02 100644 --- a/rust/crates/own-analysis/src/obligation.rs +++ b/rust/crates/own-analysis/src/obligation.rs @@ -420,7 +420,7 @@ pub fn unmatched_scopes<'a>( )] mod tests { use super::{check_protocols, unmatched_scopes, Violation}; - use own_ir::protocol::{parse_method, parse_protocol, MethodEvents, Protocol}; + use own_ir::protocol::{parse_method, parse_protocol, Door, MethodEvents, Protocol}; use serde_json::json; /// The canonical test protocol, in the document shape the grammar takes: @@ -441,7 +441,11 @@ mod tests { } fn method(events: &serde_json::Value) -> MethodEvents { - parse_method(&json!({"name": "Ns.VM.Load", "file": "VM.cs", "events": events})).unwrap() + parse_method( + &json!({"name": "Ns.VM.Load", "file": "VM.cs", "events": events}), + Door::Strict, + ) + .unwrap() } fn codes(vs: &[Violation]) -> Vec<(&'static str, bool, i64)> { diff --git a/rust/crates/own-analysis/tests/obligation_parity.rs b/rust/crates/own-analysis/tests/obligation_parity.rs index ffc03496..7fa3ac6d 100644 --- a/rust/crates/own-analysis/tests/obligation_parity.rs +++ b/rust/crates/own-analysis/tests/obligation_parity.rs @@ -31,7 +31,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use own_analysis::{check_protocols, unmatched_scopes}; -use own_ir::protocol::{parse_method, parse_protocol, MethodEvents, Protocol}; +use own_ir::protocol::{parse_method, parse_protocol, Door, MethodEvents, Protocol}; use serde_json::Value; const FIXTURE: &str = concat!( @@ -167,7 +167,7 @@ fn obligation_fact_parity() { let methods: Vec = documents(case, "methods") .iter() .map(|m| { - parse_method(m).unwrap_or_else(|e| { + parse_method(m, Door::Strict).unwrap_or_else(|e| { panic!("{name}: the port refuses a method Python accepted: {e}") }) }) @@ -247,7 +247,7 @@ fn the_walk_is_deterministic() { .collect(); let methods: Vec = documents(case, "methods") .iter() - .map(|m| parse_method(m).expect("a method the reference accepted")) + .map(|m| parse_method(m, Door::Strict).expect("a method the reference accepted")) .collect(); assert_eq!( check_protocols(&protocols, &methods), diff --git a/rust/crates/own-bridge/src/ast.rs b/rust/crates/own-bridge/src/ast.rs index b8b90465..f57a168a 100644 --- a/rust/crates/own-bridge/src/ast.rs +++ b/rust/crates/own-bridge/src/ast.rs @@ -11,14 +11,20 @@ //! composes the checkpoint-2 evidence instead of re-deriving it: a lowering //! bug is visible at the Layer 2 seam before it can hide behind a verdict. //! -//! **The one representability boundary lives here.** The core's line domain -//! is `u32` (a parser-derived position), while a fact coordinate on the -//! tolerant door is any integer the reference's `_as_int` passes through, and -//! even the strict door admits every signed 64-bit value (`spec/OwnIR.md` -//! §4.2). A coordinate outside `0..=u32::MAX` on a lowered node is therefore -//! refused loudly — a Rust-only rejection of a document the reference -//! analyzes, declared and measured as a cp4 divergence family (the verdict -//! fixture ledger pins it), never clamped or silently dropped. +//! **The coordinate boundary that used to live here is gone**, and it is worth +//! saying how rather than just that. The core's line domain is `u32` (a +//! parser-derived position); the reference's was every signed 64-bit integer, +//! so a fact coordinate could be one the core could not hold, and this module +//! refused the document — a declared, measured cp4 divergence family. +//! +//! #259's final acceptance closed it from the reference's side, not from +//! this one: `spec/OwnIR.md` §4.2 now bounds every line to `[0, 2147483647]`, +//! the int32 domain every consumer this project feeds actually has. So a +//! document that passes the strict door is inside `u32` by construction, and +//! the only remaining out-of-domain coordinate arrives through the TOLERANT +//! door — where the reference degrades it to `0` and [`core_line`] does the +//! same. Never "Rust holds `u32`, so the reference is wrong": the contract +//! moved because of what its consumers are, and the port follows it. // `redundant_pub_crate` (nursery) conflicts with the workspace's DENY of // `unreachable_pub` for items in private modules; pub(crate) is the honest @@ -33,18 +39,36 @@ use own_syntax::ast::{ Subscribe, TypeRef, Use, VarRef, While, }; -/// `u32` or refuse: the declared coordinate boundary (see the module docs). -pub(crate) fn core_line(line: i64, what: &str) -> Result { - u32::try_from(line).map_err(|_| { - BridgeError(format!( - "source line {line} on {what} is outside the core's line domain \ - (0..=4294967295): the reference analyzes this coordinate, this core \ - cannot represent it — a declared #259 cp4 divergence family, not a \ - silent clamp (spec/OwnIR.md §4.2 bounds coordinates to signed 64 bits)" - )) - }) +/// A fact coordinate as a core line: in the §4.2 domain, or degraded to `0`. +/// +/// This is the reference's `_as_line` (`ownlang/ownir.py`), member for member: +/// a line outside `[0, 2147483647]` reads as `0` — "unknown / file-level", the +/// value an absent line already reads as — and everything inside it travels +/// unchanged. +/// +/// **Degrade, never clamp.** `2147483648` does not become `2147483647` and +/// `-1` does not become `1`: a clamp moves the finding to a REAL line the +/// producer did not mean, which is worse than saying nothing. That is §4.1's +/// never-invent rule for columns, applied to lines by §4.2. +/// +/// It is INFALLIBLE, and the signature says so. It used to return a +/// `BridgeError` naming the node, so every caller carried a `?` and the whole +/// AST build was fallible for a reason that no longer exists; keeping the +/// `Result` would leave a refusal path the ledger could no longer reach and +/// no test could ever exercise. +pub(crate) fn core_line(line: i64) -> u32 { + match u32::try_from(line) { + Ok(n) if n <= LINE_MAX => n, + _ => 0, + } } +/// The top of the §4.2 line domain as the core holds it. Pinned as a literal +/// beside the `u32` conversion rather than imported: this is the number the +/// core's own type has to agree with, and a constant that moved with the +/// contract could not detect that it stopped. +const LINE_MAX: u32 = 2_147_483_647; + fn type_ref(t: &TypeShape) -> TypeRef { TypeRef { name: t.name.clone(), @@ -77,13 +101,13 @@ fn member_role(role: &str) -> Result { } } -fn param(p: &Param) -> Result { - Ok(own_syntax::ast::Param { +fn param(p: &Param) -> own_syntax::ast::Param { + own_syntax::ast::Param { name: p.handle.clone(), ty: type_ref(&p.type_shape), - line: core_line(p.line, &format!("param '{}'", p.handle))?, + line: core_line(p.line), lifetime: p.lifetime.clone(), - }) + } } fn stmts(body: &[Stmt]) -> Result, BridgeError> { @@ -97,7 +121,7 @@ fn stmt(s: &Stmt) -> Result { resource, line, } => { - let line = core_line(*line, &format!("acquire of '{handle}'"))?; + let line = core_line(*line); AstStmt::Let(Let { name: handle.clone(), rhs: Expr::Acquire(Acquire { @@ -110,29 +134,29 @@ fn stmt(s: &Stmt) -> Result { } Stmt::Release { handle, line } => AstStmt::Release(Release { var: handle.clone(), - line: core_line(*line, &format!("release of '{handle}'"))?, + line: core_line(*line), }), Stmt::Use { handle, line } => AstStmt::Use(Use { var: handle.clone(), - line: core_line(*line, &format!("use of '{handle}'"))?, + line: core_line(*line), }), Stmt::Overspan { handle, line } => AstStmt::Overspan(Overspan { var: handle.clone(), - line: core_line(*line, &format!("overspan of '{handle}'"))?, + line: core_line(*line), }), Stmt::Return { handle, line } => AstStmt::Return(Return { var: handle.clone(), - line: core_line(*line, "return")?, + line: core_line(*line), }), Stmt::AliasJoin { handle, src, line } => AstStmt::AliasJoin(AliasJoin { name: handle.clone(), src: src.clone(), - line: core_line(*line, &format!("alias_join of '{handle}'"))?, + line: core_line(*line), }), Stmt::Call { callee, args, line } => { // Python: `VarRef(localmap.get(a, a), line)` — every argument is a // name reference carrying the CALL's line. - let line = core_line(*line, &format!("call to '{callee}'"))?; + let line = core_line(*line); AstStmt::Call(Call { callee: callee.clone(), args: args @@ -149,7 +173,7 @@ fn stmt(s: &Stmt) -> Result { } Stmt::Subscribe { source, line } => AstStmt::Subscribe(Subscribe { source: source.clone(), - line: core_line(*line, &format!("subscribe to '{source}'"))?, + line: core_line(*line), }), Stmt::If { cond, @@ -160,12 +184,12 @@ fn stmt(s: &Stmt) -> Result { cond_text: cond.clone(), then_body: stmts(then)?, else_body: stmts(r#else)?, - line: core_line(*line, "if")?, + line: core_line(*line), }), Stmt::While { cond, body, line } => AstStmt::While(While { cond_text: cond.clone(), body: stmts(body)?, - line: core_line(*line, "while")?, + line: core_line(*line), }), }) } @@ -173,7 +197,7 @@ fn stmt(s: &Stmt) -> Result { fn function(f: &Function) -> Result { Ok(FnDecl { name: f.name.clone(), - params: f.params.iter().map(param).collect::>()?, + params: f.params.iter().map(param).collect(), ret: f.ret.as_ref().map(type_ref), body: stmts(&f.body)?, line: 0, @@ -186,9 +210,11 @@ fn function(f: &Function) -> Result { /// reference constructs them). /// /// # Errors -/// [`BridgeError`] for a coordinate outside the core's `u32` line domain (the -/// declared boundary above), or a Layer 2 vocabulary value with no core twin -/// (unreachable for a document the lowering itself produced). +/// [`BridgeError`] for a Layer 2 vocabulary value with no core twin +/// (unreachable for a document the lowering itself produced). A coordinate +/// outside the core's line domain is no longer among them — see [`core_line`] +/// and the module docs: §4.2 gave the reference the same domain, and both +/// tolerant doors now degrade rather than refuse. pub(crate) fn to_module(doc: &LoweredDocument) -> Result { let resources = doc .resources @@ -260,3 +286,48 @@ pub(crate) fn to_module(doc: &LoweredDocument) -> Result { lifetimes, }) } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +mod tests { + use super::{core_line, LINE_MAX}; + + /// [`core_line`]'s contract, pinned directly — and the reason it is pinned + /// directly rather than through `check_facts` is worth stating, because a + /// unit test on the function under test is normally the weak kind of + /// evidence this project refuses. + /// + /// It cannot be reached from the outside any more. `lower` reads every + /// fact coordinate through `as_line`, so the Layer 2 document this builds + /// from is already inside the domain, and every out-of-domain value dies + /// one layer earlier. That makes this a SECOND line of defence over an + /// `i64` field wider than the domain — real (nothing in the type stops a + /// future caller handing it one) and unobservable end to end. A mutation + /// campaign proved exactly that: clamping here, or accepting the whole + /// `u32` range again, changed no golden. So the honest control is this + /// one, and the honest record is that it is not an end-to-end control. + #[test] + fn core_line_degrades_the_domain_and_never_clamps() { + assert_eq!( + core_line(0), + 0, + "zero is the domain's bottom, not a degrade" + ); + assert_eq!(core_line(1), 1); + assert_eq!(core_line(i64::from(LINE_MAX)), LINE_MAX, "the top travels"); + // …and one step past each end degrades to ABSENT, not to the edge. + assert_eq!(core_line(-1), 0); + assert_eq!( + core_line(i64::from(LINE_MAX) + 1), + 0, + "never clamped to the top" + ); + assert_eq!( + core_line(i64::from(u32::MAX)), + 0, + "the core's own u32 is not the domain" + ); + assert_eq!(core_line(i64::MIN), 0); + assert_eq!(core_line(i64::MAX), 0); + } +} diff --git a/rust/crates/own-bridge/src/lower.rs b/rust/crates/own-bridge/src/lower.rs index 53ef8d26..24c60260 100644 --- a/rust/crates/own-bridge/src/lower.rs +++ b/rust/crates/own-bridge/src/lower.rs @@ -77,9 +77,13 @@ pub(crate) struct Own051 { } /// `_as_col`: the optional 1-based column, or `None` for anything that is not -/// a real coordinate (absent, `null`, a bool, a float, a string, `0`, negative). +/// a real coordinate (absent, `null`, a bool, a float, a string, `0`, negative, +/// or past the §4.2 domain). The upper bound arrived with the coordinate +/// domain, for the reason the reference states: the tolerant door must not +/// emit a column the strict door would refuse. pub(crate) fn as_col(v: Option<&Value>) -> Option { - v.and_then(Value::as_i64).filter(|c| *c >= 1) + v.and_then(Value::as_i64) + .filter(|c| (1..=2_147_483_647).contains(c)) } // --- Python-semantics helpers ------------------------------------------------ @@ -120,10 +124,27 @@ fn py_repr(v: Option<&Value>) -> String { } } -/// `_as_int`: a non-throwing int coercion (a bool is NOT an int here — serde -/// keeps them distinct, matching Python's explicit bool check). -fn as_int(v: Option<&Value>) -> i64 { - v.and_then(Value::as_i64).unwrap_or(0) +/// `_as_line`: a fact coordinate, or `0` when it is not one. +/// +/// The reference's tolerant line reader (`ownlang/ownir.py::_as_line`), and +/// the reason it is named for the field rather than the type: EVERY call site +/// reads a `line`, and what it applies is the §4.2 coordinate domain, not an +/// integer coercion. A value that is not an integer, is a `bool`, or lies +/// outside `[0, 2147483647]` reads as `0` — "unknown / file-level". +/// +/// Degrade, never clamp. This used to be a plain `as_i64().unwrap_or(0)`, and +/// the difference was unobservable only because the AST build refused any +/// document carrying an out-of-domain coordinate before a finding could be +/// built from one. Removing that refusal made the gap reachable: the reference +/// drops a slice whose acquire line degraded to `0`, and a port reading the raw +/// value would have kept the slice and anchored it at a line nothing can point +/// at. Exactly the cp5 lesson — a comparison surface that gains a member can +/// lose controls. +fn as_line(v: Option<&Value>) -> i64 { + match v.and_then(Value::as_i64) { + Some(n) if (0..=2_147_483_647).contains(&n) => n, + _ => 0, + } } /// `n.get(key)` where a present non-list / absent key reads as empty. @@ -896,7 +917,7 @@ pub(crate) fn build_skeletons(raw_fns: &[Value]) -> Vec { /// call hands an argument to a summarized callee position whose resolved /// transfer is `may`/`unknown`, in the flow walk's pre-order. Two consumers, /// as in the reference: the arg names become the untrack set, and each hit -/// on an owned local becomes an OWN051 advisory (`line` is `_as_int` of the +/// on an owned local becomes an OWN051 advisory (`line` is `as_line` of the /// call's line; the transfer is the `Transfer` enum's wire value). fn unverified_transfer_calls( nodes: &[Value], @@ -920,7 +941,7 @@ fn unverified_transfer_calls( py_str(a), callee.clone(), q.transfer.as_str(), - as_int(n.get("line")), + as_line(n.get("line")), )); } } @@ -1102,7 +1123,7 @@ fn hoisted_branch_locals( for n in nodes { let Some(n) = n.as_object() else { continue }; let op = get_str(n, "op"); - let line = as_int(n.get("line")); + let line = as_line(n.get("line")); let acq: Option = match op { Some("acquire") => Some(str_or(n, "var", "?")), Some("call") => { @@ -1408,7 +1429,7 @@ fn lower_fn_params( }; let sym = format!("parg_{loc}"); *loc = loc.saturating_add(1); - let line = as_int(p.get("line")); + let line = as_line(p.get("line")); localmap.insert(cname.clone(), sym.clone()); handles.push( &sym, @@ -1454,7 +1475,7 @@ fn lower_flow<'v>(ctx: &mut FnCtx<'v, '_>, nodes: &'v [Value]) -> Result { let name = str_or(n, "var", "?"); @@ -1778,7 +1799,7 @@ pub(crate) fn lower_full(facts: &OwnIr) -> Result { &handle, subscription_record(sub, &cname, comp.get("file"), None), )?; - let line = as_int(sub.get("line")); + let line = as_line(sub.get("line")); params.push(Param { handle: handle.clone(), type_shape: TypeShape { @@ -1819,7 +1840,7 @@ pub(crate) fn lower_full(facts: &OwnIr) -> Result { &handle, subscription_record(sub, &cname, comp.get("file"), Some(&src_life)), )?; - let line = as_int(sub.get("line")); + let line = as_line(sub.get("line")); params.push(Param { handle: handle.clone(), type_shape: TypeShape { @@ -1856,7 +1877,7 @@ pub(crate) fn lower_full(facts: &OwnIr) -> Result { spec/OwnIR.md §2)" ))); }; - let line = as_int(sub.get("line")); + let line = as_line(sub.get("line")); body.push(Stmt::Acquire { handle: handle.clone(), resource: rtype.to_owned(), diff --git a/rust/crates/own-bridge/src/render.rs b/rust/crates/own-bridge/src/render.rs index cebf95d0..e3fcfd8a 100644 --- a/rust/crates/own-bridge/src/render.rs +++ b/rust/crates/own-bridge/src/render.rs @@ -271,10 +271,25 @@ fn related_locations(related: &[Step]) -> Vec { fn code_flows(flow: &[Step]) -> Vec { let steps: Vec> = flow .iter() - .filter_map(|(file, line, label)| { - u32::try_from(*line) - .ok() - .map(|line| CoreStep { file, line, label }) + .map(|(file, line, label)| { + // Every builder of a `flow` step reads its line through + // `as_line`, and §4.2 bounds that to `[0, 2147483647]`, so the + // conversion cannot fail. This was a `filter_map` that silently + // DROPPED a step it could not convert — a rule the reference does + // not have, unreachable only because the AST build refused such a + // document before a finding could carry one, and therefore a + // silent divergence from the moment that refusal went away. The + // assert is the tripwire: if a new builder ever skips `as_line`, + // a debug build says so instead of a step going missing. + debug_assert!( + (0..=2_147_483_647).contains(line), + "flow step line {line} is outside the spec/OwnIR.md §4.2 domain" + ); + CoreStep { + file, + line: u32::try_from(*line).unwrap_or(0), + label, + } }) .collect(); code_flow(&steps) diff --git a/rust/crates/own-bridge/src/verdict.rs b/rust/crates/own-bridge/src/verdict.rs index c7f59f5c..78cefd59 100644 --- a/rust/crates/own-bridge/src/verdict.rs +++ b/rust/crates/own-bridge/src/verdict.rs @@ -209,7 +209,7 @@ fn flow_local_steps(rec: &Obj, code: &str, dline: i64, pool: bool) -> Vec let Some(violation) = flow_local_violation(code) else { return Vec::new(); }; - let acquire = as_int(rec.get("line")); + let acquire = as_line(rec.get("line")); if acquire < 1 || dline < 1 || dline == acquire { return Vec::new(); } @@ -296,9 +296,27 @@ fn get_str<'a>(rec: &'a Obj, key: &str) -> Option<&'a str> { rec.get(key).and_then(Value::as_str) } -/// `_as_int`: a non-bool integer or `0`. -fn as_int(v: Option<&Value>) -> i64 { - v.and_then(Value::as_i64).unwrap_or(0) +/// `_as_line`: a fact coordinate, or `0` when it is not one. +/// +/// The reference's tolerant line reader (`ownlang/ownir.py::_as_line`), and +/// the reason it is named for the field rather than the type: EVERY call site +/// reads a `line`, and what it applies is the §4.2 coordinate domain, not an +/// integer coercion. A value that is not an integer, is a `bool`, or lies +/// outside `[0, 2147483647]` reads as `0` — "unknown / file-level". +/// +/// Degrade, never clamp. This used to be a plain `as_i64().unwrap_or(0)`, and +/// the difference was unobservable only because the AST build refused any +/// document carrying an out-of-domain coordinate before a finding could be +/// built from one. Removing that refusal made the gap reachable: the reference +/// drops a slice whose acquire line degraded to `0`, and a port reading the raw +/// value would have kept the slice and anchored it at a line nothing can point +/// at. Exactly the cp5 lesson — a comparison surface that gains a member can +/// lose controls. +fn as_line(v: Option<&Value>) -> i64 { + match v.and_then(Value::as_i64) { + Some(n) if (0..=2_147_483_647).contains(&n) => n, + _ => 0, + } } /// Python truthiness of a present value (absent = falsy). @@ -449,7 +467,7 @@ fn map_core( .get("ignore_reason") .filter(|v| truthy(Some(v))) .map(py_str); - let anchor = as_int(rec.get("line")); + let anchor = as_line(rec.get("line")); let column = as_col(rec.get("column")); if rkind == "flow-local" { let pool = truthy(rec.get("pool")); @@ -635,22 +653,18 @@ fn map_core( Ok(out) } -/// A coordinate handed to `own-analysis` as an anchor: `u32` or refuse (the -/// same declared boundary as the AST lines, `ast::core_line`). -fn anchor_line(line: i64, what: &str) -> Result { - ast::core_line(line, what) -} - -/// A coordinate whose only reader guards on `>= 1` (a DI call/store site, an -/// effect binding's declaration line): a negative value behaves exactly like -/// `0` on every path, so it is folded to `0`; above the domain it would BE -/// read, so that side stays fail-loud. -fn guarded_line(line: i64, what: &str) -> Result { - if line < 0 { - Ok(0) - } else { - ast::core_line(line, what) - } +/// A fact coordinate as a core line, through the one reader the whole bridge +/// shares (`ast::core_line`, the reference's `_as_line`). +/// +/// Two helpers used to live here. `anchor_line` refused a coordinate the core +/// could not hold, and `guarded_line` folded a NEGATIVE one to `0` for the +/// fields whose only reader guards on `>= 1` — a DI call/store site, an effect +/// binding's declaration line — while leaving the upper end fail-loud. §4.2's +/// coordinate domain makes both of them one rule: every out-of-domain line +/// degrades to `0`, at either end, on every field. Two names for one behaviour +/// is how a port grows a second reading of one contract, so there is now one. +fn core_line(line: i64) -> u32 { + ast::core_line(line) } /// `tuple(s.get(key, []))` for the string arrays the strict door types: a @@ -669,19 +683,22 @@ fn str_array(s: &Obj, key: &str, what: &str) -> Result, BridgeError> /// `_resolve_sites`: `(type, file, line)` per dict entry; a non-list reads as /// empty, a non-dict entry is skipped. -fn sites(raw: Option<&Value>, what: &str) -> Result, BridgeError> { +/// +/// Infallible since the coordinate domain landed: the only way this could fail +/// was a site line the core could not hold, and `core_line` degrades that now. +fn sites(raw: Option<&Value>) -> Vec { let Some(Value::Array(items)) = raw else { - return Ok(Vec::new()); + return Vec::new(); }; let mut out = Vec::new(); for x in items.iter().filter_map(Value::as_object) { out.push(( get_or(x, "type", ""), get_or(x, "file", "?"), - guarded_line(as_int(x.get("line")), what)?, + core_line(as_line(x.get("line"))), )); } - Ok(out) + out } /// BR-P1: `services[]` → `di.Service` values with the reference's coercions, @@ -703,18 +720,12 @@ fn di_findings(root: &Obj) -> Result, BridgeError> { // only the JSON boolean `true` counts. disposable: s.get("disposable") == Some(&Value::Bool(true)), file: get_or(s, "file", "?"), - line: anchor_line(as_int(s.get("line")), &what)?, - root_resolve_sites: sites( - s.get("root_resolve_sites"), - &format!("{what} root_resolve_sites"), - )?, + line: core_line(as_line(s.get("line"))), + root_resolve_sites: sites(s.get("root_resolve_sites")), scope_cached: str_array(s, "scope_cached", &what)?, - scope_cache_sites: sites( - s.get("scope_cache_sites"), - &format!("{what} scope_cache_sites"), - )?, + scope_cache_sites: sites(s.get("scope_cache_sites")), ctor_file: get_or(s, "ctor_file", "?"), - ctor_line: guarded_line(as_int(s.get("ctor_line")), &what)?, + ctor_line: core_line(as_line(s.get("ctor_line"))), ctor_type: get_or(s, "ctor_type", ""), name, }); @@ -770,9 +781,9 @@ fn di_findings(root: &Obj) -> Result, BridgeError> { /// BR-P2/BR-D2: `effects[]` re-validated skip-not-coerce, then /// `find_effect_storms` (owned by `own-analysis`) at the effect's own site. -fn effect_findings(root: &Obj) -> Result, BridgeError> { +fn effect_findings(root: &Obj) -> Vec { let Some(Value::Array(raw)) = root.get("effects") else { - return Ok(Vec::new()); + return Vec::new(); }; let mut effects = Vec::new(); 'entries: for e in raw.iter().filter_map(Value::as_object) { @@ -818,7 +829,7 @@ fn effect_findings(root: &Obj) -> Result, BridgeError> { name: get_or(b, "name", "?"), init: get_or(b, "init", "unknown"), refs, - line: guarded_line(as_int(b.get("line")), "effect binding")?, + line: core_line(as_line(b.get("line"))), }); } let component = get_or(e, "component", "?"); @@ -827,11 +838,11 @@ fn effect_findings(root: &Obj) -> Result, BridgeError> { io, bindings, file: get_or(e, "file", "?"), - line: anchor_line(as_int(e.get("line")), &format!("effect in '{component}'"))?, + line: core_line(as_line(e.get("line"))), component, }); } - Ok(effect::find_effect_storms(&effects) + effect::find_effect_storms(&effects) .into_iter() .map(|s| { let mut f = Finding::new(s.file.clone(), i64::from(s.line), "EFF001", "react effect"); @@ -859,7 +870,7 @@ fn effect_findings(root: &Obj) -> Result, BridgeError> { f.event = s.dep; f }) - .collect()) + .collect() } /// `_unresolved_findings`: every `unresolved-subscription` marker as an @@ -881,7 +892,7 @@ fn unresolved_findings(root: &Obj) -> Vec { } let mut f = Finding::new( cfile.clone(), - as_int(sub.get("line")), + as_line(sub.get("line")), "OWN050", "unresolved reference", ); @@ -1003,7 +1014,7 @@ fn protocol_findings(root: &Obj) -> Vec { } let methods: Vec = raw_fns .iter() - .filter_map(|raw| protocol::parse_method(raw).ok()) + .filter_map(|raw| protocol::parse_method(raw, protocol::Door::Tolerant).ok()) .collect(); if protocols.is_empty() { return Vec::new(); @@ -1174,14 +1185,14 @@ pub(crate) fn check_facts(facts: &OwnIr) -> Result, BridgeError> { for entry in raw.iter().filter_map(Value::as_object) { svc_loc.insert( get_or(entry, "name", ""), - (get_or(entry, "file", "?"), as_int(entry.get("line"))), + (get_or(entry, "file", "?"), as_line(entry.get("line"))), ); } } let mut findings = map_core(&diags, &lowering.handles, &svc_loc)?; findings.extend(di_findings(root)?); - findings.extend(effect_findings(root)?); + findings.extend(effect_findings(root)); findings.extend(protocol_findings(root)); findings.extend(unresolved_findings(root)); findings.extend(lowering.advisories.iter().map(transfer_note)); @@ -1504,7 +1515,6 @@ mod tests { ] })); let got: Vec<(String, i64)> = effect_findings(&root) - .unwrap() .into_iter() .map(|f| (f.file, f.line)) .collect(); @@ -1515,7 +1525,7 @@ mod tests { #[test] fn effects_block_that_is_not_a_list_yields_nothing() { let root = obj(&json!({"effects": "nope"})); - assert!(effect_findings(&root).unwrap().is_empty()); + assert!(effect_findings(&root).is_empty()); } /// BR-D2 on the raw document: a `protocols` or `protocol_functions` block diff --git a/rust/crates/own-bridge/tests/verdicts.rs b/rust/crates/own-bridge/tests/verdicts.rs index d8939319..6a34e734 100644 --- a/rust/crates/own-bridge/tests/verdicts.rs +++ b/rust/crates/own-bridge/tests/verdicts.rs @@ -298,11 +298,17 @@ fn assert_exclusions_hold<'m>( e.name ); } + // Two, and both are #294 OD-1: the reference's tolerant door skips a + // malformed entry where the typed Rust constructor refuses the document + // before the bridge's skip rule can run. + // + // Six until #259's final acceptance. The four `verdict_boundary_*` cases + // were the coordinate-representability family — refused because the core + // holds a line as `u32` and §4.2 admitted every signed-64 value — and they + // are PROMOTED, not waived: the reference's own domain moved to int32 + // first (Python-first), so both doors now degrade an out-of-domain line to + // `0` and the four goldens are replayed like any other case. let expected_exclusions: BTreeSet<&str> = [ - "verdict_boundary_line_negative", - "verdict_boundary_line_above_u32", - "verdict_boundary_service_line_negative", - "verdict_boundary_effect_line_negative", "verdict_door_effect_deps_not_strings", "verdict_door_service_unknown_lifetime", ] @@ -348,6 +354,44 @@ fn assert_exclusions_hold<'m>( excluded } +/// Every source coordinate a Layer 2 document carries, at any nesting depth. +fn lowered_lines(doc: &own_lowered::LoweredDocument) -> Vec { + use own_lowered::Stmt; + fn walk(body: &[Stmt], out: &mut Vec) { + for stmt in body { + match stmt { + Stmt::Acquire { line, .. } + | Stmt::Release { line, .. } + | Stmt::Use { line, .. } + | Stmt::Overspan { line, .. } + | Stmt::Return { line, .. } + | Stmt::AliasJoin { line, .. } + | Stmt::Call { line, .. } + | Stmt::Subscribe { line, .. } => out.push(*line), + Stmt::If { + then, r#else, line, .. + } => { + out.push(*line); + walk(then, out); + walk(r#else, out); + } + Stmt::While { body, line, .. } => { + out.push(*line); + walk(body, out); + } + } + } + } + let mut out = Vec::new(); + for f in &doc.functions { + for p in &f.params { + out.push(p.line); + } + walk(&f.body, &mut out); + } + out +} + /// Replay one case against its golden: `Ok((refused, finding count))`, or the /// divergence description. fn replay_case(name: &str, facts_path: &str) -> Result<(bool, usize), String> { @@ -357,6 +401,23 @@ fn replay_case(name: &str, facts_path: &str) -> Result<(bool, usize), String> { let facts = construct(&read(facts_path)).unwrap_or_else(|e| { panic!("{name}: the typed door refused a case that is not in the exclusion ledger: {e}") }); + // Layer 2 carries the §4.2 domain too, and it is asserted here rather + // than assumed. The reference builds its AST from `_as_line`-read + // coordinates, so a Layer 2 document it produces cannot hold a line + // outside `[0, 2147483647]`; the port must not either. Nothing else pins + // it: `ast::core_line` degrades on the way in, so a port that carried the + // raw value through the lowering would look identical at Layer 3 and + // disagree with the reference at the seam the cp2 evidence is taken from. + if let Ok(lowered) = own_bridge::lower(&facts) { + for line in lowered_lines(&lowered) { + assert!( + (0..=2_147_483_647).contains(&line), + "{name}: Layer 2 carries the coordinate {line}, outside the \ + spec/OwnIR.md §4.2 domain — the reference's lowering degrades \ + it to 0 and this one did not" + ); + } + } let first = own_bridge::check_facts(&facts); let second = own_bridge::check_facts(&facts); assert_eq!( diff --git a/rust/crates/own-ir/src/lib.rs b/rust/crates/own-ir/src/lib.rs index 18dc45ab..c7776dcb 100644 --- a/rust/crates/own-ir/src/lib.rs +++ b/rust/crates/own-ir/src/lib.rs @@ -95,12 +95,15 @@ pub enum OwnIrErrorKind { /// An identity field that is empty, non-string, or duplicated. Identity, /// A **representable** source coordinate that violates its - /// coordinate-domain rule, currently the 1-based `column` contract (#317). + /// coordinate-domain rule: the 1-based `column` contract (#317), and the + /// int32 line/column domain of `spec/OwnIR.md` §4.2. /// /// The order matters and is the whole distinction: representability is /// judged first, so this variant only ever describes a value that already /// has the form the contract asks for. A bool column is `Shape`, because - /// there is no coordinate there for the 1-based rule to be about. + /// there is no coordinate there for the 1-based rule to be about — and + /// `i64::MAX` as a line is `Location` while `i64::MAX + 1` is `Shape`, + /// which is the same rule read at the other end. Location, /// Every value has the right type and the right vocabulary, and the record /// still cannot mean anything — a rule that structurally never fires, a @@ -429,6 +432,17 @@ pub struct Binding { deserialize_with = "reject_null", skip_serializing_if = "Option::is_none" )] + /// The binding's declaration line. + /// + /// `i64`, not the `u32` the §4.2 domain would fit in, and that is + /// deliberate for every `line` on this model. The strict door guarantees + /// the domain; the typed model is the CONSTRUCTOR the door hands an + /// already-accepted document to, and narrowing it here would put a second + /// door rule in serde — one that answers `Shape` for a domain violation, + /// reports it in serde's field order rather than BR-D1's, and fires on the + /// tolerant path where the reference degrades instead of refusing. A typed + /// field wider than the domain is not a hole: it is the shape that leaves + /// exactly one implementation of the rule. pub line: Option, #[serde(flatten)] pub extra: Map, diff --git a/rust/crates/own-ir/src/protocol.rs b/rust/crates/own-ir/src/protocol.rs index 525dc44a..f94f00bd 100644 --- a/rust/crates/own-ir/src/protocol.rs +++ b/rust/crates/own-ir/src/protocol.rs @@ -60,7 +60,9 @@ use std::collections::BTreeSet; use serde_json::{Map, Value}; -use crate::strict::{defaulted_int_value, name_slot, optional_string, MAX_NESTING_DEPTH}; +use crate::strict::{ + defaulted_int_value, in_line_domain, line_domain, name_slot, optional_string, MAX_NESTING_DEPTH, +}; use crate::{OwnIrError, OwnIrErrorKind}; /// The closed event vocabulary of `protocol_functions[].events` — the `ev` @@ -433,11 +435,47 @@ fn matcher(raw: &Value, what: &str, require_value: bool) -> Result Result { + if in_line_domain(value) { + return Ok(value); + } + match door { + Door::Strict => line_domain(value, what, "line").map(|()| value), + Door::Tolerant => Ok(0), + } +} + /// Parse one `protocol_functions[]` record. /// /// # Errors /// [`OwnIrError`] on any violation in the record or its event tree. -pub fn parse_method(raw: &Value) -> Result { +pub fn parse_method(raw: &Value, door: Door) -> Result { let Some(obj) = raw.as_object() else { return Err(shape(format!( "a protocol function must be an object, got {raw}" @@ -455,7 +493,7 @@ pub fn parse_method(raw: &Value) -> Result { ))) } }; - let events = events(obj.get("events"), &what, 0)?; + let events = events(obj.get("events"), &what, 0, door)?; Ok(MethodEvents { name, file, events }) } @@ -479,7 +517,12 @@ pub fn parse_method(raw: &Value) -> Result { /// `e.get("then", [])`, so an absent arm still descends a level, whereas the /// flow walker probes for a key that may not be there and must not count what /// it did not find. Two recursions, two contracts. -fn events(raw: Option<&Value>, what: &str, depth: usize) -> Result, OwnIrError> { +fn events( + raw: Option<&Value>, + what: &str, + depth: usize, + door: Door, +) -> Result, OwnIrError> { if depth > MAX_NESTING_DEPTH { return Err(shape(format!( "{what}: events nested deeper than {MAX_NESTING_DEPTH} levels" @@ -508,8 +551,9 @@ fn events(raw: Option<&Value>, what: &str, depth: usize) -> Result, O "{what}: unknown protocol event {got} — the vocabulary is {EVENT_KINDS:?}" ))); }; - // The line is checked for every kind, before the per-kind fields. - let line = defaulted_int_value(obj, "line", what)?; + // The line is checked for every kind, before the per-kind fields: + // type and representability (grammar), then the domain (the door's). + let line = event_line(defaulted_int_value(obj, "line", what)?, what, door)?; let next = depth.saturating_add(1); out.push(match kind { "assign" => { @@ -544,12 +588,12 @@ fn events(raw: Option<&Value>, what: &str, depth: usize) -> Result, O } "if" => Event::If { line, - then: events(obj.get("then"), what, next)?, - orelse: events(obj.get("else"), what, next)?, + then: events(obj.get("then"), what, next, door)?, + orelse: events(obj.get("else"), what, next, door)?, }, "while" => Event::While { line, - body: events(obj.get("body"), what, next)?, + body: events(obj.get("body"), what, next, door)?, }, "return" => Event::Return { line }, // "throw" — EVENT_KINDS is closed, and checked above. @@ -562,7 +606,8 @@ fn events(raw: Option<&Value>, what: &str, depth: usize) -> Result, O #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { - use super::{parse_method, parse_protocol, Event, Matcher, MatcherKind}; + use super::{parse_method, parse_protocol, Door, Event, Matcher, MatcherKind}; + use crate::OwnIrErrorKind; use serde_json::json; use std::collections::BTreeSet; @@ -677,16 +722,19 @@ mod tests { /// default the reference uses. #[test] fn a_method_record_becomes_its_event_tree() { - let m = parse_method(&json!({ - "name": "VM.Load", - "events": [ - {"ev": "assign", "target": "IsLoaded", "value": false, "line": 10}, - {"ev": "if", "line": 20, - "then": [{"ev": "call", "callee": "Notify", "arg": "Doc", "line": 21}], - "else": [{"ev": "return", "line": 22}]}, - {"ev": "while", "line": 30, "body": [{"ev": "throw", "line": 31}]} - ] - })) + let m = parse_method( + &json!({ + "name": "VM.Load", + "events": [ + {"ev": "assign", "target": "IsLoaded", "value": false, "line": 10}, + {"ev": "if", "line": 20, + "then": [{"ev": "call", "callee": "Notify", "arg": "Doc", "line": 21}], + "else": [{"ev": "return", "line": 22}]}, + {"ev": "while", "line": 30, "body": [{"ev": "throw", "line": 31}]} + ] + }), + Door::Strict, + ) .unwrap(); assert_eq!(m.file, "?", "an absent 'file' defaults to '?'"); let Some(Event::If { then, orelse, .. }) = m.events.get(1) else { @@ -707,16 +755,54 @@ mod tests { assert_eq!(body.first(), Some(&Event::Throw { line: 31 })); } - /// An absent `line` reads as `0`, and a negative one travels: the protocol - /// path has no `u32` coordinate domain to clamp against. + /// An absent `line` reads as `0`, and an out-of-domain one is the door's + /// decision: refused at the strict door, degraded to `0` at the tolerant. + /// + /// A negative line used to TRAVEL here, because the protocol path had no + /// coordinate domain to answer to. `spec/OwnIR.md` §4.2 gave it one in + /// #259's final acceptance, and the two doors' answers are the contract: + /// `load()` refuses, `check_facts()` degrades — never clamps, so `2^31` + /// does not become `2^31 - 1`. #[test] - fn an_absent_line_is_zero_and_a_negative_one_survives() { - let m = parse_method(&json!({ + fn an_absent_line_is_zero_and_an_out_of_domain_one_follows_the_door() { + let doc = json!({ "name": "m", "events": [{"ev": "return"}, {"ev": "throw", "line": -3}] - })) - .unwrap(); - assert_eq!(m.events.first(), Some(&Event::Return { line: 0 })); - assert_eq!(m.events.get(1), Some(&Event::Throw { line: -3 })); + }); + let tolerant = parse_method(&doc, Door::Tolerant).unwrap(); + assert_eq!(tolerant.events.first(), Some(&Event::Return { line: 0 })); + assert_eq!(tolerant.events.get(1), Some(&Event::Throw { line: 0 })); + + let strict = parse_method(&doc, Door::Strict).expect_err("out of domain"); + assert_eq!(strict.kind, OwnIrErrorKind::Location, "{}", strict.message); + assert!( + strict + .message + .contains("must be a source line in [0, 2147483647]"), + "{}", + strict.message + ); + + // The upper end, and the clamp control: `2^31` degrades to 0 rather + // than to `2^31 - 1`, which is a real line the producer did not mean. + let high = json!({"name": "m", "events": [{"ev": "throw", "line": 2_147_483_648i64}]}); + let degraded = parse_method(&high, Door::Tolerant).unwrap(); + assert_eq!(degraded.events.first(), Some(&Event::Throw { line: 0 })); + + // …and the domain's own edges travel untouched at both doors. + let edges = json!({ + "name": "m", + "events": [{"ev": "throw", "line": 0}, {"ev": "throw", "line": 2_147_483_647i64}] + }); + for door in [Door::Strict, Door::Tolerant] { + let m = parse_method(&edges, door).unwrap(); + assert_eq!(m.events.first(), Some(&Event::Throw { line: 0 })); + assert_eq!( + m.events.get(1), + Some(&Event::Throw { + line: 2_147_483_647 + }) + ); + } } } diff --git a/rust/crates/own-ir/src/strict.rs b/rust/crates/own-ir/src/strict.rs index b63c8d2b..1fb63917 100644 --- a/rust/crates/own-ir/src/strict.rs +++ b/rust/crates/own-ir/src/strict.rs @@ -48,9 +48,9 @@ //! | [`name_slot`] | `isinstance(v, str) and v` — a value other facts join on | //! | [`optional_string`] | `v is not None and not isinstance(v, str)` — null tolerated | //! | [`defaulted_string`] | `isinstance(d.get(k, "?"), str)` — null rejected | -//! | [`defaulted_int`] | `isinstance(x, int) and not isinstance(x, bool)`, plus the §4.2 range | +//! | [`defaulted_line`] | `isinstance(x, int) and not isinstance(x, bool)`, plus the §4.2 form and domain | //! | [`string_array`] | `isinstance(x, list) and all(isinstance(i, str))` | -//! | [`column`] | `_check_column` — representability, then the 1-based rule (#317) | +//! | [`column`] | `_check_column` — representability, the 1-based rule (#317), the domain | //! | [`sites`] | the `{type, file, line}` call-site record | //! //! The distinction between the two string primitives is the one place a single @@ -87,6 +87,48 @@ fn vocabulary(message: impl Into) -> OwnIrError { OwnIrError::new(OwnIrErrorKind::Vocabulary, message) } +fn location(message: impl Into) -> OwnIrError { + OwnIrError::new(OwnIrErrorKind::Location, message) +} + +/// The source-coordinate DOMAIN (`spec/OwnIR.md` §4.2) — the inner of the two +/// axes, and a different rule from [`is_representable_int`] above. +/// +/// `int32` is the line type of every consumer this project feeds (Roslyn's +/// `LinePosition.Line`, LSP's `uinteger`, .NET diagnostics), so a line wider +/// than this cannot reach the place it points at whatever it can be stored in. +/// `0` is legal and means "unknown / file-level" — the reference's own default +/// for an absent line. A column starts one higher because it is 1-based (§4.1). +pub(crate) const LINE_MIN: i64 = 0; +pub(crate) const LINE_MAX: i64 = 2_147_483_647; +pub(crate) const COLUMN_MIN: i64 = 1; +pub(crate) const COLUMN_MAX: i64 = 2_147_483_647; + +/// Is this line inside the domain? Shared with the protocol grammar, which +/// answers the same question under whichever door it was called from. +pub(crate) const fn in_line_domain(value: i64) -> bool { + LINE_MIN <= value && value <= LINE_MAX +} + +/// The domain rule for a line that already HAS a representable form. +/// +/// It answers [`OwnIrErrorKind::Location`], never `Shape`, and that is the +/// whole point of the two axes being two functions: `i64::MAX` is a +/// representable coordinate violating the rule about what a coordinate may +/// mean, while `i64::MAX + 1` is not a coordinate at all. The cp1 ledger +/// classifies by mechanism, so a single predicate over both would report one +/// of them under the other's category — the defect #326's census found in +/// `column` and this change would otherwise have re-introduced in `line`. +pub(crate) fn line_domain(value: i64, what: &str, key: &str) -> Checked { + if in_line_domain(value) { + return Ok(()); + } + Err(location(format!( + "{what} '{key}' must be a source line in [{LINE_MIN}, {LINE_MAX}], \ + got {value} (spec/OwnIR.md §4.2)" + ))) +} + /// Is this a **representable** `OwnIR` integer — `int`, not `bool`, and inside /// the signed-64 range the contract declares (`spec/OwnIR.md` §4.2)? /// @@ -188,11 +230,17 @@ fn defaulted_string(obj: &Map, key: &str, what: &str) -> Checked } } -/// `x = d.get(key, 0); isinstance(x, int) and not isinstance(x, bool)`, and +/// A validated source LINE: `x = d.get(key, 0)`, an integer and not a `bool`, /// within the representable range — see [`is_representable_int`], which carries -/// both halves so no call site can get one without the other. -pub(crate) fn defaulted_int(obj: &Map, key: &str, what: &str) -> Checked { - defaulted_int_value(obj, key, what).map(|_| ()) +/// both halves so no call site can get one without the other — and then within +/// the coordinate domain ([`line_domain`]). +/// +/// Named for the field rather than for the type since #259's final acceptance: +/// every call site reads a `line` or a `ctor_line`, and the domain rule it now +/// applies is a rule about coordinates, not about integers. +pub(crate) fn defaulted_line(obj: &Map, key: &str, what: &str) -> Checked { + let value = defaulted_int_value(obj, key, what)?; + line_domain(value, what, key) } /// [`defaulted_int`], keeping the value it validated. @@ -262,16 +310,33 @@ fn column(value: Option<&Value>, what: &str) -> Checked { range, got {v}" ))); } - if v.as_i64().is_some_and(|i| i >= 1) { - return Ok(()); + let n = v.as_i64().unwrap_or(0); + if n < COLUMN_MIN { + return Err(location(format!( + "{what} 'column' must be a 1-based integer or absent, got {v}" + ))); } - Err(OwnIrError::new( - OwnIrErrorKind::Location, - format!("{what} 'column' must be a 1-based integer or absent, got {v}"), - )) + // …and the same domain a line carries (§4.2). Both bounds answer + // `Location`, because both are rules about what a representable + // coordinate may MEAN — the 1-based rule at the bottom and the int32 + // consumer domain at the top are one axis, not two. + if n > COLUMN_MAX { + return Err(location(format!( + "{what} 'column' must be a source column in \ + [{COLUMN_MIN}, {COLUMN_MAX}], got {v} (spec/OwnIR.md §4.2)" + ))); + } + Ok(()) } -/// `column` on every flow op, recursing through `then` / `else` / `body`. +/// `line` and `column` on every flow op, recursing through `then`/`else`/`body`. +/// +/// The line joined the column here in #259's final acceptance: §4.2 used to +/// record a flow op's `line` as validated NOWHERE — not for range, not even +/// for type — on both sides, and answered the contract question it left open +/// with "yes, a coordinate no rule reads must still be well-formed", because +/// the TOLERANT door reads it and anchors findings on it. Line before column, +/// the record's own order and §4.1's: they are the same node's coordinate. /// /// A non-list body is **skipped**, not rejected, and so is a non-object op: the /// reference returns early in both cases. Tightening that would be a @@ -282,7 +347,7 @@ fn column(value: Option<&Value>, what: &str) -> Checked { /// becomes a [`Value`]. A value built **in memory** has no such bound, which is /// why [`crate::OwnIr::to_value`] depth-checks before serializing — and why /// that check is iterative. -fn flow_columns(nodes: Option<&Value>, what: &str, depth: usize) -> Checked { +fn flow_coordinates(nodes: Option<&Value>, what: &str, depth: usize) -> Checked { // The early return comes FIRST, and that order is measured rather than // reasoned. Every op is probed for `then`/`else`/`body` whether or not it // has them, so a depth check placed before this one counts the absent @@ -302,9 +367,10 @@ fn flow_columns(nodes: Option<&Value>, what: &str, depth: usize) -> Checked { let label = op .get("op") .map_or_else(|| format!("{what} op"), |name| format!("{what} op {name}")); + defaulted_line(op, "line", &label)?; column(op.get("column"), &label)?; for key in ["then", "else", "body"] { - flow_columns(op.get(key), what, depth.saturating_add(1))?; + flow_coordinates(op.get(key), what, depth.saturating_add(1))?; } } Ok(()) @@ -326,13 +392,23 @@ fn sites(obj: &Map, key: &str, what: &str) -> Checked { }) }) }); - if ok { - Ok(()) - } else { - Err(shape(format!( + if !ok { + return Err(shape(format!( "{what} '{key}' must be an array of {{type:str, file:str, line:int}} objects" - ))) + ))); } + // …then the DOMAIN, in a second pass over the same array, because that is + // where the reference puts it: the shape check is one folded `all(...)` + // whose violation is a single shape failure, and the coordinate rule is a + // separate loop right after it. One pass answering both would report a + // domain violation as the record's shape. + let singular = format!("{what} {}", key.trim_end_matches('s')); + for site in v.as_array().map_or(&[][..], Vec::as_slice) { + if let Some(obj) = site.as_object() { + defaulted_line(obj, "line", &singular)?; + } + } + Ok(()) } /// Run the whole strict door, in the reference's order. @@ -419,6 +495,12 @@ fn components(obj: &Map) -> Checked { ))); } } + // The record's own coordinate — line, then the column of the same + // node (§4.1). The line was validated NOWHERE on either side until + // #259's final acceptance, which is why it sits between the + // vocabulary check and the column rather than at the top: this is + // the reference's order, and BR-D1 makes order observable. + defaulted_line(sub, "line", "subscription")?; column(sub.get("column"), "subscription")?; optional_string(sub, "type", "subscription")?; optional_string(sub, "source_type", "subscription")?; @@ -455,9 +537,9 @@ fn services(obj: &Map) -> Checked { string_array(svc, "weak_deps", "service")?; string_array(svc, "root_resolves", "service")?; defaulted_string(svc, "file", "service")?; - defaulted_int(svc, "line", "service")?; + defaulted_line(svc, "line", "service")?; defaulted_string(svc, "ctor_file", "service")?; - defaulted_int(svc, "ctor_line", "service")?; + defaulted_line(svc, "ctor_line", "service")?; defaulted_string(svc, "ctor_type", "service")?; sites(svc, "root_resolve_sites", "service")?; string_array(svc, "scope_cached", "service")?; @@ -480,7 +562,7 @@ fn effects(obj: &Map) -> Checked { return Err(shape(format!("effect 'io' must be a boolean, got {other}"))) } } - defaulted_int(eff, "line", "effect")?; + defaulted_line(eff, "line", "effect")?; let binds = objects( eff, "bindings", @@ -490,7 +572,7 @@ fn effects(obj: &Map) -> Checked { defaulted_string(binding, "name", "binding")?; defaulted_string(binding, "init", "binding")?; string_array(binding, "refs", "binding")?; - defaulted_int(binding, "line", "binding")?; + defaulted_line(binding, "line", "binding")?; } } Ok(()) @@ -506,7 +588,7 @@ fn functions(obj: &Map) -> Checked { optional_string(function, "sig", "function")?; // The BODY's columns precede `params` — the least obvious edge in the // door, because params read like the more primitive thing. - flow_columns(function.get("body"), "function body", 0)?; + flow_coordinates(function.get("body"), "function body", 0)?; let params = objects( function, "params", @@ -514,7 +596,7 @@ fn functions(obj: &Map) -> Checked { )?; for param in params { name_slot(param, "name", "parameter")?; - defaulted_int(param, "line", "parameter")?; + defaulted_line(param, "line", "parameter")?; column(param.get("column"), "parameter")?; match param.get("effect") { None | Some(Value::Null) => {} @@ -561,7 +643,7 @@ fn protocol_functions(obj: &Map) -> Checked { "OwnIR 'protocol_functions' must be a JSON array of objects", )?; for raw in pfns { - crate::protocol::parse_method(raw)?; + crate::protocol::parse_method(raw, crate::protocol::Door::Strict)?; } Ok(()) } diff --git a/rust/crates/own-ir/tests/validation_replay.rs b/rust/crates/own-ir/tests/validation_replay.rs index 64053d1b..2b603185 100644 --- a/rust/crates/own-ir/tests/validation_replay.rs +++ b/rust/crates/own-ir/tests/validation_replay.rs @@ -420,3 +420,87 @@ fn the_depth_guard_never_fires_on_a_document_from_json_accepts() { } } } + +/// The port's OWN domain diagnostic, pinned against drift. +/// +/// This is deliberately not the cross-language comparison the rest of this +/// file makes — that one compares accept/reject and the KIND, never the text, +/// and the reason is stated at the top. What it does is stop this port's own +/// error text from rotting silently: the door now has two coordinate rules on +/// two axes, and a message that stopped naming the domain (or a domain rule +/// that started answering `Shape`) would leave a reader of a rejected document +/// unable to tell which of them fired. +/// +/// One document per rule, at the edge that discriminates: `i64::MAX` HAS a +/// representable form and violates the domain (`Location`), `i64::MAX + 1` has +/// no form at all (`Shape`), and the in-domain edges are accepted. +#[test] +fn the_coordinate_rules_report_the_axis_they_are_on() { + let cases: [(&str, Option<(OwnIrErrorKind, &str)>); 8] = [ + ( + r#"{"services":[{"name":"S","lifetime":"singleton","line":9223372036854775807}]}"#, + Some(( + OwnIrErrorKind::Location, + "must be a source line in [0, 2147483647]", + )), + ), + ( + r#"{"services":[{"name":"S","lifetime":"singleton","line":-1}]}"#, + Some(( + OwnIrErrorKind::Location, + "must be a source line in [0, 2147483647]", + )), + ), + ( + r#"{"services":[{"name":"S","lifetime":"singleton","line":9223372036854775808}]}"#, + Some((OwnIrErrorKind::Shape, "must be an integer")), + ), + ( + r#"{"components":[{"subscriptions":[{"column":2147483648}]}]}"#, + Some(( + OwnIrErrorKind::Location, + "must be a source column in [1, 2147483647]", + )), + ), + ( + r#"{"components":[{"subscriptions":[{"column":0}]}]}"#, + Some(( + OwnIrErrorKind::Location, + "must be a 1-based integer or absent", + )), + ), + ( + r#"{"functions":[{"body":[{"op":"acquire","line":-1}]}]}"#, + Some(( + OwnIrErrorKind::Location, + "must be a source line in [0, 2147483647]", + )), + ), + // …and the domain's own edges, so the rules above cannot be a door + // that simply refuses coordinates. + ( + r#"{"services":[{"name":"S","lifetime":"singleton","line":0}]}"#, + None, + ), + ( + r#"{"components":[{"subscriptions":[{"line":2147483647,"column":2147483647}]}]}"#, + None, + ), + ]; + for (text, expected) in cases { + let got = OwnIr::from_json(text).err(); + match (expected, got) { + (None, None) => {} + (None, Some(e)) => panic!("{text}: accepted document rejected: {}", e.message), + (Some((kind, _)), None) => panic!("{text}: expected a {kind:?} rejection, accepted"), + (Some((kind, needle)), Some(e)) => { + assert_eq!(e.kind, kind, "{text}: wrong axis — {}", e.message); + assert!( + e.message.contains(needle), + "{text}: the message stopped naming its rule ({needle:?}): {}", + e.message + ); + } + } + } +} diff --git a/scripts/render_checkpoint_status.py b/scripts/render_checkpoint_status.py index 39fd1596..afb11c32 100644 --- a/scripts/render_checkpoint_status.py +++ b/scripts/render_checkpoint_status.py @@ -12,6 +12,16 @@ checkpoint 4's, because that is where the fragment was introduced and two notes link it; what it DESCRIBES is the current comparison surface, which the document says in its own first paragraph. +* `docs/generated/p022-cp1-census.md` — the checkpoint-1 strict-door ledger, + counted from the ledger itself by `tests/validation_census.py`: controls by + section, by verdict and by category, plus the coordinate family on its own. + Three numbers about that ledger used to be typed on the P-022 table. +* `docs/generated/p022-coord-census.md` — every source coordinate the fixture + tree carries, classified against the §4.2 domain, from + `tests/coordinate_census.compute_coordinate_census()`. It is the measurement + the coordinate-domain decision was taken against and the one the churn budget + is checked with; it deliberately counts the GOLDENS too, because `0` staying + a legal line is a property of the outputs, not of the door. * `docs/generated/p022-cp5-inventory.md` — the checkpoint-5 SURFACE inventory, from `tests/verdict_surface_inventory.compute_surface_inventory()`: which BR-V4 wording branch, BR-V5 evidence family and BR-V9 rendered-surface rule @@ -26,6 +36,8 @@ `summarize()` as every other campaign in the tree. * `docs/generated/p022-cp4b-mutations.md` — checkpoint 4b's two campaigns (the obligation ANALYSIS and its BRIDGE half), rendered the same way. +* `docs/generated/p022-coord-mutations.md` — final acceptance's two campaigns, + split by door (strict / tolerant) rather than by sub-checkpoint. * `docs/generated/p022-shadow-census.md` — the step-7a (#260/#269) shadow-mode INFRASTRUCTURE census, from `tests/shadow_census.compute_shadow_census()` over the committed @@ -61,6 +73,11 @@ if _p not in sys.path: sys.path.insert(0, _p) +from coordinate_census import ( # noqa: E402 + CoordinateCensus, + CoordinateCensusError, + compute_coordinate_census, +) from mutate_campaign import ( # noqa: E402 (sys.path set above) CampaignError, Definition, @@ -72,6 +89,11 @@ summarize, ) from shadow_census import ShadowCensus, ShadowCensusError, compute_shadow_census # noqa: E402 +from validation_census import ( # noqa: E402 + ValidationCensus, + ValidationCensusError, + compute_validation_census, +) from verdict_census import Census, CensusError, compute_verdict_census # noqa: E402 from verdict_render_census import ( # noqa: E402 RenderCensus, @@ -88,9 +110,12 @@ GENERATED = os.path.join(ROOT, "docs", "generated") EVIDENCE = os.path.join(ROOT, "docs", "evidence") CENSUS_MD = "p022-cp4-census.md" +COORD_CENSUS_MD = "p022-coord-census.md" +CP1_CENSUS_MD = "p022-cp1-census.md" INVENTORY_MD = "p022-cp5-inventory.md" CP5_MUTATIONS_MD = "p022-cp5-mutations.md" CP4B_MUTATIONS_MD = "p022-cp4b-mutations.md" +COORD_MUTATIONS_MD = "p022-coord-mutations.md" MUTATIONS_MD = "p022-cp4-mutations.md" SHADOW_CENSUS_MD = "p022-shadow-census.md" SHADOW_MUTATIONS_MD = "p022-shadow-mutations.md" @@ -120,6 +145,14 @@ ("checkpoint 4b.1 — the obligation analysis", "p022-cp4b-1"), ("checkpoint 4b.2 — the bridge mapping (BR-P3)", "p022-cp4b-2"), ) +# Final acceptance, split by DOOR rather than by sub-checkpoint: the strict +# door refuses and the tolerant one degrades, they fail differently, and a +# campaign that measured them together could not say which half a survivor +# belonged to. +COORD_CAMPAIGNS = ( + ("the strict door — the coordinate domain, both implementations", "p022-coord-1"), + ("the tolerant door — the degrade, both implementations", "p022-coord-2"), +) SELF = "scripts/render_checkpoint_status.py" @@ -219,6 +252,167 @@ def render_census(c: Census, r: RenderCensus | None) -> str: return "\n".join(lines) +# --- checkpoint 1: the strict-door ledger census --------------------------- + + +def render_validation_census(c: ValidationCensus) -> str: + """The cp1 ledger, counted from the ledger itself. + + Three numbers about this ledger used to be typed on the P-022 table. They + were true when written and are the kind that stops being true quietly, so + the row links here instead.""" + lines = [ + _header("tests/fixtures/ownir_validation.json through tests/validation_census.py"), + "# P-022 #259 checkpoint 1 — the strict-door ledger, counted", + "", + "`tests/fixtures/ownir_validation.json` is the frozen BR-D1 acceptance language, " + "regenerated from the reference by " + "`python tests/test_ownir_validation_fixtures.py --write` and replayed with zero " + "Python by `own-ir/tests/validation_replay.rs`.", + "", + "**What the replay compares is accept/reject and, on a rejection, the CATEGORY — " + "never the message text.** The reference funnels every rejection through one " + "`OwnIRError` whose strings are a human-facing presentation aid, so byte-comparing " + "them across two languages would freeze a debug surface as a contract. The " + "`message` each record carries is Python's own, kept for the record rather than " + "for the comparison. The matrix the replay asserts — agreed accepts, agreed " + "rejects, Rust-only accepts, Rust-only rejects, kind mismatches — is a property " + "of a green `cargo test -p own-ir`, not a number reproduced here: all three " + "failure rows are zero or the build is red.", + "", + f"| {'measure'.ljust(30)} | value |", + f"|{'-' * 32}|------:|", + f"| {'controls'.ljust(30)} | {c.controls} |", + f"| {'… accepted'.ljust(30)} | {c.accepted} |", + f"| {'… rejected'.ljust(30)} | {c.rejected} |", + "", + "## By category", + "", + "Seven categories on two axes (`shape` is \"no representable primitive or " + "container form\"; `location` is \"a representable coordinate violating its " + "domain rule\"). The split is the one #326's census had to discover, and #259's " + "final acceptance moved a family across it — an `i64::MAX` line was an accept, is " + "now `location`, and `i64::MAX + 1` was and stays `shape`.", + "", + "| category | controls | what it means |", + "|---|---:|---|", + ] + counts = dict(c.by_category) + lines.append(f"| `accepted` | {counts.get('accepted', 0)} | the document is accepted |") + for name, meaning in c.categories: + lines.append(f"| `{name}` | {counts.get(name, 0)} | {meaning} |") + lines += [ + "", + "## By section", + "", + "The ledger's own grouping, which is BR-D1's check order. A section with " + "acceptances and no rejections is a door nobody probed; one with rejections and " + "no acceptance twin is a rejection nothing discriminates.", + "", + "| section | accepted | rejected | by category |", + "|---|---:|---:|---|", + ] + for row in c.sections: + detail = ", ".join(f"`{k}` {n}" for k, n in row.by_category) or "—" + lines.append(f"| `{row.section}` | {row.accepted} | {row.rejected} | {detail} |") + lines += [ + "", + "## The coordinate family", + "", + "Every control whose document carries a `line`, `ctor_line` or `column` at any " + "depth — the family #259's final acceptance moved, pulled out so a reviewer can " + "find it without reading every record. Each line-bearing field is pinned at four " + "points (`0` and `2147483647` accepted, `-1` and `2147483648` rejected), the two " + "fields §4.2 used to record as validated nowhere carry type controls as well, and " + "the flow-op line is pinned at every nesting shape because `then`/`else`/`body` " + "are three separate recursion sites.", + "", + f"| {'measure'.ljust(34)} | value |", + f"|{'-' * 36}|------:|", + f"| {'coordinate-bearing controls'.ljust(34)} | {c.coordinate_controls} |", + f"| {'… accepted'.ljust(34)} | {c.coordinate_accepted} |", + ] + for name, n in c.coordinate_by_category: + if name == "accepted": + continue + lines.append(f"| {('… rejected `' + name + '`').ljust(34)} | {n} |") + lines.append("") + return "\n".join(lines) + +# --- the coordinate census ------------------------------------------------ + + +def render_coordinate_census(c: CoordinateCensus) -> str: + """Every `line` / `column` slot in the fixture tree, by family, slot and + value class. Computed by `tests/coordinate_census.py`; nothing here is + typed, including the sentence about what did not move.""" + lines = [ + _header("tests/fixtures/**/*.json through tests/coordinate_census.py"), + "# P-022 #259 final acceptance — the source-coordinate census", + "", + "The measurement the coordinate-domain decision (`spec/OwnIR.md` §4.2) was " + "taken against: every `line`, `ctor_line` and `column` slot in every JSON file " + "under `tests/fixtures/`, at any depth and under any key.", + "", + "It is wider than the door on purpose. A **door slot** sits on an OwnIR " + "*document* and `load()` rules on it; every other row is an **observation** — " + "a golden, a ledger, a captured trace — which the door never sees and which " + "this change must therefore leave alone. `0` stays a legal line (the " + "reference's own default for an absent one), so the observation rows anchored " + "at zero are the records the decision must go on accepting, and a census that " + "read only the inputs could not see them at all.", + "", + "Value classes follow the cp1 taxonomy's axis rather than blurring it: " + "`outside-int64` has no representable integer form (`Shape`), while `negative` " + "and `above-int32` are representable coordinates violating the domain rule " + "(`Location`). `below-1` is the column's own 1-based rule.", + "", + f"| {'measure'.ljust(34)} | value |", + f"|{'-' * 36}|------:|", + f"| {'JSON files scanned'.ljust(34)} | {c.files} |", + f"| {'coordinate slots found'.ljust(34)} | {c.coordinates} |", + "", + "## By value class", + "", + "| value class | all slots | door slots |", + "|---|---:|---:|", + ] + door = dict(c.door_by_class) + for value_class, n in c.by_class: + lines.append(f"| `{value_class}` | {n} | {door.get(value_class, 0)} |") + lines += [ + "", + "## By family and slot", + "", + "`door` marks a slot the strict door rules on. Examples are shown for every " + "class outside `in-domain` / `null` / `zero`, because those are the values a " + "reader needs to see rather than count.", + "", + "| family | slot | class | door | count | files | values |", + "|---|---|---|:--:|---:|---:|---|", + ] + for row in c.rows: + shown = ", ".join(f"`{e}`" for e in row.examples) or "—" + lines.append( + f"| `{row.family}` | `{row.path}` | `{row.value_class}` | " + f"{'yes' if row.door else '—'} | {row.count} | {row.files} | {shown} |") + lines += [ + "", + "## Slot inventory reachability", + "", + "`tests/coordinate_census.SLOTS` is the door inventory, and it is asserted as " + "a set rather than spot-checked: a declared slot no fixture reaches is a " + "phantom claiming coverage it does not have.", + "", + ] + if c.unreachable_slots: + lines += [f"- **unreachable**: `{slot}`" for slot in c.unreachable_slots] + lines.append("") + else: + lines += ["Every declared slot is reached by at least one fixture.", ""] + return "\n".join(lines) + + # --- checkpoint 5: the surface inventory ---------------------------------- @@ -532,7 +726,8 @@ def render_shadow_census(c: ShadowCensus) -> str: The reducer walks the pair in pipeline order over **{scope}** and names the first place they part company: the layer, the step address and the *minimal* difference inside it. The `verdicts` layer is **refused, not skipped** — -comparing final diagnostics is #260's acceptance, blocked by #259 — and the +comparing final diagnostics is #260's own acceptance, and crossing that line is +its decision to take (#259's final acceptance is reached) — and the refusal is carried in every reduction, so "not compared" can never be read as "compared and agreed". @@ -575,8 +770,10 @@ def render_shadow_census(c: ShadowCensus) -> str: Acceptance must therefore prove the byte-level invariant separately; until it does, "same input" here means canonical identity and nothing stronger ([owner decision B-1](../notes/p022-shadow-infra-owner-decisions.md)). -- **End diagnostics compared as an acceptance surface** — #260's acceptance, - blocked by #259 (cp5 and 4b). Not attempted, not approximated. +- **End diagnostics compared as an acceptance surface** — #260's acceptance. + It was blocked by #259 while cp5, 4b and the coordinate-domain contract were + open; all three have landed, so what remains is this step's own decision to + cross the line. Not attempted, not approximated. - **The verdict layer.** Refused by the reducer, and recorded as refused in every reduction. This is the same blocker as the row above, stated where a tool could otherwise have quietly crossed it. @@ -620,6 +817,14 @@ def fragments() -> tuple[dict[str, str], list[str]]: out[CENSUS_MD] = render_census(compute_verdict_census(), renders) except CensusError as e: problems.extend(f"verdict census: {p}" for p in e.problems) + try: + out[CP1_CENSUS_MD] = render_validation_census(compute_validation_census()) + except ValidationCensusError as e: + problems.extend(f"cp1 ledger census: {p}" for p in e.problems) + try: + out[COORD_CENSUS_MD] = render_coordinate_census(compute_coordinate_census()) + except CoordinateCensusError as e: + problems.extend(f"coordinate census: {p}" for p in e.problems) try: out[INVENTORY_MD] = render_inventory(compute_surface_inventory()) except InventoryError as e: @@ -661,6 +866,19 @@ def fragments() -> tuple[dict[str, str], list[str]]: CP4B_CAMPAIGNS) out[CP4B_MUTATIONS_MD] = cp4b problems.extend(f"mutation campaign {p}" for p in cp4b_problems) + coord, coord_problems = render_campaign_set( + "# P-022 #259 final acceptance — mutation campaigns", + "The coordinate-domain contract, measured in two halves because the two doors " + "fail differently: the STRICT door refuses an out-of-domain coordinate and the " + "TOLERANT one degrades it to absent. Every rule is mutated on BOTH sides — the " + "reference and its Rust mirror — since a domain only one implementation enforces " + "is a divergence, not a rule. Every mutation edits a **production** surface " + "(P-022 discipline 2) and every declared layer runs for every mutation " + "(discipline 3: no fail-fast); the counts are derived from the recorded runs by " + "`scripts/mutate_campaign.summarize()`, never typed.", + COORD_CAMPAIGNS) + out[COORD_MUTATIONS_MD] = coord + problems.extend(f"mutation campaign {p}" for p in coord_problems) return out, problems @@ -704,9 +922,9 @@ def main(argv: list[str]) -> int: if problems: return 1 if argv: - print(f"checkpoint status fragments OK: {CENSUS_MD}, {INVENTORY_MD}, " - f"{MUTATIONS_MD}, {CP5_MUTATIONS_MD}, {SHADOW_CENSUS_MD}, " - f"{SHADOW_MUTATIONS_MD} in sync with the evidence") + print(f"checkpoint status fragments OK: {CENSUS_MD}, {CP1_CENSUS_MD}, " + f"{COORD_CENSUS_MD}, {INVENTORY_MD}, {MUTATIONS_MD}, {CP5_MUTATIONS_MD}, " + f"{SHADOW_CENSUS_MD}, {SHADOW_MUTATIONS_MD} in sync with the evidence") return 0 diff --git a/spec/Bridge.md b/spec/Bridge.md index a2f5a508..61ad2e3b 100644 --- a/spec/Bridge.md +++ b/spec/Bridge.md @@ -58,7 +58,8 @@ input. It performs, **in this order** (the order is observable through which error fires first): JSON parse → root-is-object → **version gate** (IR1; absent `ownir_version` = current; `bool` rejected — the `bool`-is-`int` trap) → `components[]` shape → each record's `resource` kind (absent defaults to -`"subscription"`; present-but-unknown rejected, IR4) → the optional per-record +`"subscription"`; present-but-unknown rejected, IR4) → the record's `line` +(int-not-bool, in the [§4.2](OwnIR.md) domain) and `column` → the optional per-record strings `type`, `source_type`, `source_provenance`, `ignore_reason` (present ⇒ must be strings) → `services[]` (lifetime enum, non-empty `name`, `deps`/ `weak_deps`/`root_resolves` string arrays, `file` string, `line` int-not-bool, @@ -81,9 +82,12 @@ drop a malformed entry — a `deps: "a"` must not become `("a",)` and mint a spurious verdict); (2) an **accepted** entry's fields go through the existing **field-specific coercions** (`_di_findings` `str()`-coerces identity/location fields and admits `disposable` only as the JSON boolean `true`); (3) `line` -**degrades to `0`** via `_as_int` on **every** finding-construction path +**degrades to `0`** via `_as_line` on **every** finding-construction path (OD-3, resolved — the strict `int(...)` anchor paths were converged onto the -tolerant helper). A **present-but-unknown `resource` kind fails loud** on the +tolerant helper), and since #259's final acceptance that helper carries the +[§4.2](OwnIR.md) coordinate domain: an out-of-domain line reads as `0` and an +out-of-domain `column` reads as absent — **degraded, never clamped**, because a +clamp moves a finding onto a real line the producer never named. A **present-but-unknown `resource` kind fails loud** on the tolerant door too, matching `load()` (OD-2, resolved). A **duplicate protocol name resolves first-wins** (deterministically) instead of raising. The two doors are deliberate: strict for external input, tolerant for already-shaped @@ -205,8 +209,9 @@ without a lowering branch raises "internal core inconsistency"; an op outside ([OwnIR.md §2](OwnIR.md)). Both are `OwnIRError`, both name file:line. **BR-L11 (source locations).** Every lowered node carries the fact's `line` -through `_as_int` (non-int → 0). The bridge never invents lines; the anchor -policy for *findings* is BR-V5's. +through `_as_line` — a non-integer, or one outside the [§4.2](OwnIR.md) +coordinate domain, reads as `0`. The bridge never invents lines and never +clamps one; the anchor policy for *findings* is BR-V5's. ## 3. Interprocedural MOS @@ -239,7 +244,7 @@ INF-A2/A4 (Tier B table `_BCL_FRESH_BY_NS`), `_definite_release`/ ## 4. Analysis input preparation **BR-P1 (DI).** `services[]` records construct `di.Service` values with: -string coercion on identity/location fields, `_as_int` on lines, `disposable` +string coercion on identity/location fields, `_as_line` on lines, `disposable` **only** for the JSON boolean `true`, tuples for the dep/site arrays. The five finders (`find_captive_dependencies`, `find_captured_transient_disposables`, `find_weak_captive_dependencies`, `find_explicit_root_resolutions`, @@ -371,9 +376,11 @@ committed regeneration path and a zero-Python steady state: (IR4-everywhere fail-loud), `tolerant_unknown_kind` is now one of those shared cases — its `Rejected` golden pins the identical error text on both sides — so there are **no `rust_replay: false` snapshots left**. Layer 1 - landed in `own-ir` (#259 cp1: 216 controls, 0/0/0); Layer 3 is built and fully - compared (below); #259 as a whole remains open on the coordinate-domain - decision alone, row 4b (the obligation-protocol analysis) having landed. + landed in `own-ir` (#259 cp1; the counts are the generated + [cp1 census](../docs/generated/p022-cp1-census.md)); Layer 3 is built and + fully compared (below); **#259's final acceptance is reached** — row 4b (the + obligation-protocol analysis) and the coordinate-domain contract have both + landed, and the declared boundary that remains is the two OD-1 door controls. - **Layer 3 — final normalized diagnostics.** The findings list per facts fixture, and its SARIF/github/msbuild renderings — the outer contract, in **two families**. Built at #259 cp4, fully compared at cp5: @@ -404,14 +411,18 @@ committed regeneration path and a zero-Python steady state: SARIF key order is part of this surface. The manifest's `rust_replay_excluded` ledger names the documents the Rust - core **refuses by a declared boundary** — a coordinate outside the core's - `u32` line domain, a shape the typed Rust door rejects before the bridge runs - (OD-1) — each with its reason and an expectation the replay executes, so an - exclusion cannot rot. It listed a third boundary until #259 checkpoint 4b: - a protocol-bearing document, which the bridge refused rather than answer for - with the OBL analysis unported. Both such documents are now **promoted** — - the family is ported (BR-P3), and a re-declared exclusion is a red build - rather than a note, because the replay runs every entry it names. The `summaries` dump (INF-R1) covers the MOS sub-surface. Which BR-V4 + core **refuses by a declared boundary** — now only a shape the typed Rust + door rejects before the bridge's tolerant skip rule can run (OD-1) — each + with its reason and an expectation the replay executes, so an exclusion + cannot rot. It listed two more boundaries, and **both are promoted**, neither + by waiving anything. #259 checkpoint 4b ported the OBL analysis the + protocol-bearing documents were refused for. #259's final acceptance moved + the coordinate boundary from the REFERENCE's side: [OwnIR.md §4.2](OwnIR.md) + now bounds a line to the int32 domain every consumer this project feeds, so + a document that passes the strict door is inside the core's `u32` by + construction and both tolerant doors degrade an out-of-domain coordinate to + `0` rather than refusing. A re-declared exclusion is a red build rather than + a note, because the replay runs every entry it names. The `summaries` dump (INF-R1) covers the MOS sub-surface. Which BR-V4 wording, BR-V5 slice family and BR-V9 rule the corpus reaches — and the recorded disposition of every one it does not — is the generated ledger [`p022-cp5-inventory.md`](../docs/generated/p022-cp5-inventory.md). @@ -519,12 +530,13 @@ issue to be settled *before* the port relies on it. already fails loud here, so the `tolerant_unknown_kind` fixture is now a shared `rust_replay` case (its `Rejected` golden pins the error text on both sides), not a Python-only snapshot. -- **OD-3 (#294 — line coercion inconsistency). RESOLVED: `_as_int` everywhere.** +- **OD-3 (#294 — line coercion inconsistency). RESOLVED: `_as_line` everywhere.** Finding construction used strict `int(...)` on the token/capture anchor paths - and `_as_int` elsewhere, so a non-int `line` on the tolerant door crashed one - path and degraded the other. All finding-construction paths now use `_as_int` - (a non-int `line` degrades to `0`); `load()` still validates `line` on the - strict door. + and the non-throwing reader elsewhere, so a non-int `line` on the tolerant + door crashed one path and degraded the other. All finding-construction paths + now use `_as_line` (named for the field since #259's final acceptance, and + carrying the [§4.2](OwnIR.md) domain: a non-int OR out-of-domain `line` + degrades to `0`); `load()` still validates `line` on the strict door. - **OD-4 (#295 — positional identity fallbacks).** `Component`/`Fn` defaults couple a nameless record's identity to the running counter (document position), so an unrelated earlier record shifts it. diff --git a/spec/BridgeBehaviorMatrix.md b/spec/BridgeBehaviorMatrix.md index 521d77ea..dc8c171c 100644 --- a/spec/BridgeBehaviorMatrix.md +++ b/spec/BridgeBehaviorMatrix.md @@ -33,6 +33,8 @@ | unknown flow op raises (via lowering guard) | `_lower_flow` | IR4, BR-L10 | L369, L508 | L1 | | optional per-record strings must be strings (`source_provenance`, `ignore_reason`) | `load` | BR-D1 | L246, L304 | L1 | | `services[]` shape: lifetime enum, name, line int-not-bool, dep/site arrays, ctor fields | `load` | BR-D1 | L1185–L1236 (9 checks) | L1 | +| every `line` is in the int32 coordinate domain and every `column` in its 1-based one; the two fields §4.2 used to record as unchecked (`subscriptions[].line`, a flow op's `line`) are validated for type and domain at every nesting shape | `load` | §4.2, BR-D1 | `test_ownir_defensive_limits.py`, `ownir_validation.json` | L1 | +| the tolerant door DEGRADES an out-of-domain coordinate — a line to `0`, a column to absent — and never clamps | `_as_line`, `_as_col`, `_opt_line` | §4.2, BR-D2 | `test_ownir_defensive_limits.py`, `verdict_domain_tolerant_readers` | L3 ✅ | | function `sig` present-but-non-string rejected (record side) | `load` | BR-D3 | L2306 | L1 | | schema↔code enum binding: `ownir_version`, `resourceKind`, `diLifetime`, `paramEffect`, `flowOp` (no gaps/dupes) | `ownir.schema.json` + authority sets | IR3/IR4 | L434–L480 (6 checks) | L1 | | every declared flow op actually lowers (no coverage gap) | `_FLOW_OPS` ↔ `_lower_flow` | BR-L10 | L485 | L2 | @@ -151,7 +153,13 @@ protocol-bearing document rather than return an incomplete list. #259 checkpoint 4b closed that — the analysis is `own-analysis`'s, the typed values come from the one grammar in `own-ir`, the bridge maps them, and both reference documents are promoted out of `rust_replay_excluded` and replayed against the -goldens exactly as they were committed. What is still outside the replayed set, -recorded in that ledger with an executable expectation, is the coordinate -boundary (a decision #259 owes) and the tolerant-door coercions the typed Rust +goldens exactly as they were committed. The **coordinate boundary** was the second, and #259's final +acceptance closed it from the reference's side rather than the port's: +[OwnIR.md §4.2](OwnIR.md) bounds every `line` to `[0, 2147483647]` and every +`column` to `[1, 2147483647]` — the int32 domain every consumer this project +feeds — the strict door refuses an out-of-domain coordinate as `Location`, and +both tolerant doors degrade it to absent rather than refusing, so the four +`verdict_boundary_*` controls are promoted and replayed like any other case. +What is still outside the replayed set, recorded in that ledger with an +executable expectation, is the tolerant-door coercions the typed Rust constructor cannot reach (OD-1). diff --git a/spec/OwnIR.md b/spec/OwnIR.md index 3939f1d1..ac0ab14e 100644 --- a/spec/OwnIR.md +++ b/spec/OwnIR.md @@ -158,8 +158,10 @@ Three rules, and they are the whole contract: integer — `0`, negative, `bool`, string, float and array all raise `OwnIRError`, and the walk recurses into `if`/`while` bodies. `bool` is rejected explicitly: `True` is an `int` in Python and would otherwise read as - column 1. `check_facts()`, which may be called directly on un-validated facts, - degrades to absent instead. + column 1. It is bounded above as well: a column above `2147483647` is + rejected, the same domain a `line` carries (§4.2), because a column no + consumer can hold is not a usable coordinate either. `check_facts()`, which + may be called directly on un-validated facts, degrades to absent instead. A flow-local handle is minted on five paths — a contract param, a direct `acquire`, an `alias_join`, a fresh-returning call `result`, and a branch acquire @@ -180,30 +182,49 @@ a language that happens to have the same capabilities the reference does. Both are now bounded, and the bound is part of the vocabulary rather than a property of whichever consumer reads it first. -**Source-coordinate integers fit a signed 64-bit integer.** - -- every **validated** `line` — `services[].line`, `services[].ctor_line`, - `services[].root_resolve_sites[].line`, `services[].scope_cache_sites[].line`, - `effects[].line`, `effects[].bindings[].line`, `functions[].params[].line`, - `protocol_functions[].events[].line` — lies in `[-2^63, 2^63 - 1]`; -- every `column` (§4.1) is `1..=2^63 - 1`, or absent, or `null`. - -The word *validated* is load-bearing, and the exception is recorded rather than -papered over. Two line-bearing fields are checked **nowhere** by `load()` — not -for range, and not even for type: `components[].subscriptions[].line` and the -`line` on a flow op inside `functions[].body`. Measured, `{"line": "x"}` and -`{"line": true}` are accepted on both. That predates this section, and both -implementations agree about it — neither the reference nor the Rust port types -those fields — so it is **not** a parity gap and closing it is not part of -removing one. It is a separate contract question: whether a coordinate that no -rule reads should nevertheless be well-formed. Until it is answered, the bound -above claims exactly the fields it covers. - -Python integers are unbounded, so the reference accepted coordinates no other -consumer could represent. That is not a generosity worth keeping: a coordinate -nothing downstream can hold is not a usable coordinate, and leaving it legal -turns every port into a source of "the reference accepted this and I cannot". -The bound is stated here and enforced in `load()`. +**Source coordinates are bounded twice: a representable form, and a domain +inside it.** + +The two are different rules about different things, and a consumer that folds +them reports the wrong reason for half its rejections. + +*Form.* Every coordinate integer — every `line` and every `column` — fits a +**signed 64-bit** integer. Python integers are unbounded, so the reference +accepted values no other consumer could represent; a coordinate nothing +downstream can hold is not a usable coordinate, and leaving it legal turns +every port into a source of "the reference accepted this and I cannot". + +*Domain.* Inside that form: + +- every `line` lies in **`[0, 2147483647]`**; +- every `column` (§4.1) lies in **`[1, 2147483647]`**, or is absent, or `null`. + +**2147483647 is `int32`, and int32 is the line type of every consumer this +project feeds**: Roslyn's `LinePosition.Line` is an `int`, LSP's `uinteger` is +capped at `2^31 - 1`, and .NET diagnostics carry the same width. A line wider +than that cannot reach the place it points at, whatever it can be stored in. + +**`0` is legal and means "unknown / file-level".** It is the reference's own +default for an absent line (`s.get("line", 0)` throughout `load()`), and the +corpus carries it in the goldens as well as the inputs, so the bottom of the +domain reads the reference rather than tightening it. A **negative** line is +rejected: no producer emits one — the Roslyn extractor writes +`StartLinePosition.Line + 1` off a 0-based position — and nothing downstream can +point at it. The lower bound of a `column` is one higher because a column is +1-based (§4.1); `0` there stays a producer bug rather than a sentinel. + +Every line-bearing field is validated: `components[].subscriptions[].line`, +`services[].line`, `services[].ctor_line`, +`services[].root_resolve_sites[].line`, `services[].scope_cache_sites[].line`, +`effects[].line`, `effects[].bindings[].line`, `functions[].params[].line`, +`protocol_functions[].events[].line`, and the `line` on a flow op inside +`functions[].body` — the last recursing through `then`, `else` and `body` +exactly as the `column` walk does. Both of the fields this section previously +recorded as validated **nowhere** — `components[].subscriptions[].line` and the +flow-op `line` — are validated now, for type as well as for domain: the open +contract question that recorded ("whether a coordinate no rule reads should +nevertheless be well-formed") is answered **yes**, because the tolerant door +does read it and anchors findings on it. **Flow bodies and protocol event trees nest at most 32 levels.** @@ -226,9 +247,23 @@ in the `OwnIR` domain — nested bodies — and not in JSON levels, because nest bodies are the thing a frontend can reason about; the ratio between the two is an encoding detail. -Both limits are **rejections at the strict door**, not coercions. `check_facts()` -on un-validated facts keeps its existing degrade-to-absent behaviour: two entry -points, two contracts, as with `column` in §4.1. +Every limit here is a **rejection at the strict door**, not a coercion. +`check_facts()` on un-validated facts degrades instead — two entry points, two +contracts, as with `column` in §4.1, and now on the same terms for lines: + +- a `column` outside its domain (or of the wrong type) reads as **absent**; +- a `line` outside its domain (or of the wrong type) reads as **`0`** — + "unknown / file-level", the value an absent line already reads as. + +**Degrade, never clamp.** `2147483648` does not become `2147483647` and `-1` +does not become `1`: a clamp moves the finding to a *real* line that is not the +one the producer meant, which is worse than saying nothing. The rule is the +same never-invent rule §4.1 states for columns. + +The strict door never reaches the degrade: a document `load()` accepts has no +out-of-domain coordinate by construction. That is asserted rather than assumed +— `tests/test_ownir_defensive_limits.py` re-reads every document the #259 cp1 +ledger records as accepted and checks each coordinate against this section. ## 5. Flow bodies (`functions[]`) diff --git a/spec/ownir.schema.json b/spec/ownir.schema.json index 90903413..bcc4fb1c 100644 --- a/spec/ownir.schema.json +++ b/spec/ownir.schema.json @@ -47,16 +47,16 @@ }, "$defs": { "sourceColumn": { - "description": "1-based source column of the SAME node `line` anchors on. Optional and additive, so OWNIR_VERSION stays 0 (spec/OwnIR.md §2): a producer that does not report one omits it and no consumer substitutes a value — not 0, not 1, and never recovered by re-reading the source line. Rides to SARIF as region.startColumn, where the OwnAudit occurrence anchor reads it (Own.NET#317).", + "description": "1-based source column of the SAME node `line` anchors on. Optional and additive, so OWNIR_VERSION stays 0 (spec/OwnIR.md §2): a producer that does not report one omits it and no consumer substitutes a value — not 0, not 1, and never recovered by re-reading the source line. Bounded above by the int32 coordinate domain (spec/OwnIR.md §4.2), the same bound a line carries: a column no consumer can hold is not a usable coordinate. Rides to SARIF as region.startColumn, where the OwnAudit occurrence anchor reads it (Own.NET#317). Pinned to ownlang/obligations.py::COLUMN_MIN/COLUMN_MAX.", "type": ["integer", "null"], "minimum": 1, - "maximum": 9223372036854775807 + "maximum": 2147483647 }, "sourceLine": { - "description": "A source line. Bounded to a signed 64-bit integer (spec/OwnIR.md \u00a74.2): Python integers are unbounded and the reference used to accept coordinates no other consumer could represent, which made the vocabulary implementable only in a language with bignums. A coordinate nothing downstream can hold is not a usable coordinate. Pinned to ownlang/obligations.py::INT64_MIN/INT64_MAX.", + "description": "A source line, in the int32 coordinate domain [0, 2147483647] (spec/OwnIR.md \u00a74.2). int32 is the line type of every consumer this project feeds — Roslyn LinePosition.Line, LSP uinteger, .NET diagnostics — so a wider line cannot reach the place it points at. 0 is legal and means unknown/file-level: it is the reference's own default for an absent line. A negative line is rejected; no producer emits one (the Roslyn extractor writes StartLinePosition.Line + 1). Every line-bearing path binds this definition, including components[].subscriptions[].line and a flow op's line, which \u00a74.2 used to record as validated nowhere. Pinned to ownlang/obligations.py::LINE_MIN/LINE_MAX.", "type": "integer", - "minimum": -9223372036854775808, - "maximum": 9223372036854775807 + "minimum": 0, + "maximum": 2147483647 }, "resourceKind": { "description": "The resource-kind discriminator (spec/OwnIR.md §4). It selects the analysis path, so a present-but-unknown value is rejected at load (fail-loud) and a new kind must bump OWNIR_VERSION. Pinned to ownlang/ownir.py::_KNOWN_RESOURCE_KINDS.", @@ -108,7 +108,7 @@ "description": "One owned-resource record (spec/OwnIR.md §4). An unreleased record is OWN001 at `line`; a released one nets balanced and stays silent.", "type": "object", "properties": { - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "event": { "description": "The event/handle identifier for this owned resource (e.g. `bus.CustomerChanged`, `_timer.Tick`); carried into the finding message and rendered output.", @@ -190,7 +190,7 @@ "description": "A new owned local (Let+Acquire); kind:\"pool\" tags it a pooled buffer.", "properties": { "op": { "const": "acquire" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "var": { "type": "string" }, "kind": { "type": "string", "enum": ["pool"] } @@ -202,7 +202,7 @@ "description": "Release of the local's handle.", "properties": { "op": { "const": "release" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "var": { "type": "string" } }, @@ -213,7 +213,7 @@ "description": "Use of the handle.", "properties": { "op": { "const": "use" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "var": { "type": "string" } }, @@ -224,7 +224,7 @@ "description": "Overspan (POOL005: a full-length view of a pooled buffer).", "properties": { "op": { "const": "overspan" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "var": { "type": "string" } }, @@ -235,7 +235,7 @@ "description": "Ownership transfer out; `var` optional (a bare return).", "properties": { "op": { "const": "return" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "var": { "type": "string" } }, @@ -246,7 +246,7 @@ "description": "A new owning handle joined to `src`'s alias set (wrap/adopt, D5.4).", "properties": { "op": { "const": "alias_join" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "var": { "type": "string" }, "src": { "type": "string" } @@ -258,7 +258,7 @@ "description": "A Call checked against the callee's contract; a fresh-returning callee mints an acquire for `result` (D5.2). An optional `sig` (the callee's canonical parameter-type list, same format as a function record's `sig`) resolves the call against that overload's own summary; absent or unmatched, the name-merged summary applies (stage 2 fallback — degraded, never a wrong overload).", "properties": { "op": { "const": "call" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "callee": { "type": "string" }, "sig": { "type": "string" }, @@ -272,7 +272,7 @@ "description": "An If with both branches lowered.", "properties": { "op": { "const": "if" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "then": { "type": "array", "items": { "$ref": "#/$defs/flowOp" } }, "else": { "type": "array", "items": { "$ref": "#/$defs/flowOp" } } @@ -284,7 +284,7 @@ "description": "A While — a back-edge the core's worklist fixpoint converges over (A1).", "properties": { "op": { "const": "while" }, - "line": { "type": "integer" }, + "line": { "$ref": "#/$defs/sourceLine" }, "column": { "$ref": "#/$defs/sourceColumn" }, "body": { "type": "array", "items": { "$ref": "#/$defs/flowOp" } } }, diff --git a/tests/coordinate_census.py b/tests/coordinate_census.py new file mode 100644 index 00000000..c0bf9cb6 --- /dev/null +++ b/tests/coordinate_census.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""Every source coordinate the fixture tree carries, classified (#259 final). + +The coordinate-domain decision (`spec/OwnIR.md` §4.2) narrows what a `line` +and a `column` may be. Before narrowing a contract, measure what the corpus +already holds against it — otherwise "no producer emits that" is a belief, and +"nothing else churns" is a hope. + +This module is that measurement, and it is deliberately WIDER than the door: + +* it reads every `.json` file under `tests/fixtures/`, not only the OwnIR + documents, because the goldens are where the decision is most easily broken. + `0` stays a legal line, so a summaries or verdict golden anchored at line 0 + must come through this change untouched; a census that only looked at inputs + could not see that at all; +* it reaches every `line` / `column` slot at any depth and under any key, so a + coordinate in a shape nobody thought to list is counted rather than missed. + +Two classifications, and the split between them is the point: + +* **value class** — where the value sits relative to the domain. `zero` is its + own class rather than folded into `in-domain`: `0` means "unknown / + file-level" and is the reference's own default for an absent line, so the + count of zeros is the count of records this contract must go on accepting. + `negative` and `above-int32` are representable coordinates violating the + domain rule (`Location` in the cp1 taxonomy); `outside-int64` has no + representable integer form at all (`Shape`). They are not one class, because + they are not one mechanism — the same distinction #326's census had to + discover the hard way. +* **door slot or observation** — a coordinate on an OwnIR *document* is + subject to `load()`; one in a golden is an *output* of the analysis and the + door never sees it. `SLOTS` below is the door inventory, and the census + asserts it is neither stale nor short: a declared slot the tree cannot reach + is a phantom, and both directions are reported. + +Pure: no `ownlang` import and no side effects, like `verdict_census.py` beside +it — the fragment renderer reads it, so it must not be the untyped link. +Held to `mypy --strict` (see `files` in pyproject.toml). +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass +from typing import Any + +HERE = os.path.dirname(os.path.abspath(__file__)) +FIXTURES = os.path.join(HERE, "fixtures") + +# The domain (`spec/OwnIR.md` §4.2) and the representable integer form the +# domain sits inside, as LITERALS. Importing them from `ownlang` would make +# this census move with the contract it exists to measure against — the same +# self-consistency trap `tests/test_ownir_defensive_limits.py` records at the +# top of its own constant block. +LINE_MIN = 0 +LINE_MAX = 2147483647 +COLUMN_MIN = 1 +COLUMN_MAX = 2147483647 +INT64_MIN = -9223372036854775808 +INT64_MAX = 9223372036854775807 + +# The keys that hold a source coordinate. `ctor_line` is deliberately absent: +# it is a service's constructor coordinate and travels the same door rule, but +# it is reached under its own key and appears in `SLOTS` below. +COORD_KEYS = ("line", "column", "ctor_line") + +# The nesting keys of a flow body / event tree. A run of them collapses to one +# marker so that `functions[].body[].then[].else[].line` and a body nested +# thirty-two deep are ONE row: the census is about which slot, not how deep. +NESTING = ("body[]", "then[]", "else[]") +NESTED = "[]" + +# The door inventory: every coordinate slot on an OwnIR document, as the tail +# of a census path. A document can sit at a file root (`*.facts.json`), or +# nested inside a ledger (`cases[].document`) or a reproduction artifact +# (`input.document`), so slots are matched on the tail rather than the whole +# path — which is also what makes an unreachable slot detectable. +SLOTS: tuple[tuple[str, str], ...] = ( + ("components[].subscriptions[].line", "line"), + ("components[].subscriptions[].column", "column"), + ("services[].line", "line"), + ("services[].ctor_line", "line"), + ("services[].root_resolve_sites[].line", "line"), + ("services[].scope_cache_sites[].line", "line"), + ("effects[].line", "line"), + ("effects[].bindings[].line", "line"), + ("functions[].params[].line", "line"), + ("functions[].params[].column", "column"), + (f"functions[].{NESTED}.line", "line"), + (f"functions[].{NESTED}.column", "column"), + ("protocol_functions[].events[].line", "line"), + (f"protocol_functions[].events[].{NESTED}.line", "line"), +) + + +@dataclass(frozen=True) +class Row: + """One census row: a family, a slot path, a value class and how many.""" + + family: str + path: str + kind: str + value_class: str + door: bool + count: int + files: int + examples: tuple[str, ...] + + +@dataclass(frozen=True) +class CoordinateCensus: + files: int + coordinates: int + by_class: tuple[tuple[str, int], ...] + door_by_class: tuple[tuple[str, int], ...] + rows: tuple[Row, ...] + unreachable_slots: tuple[str, ...] + + +class CoordinateCensusError(Exception): + def __init__(self, problems: list[str]) -> None: + super().__init__("; ".join(problems)) + self.problems = problems + + +def classify(kind: str, value: Any) -> str: + """The value's class. `kind` is `line` or `column`; they share every + non-integer class and differ only in which integers are in the domain.""" + if value is None: + return "null" + if isinstance(value, bool): + return "bool" + if isinstance(value, float): + return "float" + if isinstance(value, str): + return "string" + if not isinstance(value, int): + return "other" + # Representability first, exactly as the door orders it: a value with no + # integer form the contract can hold cannot go on to violate a rule about + # what that form means. + if not INT64_MIN <= value <= INT64_MAX: + return "outside-int64" + if kind == "column": + if value < COLUMN_MIN: + return "below-1" + return "in-domain" if value <= COLUMN_MAX else "above-int32" + if value == LINE_MIN: + return "zero" + if value < LINE_MIN: + return "negative" + return "in-domain" if value <= LINE_MAX else "above-int32" + + +def _collapse(path: str) -> str: + out: list[str] = [] + for segment in path.split("."): + if segment in NESTING: + if out and out[-1] == NESTED: + continue + out.append(NESTED) + else: + out.append(segment) + return ".".join(out) + + +def _walk(node: Any, path: str, out: list[tuple[str, str, Any]]) -> None: + if isinstance(node, dict): + for key, value in node.items(): + child = f"{path}.{key}" if path else key + if key in COORD_KEYS and not isinstance(value, (dict, list)): + out.append((child, "column" if key == "column" else "line", value)) + else: + _walk(value, child, out) + elif isinstance(node, list): + for value in node: + _walk(value, f"{path}[]", out) + + +def _is_door_slot(path: str, kind: str) -> bool: + return any(path.endswith(slot) and slot_kind == kind for slot, slot_kind in SLOTS) + + +def door_coordinates(document: Any) -> tuple[tuple[str, str, Any], ...]: + """Every DOOR coordinate in one OwnIR document, as `(slot, kind, value)`. + + The same walk the census uses, exposed so a test that needs to re-read a + document's coordinates does not grow a second reader of the same shape — + two readings of one shape is how two files come to disagree about it. + """ + found: list[tuple[str, str, Any]] = [] + _walk(document, "", found) + return tuple( + (_collapse(path), kind, value) + for path, kind, value in found + if _is_door_slot(_collapse(path), kind) + ) + + +def compute_coordinate_census() -> CoordinateCensus: + """Walk the fixture tree once and classify every coordinate it holds.""" + problems: list[str] = [] + counts: dict[tuple[str, str, str, str], int] = {} + files_per_row: dict[tuple[str, str, str, str], set[str]] = {} + examples: dict[tuple[str, str, str, str], set[str]] = {} + reached: set[str] = set() + scanned = 0 + for directory, subdirs, names in os.walk(FIXTURES): + subdirs.sort() + family = os.path.relpath(directory, FIXTURES).replace(os.sep, "/") + family = "(root)" if family == "." else family + for name in sorted(names): + if not name.endswith(".json"): + continue + path = os.path.join(directory, name) + scanned += 1 + try: + with open(path, encoding="utf-8") as f: + document = json.load(f) + except (OSError, ValueError) as e: + problems.append(f"{os.path.relpath(path, HERE)}: unreadable: {e}") + continue + found: list[tuple[str, str, Any]] = [] + _walk(document, "", found) + for raw, kind, value in found: + slot = _collapse(raw) + door = _is_door_slot(slot, kind) + if door: + reached.add(slot) + key = (family, slot, kind, classify(kind, value)) + counts[key] = counts.get(key, 0) + 1 + files_per_row.setdefault(key, set()).add(name) + if key[3] not in ("in-domain", "null", "zero"): + examples.setdefault(key, set()).add(repr(value)) + if problems: + raise CoordinateCensusError(problems) + rows = tuple( + Row( + family=family, + path=slot, + kind=kind, + value_class=value_class, + door=_is_door_slot(slot, kind), + count=count, + files=len(files_per_row[(family, slot, kind, value_class)]), + examples=tuple(sorted(examples.get((family, slot, kind, value_class), set()))), + ) + for (family, slot, kind, value_class), count in sorted(counts.items()) + ) + by_class: dict[str, int] = {} + door_by_class: dict[str, int] = {} + for row in rows: + by_class[row.value_class] = by_class.get(row.value_class, 0) + row.count + if row.door: + door_by_class[row.value_class] = door_by_class.get(row.value_class, 0) + row.count + # A declared slot no file reaches is a phantom: the inventory would go on + # claiming coverage it does not have, which is the failure mode the + # schema's binding map is asserted AS a map to avoid. + unreachable = tuple(sorted({slot for slot, _ in SLOTS} - reached)) + return CoordinateCensus( + files=scanned, + coordinates=sum(by_class.values()), + by_class=tuple(sorted(by_class.items())), + door_by_class=tuple(sorted(door_by_class.items())), + rows=rows, + unreachable_slots=unreachable, + ) diff --git a/tests/fixtures/ownir_validation.json b/tests/fixtures/ownir_validation.json index 87531530..7d87929c 100644 --- a/tests/fixtures/ownir_validation.json +++ b/tests/fixtures/ownir_validation.json @@ -7,21 +7,21 @@ "shape": "right place, but the value has no representable primitive or container form the contract requires", "vocabulary": "right JSON type, value outside a closed set", "identity": "a name slot — empty, mistyped, or duplicated", - "location": "a REPRESENTABLE source coordinate violating its coordinate-domain rule — currently the 1-based column", + "location": "a REPRESENTABLE source coordinate violating its coordinate-domain rule — the 1-based column, and the int32 line/column domain of spec/OwnIR.md §4.2", "well_formedness": "right types, legal vocabulary, and the record still cannot mean anything" }, "totals": { - "cases": 216, - "accepted": 35, - "rejected": 181, + "cases": 294, + "accepted": 60, + "rejected": 234, "by_category": { - "accepted": 35, + "accepted": 60, "identity": 22, "json": 2, - "location": 14, - "shape": 116, + "location": 50, + "shape": 132, "version": 7, - "vocabulary": 18, + "vocabulary": 19, "well_formedness": 2 } }, @@ -4064,7 +4064,7 @@ }, { "name": "accept-line-at-i64-max", - "why": "the largest representable line", + "why": "the largest REPRESENTABLE line — accepted until the coordinate domain existed, now outside it. Representability and domain are two axes: this value still HAS a signed-64 form, which is exactly why it is `location` and not `shape`", "section": "services", "document": { "ownir_version": 0, @@ -4077,13 +4077,13 @@ ] }, "raw": false, - "verdict": "accept", - "category": null, - "message": "" + "verdict": "reject", + "category": "location", + "message": "service 'line' must be a source line in [0, 2147483647], got 9223372036854775807 (spec/OwnIR.md §4.2)" }, { "name": "accept-line-at-i64-min", - "why": "…and the smallest, because the range is closed at BOTH ends and a port that bounded only the top would pass a one-sided test", + "why": "…and the smallest. The range is still closed at BOTH ends and a port that bounded only the top would pass a one-sided test — the ends have simply moved from the form to the domain", "section": "services", "document": { "ownir_version": 0, @@ -4096,13 +4096,13 @@ ] }, "raw": false, - "verdict": "accept", - "category": null, - "message": "" + "verdict": "reject", + "category": "location", + "message": "service 'line' must be a source line in [0, 2147483647], got -9223372036854775808 (spec/OwnIR.md §4.2)" }, { "name": "accept-column-at-i64-max", - "why": "…and the largest column", + "why": "…and the largest column, which now carries the same upper bound a line does", "section": "components", "document": { "ownir_version": 0, @@ -4117,13 +4117,13 @@ ] }, "raw": false, - "verdict": "accept", - "category": null, - "message": "" + "verdict": "reject", + "category": "location", + "message": "subscription 'column' must be a source column in [1, 2147483647], got 9223372036854775807 (spec/OwnIR.md §4.2)" }, { "name": "accept-negative-line", - "why": "a NEGATIVE line is accepted: only columns carry the 1-based rule, and conflating the two would tighten the door", + "why": "a negative line is REJECTED: it is not the 1-based column rule borrowed, it is the line's own domain — no producer emits one and nothing downstream can point at it (spec/OwnIR.md §4.2)", "section": "services", "document": { "ownir_version": 0, @@ -4136,13 +4136,13 @@ ] }, "raw": false, - "verdict": "accept", - "category": null, - "message": "" + "verdict": "reject", + "category": "location", + "message": "service 'line' must be a source line in [0, 2147483647], got -5 (spec/OwnIR.md §4.2)" }, { "name": "accept-zero-line", - "why": "…and zero is the line default", + "why": "…and zero is still the line default, unchanged: it means 'unknown / file-level' and is the bottom of the domain, not an edge case that survived", "section": "services", "document": { "ownir_version": 0, @@ -6106,6 +6106,1787 @@ "verdict": "reject", "category": "shape", "message": "protocol function 'M': events nested deeper than 32 levels (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-service-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level'", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "line": 0 + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-service-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "line": 2147483647 + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "service-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "line": -1 + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "service 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "service-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "line": 2147483648 + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "service 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-service-ctor-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level'", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "ctor_line": 0 + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-service-ctor-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "ctor_line": 2147483647 + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "service-ctor-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "ctor_line": -1 + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "service 'ctor_line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "service-ctor-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "ctor_line": 2147483648 + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "service 'ctor_line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-service-site-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level'", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "root_resolve_sites": [ + { + "line": 0 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-service-site-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "root_resolve_sites": [ + { + "line": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "service-site-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "root_resolve_sites": [ + { + "line": -1 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "service root_resolve_site 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "service-site-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "root_resolve_sites": [ + { + "line": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "service root_resolve_site 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-service-scope-cache-site-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level'", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "scope_cache_sites": [ + { + "line": 0 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-service-scope-cache-site-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "scope_cache_sites": [ + { + "line": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "service-scope-cache-site-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "scope_cache_sites": [ + { + "line": -1 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "service scope_cache_site 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "service-scope-cache-site-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "services", + "document": { + "ownir_version": 0, + "services": [ + { + "name": "S", + "lifetime": "singleton", + "scope_cache_sites": [ + { + "line": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "service scope_cache_site 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-effect-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level'", + "section": "effects", + "document": { + "ownir_version": 0, + "effects": [ + { + "line": 0 + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-effect-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "effects", + "document": { + "ownir_version": 0, + "effects": [ + { + "line": 2147483647 + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "effect-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "effects", + "document": { + "ownir_version": 0, + "effects": [ + { + "line": -1 + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "effect 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "effect-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "effects", + "document": { + "ownir_version": 0, + "effects": [ + { + "line": 2147483648 + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "effect 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-binding-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level'", + "section": "effects", + "document": { + "ownir_version": 0, + "effects": [ + { + "bindings": [ + { + "line": 0 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-binding-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "effects", + "document": { + "ownir_version": 0, + "effects": [ + { + "bindings": [ + { + "line": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "binding-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "effects", + "document": { + "ownir_version": 0, + "effects": [ + { + "bindings": [ + { + "line": -1 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "binding 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "binding-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "effects", + "document": { + "ownir_version": 0, + "effects": [ + { + "bindings": [ + { + "line": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "binding 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-param-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level'", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "params": [ + { + "name": "p", + "line": 0 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-param-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "params": [ + { + "name": "p", + "line": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "param-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "params": [ + { + "name": "p", + "line": -1 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "parameter 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "param-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "params": [ + { + "name": "p", + "line": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "parameter 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-event-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level'", + "section": "protocol_functions", + "document": { + "ownir_version": 0, + "protocol_functions": [ + { + "name": "M", + "events": [ + { + "ev": "return", + "line": 0 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-event-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "protocol_functions", + "document": { + "ownir_version": 0, + "protocol_functions": [ + { + "name": "M", + "events": [ + { + "ev": "return", + "line": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "event-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "protocol_functions", + "document": { + "ownir_version": 0, + "protocol_functions": [ + { + "name": "M", + "events": [ + { + "ev": "return", + "line": -1 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "protocol function 'M': 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "event-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "protocol_functions", + "document": { + "ownir_version": 0, + "protocol_functions": [ + { + "name": "M", + "events": [ + { + "ev": "return", + "line": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "protocol function 'M': 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-subscription-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level' — a field `load()` checked NOWHERE before this change, not even for type (§4.2's recorded exception, now closed)", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "line": 0 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-subscription-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "line": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "subscription-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "line": -1 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "subscription 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "subscription-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "line": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "subscription 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-flow-op-line-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level' — a field `load()` checked NOWHERE before this change, not even for type (§4.2's recorded exception, now closed)", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": 0 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-flow-op-line-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "flow-op-line-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": -1 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "flow-op-line-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-flow-op-line-then-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level' — a field `load()` checked NOWHERE before this change, not even for type (§4.2's recorded exception, now closed)", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "then": [ + { + "op": "acquire", + "line": 0 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-flow-op-line-then-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "then": [ + { + "op": "acquire", + "line": 2147483647 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "flow-op-line-then-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "then": [ + { + "op": "acquire", + "line": -1 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "flow-op-line-then-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "then": [ + { + "op": "acquire", + "line": 2147483648 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-flow-op-line-else-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level' — a field `load()` checked NOWHERE before this change, not even for type (§4.2's recorded exception, now closed)", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "else": [ + { + "op": "acquire", + "line": 0 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-flow-op-line-else-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "else": [ + { + "op": "acquire", + "line": 2147483647 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "flow-op-line-else-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "else": [ + { + "op": "acquire", + "line": -1 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "flow-op-line-else-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "else": [ + { + "op": "acquire", + "line": 2147483648 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-flow-op-line-body-zero", + "why": "`0` is the bottom of the domain and means 'unknown / file-level' — a field `load()` checked NOWHERE before this change, not even for type (§4.2's recorded exception, now closed)", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "while", + "body": [ + { + "op": "acquire", + "line": 0 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "accept-flow-op-line-body-at-int32-max", + "why": "…and 2147483647 is the top, accepted exactly", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "while", + "body": [ + { + "op": "acquire", + "line": 2147483647 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "flow-op-line-body-negative", + "why": "one below the bottom: a representable coordinate outside its domain, so `location` rather than `shape`", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "while", + "body": [ + { + "op": "acquire", + "line": -1 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "flow-op-line-body-above-int32", + "why": "…and one above the top, the value a port that kept the old signed-64 bound would still accept", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "while", + "body": [ + { + "op": "acquire", + "line": 2147483648 + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "subscription-line-string", + "why": "a string line has no integer form — `shape`, on the other axis from the domain rejections above", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "line": "x" + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "subscription 'line' must be an integer, got 'x'" + }, + { + "name": "subscription-line-bool", + "why": "…and the bool-is-int trap, which would otherwise read as line 1", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "line": true + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "subscription 'line' must be an integer, got True" + }, + { + "name": "subscription-line-null", + "why": "…and a PRESENT null, which is not the same as absent: the reference reads the slot with a `0` default, so only an absent key takes it", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "line": null + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "subscription 'line' must be an integer, got None" + }, + { + "name": "flow-op-line-string", + "why": "a string line has no integer form — `shape`, on the other axis from the domain rejections above", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": "x" + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got 'x'" + }, + { + "name": "flow-op-line-bool", + "why": "…and the bool-is-int trap, which would otherwise read as line 1", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": true + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got True" + }, + { + "name": "flow-op-line-null", + "why": "…and a PRESENT null, which is not the same as absent: the reference reads the slot with a `0` default, so only an absent key takes it", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": null + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got None" + }, + { + "name": "flow-op-line-then-string", + "why": "a string line has no integer form — `shape`, on the other axis from the domain rejections above", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "then": [ + { + "op": "acquire", + "line": "x" + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got 'x'" + }, + { + "name": "flow-op-line-then-bool", + "why": "…and the bool-is-int trap, which would otherwise read as line 1", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "then": [ + { + "op": "acquire", + "line": true + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got True" + }, + { + "name": "flow-op-line-then-null", + "why": "…and a PRESENT null, which is not the same as absent: the reference reads the slot with a `0` default, so only an absent key takes it", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "then": [ + { + "op": "acquire", + "line": null + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got None" + }, + { + "name": "flow-op-line-else-string", + "why": "a string line has no integer form — `shape`, on the other axis from the domain rejections above", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "else": [ + { + "op": "acquire", + "line": "x" + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got 'x'" + }, + { + "name": "flow-op-line-else-bool", + "why": "…and the bool-is-int trap, which would otherwise read as line 1", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "else": [ + { + "op": "acquire", + "line": true + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got True" + }, + { + "name": "flow-op-line-else-null", + "why": "…and a PRESENT null, which is not the same as absent: the reference reads the slot with a `0` default, so only an absent key takes it", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "if", + "else": [ + { + "op": "acquire", + "line": null + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got None" + }, + { + "name": "flow-op-line-body-string", + "why": "a string line has no integer form — `shape`, on the other axis from the domain rejections above", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "while", + "body": [ + { + "op": "acquire", + "line": "x" + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got 'x'" + }, + { + "name": "flow-op-line-body-bool", + "why": "…and the bool-is-int trap, which would otherwise read as line 1", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "while", + "body": [ + { + "op": "acquire", + "line": true + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got True" + }, + { + "name": "flow-op-line-body-null", + "why": "…and a PRESENT null, which is not the same as absent: the reference reads the slot with a `0` default, so only an absent key takes it", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "while", + "body": [ + { + "op": "acquire", + "line": null + } + ] + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "function body op 'acquire' 'line' must be an integer, got None" + }, + { + "name": "accept-subscription-column-at-int32-max", + "why": "the top of the column domain, accepted exactly", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "column": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "subscription-column-above-int32", + "why": "…and one past it: representable, positive, and outside the domain — `location`, the same axis the 1-based rule is on", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "column": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "subscription 'column' must be a source column in [1, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-param-column-at-int32-max", + "why": "the top of the column domain, accepted exactly", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "params": [ + { + "name": "p", + "column": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "param-column-above-int32", + "why": "…and one past it: representable, positive, and outside the domain — `location`, the same axis the 1-based rule is on", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "params": [ + { + "name": "p", + "column": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "parameter 'column' must be a source column in [1, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "accept-flow-op-column-at-int32-max", + "why": "the top of the column domain, accepted exactly", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "column": 2147483647 + } + ] + } + ] + }, + "raw": false, + "verdict": "accept", + "category": null, + "message": "" + }, + { + "name": "flow-op-column-above-int32", + "why": "…and one past it: representable, positive, and outside the domain — `location`, the same axis the 1-based rule is on", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "column": 2147483648 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'column' must be a source column in [1, 2147483647], got 2147483648 (spec/OwnIR.md §4.2)" + }, + { + "name": "column-below-i64", + "why": "a column BELOW the representable form. It is `shape` by mechanism — there is no signed-64 integer here for a 1-based rule to be about — even though the reference's message names the 1-based rule, because it tests `v < 1` before the form. The category is read off the mechanism, which is this ledger's rule and the reason the taxonomy has two axes at all", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "column": -9223372036854775809 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "shape", + "message": "subscription 'column' must be a 1-based integer or absent, got -9223372036854775809" + }, + { + "name": "order-subscription-kind-before-line", + "why": "an unknown resource kind and an out-of-domain line in one record: the kind is checked first, so this is `vocabulary`", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "resource": "nope", + "line": -1 + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "vocabulary", + "message": "unknown resource kind 'nope' — a new kind is a vocabulary change that must bump OWNIR_VERSION (see spec/OwnIR.md §2)" + }, + { + "name": "order-subscription-line-before-column", + "why": "…and the line precedes the column of the same node (§4.1), so a record breaking both reports the line's domain, not the column's type", + "section": "components", + "document": { + "ownir_version": 0, + "components": [ + { + "subscriptions": [ + { + "line": -1, + "column": true + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "subscription 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "order-flow-op-line-before-column", + "why": "…and the same order inside a flow op", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": -1, + "column": true + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" + }, + { + "name": "order-flow-op-body-before-params", + "why": "the BODY's coordinates still precede `params` — the least obvious edge in the door, and now observable through a line as well as a column", + "section": "functions", + "document": { + "ownir_version": 0, + "functions": [ + { + "body": [ + { + "op": "acquire", + "line": -1 + } + ], + "params": [ + { + "name": "" + } + ] + } + ] + }, + "raw": false, + "verdict": "reject", + "category": "location", + "message": "function body op 'acquire' 'line' must be a source line in [0, 2147483647], got -1 (spec/OwnIR.md §4.2)" } ] } diff --git a/tests/fixtures/repro/digests.json b/tests/fixtures/repro/digests.json index 5d838882..909d2dd8 100644 --- a/tests/fixtures/repro/digests.json +++ b/tests/fixtures/repro/digests.json @@ -366,26 +366,26 @@ { "case": "verdict_boundary_effect_line_negative", "corpus": "verdicts", - "digest": "0f55de6ce780977587f3684db6aafd955dc9b3e655adef6aaea451e048d75301", - "bytes": 361 + "digest": "741485197684324305e4424ef715c7bdb0eefc5fff958b9dffa879041603475c", + "bytes": 336 }, { "case": "verdict_boundary_line_above_u32", "corpus": "verdicts", - "digest": "788dbce914bfc6cdb3afd68ecc712c8f30e4f388282aa2fbf8ae773cf21bd605", - "bytes": 294 + "digest": "65bf5f41f6af899cfd01e5b8b371d1d765e477d60ed18a537eacad72de0263a1", + "bytes": 434 }, { "case": "verdict_boundary_line_negative", "corpus": "verdicts", - "digest": "94ccd117455ca0cc0c4ae33b15669dc8107d7b34444b461788ae5644a6117ca3", - "bytes": 424 + "digest": "083698faa3f6aba6d3da9ccc86b9dcdb7326f6a3c967ed6e71e250f1f6993cb5", + "bytes": 541 }, { "case": "verdict_boundary_service_line_negative", "corpus": "verdicts", - "digest": "f168053ba21a64d7d4b72a1ce8644d63308ec58f9c265fcdd266c63f23eb0b5d", - "bytes": 418 + "digest": "50b78714f0ef7f3b080cb537dd8820f80a5a17765e15db39e0d5183558142cf1", + "bytes": 480 }, { "case": "verdict_column_sort_order", @@ -447,6 +447,12 @@ "digest": "db35f35c937b9014c3c3ca14c983ad9f0504925b233cbfbb457885fd60e9ddf0", "bytes": 901 }, + { + "case": "verdict_domain_tolerant_readers", + "corpus": "verdicts", + "digest": "361da587ce14c818d776f824bb563adaa6ff3e315c337bf21f339abf0a503c55", + "bytes": 1307 + }, { "case": "verdict_door_effect_deps_not_strings", "corpus": "verdicts", diff --git a/tests/fixtures/verdicts/manifest.json b/tests/fixtures/verdicts/manifest.json index 0d52a09d..4b0bcb37 100644 --- a/tests/fixtures/verdicts/manifest.json +++ b/tests/fixtures/verdicts/manifest.json @@ -2,30 +2,6 @@ "comment": "The frozen Layer 3 case ledger (#259 cp4/cp5). The swept corpora (tests/fixtures/{ownir,lowered,summaries}) are discovered automatically; the synthetic verdict cases beside this manifest are listed exhaustively with the Bridge.md rules they pin; rust_replay_excluded names the cases whose golden is Python's truth but which the Rust core refuses by a DECLARED boundary — each with its reason and the executable expectation the Rust replay asserts (rust_refusal: 'bridge' = own_bridge::check_facts returns an error containing rust_error_contains; 'door' = the typed OwnIr constructor refuses the document, #294 OD-1). Python renders every case; an exclusion is a statement about the port, never about the reference.", "verdicts_version": 1, "rust_replay_excluded": [ - { - "name": "verdict_boundary_line_negative", - "reason": "coordinate outside the core's u32 line domain (a negative param line the strict door admits per spec/OwnIR.md §4.2); the reference anchors OWN001 at B.cs:-1, the Rust core refuses instead of clamping — declared cp4 divergence family.", - "rust_refusal": "bridge", - "rust_error_contains": "outside the core's line domain" - }, - { - "name": "verdict_boundary_line_above_u32", - "reason": "coordinate outside the core's u32 line domain (an acquire at 2^32); the reference anchors OWN001 there, the Rust core refuses — declared cp4 divergence family.", - "rust_refusal": "bridge", - "rust_error_contains": "outside the core's line domain" - }, - { - "name": "verdict_boundary_service_line_negative", - "reason": "coordinate outside the core's u32 line domain on the DI path (registration line -5); the reference anchors DI001 there, the Rust core refuses — declared cp4 divergence family.", - "rust_refusal": "bridge", - "rust_error_contains": "outside the core's line domain" - }, - { - "name": "verdict_boundary_effect_line_negative", - "reason": "coordinate outside the core's u32 line domain on the effect path (effect line -3); the reference anchors EFF001 there, the Rust core refuses — declared cp4 divergence family.", - "rust_refusal": "bridge", - "rust_error_contains": "outside the core's line domain" - }, { "name": "verdict_door_effect_deps_not_strings", "reason": "#294 OD-1: the reference's tolerant door skips a malformed effect entry (deps is a bare string) and reports the sibling; the Rust tolerant entry is the typed OwnIr constructor, which refuses the document before the bridge's skip rule can run.", @@ -304,6 +280,14 @@ "BR-V1", "BR-V8" ] + }, + { + "name": "verdict_domain_tolerant_readers", + "rules": [ + "BR-D2", + "BR-V4", + "BR-V5" + ] } ] } diff --git a/tests/fixtures/verdicts/verdict_boundary_effect_line_negative.facts.json b/tests/fixtures/verdicts/verdict_boundary_effect_line_negative.facts.json index 0dd331c9..3737429c 100644 --- a/tests/fixtures/verdicts/verdict_boundary_effect_line_negative.facts.json +++ b/tests/fixtures/verdicts/verdict_boundary_effect_line_negative.facts.json @@ -3,9 +3,8 @@ "module": "Bound", "components": [], "_doc": [ - "Declared #259 cp4 divergence family on the effect path: a negative effect", - "line is an EFF001 anchored at E.tsx:-3 for the reference; the Rust core", - "refuses." + "The promoted control on the effect path: a negative effect line degrades to", + "0 on both tolerant doors, so EFF001 anchors at E.tsx:0." ], "effects": [ { diff --git a/tests/fixtures/verdicts/verdict_boundary_effect_line_negative.verdicts.json b/tests/fixtures/verdicts/verdict_boundary_effect_line_negative.verdicts.json index bf30b33d..8ad73758 100644 --- a/tests/fixtures/verdicts/verdict_boundary_effect_line_negative.verdicts.json +++ b/tests/fixtures/verdicts/verdict_boundary_effect_line_negative.verdicts.json @@ -3,7 +3,7 @@ "findings": [ { "file": "E.tsx", - "line": -3, + "line": 0, "code": "EFF001", "component": "E", "event": "o", diff --git a/tests/fixtures/verdicts/verdict_boundary_line_above_u32.facts.json b/tests/fixtures/verdicts/verdict_boundary_line_above_u32.facts.json index 4f958902..21795e6c 100644 --- a/tests/fixtures/verdicts/verdict_boundary_line_above_u32.facts.json +++ b/tests/fixtures/verdicts/verdict_boundary_line_above_u32.facts.json @@ -2,8 +2,10 @@ "ownir_version": 0, "module": "Bound", "_doc": [ - "Declared #259 cp4 divergence family, upper edge: an acquire at line 2^32 is", - "a leak at that coordinate for the reference; the Rust core refuses." + "The upper edge of the same promoted control: an acquire at 2^32 is outside", + "the §4.2 domain, so the strict door refuses it and both tolerant doors", + "degrade to 0 — never clamped to 2147483647, which is a real line the", + "producer did not mean. The golden anchors the leak at B.cs:0." ], "functions": [ { diff --git a/tests/fixtures/verdicts/verdict_boundary_line_above_u32.verdicts.json b/tests/fixtures/verdicts/verdict_boundary_line_above_u32.verdicts.json index 295a8ec8..edc927f4 100644 --- a/tests/fixtures/verdicts/verdict_boundary_line_above_u32.verdicts.json +++ b/tests/fixtures/verdicts/verdict_boundary_line_above_u32.verdicts.json @@ -3,7 +3,7 @@ "findings": [ { "file": "B.cs", - "line": 4294967296, + "line": 0, "code": "OWN001", "component": "Bound.G", "event": "x", diff --git a/tests/fixtures/verdicts/verdict_boundary_line_negative.facts.json b/tests/fixtures/verdicts/verdict_boundary_line_negative.facts.json index ad30b9fa..98d68d2d 100644 --- a/tests/fixtures/verdicts/verdict_boundary_line_negative.facts.json +++ b/tests/fixtures/verdicts/verdict_boundary_line_negative.facts.json @@ -2,10 +2,11 @@ "ownir_version": 0, "module": "Bound", "_doc": [ - "Declared #259 cp4 divergence family: a coordinate outside the core's u32", - "line domain. The strict door admits every signed 64-bit line, so the", - "reference leaks the consume parameter at B.cs:-1; the Rust core refuses the", - "document rather than clamp the coordinate." + "A promoted #259 coordinate-domain control: a negative param line. Before", + "the domain existed the strict door admitted it and the reference leaked the", + "consume parameter at B.cs:-1 while the Rust core refused the document; now", + "spec/OwnIR.md §4.2 refuses -1 at the strict door and BOTH tolerant doors", + "degrade it to 0 — absent, never clamped to 1. The golden anchors at B.cs:0." ], "functions": [ { diff --git a/tests/fixtures/verdicts/verdict_boundary_line_negative.verdicts.json b/tests/fixtures/verdicts/verdict_boundary_line_negative.verdicts.json index cd70477d..d7e68959 100644 --- a/tests/fixtures/verdicts/verdict_boundary_line_negative.verdicts.json +++ b/tests/fixtures/verdicts/verdict_boundary_line_negative.verdicts.json @@ -3,7 +3,7 @@ "findings": [ { "file": "B.cs", - "line": -1, + "line": 0, "code": "OWN001", "component": "Bound.F", "event": "p", diff --git a/tests/fixtures/verdicts/verdict_boundary_service_line_negative.facts.json b/tests/fixtures/verdicts/verdict_boundary_service_line_negative.facts.json index a121e7f6..89253f3b 100644 --- a/tests/fixtures/verdicts/verdict_boundary_service_line_negative.facts.json +++ b/tests/fixtures/verdicts/verdict_boundary_service_line_negative.facts.json @@ -3,9 +3,10 @@ "module": "Bound", "components": [], "_doc": [ - "Declared #259 cp4 divergence family on the DI path: a negative registration", - "line is a DI001 anchored at reg.cs:-5 for the reference; the Rust core", - "refuses (the anchor cannot be represented)." + "The promoted control on the DI path: a negative registration line degrades", + "to 0 on both tolerant doors, so DI001 anchors at reg.cs:0. The captor", + "evidence step is dropped by the >= 1 guard exactly as it was at -5, which", + "is why the slice does not move." ], "services": [ { diff --git a/tests/fixtures/verdicts/verdict_boundary_service_line_negative.verdicts.json b/tests/fixtures/verdicts/verdict_boundary_service_line_negative.verdicts.json index aa2759c0..bc9783a3 100644 --- a/tests/fixtures/verdicts/verdict_boundary_service_line_negative.verdicts.json +++ b/tests/fixtures/verdicts/verdict_boundary_service_line_negative.verdicts.json @@ -3,7 +3,7 @@ "findings": [ { "file": "reg.cs", - "line": -5, + "line": 0, "code": "DI001", "component": "App", "event": "Db", diff --git a/tests/fixtures/verdicts/verdict_domain_tolerant_readers.facts.json b/tests/fixtures/verdicts/verdict_domain_tolerant_readers.facts.json new file mode 100644 index 00000000..b946ab0a --- /dev/null +++ b/tests/fixtures/verdicts/verdict_domain_tolerant_readers.facts.json @@ -0,0 +1,48 @@ +{ + "ownir_version": 0, + "module": "Dom", + "_doc": [ + "The tolerant coordinate readers, where nothing else reaches them. The four", + "verdict_boundary_* controls pin the DEGRADE on paths that also pass through", + "the AST build, so a port whose raw fact readers kept an out-of-domain value", + "would still produce the right anchor. These three do not:", + " * the DI registration line an escape slice's source hop reads is guarded", + " on >= 1 and never narrowed again, so 2^31 degraded to 0 DROPS the hop", + " while a pass-through keeps it at a coordinate nothing can point at;", + " * the subscription column is read by the tolerant column reader alone, so", + " a column past the domain must read as absent;", + " * the in-domain twin beside each proves the readers still work." + ], + "components": [ + { + "name": "Dom.View", + "file": "V.cs", + "subscriptions": [ + { + "line": 5, + "column": 2147483648, + "event": "bus.Changed", + "handler": "OnChanged", + "released": false, + "resource": "subscription", + "source": "injected", + "source_type": "Bus" + }, + { + "line": 6, + "column": 2147483647, + "event": "bus.Other", + "handler": "OnOther", + "released": false, + "resource": "subscription", + "source": "injected", + "source_type": "Ok" + } + ] + } + ], + "services": [ + {"name": "Bus", "lifetime": "singleton", "file": "reg.cs", "line": 2147483648, "deps": []}, + {"name": "Ok", "lifetime": "singleton", "file": "reg.cs", "line": 12, "deps": []} + ] +} diff --git a/tests/fixtures/verdicts/verdict_domain_tolerant_readers.verdicts.json b/tests/fixtures/verdicts/verdict_domain_tolerant_readers.verdicts.json new file mode 100644 index 00000000..d142e163 --- /dev/null +++ b/tests/fixtures/verdicts/verdict_domain_tolerant_readers.verdicts.json @@ -0,0 +1,48 @@ +{ + "verdicts_version": 1, + "findings": [ + { + "file": "V.cs", + "line": 5, + "code": "OWN014", + "component": "Dom.View", + "event": "bus.Changed", + "handler": "OnChanged", + "message": "event 'bus.Changed' is subscribed (handler 'OnChanged') to 'Bus' — a DI singleton (application-lifetime) service that outlives 'Dom.View'; the strong subscription promotes 'Dom.View' to the source's lifetime, so it can never be collected — a captive/region escape (leak, no release path)", + "kind": "subscription token", + "advisory": false, + "severity": null, + "related": [], + "flow": [], + "ignore_reason": null, + "column": null + }, + { + "file": "V.cs", + "line": 6, + "code": "OWN014", + "component": "Dom.View", + "event": "bus.Other", + "handler": "OnOther", + "message": "event 'bus.Other' is subscribed (handler 'OnOther') to 'Ok' — a DI singleton (application-lifetime) service that outlives 'Dom.View'; the strong subscription promotes 'Dom.View' to the source's lifetime, so it can never be collected — a captive/region escape (leak, no release path)", + "kind": "subscription token", + "advisory": false, + "severity": null, + "related": [], + "flow": [ + [ + "V.cs", + 6, + "'Dom.View' subscribes 'bus.Other' to 'Ok' here" + ], + [ + "reg.cs", + 12, + "source 'Ok' (singleton) registered here — outlives 'Dom.View'" + ] + ], + "ignore_reason": null, + "column": 2147483647 + } + ] +} diff --git a/tests/test_checkpoint_status.py b/tests/test_checkpoint_status.py index a5d786dd..1962ba08 100644 --- a/tests/test_checkpoint_status.py +++ b/tests/test_checkpoint_status.py @@ -43,6 +43,10 @@ from render_checkpoint_status import ( # noqa: E402 CAMPAIGN, CENSUS_MD, + COORD_CAMPAIGNS, + COORD_CENSUS_MD, + COORD_MUTATIONS_MD, + CP1_CENSUS_MD, CP4B_CAMPAIGNS, CP4B_MUTATIONS_MD, CP5_CAMPAIGNS, @@ -60,7 +64,8 @@ # nobody listed is a campaign nobody re-anchors. DEFINITIONS = (CAMPAIGN, *(os.path.join(EVIDENCE, f"{campaign}.json") - for _, campaign in (*CP4B_CAMPAIGNS, *CP5_CAMPAIGNS, *SHADOW_CAMPAIGNS))) + for _, campaign in (*CP4B_CAMPAIGNS, *CP5_CAMPAIGNS, *SHADOW_CAMPAIGNS, + *COORD_CAMPAIGNS))) def _anchors() -> list[str]: @@ -89,9 +94,10 @@ def run() -> int: f"tree; re-anchor it (the recorded result stays valid for the commit it names)") if problems or anchors: return 1 - print(f"checkpoint status fragments OK: {CENSUS_MD}, {INVENTORY_MD}, {MUTATIONS_MD}, " - f"{CP4B_MUTATIONS_MD}, {CP5_MUTATIONS_MD}, {SHADOW_CENSUS_MD}, " - f"{SHADOW_MUTATIONS_MD} in sync with the evidence; " + print(f"checkpoint status fragments OK: {CENSUS_MD}, {CP1_CENSUS_MD}, " + f"{COORD_CENSUS_MD}, {INVENTORY_MD}, " + f"{MUTATIONS_MD}, {CP4B_MUTATIONS_MD}, {CP5_MUTATIONS_MD}, {COORD_MUTATIONS_MD}, " + f"{SHADOW_CENSUS_MD}, {SHADOW_MUTATIONS_MD} in sync with the evidence; " f"{len(DEFINITIONS)} campaign definitions still anchor") return 0 diff --git a/tests/test_ownir.py b/tests/test_ownir.py index d767c888..75a3d589 100644 --- a/tests/test_ownir.py +++ b/tests/test_ownir.py @@ -428,9 +428,11 @@ def _sub(source: str | None) -> list[Finding]: # --- #294 OD-3: `line` coercion must be uniform. Some finding-construction # paths used strict int(sub.get("line", 0)) while others used the - # non-throwing _as_int; a non-int `line` on the tolerant door crashed one - # path and degraded the other. _as_int everywhere: a bad `line` degrades - # to 0, never raises (load() still validates `line` on the strict door). + # non-throwing reader; a non-int `line` on the tolerant door crashed one + # path and degraded the other. `_as_line` everywhere: a bad `line` + # degrades to 0, never raises (load() still validates `line` on the + # strict door). Since #259's final acceptance `_as_line` also degrades a + # line OUTSIDE the §4.2 domain, which is the same rule one axis over. for _bad in (None, "oops", 3.5): checks += 1 tolerant_bad_line = {"ownir_version": OWNIR_VERSION, "module": "X", @@ -444,7 +446,7 @@ def _sub(source: str | None) -> list[Finding]: f"[(OWN001, 0)], got {[(f.code, f.line) for f in _tl]} (#294 OD-3)") except (TypeError, ValueError) as _e: fails.append(f"non-int line {_bad!r} crashed a strict int() finding path " - f"on the tolerant door — should degrade to 0 via _as_int " + f"on the tolerant door — should degrade to 0 via _as_line " f"(#294 OD-3): {type(_e).__name__}: {_e}") # control: a valid int `line` is preserved unchanged. checks += 1 diff --git a/tests/test_ownir_defensive_limits.py b/tests/test_ownir_defensive_limits.py index 5897359f..19bf1e78 100644 --- a/tests/test_ownir_defensive_limits.py +++ b/tests/test_ownir_defensive_limits.py @@ -20,7 +20,7 @@ Each limit is pinned at three points: below, exactly at, and one past. A test that only checks "something far too big is rejected" cannot tell a correct limit from one that is off by one — and the nesting limit *was* off by one when first -written, because `_check_flow_columns` probes every op for `then`/`else`/`body` +written, because `_check_flow_coordinates` probes every op for `then`/`else`/`body` whether or not it has them, so the absent ones were counting as levels. At-limit rejected. Only the boundary case could catch that. @@ -45,9 +45,20 @@ from typing import Any sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from ownlang.obligations import INT64_MAX, INT64_MIN, MAX_NESTING_DEPTH -from ownlang.ownir import OwnIRError, load +from coordinate_census import classify, door_coordinates + +from ownlang.obligations import ( + COLUMN_MAX, + COLUMN_MIN, + INT64_MAX, + INT64_MIN, + LINE_MAX, + LINE_MIN, + MAX_NESTING_DEPTH, +) +from ownlang.ownir import OwnIRError, check_facts, load def _load(document: Any) -> str | None: @@ -109,8 +120,32 @@ def _events(depth: int, key: str = "then") -> dict[str, Any]: ("protocol_functions[].events[].line", lambda v: {"protocol_functions": [ {"name": "M", "events": [{"ev": "return", "line": v}]}]}), + # The two fields §4.2 used to record as validated NOWHERE. They are here + # rather than in a block of their own because the point of #259's final + # acceptance is that they are no longer special: one domain, one table. + ("components[].subscriptions[].line", + lambda v: {"components": [{"subscriptions": [{"line": v}]}]}), + ("functions[].body[].line", + lambda v: {"functions": [{"body": [{"op": "acquire", "line": v}]}]}), + # …and at every nesting shape, because `then`/`else`/`body` are three + # separate recursive call sites: a check added to one of them would pass a + # top-level-only test, which is the same trap the nesting limit records. + ("functions[].body[].then[].line", + lambda v: {"functions": [{"body": [ + {"op": "if", "then": [{"op": "acquire", "line": v}]}]}]}), + ("functions[].body[].else[].line", + lambda v: {"functions": [{"body": [ + {"op": "if", "else": [{"op": "acquire", "line": v}]}]}]}), + ("functions[].body[].body[].line", + lambda v: {"functions": [{"body": [ + {"op": "while", "body": [{"op": "acquire", "line": v}]}]}]}), ] +# The same paths, for the TYPE rule. Every line field rejects a non-integer; +# the two newly validated ones had no type check at all before this change, so +# `{"line": "x"}` and `{"line": true}` were accepted documents. +TYPE_REJECTED = ("x", True, None, 1.5) + COLUMN_PATHS: list[tuple[str, Any]] = [ ("subscriptions[].column", lambda v: {"components": [{"subscriptions": [{"column": v}]}]}), @@ -134,11 +169,21 @@ def _events(depth: int, key: str = "then") -> dict[str, Any]: # self-consistency, not correctness. SPEC_INT64_MIN = -9223372036854775808 SPEC_INT64_MAX = 9223372036854775807 +SPEC_LINE_MIN = 0 +SPEC_LINE_MAX = 2147483647 +SPEC_COLUMN_MIN = 1 +SPEC_COLUMN_MAX = 2147483647 SPEC_MAX_NESTING_DEPTH = 32 -def _fail(message: str) -> int: - print(f"FAIL: {message}") +def _fail(message: str, check: str = "defensive-limits") -> int: + """One failure, named by the CHECK it violated. + + The bracketed name is what a mutation campaign reads (`python-fail` in + `scripts/mutate_campaign.py`), so a mutation's expected catcher can be the + rule it attacks rather than "this file exited non-zero" — which would make + every mutation in this file look identically caught.""" + print(f"FAIL[{check}]: {message}") return 1 @@ -151,13 +196,17 @@ def run() -> int: for name, actual, expected in ( ("INT64_MIN", INT64_MIN, SPEC_INT64_MIN), ("INT64_MAX", INT64_MAX, SPEC_INT64_MAX), + ("LINE_MIN", LINE_MIN, SPEC_LINE_MIN), + ("LINE_MAX", LINE_MAX, SPEC_LINE_MAX), + ("COLUMN_MIN", COLUMN_MIN, SPEC_COLUMN_MIN), + ("COLUMN_MAX", COLUMN_MAX, SPEC_COLUMN_MAX), ("MAX_NESTING_DEPTH", MAX_NESTING_DEPTH, SPEC_MAX_NESTING_DEPTH), ): if actual != expected: failures += _fail( f"{name} is {actual}, spec/OwnIR.md §4.2 says {expected}. " f"Changing a defensive limit is a contract change: update the " - f"spec, this literal, and the Rust side together") + f"spec, this literal, and the Rust side together", check="spec-literals") # ---- the JSON schema carries the same numbers ------------------------- # `spec/ownir.schema.json` is the contract a NON-Python consumer validates @@ -169,10 +218,10 @@ def run() -> int: with open(schema_path, encoding="utf-8") as f: defs = json.load(f)["$defs"] for name, key, expected in ( - ("sourceLine", "minimum", SPEC_INT64_MIN), - ("sourceLine", "maximum", SPEC_INT64_MAX), - ("sourceColumn", "minimum", 1), - ("sourceColumn", "maximum", SPEC_INT64_MAX), + ("sourceLine", "minimum", SPEC_LINE_MIN), + ("sourceLine", "maximum", SPEC_LINE_MAX), + ("sourceColumn", "minimum", SPEC_COLUMN_MIN), + ("sourceColumn", "maximum", SPEC_COLUMN_MAX), # `column: null` is accepted by `load()`, so the schema must permit it. # `minimum`/`maximum` only constrain numbers, so the bounds above still # apply to real values. @@ -183,7 +232,8 @@ def run() -> int: failures += _fail( f"spec/ownir.schema.json $defs.{name}.{key} is {actual!r}, " f"expected {expected!r} — the schema and `load()` must state the " - f"same bound or a producer can satisfy one and fail the other") + f"same bound or a producer can satisfy one and fail the other", + check="schema-numbers") # …and the schema binds `sourceLine` on EXACTLY the paths `load()` checks. # @@ -192,14 +242,21 @@ def run() -> int: # invalid while the door accepts it; leaving a checked path unbound makes it # schema-valid while the door refuses it. A blanket search-and-replace over # `"line"` did the first to `resourceRecord` and `flowOp` — the two paths - # §4.2 documents as unvalidated — and the second to `ctor_line`, whose key - # simply differs. + # §4.2 then documented as unvalidated — and the second to `ctor_line`, whose + # key simply differs. # # So the map is asserted as a map, not spot-checked. + # + # `UNBOUND` is EMPTY since #259's final acceptance: §4.2's exception is + # closed and every coordinate-bearing path is checked by `load()`. The + # machinery stays rather than being deleted with its last entry, because + # what it enforces is that a path is *classified* — the next unbound path + # somebody adds must be declared here, not discovered by nobody. BOUND = {"service": ["line", "ctor_line"], "site": ["line"], "effect": ["line"], "binding": ["line"], "param": ["line"], - "protocolEvent": ["line"]} - UNBOUND = {"resourceRecord": ["line"], "flowOp": ["line"]} + "protocolEvent": ["line"], "resourceRecord": ["line"], + "flowOp": ["line"]} + UNBOUND: dict[str, list[str]] = {} # …and the map is CLOSED over the schema, which the per-member checks below # cannot establish on their own. @@ -239,12 +296,12 @@ def _coordinate_defs(node: Any, found: set[str], name: str = "") -> set[str]: f"$defs {unclassified} carry a source coordinate and appear in " f"neither BOUND nor UNBOUND — every coordinate-bearing def must " f"be classified, because an unclassified one is checked by " - f"nothing at all") + f"nothing at all", check="schema-binding") if phantom: failures += _fail( f"the binding map names {phantom}, which no longer carry a " f"coordinate — a map entry pointing at nothing has stopped " - f"being evidence") + f"being evidence", check="schema-binding") # Whole subschemas, not just their `$ref`. Asserting only "the ref is not # sourceLine" left an unbound path free to be tightened another way — @@ -273,7 +330,7 @@ def _subschemas(node: Any, key: str, out: list[Any]) -> None: if not found: failures += _fail( f"$defs.{def_name} has no {key!r} — the binding map is " - f"stale, which means it is no longer evidence") + f"stale, which means it is no longer evidence", check="schema-binding") for sub in found: if expect_bound: if sub.get("$ref") != "#/$defs/sourceLine": @@ -281,7 +338,7 @@ def _subschemas(node: Any, key: str, out: list[Any]) -> None: f"$defs.{def_name}.{key} is {sub!r}, expected " f"$ref sourceLine — `load()` checks this path, " f"so a schema-valid document must not be able " - f"to fail at the door") + f"to fail at the door", check="schema-binding") continue narrowed = [k for k in NARROWING if k in sub] if narrowed or sub.get("type") != "integer": @@ -290,35 +347,71 @@ def _subschemas(node: Any, key: str, out: list[Any]) -> None: f"plain integer with no {'/'.join(NARROWING[:3])}… " f"— `load()` does NOT check this path (§4.2), so " f"any narrowing makes a document schema-invalid " - f"that the door accepts") + f"that the door accepts", check="schema-binding") - # ---- line: the full signed-64 range, and one step outside each end ----- + # ---- line: the DOMAIN, pinned at both ends and one step outside each -- + # + # Two axes, in the order the door checks them, so neither can silently + # absorb the other: a value with no representable signed-64 form reports + # the FORM rule, and a representable value outside `[0, 2^31-1]` reports + # the DOMAIN rule. `i64::MAX` is the control that keeps them apart — it was + # an accepted line until #259's final acceptance, and it is now a domain + # rejection rather than a form one. for label, build in LINE_PATHS: - for value, expect_reject in ((INT64_MIN, False), (0, False), - (INT64_MAX, False), - (INT64_MIN - 1, True), (INT64_MAX + 1, True)): + for value, expected in ((LINE_MIN, None), (1, None), (LINE_MAX, None), + (LINE_MIN - 1, "source line in"), + (LINE_MAX + 1, "source line in"), + (INT64_MIN, "source line in"), + (INT64_MAX, "source line in"), + (INT64_MIN - 1, "signed 64-bit"), + (INT64_MAX + 1, "signed 64-bit")): err = _load(build(value)) - if expect_reject and err is None: - failures += _fail(f"{label}: {value} accepted, expected reject") - elif not expect_reject and err is not None: - failures += _fail(f"{label}: {value} rejected — {err}") - elif expect_reject and "signed 64-bit" not in (err or ""): + if expected is None and err is not None: + failures += _fail(f"{label}: {value} rejected — {err}", check="line-domain") + elif expected is not None and err is None: + failures += _fail(f"{label}: {value} accepted, expected reject", + check="line-domain") + elif expected is not None and expected not in (err or ""): failures += _fail( - f"{label}: {value} rejected for the wrong reason — {err}") + f"{label}: {value} rejected for the wrong reason — " + f"expected the {expected!r} rule, got {err}", check="line-domain") - # ---- column: 1-based below, and the same upper bound ------------------ + # ---- line: the TYPE rule, on every path ------------------------------- + # + # Only the rejection is asserted, not the message. The reference folds a + # site record's whole check into one `all(...)`, so a mistyped site line + # reports the record's shape rather than the field's — one violation, one + # message, and the cp1 ledger is where the CATEGORY of each is pinned. + for label, build in LINE_PATHS: + for value in TYPE_REJECTED: + if _load(build(value)) is None: + failures += _fail( + f"{label}: {value!r} accepted — a line must be an integer", check="line-type") + + # ---- column: 1-based below, the domain above -------------------------- for label, build in COLUMN_PATHS: - for value, expect_reject in ((1, False), (INT64_MAX, False), - (INT64_MAX + 1, True)): + for value, expected in ((COLUMN_MIN, None), (COLUMN_MAX, None), + (COLUMN_MAX + 1, "source column in"), + (INT64_MAX, "source column in"), + (INT64_MAX + 1, "signed 64-bit")): err = _load(build(value)) - if expect_reject and err is None: - failures += _fail(f"{label}: {value} accepted, expected reject") - elif not expect_reject and err is not None: - failures += _fail(f"{label}: {value} rejected — {err}") - # The 1-based rule still fires first for a low column, so the new upper - # bound cannot have replaced it. - if "1-based" not in (_load(build(0)) or ""): - failures += _fail(f"{label}: 0 no longer reports the 1-based rule") + if expected is None and err is not None: + failures += _fail(f"{label}: {value} rejected — {err}", check="column-domain") + elif expected is not None and err is None: + failures += _fail(f"{label}: {value} accepted, expected reject", + check="column-domain") + elif expected is not None and expected not in (err or ""): + failures += _fail( + f"{label}: {value} rejected for the wrong reason — " + f"expected the {expected!r} rule, got {err}", check="column-domain") + # The 1-based rule still fires FIRST for every low column, so neither + # new bound can have replaced it — including for a value that is also + # outside the representable form, where the order is what decides which + # rule the reader is told about. + for low in (0, -1, INT64_MIN, INT64_MIN - 1): + if "1-based" not in (_load(build(low)) or ""): + failures += _fail( + f"{label}: {low} no longer reports the 1-based rule", check="column-domain") # ---- nesting: below, exactly at, one past — for both trees and both # recursive keys, because `then`/`else`/`body` are three separate call @@ -333,14 +426,14 @@ def _subschemas(node: Any, key: str, out: list[Any]) -> None: if expect_reject and err is None: failures += _fail( f"{label} via {key!r}: depth {depth} accepted, " - f"expected reject (the limit is {MAX_NESTING_DEPTH})") + f"expected reject (the limit is {MAX_NESTING_DEPTH})", check="nesting") elif not expect_reject and err is not None: failures += _fail( - f"{label} via {key!r}: depth {depth} rejected — {err}") + f"{label} via {key!r}: depth {depth} rejected — {err}", check="nesting") elif expect_reject and "nested deeper" not in (err or ""): failures += _fail( f"{label} via {key!r}: depth {depth} rejected for the " - f"wrong reason — {err}") + f"wrong reason — {err}", check="nesting") # ---- the tolerances the limits must NOT have tightened ---------------- # A non-list body is skipped, not rejected; the reference returns early. @@ -357,15 +450,135 @@ def _subschemas(node: Any, key: str, out: list[Any]) -> None: ): err = _load(document) if err is not None: - failures += _fail(f"{label} must still be accepted — {err}") + failures += _fail(f"{label} must still be accepted — {err}", check="tolerances") + + # ---- the TOLERANT door degrades; it never raises and never clamps ----- + # + # `check_facts()` is the door tests and embedders take, and it may be + # handed facts `load()` never saw. An out-of-domain line there reads as + # `0` — the value an absent line already reads as — and the finding is + # still reported at its file. The clamp controls are the point: `2^31` + # must not become `2^31 - 1` and `-1` must not become `1`, because a + # clamped anchor points at a REAL line the producer did not mean. + def _sub(line: Any) -> dict[str, Any]: + return {"ownir_version": 0, "module": "X", + "components": [{"name": "C", "file": "C.cs", "subscriptions": [ + {"event": "e", "handler": "h", "line": line, + "source": "static"}]}]} + + def _acquire(line: Any) -> dict[str, Any]: + return {"ownir_version": 0, "module": "X", "functions": [ + {"name": "F", "file": "F.cs", + "body": [{"op": "acquire", "var": "x", "line": line}]}]} + + def _event(line: Any) -> dict[str, Any]: + return {"ownir_version": 0, "module": "X", + "protocols": [{"name": "P", + "opens": {"kind": "assign", "target": "f", + "value": True}, + "closes": {"kind": "assign", "target": "f", + "value": False}, + "barriers": [{"kind": "call", "callee": "B"}]}], + "protocol_functions": [{"name": "M", "file": "M.cs", "events": [ + {"ev": "assign", "target": "f", "value": True, "line": line}, + {"ev": "call", "callee": "B", "line": 9}]}]} + + # Out-of-DOMAIN values degrade on every path… + OUT_OF_DOMAIN = (-1, LINE_MIN - 1, LINE_MAX + 1, 2 ** 31, INT64_MAX) + # …while a value with no integer form at all degrades on the two paths + # whose reader is `_as_line`, and SKIPS the entry on the protocol path. + # That asymmetry is the obligation family's own tolerant rule ("a + # malformed entry is skipped whole", cp4b) and is deliberately untouched: + # type and representability are grammar, the domain is the door's. + for label, build_facts, code in (("subscription", _sub, "OWN001"), + ("flow acquire", _acquire, "OWN001"), + ("protocol event", _event, "OBL003")): + values = OUT_OF_DOMAIN if label == "protocol event" else ( + *OUT_OF_DOMAIN, "x", True, None) + for value in values: + anchors = [(f.code, f.line) for f in check_facts(build_facts(value)) + if f.code == code] + if anchors != [(code, 0)]: + failures += _fail( + f"tolerant {label} line {value!r}: expected " + f"[({code}, 0)] — degrade to absent, never clamp — got " + f"{anchors}", check="tolerant-degrade") + for value in ("x", True, None, INT64_MAX + 1): + if [f for f in check_facts(_event(value)) if f.code == "OBL003"]: + failures += _fail( + f"tolerant protocol event line {value!r}: the grammar rejects " + f"it, so the ENTRY is skipped whole (cp4b) — a finding here " + f"means the domain degrade swallowed a grammar rule", check="tolerant-degrade") + # …and a line INSIDE the domain is preserved exactly, so the degrade + # cannot have swallowed the ordinary path. + for value in (1, LINE_MAX): + anchors = [(f.code, f.line) for f in check_facts(build_facts(value)) + if f.code == code] + if anchors != [(code, value)]: + failures += _fail( + f"tolerant {label} line {value}: in-domain lines must be " + f"preserved, got {anchors}", check="tolerant-degrade") + + # …and the COLUMN degrades the same way, on its own domain. Its reader is + # `_as_col`, which the four verdict_boundary_* controls never reach — the + # only thing that pins the tolerant column bound is right here and the one + # synthetic Layer 3 case written for it. + def _sub_col(column: Any) -> dict[str, Any]: + return {"ownir_version": 0, "module": "X", + "components": [{"name": "C", "file": "C.cs", "subscriptions": [ + {"event": "e", "handler": "h", "line": 7, "column": column, + "source": "static"}]}]} + + for value, want in ((COLUMN_MIN, COLUMN_MIN), (COLUMN_MAX, COLUMN_MAX), + (COLUMN_MAX + 1, None), (INT64_MAX, None), (0, None), + (-1, None), (True, None), ("x", None), (None, None)): + cols = [f.column for f in check_facts(_sub_col(value)) if f.code == "OWN001"] + if cols != [want]: + failures += _fail( + f"tolerant column {value!r}: expected [{want!r}] — a column " + f"outside the domain is ABSENT, never clamped — got {cols}", + check="tolerant-degrade") + + # ---- and the strict door never REACHES the degrade -------------------- + # + # The two-doors contract only holds if the strict door's output is a + # subset of the tolerant door's input domain: a document `load()` accepts + # must carry no out-of-domain coordinate at all, or `_as_line` would be + # silently rewriting validated facts. That is asserted over every document + # the #259 cp1 ledger records as ACCEPTED, rather than stated in prose — + # the ledger is the widest set of accepted documents in the tree, and it + # is derived from the reference itself. + ledger_path = os.path.join(os.path.dirname(__file__), "fixtures", + "ownir_validation.json") + with open(ledger_path, encoding="utf-8") as f: + ledger = json.load(f) + accepted = 0 + for case in ledger["cases"]: + if case.get("verdict") != "accept" or case.get("raw"): + continue + accepted += 1 + for slot, kind, value in door_coordinates(case["document"]): + if classify(kind, value) not in ("in-domain", "zero", "null"): + failures += _fail( + f"cp1 control {case['name']!r} is ACCEPTED by the strict " + f"door and carries {slot} = {value!r}, which the tolerant " + f"door would degrade — the strict door must never reach " + f"the degrade (spec/OwnIR.md §4.2)", check="strict-subset") + if not accepted: + failures += _fail( + "no accepted cp1 control was read — the subset assertion above " + "passed over an empty set, which is not evidence", check="strict-subset") if failures: return 1 print( - f"ownir defensive limits OK: coordinates in " - f"[{INT64_MIN}, {INT64_MAX}] over {len(LINE_PATHS)} line paths and " - f"{len(COLUMN_PATHS)} column paths; nesting <= {MAX_NESTING_DEPTH} " - f"over 2 trees x 3 keys, each pinned at limit and limit+1" + f"ownir defensive limits OK: lines in [{LINE_MIN}, {LINE_MAX}] and " + f"columns in [{COLUMN_MIN}, {COLUMN_MAX}] inside the signed-64 form, " + f"over {len(LINE_PATHS)} line paths and {len(COLUMN_PATHS)} column " + f"paths; nesting <= {MAX_NESTING_DEPTH} over 2 trees x 3 keys, each " + f"pinned at limit and limit+1; the tolerant door degrades on 3 paths " + f"and the strict door reaches it on none of {accepted} accepted cp1 " + f"controls" ) return 0 diff --git a/tests/test_ownir_validation_fixtures.py b/tests/test_ownir_validation_fixtures.py index 9d0d4a60..30f91d9f 100644 --- a/tests/test_ownir_validation_fixtures.py +++ b/tests/test_ownir_validation_fixtures.py @@ -87,11 +87,31 @@ 32-level nesting limit, written into `spec/OwnIR.md` §4.2 as defensive limits on externally supplied structure. Both families are now controls here. -The one thing still not covered is stated where it lives: -`components[].subscriptions[].line` and flow-op `line` are validated **nowhere** -by `load()`, not even for type, and both implementations agree about that. It -is an open contract question recorded in §4.2, not a parity gap, and inventing -a control for it here would be inventing a rule neither loader has. +## The third family: the coordinate DOMAIN + +The previous round left one thing uncovered and said so: +`components[].subscriptions[].line` and flow-op `line` were validated +**nowhere** by `load()`, not even for type, and both implementations agreed +about it — an open contract question recorded in §4.2 rather than a parity gap, +where inventing a control would have been inventing a rule neither loader had. + +#259's final acceptance answers the question. §4.2 now bounds every `line` to +`[0, 2147483647]` and every `column` to `[1, 2147483647]` — the int32 domain +every consumer this project feeds actually has — and validates both of those +fields for type as well. So the family is controls here, generated per field +by `_domain_controls()` below. + +Four of this file's ACCEPT controls flip with it, and that is a **re-measure, +not a weakening**: the reference changed first (Python-first, #326's order), so +the standing rule "a Rust/Python divergence is a Rust bug" never applied. They +keep their `accept-` names on purpose, so the flip reads as one re-measured +control rather than as a new control beside a deleted one. + +The two axes stay apart, and the flip is what proves they are not one rule +wearing two names: `i64::MAX` as a line was accepted and is now `location`, +because it still HAS a representable signed-64 form and violates only the rule +about what that form may mean; `i64::MAX + 1` was and stays `shape`, because it +has no such form at all. ## What is compared, and what deliberately is not @@ -160,13 +180,15 @@ "vocabulary": "right JSON type, value outside a closed set", "identity": "a name slot — empty, mistyped, or duplicated", "location": ("a REPRESENTABLE source coordinate violating its " - "coordinate-domain rule — currently the 1-based column"), + "coordinate-domain rule — the 1-based column, and the " + "int32 line/column domain of spec/OwnIR.md §4.2"), "well_formedness": ("right types, legal vocabulary, and the record still " "cannot mean anything"), } -# The representable integer form of an OwnIR source coordinate (spec/OwnIR.md -# §4.2). Both loaders accept the closed range and reject everything outside it. +# The representable integer FORM of an OwnIR source coordinate (spec/OwnIR.md +# §4.2) — the outer of the two axes. Both loaders reject everything outside it +# as `shape`; what a value inside it may MEAN is the domain, below. I64_MIN = -9223372036854775808 I64_MAX = 9223372036854775807 @@ -195,6 +217,17 @@ # which is how the reference's own off-by-one was found. MAX_NESTING = 32 +# The coordinate DOMAIN inside that form (spec/OwnIR.md §4.2, #259 final +# acceptance). Literals, not imports: a ledger written in terms of the value +# under test asserts self-consistency rather than correctness — the failure +# `tests/test_ownir_defensive_limits.py` records at the top of its own +# constant block, one layer down. +LINE_MIN = 0 +LINE_MAX = 2147483647 +COLUMN_MIN = 1 +COLUMN_MAX = 2147483647 +ABOVE_LINE_MAX = LINE_MAX + 1 + def _c(name: str, section: str, why: str, document: Any, category: str | None, raw: bool = False) -> dict[str, Any]: @@ -1069,22 +1102,40 @@ def _controls() -> list[dict[str, Any]]: # represent. #326 closed that Python-first, so the family belongs in # the ledger now — and "0/0/0 over a set with a known divergence # removed from it" stops being the result on offer. + # FOUR OF THESE FLIPPED in #259's final acceptance, and they keep + # their `accept-` names on purpose: the name records the contract the + # coordinate-domain decision replaced, so the flip reads as one + # re-measured control rather than as a new control beside a deleted + # one. Each `why` states the position it replaced; the note quotes the + # old wording in full (docs/notes/p022-bridge-verdict-final-acceptance.md + # §2.3). This is a re-measure, not a weakening: the reference changed + # first, so "a divergence is a Rust bug" never applied. _c("accept-line-at-i64-max", "services", - "the largest representable line", - {"ownir_version": 0, "services": [_svc(line=I64_MAX)]}, None), + "the largest REPRESENTABLE line — accepted until the coordinate " + "domain existed, now outside it. Representability and domain are " + "two axes: this value still HAS a signed-64 form, which is exactly " + "why it is `location` and not `shape`", + {"ownir_version": 0, "services": [_svc(line=I64_MAX)]}, "location"), _c("accept-line-at-i64-min", "services", - "…and the smallest, because the range is closed at BOTH ends and a " - "port that bounded only the top would pass a one-sided test", - {"ownir_version": 0, "services": [_svc(line=I64_MIN)]}, None), + "…and the smallest. The range is still closed at BOTH ends and a " + "port that bounded only the top would pass a one-sided test — the " + "ends have simply moved from the form to the domain", + {"ownir_version": 0, "services": [_svc(line=I64_MIN)]}, "location"), _c("accept-column-at-i64-max", "components", - "…and the largest column", + "…and the largest column, which now carries the same upper bound a " + "line does", {"ownir_version": 0, - "components": [{"subscriptions": [{"column": I64_MAX}]}]}, None), + "components": [{"subscriptions": [{"column": I64_MAX}]}]}, + "location"), _c("accept-negative-line", "services", - "a NEGATIVE line is accepted: only columns carry the 1-based rule, " - "and conflating the two would tighten the door", - {"ownir_version": 0, "services": [_svc(line=-5)]}, None), - _c("accept-zero-line", "services", "…and zero is the line default", + "a negative line is REJECTED: it is not the 1-based column rule " + "borrowed, it is the line's own domain — no producer emits one and " + "nothing downstream can point at it (spec/OwnIR.md §4.2)", + {"ownir_version": 0, "services": [_svc(line=-5)]}, "location"), + _c("accept-zero-line", "services", + "…and zero is still the line default, unchanged: it means " + "'unknown / file-level' and is the bottom of the domain, not an " + "edge case that survived", {"ownir_version": 0, "services": [_svc(line=0)]}, None), # One step past each end, and both ends of the u64 band. The four @@ -1212,7 +1263,182 @@ def _controls() -> list[dict[str, Any]]: {"ownir_version": 0, "protocol_functions": [ {"name": "M", "events": [_nest_ev(MAX_NESTING + 1, "body")]}]}, "shape"), + # …and the coordinate-domain family, appended (insertion-stable). + *_domain_controls(), + ] + + +# --- the coordinate domain, one family per field (#259 final acceptance) ----- + + +def _line_slots() -> tuple[tuple[str, str, Any], ...]: + """Every line-bearing field, as `(stem, section, build)`. + + `build(value)` returns a MINIMAL document carrying that value in exactly + one coordinate slot, so a control isolates the field rather than the + section. The two fields §4.2 used to record as validated nowhere are in + this table beside the rest, because after this change they are not + special — and the flow-op line appears once per nesting shape, since + `then`/`else`/`body` are three separate recursion sites in the reference. + """ + return ( + ("service-line", "services", + lambda v: {"ownir_version": 0, "services": [_svc(line=v)]}), + ("service-ctor-line", "services", + lambda v: {"ownir_version": 0, "services": [_svc(ctor_line=v)]}), + ("service-site-line", "services", + lambda v: {"ownir_version": 0, + "services": [_svc(root_resolve_sites=[{"line": v}])]}), + ("service-scope-cache-site-line", "services", + lambda v: {"ownir_version": 0, + "services": [_svc(scope_cache_sites=[{"line": v}])]}), + ("effect-line", "effects", + lambda v: {"ownir_version": 0, "effects": [{"line": v}]}), + ("binding-line", "effects", + lambda v: {"ownir_version": 0, + "effects": [{"bindings": [{"line": v}]}]}), + ("param-line", "functions", + lambda v: {"ownir_version": 0, + "functions": [{"params": [{"name": "p", "line": v}]}]}), + ("event-line", "protocol_functions", + lambda v: {"ownir_version": 0, "protocol_functions": [ + {"name": "M", "events": [{"ev": "return", "line": v}]}]}), + ("subscription-line", "components", + lambda v: {"ownir_version": 0, + "components": [{"subscriptions": [{"line": v}]}]}), + ("flow-op-line", "functions", + lambda v: {"ownir_version": 0, + "functions": [{"body": [{"op": "acquire", "line": v}]}]}), + ("flow-op-line-then", "functions", + lambda v: {"ownir_version": 0, "functions": [{"body": [ + {"op": "if", "then": [{"op": "acquire", "line": v}]}]}]}), + ("flow-op-line-else", "functions", + lambda v: {"ownir_version": 0, "functions": [{"body": [ + {"op": "if", "else": [{"op": "acquire", "line": v}]}]}]}), + ("flow-op-line-body", "functions", + lambda v: {"ownir_version": 0, "functions": [{"body": [ + {"op": "while", "body": [{"op": "acquire", "line": v}]}]}]}), + ) + + +def _column_slots() -> tuple[tuple[str, str, Any], ...]: + return ( + ("subscription-column", "components", + lambda v: {"ownir_version": 0, + "components": [{"subscriptions": [{"column": v}]}]}), + ("param-column", "functions", + lambda v: {"ownir_version": 0, + "functions": [{"params": [{"name": "p", "column": v}]}]}), + ("flow-op-column", "functions", + lambda v: {"ownir_version": 0, + "functions": [{"body": [{"op": "acquire", "column": v}]}]}), + ) + + +def _domain_controls() -> list[dict[str, Any]]: + """The domain pinned at both ends and one step outside each, per field. + + Four values per line field is the minimum that discriminates: without the + at-limit pair a port could bound the domain one narrow and still pass, and + without `0` it could bound it at 1 and pass. The same off-by-one that made + the nesting limit reject a body AT the limit is the reason the shape of + this table is below/at/past rather than "something far outside". + + Appended after the existing controls rather than interleaved: a + vocabulary-derived ledger must stay insertion-stable (P-022 discipline 4), + so adding a family churns no existing record. + """ + out: list[dict[str, Any]] = [] + newly_validated = {"subscription-line", "flow-op-line", "flow-op-line-then", + "flow-op-line-else", "flow-op-line-body"} + for stem, section, build in _line_slots(): + note = (" — a field `load()` checked NOWHERE before this change, not " + "even for type (§4.2's recorded exception, now closed)" + if stem in newly_validated else "") + out += [ + _c(f"accept-{stem}-zero", section, + f"`0` is the bottom of the domain and means 'unknown / " + f"file-level'{note}", build(LINE_MIN), None), + _c(f"accept-{stem}-at-int32-max", section, + "…and 2147483647 is the top, accepted exactly", + build(LINE_MAX), None), + _c(f"{stem}-negative", section, + "one below the bottom: a representable coordinate outside its " + "domain, so `location` rather than `shape`", + build(LINE_MIN - 1), "location"), + _c(f"{stem}-above-int32", section, + "…and one above the top, the value a port that kept the old " + "signed-64 bound would still accept", + build(ABOVE_LINE_MAX), "location"), + ] + # Type controls for the two fields that had none. Every other line field + # already carries them above; these two were accepted with any value at + # all, which is what §4.2 recorded as an open contract question. + for stem, section, build in _line_slots(): + if stem not in newly_validated: + continue + out += [ + _c(f"{stem}-string", section, + "a string line has no integer form — `shape`, on the other " + "axis from the domain rejections above", + build("x"), "shape"), + _c(f"{stem}-bool", section, + "…and the bool-is-int trap, which would otherwise read as " + "line 1", + build(True), "shape"), + _c(f"{stem}-null", section, + "…and a PRESENT null, which is not the same as absent: the " + "reference reads the slot with a `0` default, so only an " + "absent key takes it", + build(None), "shape"), + ] + for stem, section, build in _column_slots(): + out += [ + _c(f"accept-{stem}-at-int32-max", section, + "the top of the column domain, accepted exactly", + build(COLUMN_MAX), None), + _c(f"{stem}-above-int32", section, + "…and one past it: representable, positive, and outside the " + "domain — `location`, the same axis the 1-based rule is on", + build(COLUMN_MAX + 1), "location"), + ] + out += [ + _c("column-below-i64", "components", + "a column BELOW the representable form. It is `shape` by mechanism " + "— there is no signed-64 integer here for a 1-based rule to be " + "about — even though the reference's message names the 1-based " + "rule, because it tests `v < 1` before the form. The category is " + "read off the mechanism, which is this ledger's rule and the " + "reason the taxonomy has two axes at all", + {"ownir_version": 0, + "components": [{"subscriptions": [{"column": BELOW_I64}]}]}, + "shape"), + # Order, which BR-D1 makes observable. Each pair breaks two rules whose + # categories differ, so the answer names which check ran first. + _c("order-subscription-kind-before-line", "components", + "an unknown resource kind and an out-of-domain line in one record: " + "the kind is checked first, so this is `vocabulary`", + {"ownir_version": 0, "components": [{"subscriptions": [ + {"resource": "nope", "line": -1}]}]}, "vocabulary"), + _c("order-subscription-line-before-column", "components", + "…and the line precedes the column of the same node (§4.1), so a " + "record breaking both reports the line's domain, not the column's " + "type", + {"ownir_version": 0, "components": [{"subscriptions": [ + {"line": -1, "column": True}]}]}, "location"), + _c("order-flow-op-line-before-column", "functions", + "…and the same order inside a flow op", + {"ownir_version": 0, "functions": [{"body": [ + {"op": "acquire", "line": -1, "column": True}]}]}, "location"), + _c("order-flow-op-body-before-params", "functions", + "the BODY's coordinates still precede `params` — the least obvious " + "edge in the door, and now observable through a line as well as a " + "column", + {"ownir_version": 0, "functions": [{ + "body": [{"op": "acquire", "line": -1}], + "params": [{"name": ""}]}]}, "location"), ] + return out def _oracle(document: Any, raw: bool) -> tuple[str, str]: @@ -1299,13 +1525,13 @@ def run() -> int: fresh = build() expected = _render_json(fresh) if not os.path.exists(FIXTURE): - print(f"FAIL: {FIXTURE} missing; regenerate with " + print(f"FAIL[ledger-missing]: {FIXTURE} missing; regenerate with " f"'python tests/test_ownir_validation_fixtures.py --write'") return 1 with open(FIXTURE, encoding="utf-8") as f: actual = f.read() if actual != expected: - print(f"FAIL: {FIXTURE} is stale (the strict door's acceptance changed); " + print(f"FAIL[ledger-stale]: {FIXTURE} is stale (the strict door's acceptance changed); " f"regenerate with 'python tests/test_ownir_validation_fixtures.py " f"--write' and re-run the Rust side " f"(cd rust && cargo test -p own-ir)") @@ -1313,11 +1539,11 @@ def run() -> int: data = json.loads(actual) if set(data) != ROOT_KEYS: - print(f"FAIL: fixture root keys {sorted(data)} != {sorted(ROOT_KEYS)}") + print(f"FAIL[ledger-shape]: fixture root keys {sorted(data)} != {sorted(ROOT_KEYS)}") return 1 for case in data["cases"]: if set(case) != CASE_KEYS: - print(f"FAIL: case {case.get('name')!r} keys {sorted(case)} " + print(f"FAIL[ledger-shape]: case {case.get('name')!r} keys {sorted(case)} " f"!= {sorted(CASE_KEYS)}") return 1 @@ -1325,10 +1551,10 @@ def run() -> int: # one, would make the Rust comparison vacuous for that case. for case in data["cases"]: if case["verdict"] == "reject" and not case["category"]: - print(f"FAIL: {case['name']}: rejected with no category") + print(f"FAIL[ledger-category]: {case['name']}: rejected with no category") return 1 if case["verdict"] == "accept" and case["category"]: - print(f"FAIL: {case['name']}: accepted but carries a category") + print(f"FAIL[ledger-category]: {case['name']}: accepted but carries a category") return 1 # Every declared category must be exercised. An unused category is a claim @@ -1336,7 +1562,7 @@ def run() -> int: used = {c["category"] for c in data["cases"] if c["category"]} unused = sorted(set(data["categories"]) - used) if unused: - print(f"FAIL: declared categories with no control: {unused}. A category " + print(f"FAIL[ledger-coverage]: declared categories with no control: {unused}. A category " f"is only worth having if a case exercises it") return 1 @@ -1344,8 +1570,9 @@ def run() -> int: # them the ledger would pass against a loader that rejects everything. accepted = data["totals"]["accepted"] if accepted < 5: - print(f"FAIL: only {accepted} acceptance control(s); the rejections are " - f"not discriminating without valid twins") + print(f"FAIL[ledger-discriminating]: only {accepted} acceptance " + f"control(s); the rejections are not discriminating without " + f"valid twins") return 1 # The oracle must be a pure function of the control. It was not, on the diff --git a/tests/test_verdict_fixtures.py b/tests/test_verdict_fixtures.py index a7f91a23..f2a92fa1 100644 --- a/tests/test_verdict_fixtures.py +++ b/tests/test_verdict_fixtures.py @@ -30,10 +30,14 @@ view anchor, and the declared-boundary controls below. * **The Rust exclusion ledger** — `rust_replay_excluded` names the cases whose golden is Python's truth but which the Rust core REFUSES by a declared - boundary (a coordinate outside the core's `u32` line domain; a shape the - typed Rust door rejects before the bridge runs — #294 OD-1). It named a - third until #259 checkpoint 4b — a protocol-bearing document, refused while - the OBL analysis had no port — and both such documents are now promoted. + boundary. Two remain, both #294 OD-1: a shape the typed Rust door rejects + before the bridge's tolerant skip rule can run. Two families have been + promoted out of it, neither by waiving anything. Checkpoint 4b ported the + OBL analysis the protocol-bearing documents were refused for. #259's final + acceptance closed the coordinate family from the REFERENCE's side: §4.2 now + bounds a line to the int32 domain every consumer actually has, so both + tolerant doors degrade an out-of-domain coordinate to `0` and the four + `verdict_boundary_*` controls replay like any other case. Each entry carries its reason and an executable expectation (`rust_refusal`: `bridge` or `door`, plus an error substring) that the Rust replay asserts, so an exclusion cannot rot @@ -127,7 +131,7 @@ def run() -> int: fails.extend(e.problems) if fails: for f_ in fails: - print(f"FAIL: verdict fixture {f_}") + print(f"FAIL[verdict-fixture]: {f_}") return 1 print(f"verdicts (Layer 3) fixtures OK: {census.goldens} cases " f"({census.python_refusals} refusals, {census.python_findings} findings; " diff --git a/tests/validation_census.py b/tests/validation_census.py new file mode 100644 index 00000000..9e5137be --- /dev/null +++ b/tests/validation_census.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""The #259 checkpoint-1 ledger, interpreted once (the fourth census). + +`tests/fixtures/ownir_validation.json` is the frozen BR-D1 acceptance language: +every control, the verdict the reference gives it, and — on a rejection — the +category the ledger claims for the MECHANISM that produced it. Three numbers +about it have been typed into status surfaces so far (`216 controls`, +`matrix 35/181`, `48 mutations`), and typed numbers are how a status table +drifts from the tree it describes. This module is the one reader, so the +census a document shows is computed from the ledger rather than remembered. + +Pure: no `ownlang` import and no side effects, like `verdict_census.py` and +`coordinate_census.py` beside it. It does NOT run the door — the ledger is +already the reference's recorded answer, regenerated by +`python tests/test_ownir_validation_fixtures.py --write`, and re-deriving it +here would be a second oracle with a second opinion. + +What it counts, and why each split is the one that matters: + +* **by section** — the ledger's own grouping, which is BR-D1's check order. + A section with acceptances and no rejections is a door nobody probed; one + with rejections and no acceptance twin is a rejection nothing discriminates. +* **by category** — the seven-category taxonomy on two axes. `shape` versus + `location` is the split #326's census had to discover, and this change moved + a family across it, so the two counts are the ones a reader compares. +* **the coordinate family on its own** — every control whose document carries + a `line` or a `column`, because that is the family #259's final acceptance + moved and the one a reviewer needs to find without reading 294 records. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass + +HERE = os.path.dirname(os.path.abspath(__file__)) +LEDGER = os.path.join(HERE, "fixtures", "ownir_validation.json") +SCHEMA_VERSION = 1 +ACCEPTED = "accepted" + + +@dataclass(frozen=True) +class SectionRow: + section: str + accepted: int + rejected: int + by_category: tuple[tuple[str, int], ...] + + +@dataclass(frozen=True) +class ValidationCensus: + schema_version: int + controls: int + accepted: int + rejected: int + categories: tuple[tuple[str, str], ...] + by_category: tuple[tuple[str, int], ...] + sections: tuple[SectionRow, ...] + coordinate_controls: int + coordinate_accepted: int + coordinate_by_category: tuple[tuple[str, int], ...] + + +class ValidationCensusError(Exception): + def __init__(self, problems: list[str]) -> None: + super().__init__("; ".join(problems)) + self.problems = problems + + +def _carries_coordinate(node: object) -> bool: + """Does this control's document carry a `line`, `ctor_line` or `column` + anywhere? Structural, at any depth, so a control nested inside a function + body counts exactly like one at the top level.""" + if isinstance(node, dict): + if {"line", "ctor_line", "column"} & node.keys(): + return True + return any(_carries_coordinate(v) for v in node.values()) + if isinstance(node, list): + return any(_carries_coordinate(v) for v in node) + return False + + +def compute_validation_census() -> ValidationCensus: + problems: list[str] = [] + try: + with open(LEDGER, encoding="utf-8") as f: + data = json.load(f) + except (OSError, ValueError) as e: + raise ValidationCensusError([f"ledger unreadable: {e}"]) from e + if data.get("schema_version") != SCHEMA_VERSION: + problems.append( + f"ledger schema_version {data.get('schema_version')!r} != {SCHEMA_VERSION} — " + f"a reviewed contract, not a passing reshape") + cases = data.get("cases") + if not isinstance(cases, list) or not cases: + raise ValidationCensusError([*problems, "ledger 'cases' must be a non-empty array"]) + declared = data.get("categories") + if not isinstance(declared, dict): + raise ValidationCensusError([*problems, "ledger 'categories' must be an object"]) + + by_category: dict[str, int] = {} + per_section: dict[str, dict[str, int]] = {} + accepted = rejected = 0 + coord = coord_accepted = 0 + coord_by_category: dict[str, int] = {} + for i, case in enumerate(cases): + if not isinstance(case, dict): + problems.append(f"cases[{i}] is not an object") + continue + name = case.get("name", f"cases[{i}]") + section = case.get("section") + verdict = case.get("verdict") + category = case.get("category") + if not isinstance(section, str) or not section: + problems.append(f"{name}: 'section' must be a non-empty string") + continue + if verdict == "accept": + if category is not None: + problems.append(f"{name}: accepted, but carries a category") + key = ACCEPTED + accepted += 1 + elif verdict == "reject": + if not isinstance(category, str) or category not in declared: + problems.append(f"{name}: rejected with category {category!r}, " + f"which the ledger does not declare") + continue + key = category + rejected += 1 + else: + problems.append(f"{name}: verdict {verdict!r} is neither accept nor reject") + continue + by_category[key] = by_category.get(key, 0) + 1 + section_counts = per_section.setdefault(section, {}) + section_counts[key] = section_counts.get(key, 0) + 1 + if _carries_coordinate(case.get("document")) and not case.get("raw"): + coord += 1 + coord_by_category[key] = coord_by_category.get(key, 0) + 1 + if key == ACCEPTED: + coord_accepted += 1 + if problems: + raise ValidationCensusError(problems) + + sections = tuple( + SectionRow( + section=section, + accepted=counts.get(ACCEPTED, 0), + rejected=sum(n for k, n in counts.items() if k != ACCEPTED), + by_category=tuple(sorted((k, n) for k, n in counts.items() if k != ACCEPTED)), + ) + for section, counts in sorted(per_section.items()) + ) + return ValidationCensus( + schema_version=SCHEMA_VERSION, + controls=accepted + rejected, + accepted=accepted, + rejected=rejected, + categories=tuple(sorted((k, str(v)) for k, v in declared.items())), + by_category=tuple(sorted(by_category.items())), + sections=sections, + coordinate_controls=coord, + coordinate_accepted=coord_accepted, + coordinate_by_category=tuple(sorted(coord_by_category.items())), + )