From 96a01c17937dc0de5539c15215c4ed67051110b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 05:20:25 +0000 Subject: [PATCH 1/2] test(diagnostics): complete self-policing family ledger (#255 PR 3/3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #255. PR 1 pinned structural identity and PR 2 pinned canonical rendering and emission order — both over curated shapes chosen to exercise specific hazards. Neither could tell you whether a diagnostic family had been added without a fixture. This slice makes the coverage total and self-policing: every code in TITLES has a ledger case, and the replay fails by name if one does not. Three failure modes, each its own test so a red build names the actual problem rather than "something changed": - missing — a TITLES code with no ledger case. Verified by mutation: deleting OWN014's case fails with the code named. - orphan — a ledger case naming a code absent from TITLES, which catches a removed or renamed code leaving its fixture behind. Verified by mutation: renaming a case to OWN999 fails with it named. - stale — the standing `--write` discipline, as in the other two slices. The honest part is what the ledger REFUSES to claim. A case constructs a real Diagnostic and freezes the reference's rendering of it, which pins the diagnostic-layer contract for that code. It does not claim the analyzer emits that code on any input — that is step 4's contract, already pinned over the real `.own` corpus by diag_parity.json. Those are different questions, and the numbers differ sharply: rendering coverage is 47/47, but the `.own` sweep only produces **13** of them. The rest are either bridge-only families (DI/EFF/OBL come from ownlang.ownir, not the `.own` core) or core codes no corpus input happens to reach. Rendering all 47 and calling it "full parity" would have overstated the evidence, so `analyzer_corpus` is carried per code, read out of diag_parity.json rather than restated, and asserted to stay below the total — if it ever reaches 47 the corpus genuinely grew, which deserves a deliberate look rather than a silent pass. The migration packet's counters are computed by the replay, not asserted by hand, and the first divergence is printed with its code so a failure is actionable on sight. Completed checkpoint: #255 complete — PR 1 identity, PR 2 rendering + ordering, PR 3 total ledger Remaining #255 acceptance: none Python source of truth: ownlang/diagnostics.py (TITLES, Diagnostic, Evidence, render), tests/fixtures/diag_parity.json (the analyzer-corpus flag) Fixture subset: 47/47 codes — OWN 36, DI 5, OBL 5, EFF 1; 13 also produced by the `.own` corpus sweep Regeneration command: python tests/test_diag_ledger_fixtures.py --write Zero-Python replay command: cd rust && cargo test -p own-diagnostics Acceptance changed: no Behavior changed: no Python-only: 0 Rust-only: 0 Changed: 0 Ordering-only: 0 Unexplained: 0 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CJF7MBi1ijU5m9cJVWgQsM --- .../own-diagnostics/tests/ledger_replay.rs | 228 ++++ tests/fixtures/diag_ledger.json | 1055 +++++++++++++++++ tests/test_diag_ledger_fixtures.py | 216 ++++ 3 files changed, 1499 insertions(+) create mode 100644 rust/crates/own-diagnostics/tests/ledger_replay.rs create mode 100644 tests/fixtures/diag_ledger.json create mode 100644 tests/test_diag_ledger_fixtures.py diff --git a/rust/crates/own-diagnostics/tests/ledger_replay.rs b/rust/crates/own-diagnostics/tests/ledger_replay.rs new file mode 100644 index 00000000..ecad8e16 --- /dev/null +++ b/rust/crates/own-diagnostics/tests/ledger_replay.rs @@ -0,0 +1,228 @@ +//! Zero-Python replay of the complete diagnostic-family ledger +//! (`tests/fixtures/diag_ledger.json`, authoritative via +//! `python tests/test_diag_ledger_fixtures.py --write`) — P-022 step 5a, #255, +//! PR 3 of 3. +//! +//! PR 1 pinned identity and PR 2 pinned rendering, both on curated shapes. This +//! makes the coverage total and self-policing: **every** code in +//! [`own_diagnostics::TITLES`] must have a case, so a family cannot be added +//! without a fixture. +//! +//! The three failure modes, each with its own test so a red build names the +//! actual problem: +//! +//! * **missing** — a `TITLES` code with no ledger case; +//! * **orphan** — a ledger case naming a code absent from `TITLES`; +//! * **divergence** — the rendered text disagrees with the reference. +//! +//! What this does NOT claim: that the analyzer emits a given code. That is step +//! 4's contract, pinned over the real `.own` corpus by `diag_parity.json`. The +//! ledger carries `analyzer_corpus` per code so the two claims stay separable — +//! rendering coverage is 47/47, analyzer-corpus coverage is not, and conflating +//! them would overstate the evidence. + +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use std::collections::BTreeSet; + +use own_diagnostics::{title, Diagnostic, TITLES}; +use serde_json::Value; + +const FIXTURE: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../tests/fixtures/diag_ledger.json" +); + +const SCHEMA_VERSION: u64 = 1; + +fn load() -> Value { + let raw = std::fs::read_to_string(FIXTURE) + .expect("fixture missing — regenerate: python tests/test_diag_ledger_fixtures.py --write"); + let root: Value = serde_json::from_str(&raw).expect("diag_ledger.json parses"); + assert_eq!( + root.get("schema_version").and_then(Value::as_u64), + Some(SCHEMA_VERSION), + "fixture schema_version changed — a reviewed contract, not a passing reshape" + ); + root +} + +fn cases(root: &Value) -> &Vec { + root.get("cases") + .and_then(Value::as_array) + .expect("'cases' array") +} + +fn code_of(case: &Value) -> &str { + case.get("code") + .and_then(Value::as_str) + .expect("case 'code'") +} + +#[test] +fn every_title_code_has_a_ledger_case() { + let root = load(); + let covered: BTreeSet<&str> = cases(&root).iter().map(code_of).collect(); + let missing: Vec<&str> = TITLES + .iter() + .map(|(code, _)| *code) + .filter(|code| !covered.contains(code)) + .collect(); + assert!( + missing.is_empty(), + "{} diagnostic code(s) have no ledger case: {missing:?}. A new family must \ + ship with a fixture — regenerate: python tests/test_diag_ledger_fixtures.py --write", + missing.len() + ); +} + +#[test] +fn no_ledger_case_names_an_unknown_code() { + let root = load(); + let orphans: Vec<&str> = cases(&root) + .iter() + .map(code_of) + .filter(|code| title(code).is_none()) + .collect(); + assert!( + orphans.is_empty(), + "{} ledger case(s) name a code absent from TITLES: {orphans:?}. A removed or \ + renamed code must not leave its fixture behind", + orphans.len() + ); +} + +#[test] +fn ledger_case_count_matches_the_vocabulary() { + let root = load(); + let declared = root + .get("totals") + .and_then(|t| t.get("codes")) + .and_then(Value::as_u64) + .expect("'totals.codes'"); + assert_eq!( + usize::try_from(declared).expect("fits usize"), + cases(&root).len(), + "the ledger's own total disagrees with the number of cases it carries" + ); + assert_eq!( + cases(&root).len(), + TITLES.len(), + "the ledger and TITLES disagree on the size of the vocabulary" + ); +} + +#[test] +fn every_case_title_matches_the_ported_vocabulary() { + let root = load(); + for case in cases(&root) { + let code = code_of(case); + let expected = case + .get("title") + .and_then(Value::as_str) + .expect("case 'title'"); + assert_eq!( + title(code), + Some(expected), + "code {code:?}: the ported TITLES entry diverged from the reference" + ); + } +} + +/// The migration packet's counters, computed rather than asserted by hand. +/// +/// `Unexplained` is the one that must be zero; the others are reported so a +/// reviewer sees the shape of any difference instead of a bare boolean. +#[derive(Default, Debug)] +struct Counters { + python_only: usize, + rust_only: usize, + changed: usize, + unexplained: usize, +} + +#[test] +fn rendered_text_matches_the_reference_for_every_family() { + let root = load(); + let mut counters = Counters::default(); + let mut first_divergence: Option = None; + + let covered: BTreeSet<&str> = cases(&root).iter().map(code_of).collect(); + counters.python_only = TITLES + .iter() + .filter(|(code, _)| !covered.contains(code)) + .count(); + counters.rust_only = cases(&root) + .iter() + .map(code_of) + .filter(|code| title(code).is_none()) + .count(); + + for case in cases(&root) { + let code = code_of(case); + let path = case + .get("path") + .and_then(Value::as_str) + .expect("case 'path'"); + let diagnostic: Diagnostic = + serde_json::from_value(case.get("diagnostic").expect("'diagnostic'").clone()) + .unwrap_or_else(|e| panic!("code {code:?}: diagnostic does not load: {e}")); + let expected = case + .get("rendered") + .and_then(Value::as_str) + .expect("case 'rendered'"); + let produced = diagnostic.render(path); + if produced != expected { + counters.changed = counters.changed.saturating_add(1); + counters.unexplained = counters.unexplained.saturating_add(1); + if first_divergence.is_none() { + first_divergence = Some(format!( + "code {code:?}\n expected: {expected:?}\n produced: {produced:?}" + )); + } + } + } + + assert_eq!( + counters.unexplained, + 0, + "the migration packet requires Unexplained == 0. Counters: {counters:?}\n\ + first divergence:\n{}", + first_divergence.as_deref().unwrap_or("") + ); + assert_eq!(counters.python_only, 0, "counters: {counters:?}"); + assert_eq!(counters.rust_only, 0, "counters: {counters:?}"); + assert_eq!(counters.changed, 0, "counters: {counters:?}"); +} + +#[test] +fn analyzer_corpus_coverage_is_recorded_not_assumed() { + // Rendering coverage is total; analyzer-corpus coverage is not, and the + // ledger must keep saying so. If this ever reaches 47 it means the `.own` + // corpus genuinely grew — a real event worth a deliberate fixture update, + // not something to discover silently. + let root = load(); + let declared = root + .get("totals") + .and_then(|t| t.get("analyzer_corpus")) + .and_then(Value::as_u64) + .expect("'totals.analyzer_corpus'"); + let counted = cases(&root) + .iter() + .filter(|c| { + c.get("analyzer_corpus") + .and_then(Value::as_bool) + .unwrap_or(false) + }) + .count(); + assert_eq!( + usize::try_from(declared).expect("fits usize"), + counted, + "the ledger's analyzer_corpus total disagrees with its own per-code flags" + ); + assert!( + counted < cases(&root).len(), + "every code now claims analyzer-corpus coverage — verify that against \ + diag_parity.json before believing it" + ); +} diff --git a/tests/fixtures/diag_ledger.json b/tests/fixtures/diag_ledger.json new file mode 100644 index 00000000..ea8449c4 --- /dev/null +++ b/tests/fixtures/diag_ledger.json @@ -0,0 +1,1055 @@ +{ + "comment": "GENERATED by tests/test_diag_ledger_fixtures.py --write; do not edit. Python (ownlang) is authoritative. The COMPLETE diagnostic-family ledger (P-022 step 5a, issue #255, PR 3 of 3): every TITLES code has a case, so a new family cannot ship without a fixture. `analyzer_corpus` records whether the real .own sweep produces that code today -- rendering coverage is not the same claim as analyzer coverage, and this file does not conflate them.", + "schema_version": 1, + "totals": { + "codes": 47, + "by_family": { + "DI": 5, + "EFF": 1, + "OBL": 5, + "OWN": 36 + }, + "analyzer_corpus": 13 + }, + "cases": [ + { + "code": "DI001", + "family": "DI", + "analyzer_corpus": false, + "title": "captive dependency: a shorter-lived service is captured by a longer-lived one", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "DI001", + "message": "'di001_subject' -- captive dependency: a shorter-lived service is captured by a longer-lived one", + "line": 1, + "severity": "warning", + "subject": "di001_subject#1", + "resource_kind": "subscription token", + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:1: warning: [DI001] 'di001_subject' -- captive dependency: a shorter-lived service is captured by a longer-lived one [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "DI002", + "family": "DI", + "analyzer_corpus": false, + "title": "singleton captures a scoped service (captive dependency)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "DI002", + "message": "'di002_subject' -- singleton captures a scoped service (captive dependency)", + "line": 2, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:2: error: [DI002] 'di002_subject' -- singleton captures a scoped service (captive dependency)\n note: related step at src/Ledger.cs:1" + }, + { + "code": "DI003", + "family": "DI", + "analyzer_corpus": false, + "title": "singleton captures a transient service (captive dependency)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "DI003", + "message": "'di003_subject' -- singleton captures a transient service (captive dependency)", + "line": 3, + "severity": "error", + "subject": "di003_subject#3", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:3: error: [DI003] 'di003_subject' -- singleton captures a transient service (captive dependency)" + }, + { + "code": "DI004", + "family": "DI", + "analyzer_corpus": false, + "title": "scoped service resolved from the root provider (captured for the app lifetime)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "DI004", + "message": "'di004_subject' -- scoped service resolved from the root provider (captured for the app lifetime)", + "line": 4, + "severity": "error", + "subject": null, + "resource_kind": "subscription token", + "evidence": [] + }, + "rendered": "src/Ledger.cs:4: error: [DI004] 'di004_subject' -- scoped service resolved from the root provider (captured for the app lifetime) [resource: subscription token]" + }, + { + "code": "DI005", + "family": "DI", + "analyzer_corpus": false, + "title": "disposable transient resolved from a long-lived scope (delayed disposal)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "DI005", + "message": "'di005_subject' -- disposable transient resolved from a long-lived scope (delayed disposal)", + "line": 5, + "severity": "error", + "subject": "di005_subject#5", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:5: error: [DI005] 'di005_subject' -- disposable transient resolved from a long-lived scope (delayed disposal)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "EFF001", + "family": "EFF", + "analyzer_corpus": false, + "title": "reactive effect re-runs on an unstable dependency identity (render-time IO storm)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "EFF001", + "message": "'eff001_subject' -- reactive effect re-runs on an unstable dependency identity (render-time IO storm)", + "line": 6, + "severity": "warning", + "subject": null, + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:6: warning: [EFF001] 'eff001_subject' -- reactive effect re-runs on an unstable dependency identity (render-time IO storm)\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OBL001", + "family": "OBL", + "analyzer_corpus": false, + "title": "obligation still open when a barrier fires (open on every path)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OBL001", + "message": "'obl001_subject' -- obligation still open when a barrier fires (open on every path)", + "line": 7, + "severity": "error", + "subject": "obl001_subject#7", + "resource_kind": "subscription token", + "evidence": [] + }, + "rendered": "src/Ledger.cs:7: error: [OBL001] 'obl001_subject' -- obligation still open when a barrier fires (open on every path) [resource: subscription token]" + }, + { + "code": "OBL002", + "family": "OBL", + "analyzer_corpus": false, + "title": "obligation may still be open when a barrier fires (open on some path)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OBL002", + "message": "'obl002_subject' -- obligation may still be open when a barrier fires (open on some path)", + "line": 8, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:8: error: [OBL002] 'obl002_subject' -- obligation may still be open when a barrier fires (open on some path)" + }, + { + "code": "OBL003", + "family": "OBL", + "analyzer_corpus": false, + "title": "obligation not closed before the method exits (on every path)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OBL003", + "message": "'obl003_subject' -- obligation not closed before the method exits (on every path)", + "line": 9, + "severity": "error", + "subject": "obl003_subject#9", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:9: error: [OBL003] 'obl003_subject' -- obligation not closed before the method exits (on every path)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OBL004", + "family": "OBL", + "analyzer_corpus": false, + "title": "obligation may not be closed before the method exits (on some path)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OBL004", + "message": "'obl004_subject' -- obligation may not be closed before the method exits (on some path)", + "line": 10, + "severity": "error", + "subject": null, + "resource_kind": "subscription token", + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:10: error: [OBL004] 'obl004_subject' -- obligation may not be closed before the method exits (on some path) [resource: subscription token]\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OBL005", + "family": "OBL", + "analyzer_corpus": false, + "title": "protocol scope matched no reported method -- rule is dead (advisory)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OBL005", + "message": "'obl005_subject' -- protocol scope matched no reported method -- rule is dead (advisory)", + "line": 11, + "severity": "warning", + "subject": "obl005_subject#11", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:11: warning: [OBL005] 'obl005_subject' -- protocol scope matched no reported method -- rule is dead (advisory)" + }, + { + "code": "OWN001", + "family": "OWN", + "analyzer_corpus": true, + "title": "owned resource not released on all paths (possible leak)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN001", + "message": "'own001_subject' -- owned resource not released on all paths (possible leak)", + "line": 12, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:12: error: [OWN001] 'own001_subject' -- owned resource not released on all paths (possible leak)" + }, + { + "code": "OWN002", + "family": "OWN", + "analyzer_corpus": true, + "title": "use after release", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN002", + "message": "'own002_subject' -- use after release", + "line": 13, + "severity": "error", + "subject": "own002_subject#13", + "resource_kind": "subscription token", + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:13: error: [OWN002] 'own002_subject' -- use after release [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN003", + "family": "OWN", + "analyzer_corpus": true, + "title": "double release", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN003", + "message": "'own003_subject' -- double release", + "line": 14, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:14: error: [OWN003] 'own003_subject' -- double release\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN004", + "family": "OWN", + "analyzer_corpus": false, + "title": "borrow escapes its scope", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN004", + "message": "'own004_subject' -- borrow escapes its scope", + "line": 15, + "severity": "error", + "subject": "own004_subject#15", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:15: error: [OWN004] 'own004_subject' -- borrow escapes its scope" + }, + { + "code": "OWN005", + "family": "OWN", + "analyzer_corpus": true, + "title": "use after move", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN005", + "message": "'own005_subject' -- use after move", + "line": 16, + "severity": "warning", + "subject": null, + "resource_kind": "subscription token", + "evidence": [] + }, + "rendered": "src/Ledger.cs:16: warning: [OWN005] 'own005_subject' -- use after move [resource: subscription token]" + }, + { + "code": "OWN006", + "family": "OWN", + "analyzer_corpus": true, + "title": "mutable borrow while a shared borrow is live", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN006", + "message": "'own006_subject' -- mutable borrow while a shared borrow is live", + "line": 17, + "severity": "error", + "subject": "own006_subject#17", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:17: error: [OWN006] 'own006_subject' -- mutable borrow while a shared borrow is live\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN007", + "family": "OWN", + "analyzer_corpus": false, + "title": "move while borrowed", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN007", + "message": "'own007_subject' -- move while borrowed", + "line": 18, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:18: error: [OWN007] 'own007_subject' -- move while borrowed\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN008", + "family": "OWN", + "analyzer_corpus": true, + "title": "release while borrowed", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN008", + "message": "'own008_subject' -- release while borrowed", + "line": 19, + "severity": "error", + "subject": "own008_subject#19", + "resource_kind": "subscription token", + "evidence": [] + }, + "rendered": "src/Ledger.cs:19: error: [OWN008] 'own008_subject' -- release while borrowed [resource: subscription token]" + }, + { + "code": "OWN009", + "family": "OWN", + "analyzer_corpus": true, + "title": "use after possible release (released on some path)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN009", + "message": "'own009_subject' -- use after possible release (released on some path)", + "line": 20, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:20: error: [OWN009] 'own009_subject' -- use after possible release (released on some path)" + }, + { + "code": "OWN010", + "family": "OWN", + "analyzer_corpus": false, + "title": "use after possible move (moved on some path)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN010", + "message": "'own010_subject' -- use after possible move (moved on some path)", + "line": 21, + "severity": "warning", + "subject": "own010_subject#21", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:21: warning: [OWN010] 'own010_subject' -- use after possible move (moved on some path)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN011", + "family": "OWN", + "analyzer_corpus": false, + "title": "mutable borrow while another mutable borrow is live", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN011", + "message": "'own011_subject' -- mutable borrow while another mutable borrow is live", + "line": 22, + "severity": "error", + "subject": null, + "resource_kind": "subscription token", + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:22: error: [OWN011] 'own011_subject' -- mutable borrow while another mutable borrow is live [resource: subscription token]\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN012", + "family": "OWN", + "analyzer_corpus": false, + "title": "shared borrow while a mutable borrow is live", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN012", + "message": "'own012_subject' -- shared borrow while a mutable borrow is live", + "line": 23, + "severity": "error", + "subject": "own012_subject#23", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:23: error: [OWN012] 'own012_subject' -- shared borrow while a mutable borrow is live" + }, + { + "code": "OWN013", + "family": "OWN", + "analyzer_corpus": false, + "title": "owner accessed while it is mutably borrowed", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN013", + "message": "'own013_subject' -- owner accessed while it is mutably borrowed", + "line": 24, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:24: error: [OWN013] 'own013_subject' -- owner accessed while it is mutably borrowed" + }, + { + "code": "OWN014", + "family": "OWN", + "analyzer_corpus": true, + "title": "value escapes to a longer-lived region (lifetime promotion)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN014", + "message": "'own014_subject' -- value escapes to a longer-lived region (lifetime promotion)", + "line": 25, + "severity": "error", + "subject": "own014_subject#25", + "resource_kind": "subscription token", + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:25: error: [OWN014] 'own014_subject' -- value escapes to a longer-lived region (lifetime promotion) [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN015", + "family": "OWN", + "analyzer_corpus": true, + "title": "stack-backed buffer cannot escape the current function", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN015", + "message": "'own015_subject' -- stack-backed buffer cannot escape the current function", + "line": 26, + "severity": "warning", + "subject": null, + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:26: warning: [OWN015] 'own015_subject' -- stack-backed buffer cannot escape the current function\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN016", + "family": "OWN", + "analyzer_corpus": false, + "title": "stack-backed buffer moved to a longer-lived owner", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN016", + "message": "'own016_subject' -- stack-backed buffer moved to a longer-lived owner", + "line": 27, + "severity": "error", + "subject": "own016_subject#27", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:27: error: [OWN016] 'own016_subject' -- stack-backed buffer moved to a longer-lived owner" + }, + { + "code": "OWN017", + "family": "OWN", + "analyzer_corpus": false, + "title": "movable buffer escape is not supported by code generation (PoC limitation)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN017", + "message": "'own017_subject' -- movable buffer escape is not supported by code generation (PoC limitation)", + "line": 28, + "severity": "error", + "subject": null, + "resource_kind": "subscription token", + "evidence": [] + }, + "rendered": "src/Ledger.cs:28: error: [OWN017] 'own017_subject' -- movable buffer escape is not supported by code generation (PoC limitation) [resource: subscription token]" + }, + { + "code": "OWN018", + "family": "OWN", + "analyzer_corpus": false, + "title": "buffer size must be an integer", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN018", + "message": "'own018_subject' -- buffer size must be an integer", + "line": 29, + "severity": "error", + "subject": "own018_subject#29", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:29: error: [OWN018] 'own018_subject' -- buffer size must be an integer\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN019", + "family": "OWN", + "analyzer_corpus": true, + "title": "inline capacity too large for a stack-backed policy", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN019", + "message": "'own019_subject' -- inline capacity too large for a stack-backed policy", + "line": 30, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:30: error: [OWN019] 'own019_subject' -- inline capacity too large for a stack-backed policy\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN020", + "family": "OWN", + "analyzer_corpus": true, + "title": "unsupported construct (out of scope for the MVP)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN020", + "message": "'own020_subject' -- unsupported construct (out of scope for the MVP)", + "line": 31, + "severity": "warning", + "subject": "own020_subject#31", + "resource_kind": "subscription token", + "evidence": [] + }, + "rendered": "src/Ledger.cs:31: warning: [OWN020] 'own020_subject' -- unsupported construct (out of scope for the MVP) [resource: subscription token]" + }, + { + "code": "OWN021", + "family": "OWN", + "analyzer_corpus": false, + "title": "stack allocation requires a statically known bound", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN021", + "message": "'own021_subject' -- stack allocation requires a statically known bound", + "line": 32, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:32: error: [OWN021] 'own021_subject' -- stack allocation requires a statically known bound" + }, + { + "code": "OWN023", + "family": "OWN", + "analyzer_corpus": false, + "title": "scratch fallback forbidden but the size may exceed the inline limit", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN023", + "message": "'own023_subject' -- scratch fallback forbidden but the size may exceed the inline limit", + "line": 33, + "severity": "error", + "subject": "own023_subject#33", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:33: error: [OWN023] 'own023_subject' -- scratch fallback forbidden but the size may exceed the inline limit\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN024", + "family": "OWN", + "analyzer_corpus": false, + "title": "sensitive buffer is not cleared on release", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN024", + "message": "'own024_subject' -- sensitive buffer is not cleared on release", + "line": 34, + "severity": "error", + "subject": null, + "resource_kind": "subscription token", + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:34: error: [OWN024] 'own024_subject' -- sensitive buffer is not cleared on release [resource: subscription token]\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN025", + "family": "OWN", + "analyzer_corpus": true, + "title": "full-length view of a pooled buffer reaches past its logical length", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN025", + "message": "'own025_subject' -- full-length view of a pooled buffer reaches past its logical length", + "line": 35, + "severity": "error", + "subject": "own025_subject#35", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:35: error: [OWN025] 'own025_subject' -- full-length view of a pooled buffer reaches past its logical length" + }, + { + "code": "OWN030", + "family": "OWN", + "analyzer_corpus": false, + "title": "undefined name", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN030", + "message": "'own030_subject' -- undefined name", + "line": 36, + "severity": "warning", + "subject": null, + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:36: warning: [OWN030] 'own030_subject' -- undefined name" + }, + { + "code": "OWN031", + "family": "OWN", + "analyzer_corpus": false, + "title": "name already defined in this scope", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN031", + "message": "'own031_subject' -- name already defined in this scope", + "line": 37, + "severity": "error", + "subject": "own031_subject#37", + "resource_kind": "subscription token", + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:37: error: [OWN031] 'own031_subject' -- name already defined in this scope [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN032", + "family": "OWN", + "analyzer_corpus": false, + "title": "owned resource copied without 'move'", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN032", + "message": "'own032_subject' -- owned resource copied without 'move'", + "line": 38, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:38: error: [OWN032] 'own032_subject' -- owned resource copied without 'move'\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN033", + "family": "OWN", + "analyzer_corpus": false, + "title": "function must return a value on all paths", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN033", + "message": "'own033_subject' -- function must return a value on all paths", + "line": 39, + "severity": "error", + "subject": "own033_subject#39", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:39: error: [OWN033] 'own033_subject' -- function must return a value on all paths" + }, + { + "code": "OWN034", + "family": "OWN", + "analyzer_corpus": false, + "title": "operation requires an owned resource", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN034", + "message": "'own034_subject' -- operation requires an owned resource", + "line": 40, + "severity": "error", + "subject": null, + "resource_kind": "subscription token", + "evidence": [] + }, + "rendered": "src/Ledger.cs:40: error: [OWN034] 'own034_subject' -- operation requires an owned resource [resource: subscription token]" + }, + { + "code": "OWN035", + "family": "OWN", + "analyzer_corpus": false, + "title": "return type mismatch", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN035", + "message": "'own035_subject' -- return type mismatch", + "line": 41, + "severity": "warning", + "subject": "own035_subject#41", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:41: warning: [OWN035] 'own035_subject' -- return type mismatch\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN036", + "family": "OWN", + "analyzer_corpus": false, + "title": "cyclic lifetime ordering", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN036", + "message": "'own036_subject' -- cyclic lifetime ordering", + "line": 42, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:42: error: [OWN036] 'own036_subject' -- cyclic lifetime ordering\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN040", + "family": "OWN", + "analyzer_corpus": true, + "title": "call to an undeclared function (unknown calls are forbidden)", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN040", + "message": "'own040_subject' -- call to an undeclared function (unknown calls are forbidden)", + "line": 43, + "severity": "error", + "subject": "own040_subject#43", + "resource_kind": "subscription token", + "evidence": [] + }, + "rendered": "src/Ledger.cs:43: error: [OWN040] 'own040_subject' -- call to an undeclared function (unknown calls are forbidden) [resource: subscription token]" + }, + { + "code": "OWN041", + "family": "OWN", + "analyzer_corpus": false, + "title": "call argument mismatch", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN041", + "message": "'own041_subject' -- call argument mismatch", + "line": 44, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:44: error: [OWN041] 'own041_subject' -- call argument mismatch" + }, + { + "code": "OWN050", + "family": "OWN", + "analyzer_corpus": false, + "title": "declaring type unresolved -- leakage analysis skipped", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN050", + "message": "'own050_subject' -- declaring type unresolved -- leakage analysis skipped", + "line": 45, + "severity": "error", + "subject": "own050_subject#45", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] + }, + "rendered": "src/Ledger.cs:45: error: [OWN050] 'own050_subject' -- declaring type unresolved -- leakage analysis skipped\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN051", + "family": "OWN", + "analyzer_corpus": false, + "title": "ownership transfer unverified -- local not checked past this call", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN051", + "message": "'own051_subject' -- ownership transfer unverified -- local not checked past this call", + "line": 46, + "severity": "warning", + "subject": null, + "resource_kind": "subscription token", + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] + }, + "rendered": "src/Ledger.cs:46: warning: [OWN051] 'own051_subject' -- ownership transfer unverified -- local not checked past this call [resource: subscription token]\n note: related step at src/Ledger.cs:1" + }, + { + "code": "OWN052", + "family": "OWN", + "analyzer_corpus": false, + "title": "interprocedural summary inference failed -- method summaries skipped", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN052", + "message": "'own052_subject' -- interprocedural summary inference failed -- method summaries skipped", + "line": 47, + "severity": "error", + "subject": "own052_subject#47", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:47: error: [OWN052] 'own052_subject' -- interprocedural summary inference failed -- method summaries skipped" + } + ] +} diff --git a/tests/test_diag_ledger_fixtures.py b/tests/test_diag_ledger_fixtures.py new file mode 100644 index 00000000..7c5926a8 --- /dev/null +++ b/tests/test_diag_ledger_fixtures.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Complete diagnostic-family ledger (P-022 step 5a, issue #255) — Python side. + +PR 3 of three, and the one that closes #255. PR 1 pinned structural identity, +PR 2 pinned canonical rendering and emission order — both on curated shapes. +This slice makes the coverage **total and self-policing**: every code in +`ownlang.diagnostics.TITLES` gets a fixture case, and a new family cannot be +added without one. + +## What the ledger proves, and what it deliberately does not + +A case here constructs a real `Diagnostic` and freezes the reference's rendering +of it. That pins the **diagnostic-layer** contract for that code — message, +severity, subject, resource kind, ordered Evidence, and the exact text — which +is what #255 is about. + +It does **not** claim the analyzer fires that code on any particular input. +That is step 4's contract (#214), already pinned by `tests/fixtures/diag_parity.json` +over the real `.own` corpus. The two are different questions and the ledger says +which is which per code: `analyzer_corpus: true` means the `.own` sweep actually +produces it today. + +That distinction is the honest part. The `.own` sweep exercises **13 of 47** +codes; the rest are either bridge-only families (DI/EFF/OBL come from +`ownlang.ownir`, not the `.own` core) or core codes no corpus input happens to +reach. Silently rendering all 47 and calling it "full parity" would overstate +the evidence, so the coverage flag is carried per code and summarised. + +## The three failure modes it closes + +* **missing** — a code in `TITLES` with no case. The replay fails naming it, so + adding a diagnostic without a fixture is a red build. +* **orphan** — a case whose code is not in `TITLES`. Catches a removed or + renamed code leaving a fixture behind. +* **stale** — the generated file differs from what the current reference + produces (the standing `--write` discipline, as in the other two slices). + +Run: python tests/test_diag_ledger_fixtures.py (verify) + python tests/test_diag_ledger_fixtures.py --write (regenerate) + python tests/run_tests.py (runs it as part of the suite) +""" + +from __future__ import annotations + +import json +import os +import re +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from ownlang.diagnostics import TITLES, Diagnostic, Evidence, Severity + +FIXTURE = os.path.join(os.path.dirname(__file__), "fixtures", "diag_ledger.json") +CORPUS_PARITY = os.path.join(os.path.dirname(__file__), "fixtures", "diag_parity.json") + +SCHEMA_VERSION = 1 + +# The anchor path every ledger case renders against; a fixed value keeps the +# golden diffable when a case is added in the middle. +_PATH = "src/Ledger.cs" + + +def _corpus_codes() -> set[str]: + """Codes the real `.own` corpus sweep actually produces today. + + Read from the step-4 fixture rather than restated, so this flag cannot drift + from the analyzer's true reach.""" + with open(CORPUS_PARITY, encoding="utf-8") as f: + data = json.load(f) + return {code for case in data["cases"] for _line, code in case["diags"]} + + +def _family(code: str) -> str: + match = re.match(r"[A-Z]+", code) + return match.group(0) if match else "?" + + +def _shape_for(index: int, code: str) -> Diagnostic: + """A representative `Diagnostic` for `code`. + + The message is derived from the reference's own TITLE (so it is meaningful + and cannot drift from the vocabulary), with a quoted subject spliced in so + the caret heuristic has something to find. Optional fields rotate by index so + the ledger exercises every shape across the family set rather than 47 copies + of the same one -- deterministic, so the golden stays stable. + """ + title = TITLES[code] + message = f"'{code.lower()}_subject' -- {title}" + kwargs: dict[str, object] = {} + if index % 2 == 0: + kwargs["subject"] = f"{code.lower()}_subject#{index + 1}" + if index % 3 == 0: + kwargs["resource_kind"] = "subscription token" + if index % 5 == 0: + kwargs["severity"] = Severity.WARNING + if index % 4 == 0: + kwargs["evidence"] = ( + Evidence(line=1, label="acquired here", role="acquired"), + Evidence(line=2, label="escapes here", role="escaped", + file="src/Other.cs"), + ) + elif index % 4 == 1: + kwargs["evidence"] = (Evidence(line=1, label="related step"),) + return Diagnostic(code=code, message=message, line=index + 1, **kwargs) # type: ignore[arg-type] + + +def _evidence_json(ev: Evidence) -> dict[str, object]: + return {"line": ev.line, "label": ev.label, "file": ev.file, "role": ev.role} + + +def _diagnostic_json(d: Diagnostic) -> dict[str, object]: + return { + "code": d.code, + "message": d.message, + "line": d.line, + "severity": d.severity.value, + "subject": d.subject, + "resource_kind": d.resource_kind, + "evidence": [_evidence_json(e) for e in d.evidence], + } + + +def build() -> dict[str, object]: + corpus = _corpus_codes() + cases: list[dict[str, object]] = [] + for index, code in enumerate(sorted(TITLES)): + diag = _shape_for(index, code) + cases.append({ + "code": code, + "family": _family(code), + # True when the real `.own` corpus sweep produces this code today + # (read from diag_parity.json, never restated). + "analyzer_corpus": code in corpus, + "title": TITLES[code], + "path": _PATH, + "diagnostic": _diagnostic_json(diag), + "rendered": diag.render(_PATH), + }) + + families: dict[str, int] = {} + for case in cases: + family = str(case["family"]) + families[family] = families.get(family, 0) + 1 + + return { + "comment": ( + "GENERATED by tests/test_diag_ledger_fixtures.py --write; do not edit. " + "Python (ownlang) is authoritative. The COMPLETE diagnostic-family ledger " + "(P-022 step 5a, issue #255, PR 3 of 3): every TITLES code has a case, so a " + "new family cannot ship without a fixture. `analyzer_corpus` records whether " + "the real .own sweep produces that code today -- rendering coverage is not " + "the same claim as analyzer coverage, and this file does not conflate them." + ), + "schema_version": SCHEMA_VERSION, + "totals": { + "codes": len(cases), + "by_family": dict(sorted(families.items())), + "analyzer_corpus": sum(1 for c in cases if c["analyzer_corpus"]), + }, + "cases": cases, + } + + +def _render_json(data: dict[str, object]) -> str: + return json.dumps(data, indent=2, ensure_ascii=False) + "\n" + + +def run() -> int: + expected = _render_json(build()) + if not os.path.exists(FIXTURE): + print(f"FAIL: {FIXTURE} missing; regenerate with " + f"'python tests/test_diag_ledger_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 (a code, title or renderer changed); " + f"regenerate with 'python tests/test_diag_ledger_fixtures.py --write' " + f"and re-run the Rust side (cd rust && cargo test -p own-diagnostics)") + return 1 + + data = json.loads(actual) + if data.get("schema_version") != SCHEMA_VERSION: + print(f"FAIL: {FIXTURE} schema_version " + f"{data.get('schema_version')!r} != {SCHEMA_VERSION}") + return 1 + + # The two failure modes the LEDGER exists for, asserted on the Python side + # too so a regeneration cannot quietly paper over either. + covered = {str(case["code"]) for case in data["cases"]} + missing = sorted(set(TITLES) - covered) + orphans = sorted(covered - set(TITLES)) + if missing: + print(f"FAIL: {len(missing)} code(s) in TITLES have no ledger case: {missing}") + return 1 + if orphans: + print(f"FAIL: {len(orphans)} ledger case(s) name a code absent from TITLES: " + f"{orphans}") + return 1 + + totals = data["totals"] + print(f"diagnostic ledger OK: {totals['codes']}/{len(TITLES)} codes covered " + f"({totals['by_family']}), " + f"{totals['analyzer_corpus']} of them also produced by the .own corpus sweep") + return 0 + + +if __name__ == "__main__": + if "--write" in sys.argv[1:]: + os.makedirs(os.path.dirname(FIXTURE), exist_ok=True) + with open(FIXTURE, "w", encoding="utf-8") as f: + f.write(_render_json(build())) + print(f"wrote {FIXTURE}") + raise SystemExit(0) + raise SystemExit(run()) From b04bde58c68a27b55640255b1d9a6b368d33ef38 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 05:27:38 +0000 Subject: [PATCH 2/2] fix(diagnostics): make ledger shapes position-independent (#255 PR 3/3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on 96a01c1, and it lands squarely on my own claim. The generator's docstring said the index-derived shapes were "deterministic, so the golden stays stable" — true only while TITLES does not change, which is precisely the event this ledger exists to make visible. Measured before fixing: inserting `DI006` mid-vocabulary rewrote **42 of 47** existing records. Every later sorted index shifts, so each affected case changes its line, subject suffix, severity, resource kind and evidence selection at once. That defeats the purpose. Adding a family has to read as a ONE-record diff a reviewer can check at a glance; 42 records of unrelated churn would hide whether the renderer moved too — the exact signal the ledger is supposed to isolate. Shapes now derive from a stable per-code SHA-256 seed instead of the sorted position, so a code's shape is fixed for good whatever else joins the vocabulary. SHA-256 rather than `hash()`, whose string hashing is randomised per process and would make the golden unreproducible. Re-measured after: **0 of 47**. Added `_insertion_churn()` as a standing guard rather than trusting the fix to stay: it inserts a probe code mid-vocabulary, rebuilds, counts changed records, and removes the probe in a `finally` so the live TITLES the rest of the suite sees is untouched. Verified the guard is not vacuous by reverting to index-derived shapes and regenerating first (so the stale check could not mask it) — it then fails with the exact count and the reason. Completed checkpoint: unchanged — #255 PR 3/3, Closes #255 Remaining #255 acceptance: none Python source of truth: ownlang/diagnostics.py (TITLES, Diagnostic, render) Fixture subset: unchanged — 47/47 codes; 13 also produced by the .own sweep Regeneration command: python tests/test_diag_ledger_fixtures.py --write Zero-Python replay command: cd rust && cargo test -p own-diagnostics Acceptance changed: no Behavior changed: no Python-only: 0 Rust-only: 0 Changed: 0 Ordering-only: 0 Unexplained: 0 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CJF7MBi1ijU5m9cJVWgQsM --- tests/fixtures/diag_ledger.json | 622 +++++++++++++++-------------- tests/test_diag_ledger_fixtures.py | 80 +++- 2 files changed, 391 insertions(+), 311 deletions(-) diff --git a/tests/fixtures/diag_ledger.json b/tests/fixtures/diag_ledger.json index ea8449c4..83afc5e3 100644 --- a/tests/fixtures/diag_ledger.json +++ b/tests/fixtures/diag_ledger.json @@ -21,26 +21,13 @@ "diagnostic": { "code": "DI001", "message": "'di001_subject' -- captive dependency: a shorter-lived service is captured by a longer-lived one", - "line": 1, - "severity": "warning", - "subject": "di001_subject#1", - "resource_kind": "subscription token", - "evidence": [ - { - "line": 1, - "label": "acquired here", - "file": null, - "role": "acquired" - }, - { - "line": 2, - "label": "escapes here", - "file": "src/Other.cs", - "role": "escaped" - } - ] + "line": 720, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [] }, - "rendered": "src/Ledger.cs:1: warning: [DI001] 'di001_subject' -- captive dependency: a shorter-lived service is captured by a longer-lived one [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:720: error: [DI001] 'di001_subject' -- captive dependency: a shorter-lived service is captured by a longer-lived one" }, { "code": "DI002", @@ -51,7 +38,7 @@ "diagnostic": { "code": "DI002", "message": "'di002_subject' -- singleton captures a scoped service (captive dependency)", - "line": 2, + "line": 458, "severity": "error", "subject": null, "resource_kind": null, @@ -64,7 +51,7 @@ } ] }, - "rendered": "src/Ledger.cs:2: error: [DI002] 'di002_subject' -- singleton captures a scoped service (captive dependency)\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:458: error: [DI002] 'di002_subject' -- singleton captures a scoped service (captive dependency)\n note: related step at src/Ledger.cs:1" }, { "code": "DI003", @@ -75,13 +62,13 @@ "diagnostic": { "code": "DI003", "message": "'di003_subject' -- singleton captures a transient service (captive dependency)", - "line": 3, + "line": 347, "severity": "error", - "subject": "di003_subject#3", + "subject": "di003_subject#347", "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:3: error: [DI003] 'di003_subject' -- singleton captures a transient service (captive dependency)" + "rendered": "src/Ledger.cs:347: error: [DI003] 'di003_subject' -- singleton captures a transient service (captive dependency)" }, { "code": "DI004", @@ -92,13 +79,13 @@ "diagnostic": { "code": "DI004", "message": "'di004_subject' -- scoped service resolved from the root provider (captured for the app lifetime)", - "line": 4, + "line": 319, "severity": "error", - "subject": null, + "subject": "di004_subject#319", "resource_kind": "subscription token", "evidence": [] }, - "rendered": "src/Ledger.cs:4: error: [DI004] 'di004_subject' -- scoped service resolved from the root provider (captured for the app lifetime) [resource: subscription token]" + "rendered": "src/Ledger.cs:319: error: [DI004] 'di004_subject' -- scoped service resolved from the root provider (captured for the app lifetime) [resource: subscription token]" }, { "code": "DI005", @@ -109,26 +96,20 @@ "diagnostic": { "code": "DI005", "message": "'di005_subject' -- disposable transient resolved from a long-lived scope (delayed disposal)", - "line": 5, - "severity": "error", - "subject": "di005_subject#5", - "resource_kind": null, + "line": 886, + "severity": "warning", + "subject": null, + "resource_kind": "subscription token", "evidence": [ { "line": 1, - "label": "acquired here", + "label": "related step", "file": null, - "role": "acquired" - }, - { - "line": 2, - "label": "escapes here", - "file": "src/Other.cs", - "role": "escaped" + "role": "related" } ] }, - "rendered": "src/Ledger.cs:5: error: [DI005] 'di005_subject' -- disposable transient resolved from a long-lived scope (delayed disposal)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:886: warning: [DI005] 'di005_subject' -- disposable transient resolved from a long-lived scope (delayed disposal) [resource: subscription token]\n note: related step at src/Ledger.cs:1" }, { "code": "EFF001", @@ -139,20 +120,13 @@ "diagnostic": { "code": "EFF001", "message": "'eff001_subject' -- reactive effect re-runs on an unstable dependency identity (render-time IO storm)", - "line": 6, + "line": 171, "severity": "warning", - "subject": null, + "subject": "eff001_subject#171", "resource_kind": null, - "evidence": [ - { - "line": 1, - "label": "related step", - "file": null, - "role": "related" - } - ] + "evidence": [] }, - "rendered": "src/Ledger.cs:6: warning: [EFF001] 'eff001_subject' -- reactive effect re-runs on an unstable dependency identity (render-time IO storm)\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:171: warning: [EFF001] 'eff001_subject' -- reactive effect re-runs on an unstable dependency identity (render-time IO storm)" }, { "code": "OBL001", @@ -163,13 +137,26 @@ "diagnostic": { "code": "OBL001", "message": "'obl001_subject' -- obligation still open when a barrier fires (open on every path)", - "line": 7, + "line": 869, "severity": "error", - "subject": "obl001_subject#7", - "resource_kind": "subscription token", - "evidence": [] + "subject": "obl001_subject#869", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] }, - "rendered": "src/Ledger.cs:7: error: [OBL001] 'obl001_subject' -- obligation still open when a barrier fires (open on every path) [resource: subscription token]" + "rendered": "src/Ledger.cs:869: error: [OBL001] 'obl001_subject' -- obligation still open when a barrier fires (open on every path)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OBL002", @@ -180,13 +167,26 @@ "diagnostic": { "code": "OBL002", "message": "'obl002_subject' -- obligation may still be open when a barrier fires (open on some path)", - "line": 8, - "severity": "error", - "subject": null, + "line": 581, + "severity": "warning", + "subject": "obl002_subject#581", "resource_kind": null, - "evidence": [] + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] }, - "rendered": "src/Ledger.cs:8: error: [OBL002] 'obl002_subject' -- obligation may still be open when a barrier fires (open on some path)" + "rendered": "src/Ledger.cs:581: warning: [OBL002] 'obl002_subject' -- obligation may still be open when a barrier fires (open on some path)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OBL003", @@ -197,26 +197,20 @@ "diagnostic": { "code": "OBL003", "message": "'obl003_subject' -- obligation not closed before the method exits (on every path)", - "line": 9, + "line": 514, "severity": "error", - "subject": "obl003_subject#9", - "resource_kind": null, + "subject": null, + "resource_kind": "subscription token", "evidence": [ { "line": 1, - "label": "acquired here", + "label": "related step", "file": null, - "role": "acquired" - }, - { - "line": 2, - "label": "escapes here", - "file": "src/Other.cs", - "role": "escaped" + "role": "related" } ] }, - "rendered": "src/Ledger.cs:9: error: [OBL003] 'obl003_subject' -- obligation not closed before the method exits (on every path)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:514: error: [OBL003] 'obl003_subject' -- obligation not closed before the method exits (on every path) [resource: subscription token]\n note: related step at src/Ledger.cs:1" }, { "code": "OBL004", @@ -227,10 +221,10 @@ "diagnostic": { "code": "OBL004", "message": "'obl004_subject' -- obligation may not be closed before the method exits (on some path)", - "line": 10, + "line": 758, "severity": "error", "subject": null, - "resource_kind": "subscription token", + "resource_kind": null, "evidence": [ { "line": 1, @@ -240,7 +234,7 @@ } ] }, - "rendered": "src/Ledger.cs:10: error: [OBL004] 'obl004_subject' -- obligation may not be closed before the method exits (on some path) [resource: subscription token]\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:758: error: [OBL004] 'obl004_subject' -- obligation may not be closed before the method exits (on some path)\n note: related step at src/Ledger.cs:1" }, { "code": "OBL005", @@ -251,13 +245,26 @@ "diagnostic": { "code": "OBL005", "message": "'obl005_subject' -- protocol scope matched no reported method -- rule is dead (advisory)", - "line": 11, - "severity": "warning", - "subject": "obl005_subject#11", + "line": 893, + "severity": "error", + "subject": "obl005_subject#893", "resource_kind": null, - "evidence": [] + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] }, - "rendered": "src/Ledger.cs:11: warning: [OBL005] 'obl005_subject' -- protocol scope matched no reported method -- rule is dead (advisory)" + "rendered": "src/Ledger.cs:893: error: [OBL005] 'obl005_subject' -- protocol scope matched no reported method -- rule is dead (advisory)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OWN001", @@ -268,13 +275,13 @@ "diagnostic": { "code": "OWN001", "message": "'own001_subject' -- owned resource not released on all paths (possible leak)", - "line": 12, + "line": 564, "severity": "error", "subject": null, "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:12: error: [OWN001] 'own001_subject' -- owned resource not released on all paths (possible leak)" + "rendered": "src/Ledger.cs:564: error: [OWN001] 'own001_subject' -- owned resource not released on all paths (possible leak)" }, { "code": "OWN002", @@ -285,26 +292,13 @@ "diagnostic": { "code": "OWN002", "message": "'own002_subject' -- use after release", - "line": 13, + "line": 140, "severity": "error", - "subject": "own002_subject#13", - "resource_kind": "subscription token", - "evidence": [ - { - "line": 1, - "label": "acquired here", - "file": null, - "role": "acquired" - }, - { - "line": 2, - "label": "escapes here", - "file": "src/Other.cs", - "role": "escaped" - } - ] + "subject": null, + "resource_kind": null, + "evidence": [] }, - "rendered": "src/Ledger.cs:13: error: [OWN002] 'own002_subject' -- use after release [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:140: error: [OWN002] 'own002_subject' -- use after release" }, { "code": "OWN003", @@ -315,7 +309,7 @@ "diagnostic": { "code": "OWN003", "message": "'own003_subject' -- double release", - "line": 14, + "line": 18, "severity": "error", "subject": null, "resource_kind": null, @@ -328,7 +322,7 @@ } ] }, - "rendered": "src/Ledger.cs:14: error: [OWN003] 'own003_subject' -- double release\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:18: error: [OWN003] 'own003_subject' -- double release\n note: related step at src/Ledger.cs:1" }, { "code": "OWN004", @@ -339,13 +333,20 @@ "diagnostic": { "code": "OWN004", "message": "'own004_subject' -- borrow escapes its scope", - "line": 15, - "severity": "error", - "subject": "own004_subject#15", + "line": 786, + "severity": "warning", + "subject": null, "resource_kind": null, - "evidence": [] + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] }, - "rendered": "src/Ledger.cs:15: error: [OWN004] 'own004_subject' -- borrow escapes its scope" + "rendered": "src/Ledger.cs:786: warning: [OWN004] 'own004_subject' -- borrow escapes its scope\n note: related step at src/Ledger.cs:1" }, { "code": "OWN005", @@ -356,13 +357,20 @@ "diagnostic": { "code": "OWN005", "message": "'own005_subject' -- use after move", - "line": 16, + "line": 286, "severity": "warning", "subject": null, "resource_kind": "subscription token", - "evidence": [] + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] }, - "rendered": "src/Ledger.cs:16: warning: [OWN005] 'own005_subject' -- use after move [resource: subscription token]" + "rendered": "src/Ledger.cs:286: warning: [OWN005] 'own005_subject' -- use after move [resource: subscription token]\n note: related step at src/Ledger.cs:1" }, { "code": "OWN006", @@ -373,9 +381,9 @@ "diagnostic": { "code": "OWN006", "message": "'own006_subject' -- mutable borrow while a shared borrow is live", - "line": 17, + "line": 213, "severity": "error", - "subject": "own006_subject#17", + "subject": "own006_subject#213", "resource_kind": null, "evidence": [ { @@ -392,7 +400,7 @@ } ] }, - "rendered": "src/Ledger.cs:17: error: [OWN006] 'own006_subject' -- mutable borrow while a shared borrow is live\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:213: error: [OWN006] 'own006_subject' -- mutable borrow while a shared borrow is live\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OWN007", @@ -403,7 +411,7 @@ "diagnostic": { "code": "OWN007", "message": "'own007_subject' -- move while borrowed", - "line": 18, + "line": 678, "severity": "error", "subject": null, "resource_kind": null, @@ -416,7 +424,7 @@ } ] }, - "rendered": "src/Ledger.cs:18: error: [OWN007] 'own007_subject' -- move while borrowed\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:678: error: [OWN007] 'own007_subject' -- move while borrowed\n note: related step at src/Ledger.cs:1" }, { "code": "OWN008", @@ -427,13 +435,20 @@ "diagnostic": { "code": "OWN008", "message": "'own008_subject' -- release while borrowed", - "line": 19, + "line": 22, "severity": "error", - "subject": "own008_subject#19", + "subject": null, "resource_kind": "subscription token", - "evidence": [] + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] }, - "rendered": "src/Ledger.cs:19: error: [OWN008] 'own008_subject' -- release while borrowed [resource: subscription token]" + "rendered": "src/Ledger.cs:22: error: [OWN008] 'own008_subject' -- release while borrowed [resource: subscription token]\n note: related step at src/Ledger.cs:1" }, { "code": "OWN009", @@ -444,13 +459,20 @@ "diagnostic": { "code": "OWN009", "message": "'own009_subject' -- use after possible release (released on some path)", - "line": 20, + "line": 294, "severity": "error", "subject": null, "resource_kind": null, - "evidence": [] + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] }, - "rendered": "src/Ledger.cs:20: error: [OWN009] 'own009_subject' -- use after possible release (released on some path)" + "rendered": "src/Ledger.cs:294: error: [OWN009] 'own009_subject' -- use after possible release (released on some path)\n note: related step at src/Ledger.cs:1" }, { "code": "OWN010", @@ -461,26 +483,13 @@ "diagnostic": { "code": "OWN010", "message": "'own010_subject' -- use after possible move (moved on some path)", - "line": 21, - "severity": "warning", - "subject": "own010_subject#21", - "resource_kind": null, - "evidence": [ - { - "line": 1, - "label": "acquired here", - "file": null, - "role": "acquired" - }, - { - "line": 2, - "label": "escapes here", - "file": "src/Other.cs", - "role": "escaped" - } - ] + "line": 112, + "severity": "error", + "subject": null, + "resource_kind": "subscription token", + "evidence": [] }, - "rendered": "src/Ledger.cs:21: warning: [OWN010] 'own010_subject' -- use after possible move (moved on some path)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:112: error: [OWN010] 'own010_subject' -- use after possible move (moved on some path) [resource: subscription token]" }, { "code": "OWN011", @@ -491,10 +500,10 @@ "diagnostic": { "code": "OWN011", "message": "'own011_subject' -- mutable borrow while another mutable borrow is live", - "line": 22, + "line": 194, "severity": "error", "subject": null, - "resource_kind": "subscription token", + "resource_kind": null, "evidence": [ { "line": 1, @@ -504,7 +513,7 @@ } ] }, - "rendered": "src/Ledger.cs:22: error: [OWN011] 'own011_subject' -- mutable borrow while another mutable borrow is live [resource: subscription token]\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:194: error: [OWN011] 'own011_subject' -- mutable borrow while another mutable borrow is live\n note: related step at src/Ledger.cs:1" }, { "code": "OWN012", @@ -515,13 +524,13 @@ "diagnostic": { "code": "OWN012", "message": "'own012_subject' -- shared borrow while a mutable borrow is live", - "line": 23, + "line": 140, "severity": "error", - "subject": "own012_subject#23", + "subject": null, "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:23: error: [OWN012] 'own012_subject' -- shared borrow while a mutable borrow is live" + "rendered": "src/Ledger.cs:140: error: [OWN012] 'own012_subject' -- shared borrow while a mutable borrow is live" }, { "code": "OWN013", @@ -532,13 +541,13 @@ "diagnostic": { "code": "OWN013", "message": "'own013_subject' -- owner accessed while it is mutably borrowed", - "line": 24, + "line": 132, "severity": "error", "subject": null, "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:24: error: [OWN013] 'own013_subject' -- owner accessed while it is mutably borrowed" + "rendered": "src/Ledger.cs:132: error: [OWN013] 'own013_subject' -- owner accessed while it is mutably borrowed" }, { "code": "OWN014", @@ -549,26 +558,20 @@ "diagnostic": { "code": "OWN014", "message": "'own014_subject' -- value escapes to a longer-lived region (lifetime promotion)", - "line": 25, + "line": 290, "severity": "error", - "subject": "own014_subject#25", - "resource_kind": "subscription token", + "subject": null, + "resource_kind": null, "evidence": [ { "line": 1, - "label": "acquired here", + "label": "related step", "file": null, - "role": "acquired" - }, - { - "line": 2, - "label": "escapes here", - "file": "src/Other.cs", - "role": "escaped" + "role": "related" } ] }, - "rendered": "src/Ledger.cs:25: error: [OWN014] 'own014_subject' -- value escapes to a longer-lived region (lifetime promotion) [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:290: error: [OWN014] 'own014_subject' -- value escapes to a longer-lived region (lifetime promotion)\n note: related step at src/Ledger.cs:1" }, { "code": "OWN015", @@ -579,20 +582,26 @@ "diagnostic": { "code": "OWN015", "message": "'own015_subject' -- stack-backed buffer cannot escape the current function", - "line": 26, - "severity": "warning", - "subject": null, + "line": 5, + "severity": "error", + "subject": "own015_subject#5", "resource_kind": null, "evidence": [ { "line": 1, - "label": "related step", + "label": "acquired here", "file": null, - "role": "related" + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" } ] }, - "rendered": "src/Ledger.cs:26: warning: [OWN015] 'own015_subject' -- stack-backed buffer cannot escape the current function\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:5: error: [OWN015] 'own015_subject' -- stack-backed buffer cannot escape the current function\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OWN016", @@ -603,13 +612,20 @@ "diagnostic": { "code": "OWN016", "message": "'own016_subject' -- stack-backed buffer moved to a longer-lived owner", - "line": 27, + "line": 894, "severity": "error", - "subject": "own016_subject#27", + "subject": null, "resource_kind": null, - "evidence": [] + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] }, - "rendered": "src/Ledger.cs:27: error: [OWN016] 'own016_subject' -- stack-backed buffer moved to a longer-lived owner" + "rendered": "src/Ledger.cs:894: error: [OWN016] 'own016_subject' -- stack-backed buffer moved to a longer-lived owner\n note: related step at src/Ledger.cs:1" }, { "code": "OWN017", @@ -620,26 +636,9 @@ "diagnostic": { "code": "OWN017", "message": "'own017_subject' -- movable buffer escape is not supported by code generation (PoC limitation)", - "line": 28, + "line": 657, "severity": "error", - "subject": null, - "resource_kind": "subscription token", - "evidence": [] - }, - "rendered": "src/Ledger.cs:28: error: [OWN017] 'own017_subject' -- movable buffer escape is not supported by code generation (PoC limitation) [resource: subscription token]" - }, - { - "code": "OWN018", - "family": "OWN", - "analyzer_corpus": false, - "title": "buffer size must be an integer", - "path": "src/Ledger.cs", - "diagnostic": { - "code": "OWN018", - "message": "'own018_subject' -- buffer size must be an integer", - "line": 29, - "severity": "error", - "subject": "own018_subject#29", + "subject": "own017_subject#657", "resource_kind": null, "evidence": [ { @@ -656,7 +655,24 @@ } ] }, - "rendered": "src/Ledger.cs:29: error: [OWN018] 'own018_subject' -- buffer size must be an integer\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:657: error: [OWN017] 'own017_subject' -- movable buffer escape is not supported by code generation (PoC limitation)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN018", + "family": "OWN", + "analyzer_corpus": false, + "title": "buffer size must be an integer", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN018", + "message": "'own018_subject' -- buffer size must be an integer", + "line": 827, + "severity": "error", + "subject": "own018_subject#827", + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:827: error: [OWN018] 'own018_subject' -- buffer size must be an integer" }, { "code": "OWN019", @@ -667,7 +683,7 @@ "diagnostic": { "code": "OWN019", "message": "'own019_subject' -- inline capacity too large for a stack-backed policy", - "line": 30, + "line": 182, "severity": "error", "subject": null, "resource_kind": null, @@ -680,7 +696,7 @@ } ] }, - "rendered": "src/Ledger.cs:30: error: [OWN019] 'own019_subject' -- inline capacity too large for a stack-backed policy\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:182: error: [OWN019] 'own019_subject' -- inline capacity too large for a stack-backed policy\n note: related step at src/Ledger.cs:1" }, { "code": "OWN020", @@ -691,13 +707,26 @@ "diagnostic": { "code": "OWN020", "message": "'own020_subject' -- unsupported construct (out of scope for the MVP)", - "line": 31, + "line": 881, "severity": "warning", - "subject": "own020_subject#31", - "resource_kind": "subscription token", - "evidence": [] + "subject": "own020_subject#881", + "resource_kind": null, + "evidence": [ + { + "line": 1, + "label": "acquired here", + "file": null, + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" + } + ] }, - "rendered": "src/Ledger.cs:31: warning: [OWN020] 'own020_subject' -- unsupported construct (out of scope for the MVP) [resource: subscription token]" + "rendered": "src/Ledger.cs:881: warning: [OWN020] 'own020_subject' -- unsupported construct (out of scope for the MVP)\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OWN021", @@ -708,13 +737,13 @@ "diagnostic": { "code": "OWN021", "message": "'own021_subject' -- stack allocation requires a statically known bound", - "line": 32, + "line": 600, "severity": "error", "subject": null, "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:32: error: [OWN021] 'own021_subject' -- stack allocation requires a statically known bound" + "rendered": "src/Ledger.cs:600: error: [OWN021] 'own021_subject' -- stack allocation requires a statically known bound" }, { "code": "OWN023", @@ -725,26 +754,13 @@ "diagnostic": { "code": "OWN023", "message": "'own023_subject' -- scratch fallback forbidden but the size may exceed the inline limit", - "line": 33, - "severity": "error", - "subject": "own023_subject#33", + "line": 231, + "severity": "warning", + "subject": "own023_subject#231", "resource_kind": null, - "evidence": [ - { - "line": 1, - "label": "acquired here", - "file": null, - "role": "acquired" - }, - { - "line": 2, - "label": "escapes here", - "file": "src/Other.cs", - "role": "escaped" - } - ] + "evidence": [] }, - "rendered": "src/Ledger.cs:33: error: [OWN023] 'own023_subject' -- scratch fallback forbidden but the size may exceed the inline limit\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:231: warning: [OWN023] 'own023_subject' -- scratch fallback forbidden but the size may exceed the inline limit" }, { "code": "OWN024", @@ -755,10 +771,10 @@ "diagnostic": { "code": "OWN024", "message": "'own024_subject' -- sensitive buffer is not cleared on release", - "line": 34, - "severity": "error", + "line": 866, + "severity": "warning", "subject": null, - "resource_kind": "subscription token", + "resource_kind": null, "evidence": [ { "line": 1, @@ -768,7 +784,7 @@ } ] }, - "rendered": "src/Ledger.cs:34: error: [OWN024] 'own024_subject' -- sensitive buffer is not cleared on release [resource: subscription token]\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:866: warning: [OWN024] 'own024_subject' -- sensitive buffer is not cleared on release\n note: related step at src/Ledger.cs:1" }, { "code": "OWN025", @@ -779,13 +795,13 @@ "diagnostic": { "code": "OWN025", "message": "'own025_subject' -- full-length view of a pooled buffer reaches past its logical length", - "line": 35, + "line": 303, "severity": "error", - "subject": "own025_subject#35", + "subject": "own025_subject#303", "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:35: error: [OWN025] 'own025_subject' -- full-length view of a pooled buffer reaches past its logical length" + "rendered": "src/Ledger.cs:303: error: [OWN025] 'own025_subject' -- full-length view of a pooled buffer reaches past its logical length" }, { "code": "OWN030", @@ -796,13 +812,13 @@ "diagnostic": { "code": "OWN030", "message": "'own030_subject' -- undefined name", - "line": 36, - "severity": "warning", - "subject": null, + "line": 627, + "severity": "error", + "subject": "own030_subject#627", "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:36: warning: [OWN030] 'own030_subject' -- undefined name" + "rendered": "src/Ledger.cs:627: error: [OWN030] 'own030_subject' -- undefined name" }, { "code": "OWN031", @@ -813,10 +829,10 @@ "diagnostic": { "code": "OWN031", "message": "'own031_subject' -- name already defined in this scope", - "line": 37, + "line": 677, "severity": "error", - "subject": "own031_subject#37", - "resource_kind": "subscription token", + "subject": "own031_subject#677", + "resource_kind": null, "evidence": [ { "line": 1, @@ -832,7 +848,7 @@ } ] }, - "rendered": "src/Ledger.cs:37: error: [OWN031] 'own031_subject' -- name already defined in this scope [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:677: error: [OWN031] 'own031_subject' -- name already defined in this scope\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OWN032", @@ -843,20 +859,26 @@ "diagnostic": { "code": "OWN032", "message": "'own032_subject' -- owned resource copied without 'move'", - "line": 38, + "line": 465, "severity": "error", - "subject": null, + "subject": "own032_subject#465", "resource_kind": null, "evidence": [ { "line": 1, - "label": "related step", + "label": "acquired here", "file": null, - "role": "related" + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" } ] }, - "rendered": "src/Ledger.cs:38: error: [OWN032] 'own032_subject' -- owned resource copied without 'move'\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:465: error: [OWN032] 'own032_subject' -- owned resource copied without 'move'\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OWN033", @@ -867,13 +889,13 @@ "diagnostic": { "code": "OWN033", "message": "'own033_subject' -- function must return a value on all paths", - "line": 39, - "severity": "error", - "subject": "own033_subject#39", + "line": 96, + "severity": "warning", + "subject": null, "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:39: error: [OWN033] 'own033_subject' -- function must return a value on all paths" + "rendered": "src/Ledger.cs:96: warning: [OWN033] 'own033_subject' -- function must return a value on all paths" }, { "code": "OWN034", @@ -884,13 +906,13 @@ "diagnostic": { "code": "OWN034", "message": "'own034_subject' -- operation requires an owned resource", - "line": 40, + "line": 203, "severity": "error", - "subject": null, - "resource_kind": "subscription token", + "subject": "own034_subject#203", + "resource_kind": null, "evidence": [] }, - "rendered": "src/Ledger.cs:40: error: [OWN034] 'own034_subject' -- operation requires an owned resource [resource: subscription token]" + "rendered": "src/Ledger.cs:203: error: [OWN034] 'own034_subject' -- operation requires an owned resource" }, { "code": "OWN035", @@ -901,9 +923,9 @@ "diagnostic": { "code": "OWN035", "message": "'own035_subject' -- return type mismatch", - "line": 41, - "severity": "warning", - "subject": "own035_subject#41", + "line": 509, + "severity": "error", + "subject": "own035_subject#509", "resource_kind": null, "evidence": [ { @@ -920,7 +942,7 @@ } ] }, - "rendered": "src/Ledger.cs:41: warning: [OWN035] 'own035_subject' -- return type mismatch\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:509: error: [OWN035] 'own035_subject' -- return type mismatch\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OWN036", @@ -931,20 +953,13 @@ "diagnostic": { "code": "OWN036", "message": "'own036_subject' -- cyclic lifetime ordering", - "line": 42, + "line": 235, "severity": "error", - "subject": null, - "resource_kind": null, - "evidence": [ - { - "line": 1, - "label": "related step", - "file": null, - "role": "related" - } - ] + "subject": "own036_subject#235", + "resource_kind": "subscription token", + "evidence": [] }, - "rendered": "src/Ledger.cs:42: error: [OWN036] 'own036_subject' -- cyclic lifetime ordering\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:235: error: [OWN036] 'own036_subject' -- cyclic lifetime ordering [resource: subscription token]" }, { "code": "OWN040", @@ -955,13 +970,13 @@ "diagnostic": { "code": "OWN040", "message": "'own040_subject' -- call to an undeclared function (unknown calls are forbidden)", - "line": 43, + "line": 244, "severity": "error", - "subject": "own040_subject#43", + "subject": null, "resource_kind": "subscription token", "evidence": [] }, - "rendered": "src/Ledger.cs:43: error: [OWN040] 'own040_subject' -- call to an undeclared function (unknown calls are forbidden) [resource: subscription token]" + "rendered": "src/Ledger.cs:244: error: [OWN040] 'own040_subject' -- call to an undeclared function (unknown calls are forbidden) [resource: subscription token]" }, { "code": "OWN041", @@ -972,27 +987,10 @@ "diagnostic": { "code": "OWN041", "message": "'own041_subject' -- call argument mismatch", - "line": 44, - "severity": "error", - "subject": null, - "resource_kind": null, - "evidence": [] - }, - "rendered": "src/Ledger.cs:44: error: [OWN041] 'own041_subject' -- call argument mismatch" - }, - { - "code": "OWN050", - "family": "OWN", - "analyzer_corpus": false, - "title": "declaring type unresolved -- leakage analysis skipped", - "path": "src/Ledger.cs", - "diagnostic": { - "code": "OWN050", - "message": "'own050_subject' -- declaring type unresolved -- leakage analysis skipped", - "line": 45, + "line": 373, "severity": "error", - "subject": "own050_subject#45", - "resource_kind": null, + "subject": "own041_subject#373", + "resource_kind": "subscription token", "evidence": [ { "line": 1, @@ -1008,7 +1006,24 @@ } ] }, - "rendered": "src/Ledger.cs:45: error: [OWN050] 'own050_subject' -- declaring type unresolved -- leakage analysis skipped\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + "rendered": "src/Ledger.cs:373: error: [OWN041] 'own041_subject' -- call argument mismatch [resource: subscription token]\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" + }, + { + "code": "OWN050", + "family": "OWN", + "analyzer_corpus": false, + "title": "declaring type unresolved -- leakage analysis skipped", + "path": "src/Ledger.cs", + "diagnostic": { + "code": "OWN050", + "message": "'own050_subject' -- declaring type unresolved -- leakage analysis skipped", + "line": 612, + "severity": "error", + "subject": null, + "resource_kind": null, + "evidence": [] + }, + "rendered": "src/Ledger.cs:612: error: [OWN050] 'own050_subject' -- declaring type unresolved -- leakage analysis skipped" }, { "code": "OWN051", @@ -1019,20 +1034,26 @@ "diagnostic": { "code": "OWN051", "message": "'own051_subject' -- ownership transfer unverified -- local not checked past this call", - "line": 46, + "line": 701, "severity": "warning", - "subject": null, - "resource_kind": "subscription token", + "subject": "own051_subject#701", + "resource_kind": null, "evidence": [ { "line": 1, - "label": "related step", + "label": "acquired here", "file": null, - "role": "related" + "role": "acquired" + }, + { + "line": 2, + "label": "escapes here", + "file": "src/Other.cs", + "role": "escaped" } ] }, - "rendered": "src/Ledger.cs:46: warning: [OWN051] 'own051_subject' -- ownership transfer unverified -- local not checked past this call [resource: subscription token]\n note: related step at src/Ledger.cs:1" + "rendered": "src/Ledger.cs:701: warning: [OWN051] 'own051_subject' -- ownership transfer unverified -- local not checked past this call\n note: acquired here at src/Ledger.cs:1\n note: escapes here at src/Other.cs:2" }, { "code": "OWN052", @@ -1043,13 +1064,20 @@ "diagnostic": { "code": "OWN052", "message": "'own052_subject' -- interprocedural summary inference failed -- method summaries skipped", - "line": 47, + "line": 882, "severity": "error", - "subject": "own052_subject#47", + "subject": null, "resource_kind": null, - "evidence": [] + "evidence": [ + { + "line": 1, + "label": "related step", + "file": null, + "role": "related" + } + ] }, - "rendered": "src/Ledger.cs:47: error: [OWN052] 'own052_subject' -- interprocedural summary inference failed -- method summaries skipped" + "rendered": "src/Ledger.cs:882: error: [OWN052] 'own052_subject' -- interprocedural summary inference failed -- method summaries skipped\n note: related step at src/Ledger.cs:1" } ] } diff --git a/tests/test_diag_ledger_fixtures.py b/tests/test_diag_ledger_fixtures.py index 7c5926a8..aa941be0 100644 --- a/tests/test_diag_ledger_fixtures.py +++ b/tests/test_diag_ledger_fixtures.py @@ -42,6 +42,7 @@ from __future__ import annotations +import hashlib import json import os import re @@ -76,33 +77,51 @@ def _family(code: str) -> str: return match.group(0) if match else "?" -def _shape_for(index: int, code: str) -> Diagnostic: +def _seed(code: str) -> int: + """A stable per-code value, independent of the code's position in TITLES. + + Deliberately NOT the sorted index. Adding a family is the single event this + ledger exists to make visible, and an index-derived shape sabotages exactly + that: inserting one code shifts every later index, so `DI006` would rewrite + 42 of 47 existing records (measured). A one-record change is reviewable at a + glance; 42 records of churn hide whether the renderer also moved. + + SHA-256 rather than `hash()`, whose string hashing is randomised per process + and would make the golden unreproducible.""" + digest = hashlib.sha256(code.encode("utf-8")).hexdigest() + return int(digest[:8], 16) + + +def _shape_for(code: str) -> Diagnostic: """A representative `Diagnostic` for `code`. The message is derived from the reference's own TITLE (so it is meaningful and cannot drift from the vocabulary), with a quoted subject spliced in so - the caret heuristic has something to find. Optional fields rotate by index so - the ledger exercises every shape across the family set rather than 47 copies - of the same one -- deterministic, so the golden stays stable. + the caret heuristic has something to find. Optional fields rotate by the + code's own [`_seed`], so the ledger exercises every shape across the family + set rather than 47 copies of one -- and each code's shape is fixed for good, + whatever else joins the vocabulary. """ title = TITLES[code] message = f"'{code.lower()}_subject' -- {title}" + seed = _seed(code) + line = seed % 900 + 1 kwargs: dict[str, object] = {} - if index % 2 == 0: - kwargs["subject"] = f"{code.lower()}_subject#{index + 1}" - if index % 3 == 0: + if seed % 2 == 0: + kwargs["subject"] = f"{code.lower()}_subject#{line}" + if seed % 3 == 0: kwargs["resource_kind"] = "subscription token" - if index % 5 == 0: + if seed % 5 == 0: kwargs["severity"] = Severity.WARNING - if index % 4 == 0: + if seed % 4 == 0: kwargs["evidence"] = ( Evidence(line=1, label="acquired here", role="acquired"), Evidence(line=2, label="escapes here", role="escaped", file="src/Other.cs"), ) - elif index % 4 == 1: + elif seed % 4 == 1: kwargs["evidence"] = (Evidence(line=1, label="related step"),) - return Diagnostic(code=code, message=message, line=index + 1, **kwargs) # type: ignore[arg-type] + return Diagnostic(code=code, message=message, line=line, **kwargs) # type: ignore[arg-type] def _evidence_json(ev: Evidence) -> dict[str, object]: @@ -124,8 +143,8 @@ def _diagnostic_json(d: Diagnostic) -> dict[str, object]: def build() -> dict[str, object]: corpus = _corpus_codes() cases: list[dict[str, object]] = [] - for index, code in enumerate(sorted(TITLES)): - diag = _shape_for(index, code) + for code in sorted(TITLES): + diag = _shape_for(code) cases.append({ "code": code, "family": _family(code), @@ -199,13 +218,46 @@ def run() -> int: f"{orphans}") return 1 + churn = _insertion_churn() + if churn: + print(f"FAIL: inserting one code rewrote {churn} existing ledger record(s). " + f"Shapes must derive from the code itself, never from its position in " + f"the sorted vocabulary — adding a family has to be a ONE-record diff, " + f"or a real renderer change hides in the churn") + return 1 + totals = data["totals"] print(f"diagnostic ledger OK: {totals['codes']}/{len(TITLES)} codes covered " f"({totals['by_family']}), " - f"{totals['analyzer_corpus']} of them also produced by the .own corpus sweep") + f"{totals['analyzer_corpus']} of them also produced by the .own corpus sweep; " + f"a new code rewrites {churn} existing record(s)") return 0 +def _insertion_churn() -> int: + """How many EXISTING records change when one new code joins the vocabulary. + + Must be zero. This is the property that makes the ledger readable: the whole + point is that adding a diagnostic family shows up as a single new record a + reviewer can check, not as a wall of unrelated edits. An index-derived shape + scored 42 of 47 here before the seed was made position-independent. + + A probe code is inserted mid-vocabulary (so it shifts sorted positions) and + removed again in a `finally`, so the live TITLES the rest of the suite sees + is untouched.""" + probe = "DI999" + if probe in TITLES: # pragma: no cover - defensive + return 0 + before = {case["code"]: case for case in build()["cases"]} + TITLES[probe] = "ledger insertion probe (not a real diagnostic)" + try: + after = {case["code"]: case for case in build()["cases"]} + finally: + del TITLES[probe] + shared = set(before) & set(after) - {probe} + return sum(1 for code in shared if before[code] != after[code]) + + if __name__ == "__main__": if "--write" in sys.argv[1:]: os.makedirs(os.path.dirname(FIXTURE), exist_ok=True)