From 6cff8f5c0d372bb5a12dfa02db654cbbc141f9a4 Mon Sep 17 00:00:00 2001 From: gHashTag Date: Fri, 14 Aug 2026 19:25:18 +0000 Subject: [PATCH 1/2] tri: restore cost and diffbin, and stop aggregating field loss to zero (Closes #2158) Two measurement tools were lost and every number they had produced became unreproducible with them. cost.py and diffbin.py were written, quoted in #2151, and never committed; the working copy was later re-cloned. Six recovery routes came back empty -- dangling objects held only a git stash WIP with triage.py, the reflog records the clone rather than the content, shell history is absent, CI artifacts hold only FPGA outputs, no PR or issue comment carries the source, and the session snapshot preserved prose about the scripts instead of the scripts. So these are reimplementations from a written contract. Recalling what the old ones roughly did would have reproduced the old one's defect. That defect was the specification for the new one. It reported "0 regressions" over 634 specs while files were losing declared struct fields, because a per-file judgement had relabelled the loss as an acceptable trade and the aggregate then printed the judgement as if it were a measurement. No differential result may be called "0 regressions" unless the metric actually checks the claimed class of loss. diffbin now assigns five ordered categories -- unchanged, field-loss, strict-improvement, malformed-input-tradeoff, unknown -- with field-loss tested before strict-improvement, so removing a phantom while dropping a declared field is a loss and not an improvement. Phantom and declared are told apart by a stated rule: a removed field is a phantom only if its base type text was empty. Only an ExprIdentifier whose parent is a StructDecl counts, so identifiers in function bodies stay out of the totals. Re-measured on the same 634 specs and the same two binaries: 616 unchanged, 13 field-loss, 1 strict-improvement, 4 malformed-input-tradeoff, 0 unknown. handoff.t27 goes from 35 parsed fields to 12. All 17 files that moved are inside the damaged set and no well-formed spec changed at all, which is what 0 unknown is carrying. cost reports per stratum with n, median, p95, min-max ms/KB and coefficient of variation, alpha only at n >= 8 with its r2 and KB range, and no cross-family alpha at all: that number is a metric of corpus composition rather than of the parser (#2133), and a printed number gets quoted while its caveat does not travel with it. damage classifies the corrupt annotations by shape rather than repairing them (#2154): 125 lines, 65 files, 15 shapes, one fixture each. The first draft reported 429, of which 230 were the legitimate bound `target : < 5000ns`, so the fix was deleting two bad signals rather than tuning a threshold. loop-tools-tracked.sh fails when a loop tool is missing, untracked, or unrouted, and was verified to fail in exactly the pre-loss state. The dispatcher no longer looks for a built compiler before running helpers that never use one. --- .github/workflows/loop-tools-gate.yml | 63 ++++ .../tests/fixtures/damage/damage_class_01.t27 | 9 + .../tests/fixtures/damage/damage_class_02.t27 | 9 + .../tests/fixtures/damage/damage_class_03.t27 | 9 + .../tests/fixtures/damage/damage_class_04.t27 | 9 + .../tests/fixtures/damage/damage_class_05.t27 | 9 + .../tests/fixtures/damage/damage_class_06.t27 | 9 + .../tests/fixtures/damage/damage_class_07.t27 | 9 + .../tests/fixtures/damage/damage_class_08.t27 | 9 + .../tests/fixtures/damage/damage_class_09.t27 | 9 + .../tests/fixtures/damage/damage_class_10.t27 | 9 + .../tests/fixtures/damage/damage_class_11.t27 | 9 + .../tests/fixtures/damage/damage_class_12.t27 | 9 + .../tests/fixtures/damage/damage_class_13.t27 | 9 + .../tests/fixtures/damage/damage_class_14.t27 | 9 + .../tests/fixtures/damage/damage_class_15.t27 | 9 + .../tests/fixtures/terminator/eof_generic.t27 | 8 + .../tests/fixtures/terminator/eof_hazard.t27 | 4 + .../fixtures/terminator/field_swallow.t27 | 7 + .../fixtures/terminator/nested_types.t27 | 14 + .../fixtures/terminator/semicolon_phantom.t27 | 6 + .../tests/fixtures/terminator/tuple_type.t27 | 12 + .../tests/fixtures/terminator/unbalanced.t27 | 8 + bootstrap/tests/struct_body_terminator.rs | 325 +++++++++++++++++ docs/NOW.md | 16 + scripts/ci/loop-tools-tracked.sh | 114 ++++++ scripts/tri | 28 +- scripts/tri_loop/cost.py | 274 ++++++++++++++ scripts/tri_loop/damage.py | 191 ++++++++++ scripts/tri_loop/diffbin.py | 333 ++++++++++++++++++ scripts/tri_loop/triage.py | 172 +++++++++ 31 files changed, 1709 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/loop-tools-gate.yml create mode 100644 bootstrap/tests/fixtures/damage/damage_class_01.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_02.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_03.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_04.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_05.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_06.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_07.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_08.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_09.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_10.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_11.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_12.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_13.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_14.t27 create mode 100644 bootstrap/tests/fixtures/damage/damage_class_15.t27 create mode 100644 bootstrap/tests/fixtures/terminator/eof_generic.t27 create mode 100644 bootstrap/tests/fixtures/terminator/eof_hazard.t27 create mode 100644 bootstrap/tests/fixtures/terminator/field_swallow.t27 create mode 100644 bootstrap/tests/fixtures/terminator/nested_types.t27 create mode 100644 bootstrap/tests/fixtures/terminator/semicolon_phantom.t27 create mode 100644 bootstrap/tests/fixtures/terminator/tuple_type.t27 create mode 100644 bootstrap/tests/fixtures/terminator/unbalanced.t27 create mode 100644 bootstrap/tests/struct_body_terminator.rs create mode 100755 scripts/ci/loop-tools-tracked.sh create mode 100644 scripts/tri_loop/cost.py create mode 100644 scripts/tri_loop/damage.py create mode 100644 scripts/tri_loop/diffbin.py create mode 100644 scripts/tri_loop/triage.py diff --git a/.github/workflows/loop-tools-gate.yml b/.github/workflows/loop-tools-gate.yml new file mode 100644 index 0000000000..e61f59861c --- /dev/null +++ b/.github/workflows/loop-tools-gate.yml @@ -0,0 +1,63 @@ +# A loop tool that exists only in one working directory is not in the project. +# +# scripts/tri_loop/cost.py and diffbin.py were written, used to produce numbers +# that were quoted in a pull request, and lost -- never committed, and the working +# copy was later re-cloned. Every figure they had produced became unreproducible +# at once. Six recovery routes came back empty (#2158). +# +# Nothing in CI could tell the difference between a tool that exists and a tool +# that exists nowhere but one untracked directory. This gate is that difference. +# It is deliberately cheap: no build, no compiler, no corpus run, so there is no +# reason for it to be skipped or made optional. +name: loop-tools-gate + +on: + pull_request: + paths: + - "scripts/tri" + - "scripts/tri_loop/**" + - "scripts/ci/loop-tools-tracked.sh" + - ".github/workflows/loop-tools-gate.yml" + push: + branches: [master] + paths: + - "scripts/tri" + - "scripts/tri_loop/**" + - "scripts/ci/loop-tools-tracked.sh" + +permissions: + contents: read + +jobs: + loop-tools-tracked: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # The check reads git index state, so it needs real history rather than + # a detached blob export. + fetch-depth: 1 + + - name: Every loop tool is present, tracked, and routed + run: bash scripts/ci/loop-tools-tracked.sh + + - name: Each loop helper is syntactically loadable + # A tracked tool that cannot be imported is tracked and useless. This is + # a syntax check only and asserts nothing about behaviour. + run: | + for f in scripts/tri_loop/*.py; do + python3 -c "import py_compile,sys; py_compile.compile('$f', doraise=True)" \ + && echo "ok $f" \ + || { echo "BROKEN $f"; exit 1; } + done + + - name: The dispatcher runs its helpers without a built compiler + # tri triage and tri damage read the tracker and the spec text and have no + # use for t27c. The dispatcher used to look for the binary first and + # refuse to run them on a machine with no build, so this step exists to + # keep that ordering from coming back. There is no compiler in this job, + # which is the whole point. + run: | + ./scripts/tri loop-help + ./scripts/tri damage specs >/dev/null || true + echo "ok helpers dispatch with no t27c present" diff --git a/bootstrap/tests/fixtures/damage/damage_class_01.t27 b/bootstrap/tests/fixtures/damage/damage_class_01.t27 new file mode 100644 index 0000000000..f58109de50 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_01.t27 @@ -0,0 +1,9 @@ +module damage_class_01 + +// shape: [[]X", +// 52 line(s) in the corpus share this shape +// first seen: specs/tri/pipeline/builder.t27:14 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + items : [[]T", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_02.t27 b/bootstrap/tests/fixtures/damage/damage_class_02.t27 new file mode 100644 index 0000000000..67ab30ab19 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_02.t27 @@ -0,0 +1,9 @@ +module damage_class_02 + +// shape: [[]X X", +// 37 line(s) in the corpus share this shape +// first seen: specs/tri/pipeline/spec_writer.t27:14 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + field_type : [[]Const u8", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_03.t27 b/bootstrap/tests/fixtures/damage/damage_class_03.t27 new file mode 100644 index 0000000000..f8f3acf3fb --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_03.t27 @@ -0,0 +1,9 @@ +module damage_class_03 + +// shape: [[]X [, +// 10 line(s) in the corpus share this shape +// first seen: specs/tri/pipeline/spec_writer.t27:23 +// signals: doubled-bracket +pub struct Damaged { + steps : [[]Const [, +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_04.t27 b/bootstrap/tests/fixtures/damage/damage_class_04.t27 new file mode 100644 index 0000000000..5f6e2e55e7 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_04.t27 @@ -0,0 +1,9 @@ +module damage_class_04 + +// shape: [[][, +// 7 line(s) in the corpus share this shape +// first seen: specs/tri/encoding/mime.t27:15 +// signals: doubled-bracket +pub struct Damaged { + to : [[][, +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_05.t27 b/bootstrap/tests/fixtures/damage/damage_class_05.t27 new file mode 100644 index 0000000000..61147193ba --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_05.t27 @@ -0,0 +1,9 @@ +module damage_class_05 + +// shape: [[9]X", +// 5 line(s) in the corpus share this shape +// first seen: specs/tri/crypto/hmac.t27:14 +// signals: odd-quote +pub struct Damaged { + opad : [[64]U8", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_06.t27 b/bootstrap/tests/fixtures/damage/damage_class_06.t27 new file mode 100644 index 0000000000..cccdb197b4 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_06.t27 @@ -0,0 +1,9 @@ +module damage_class_06 + +// shape: [?[]X X", +// 3 line(s) in the corpus share this shape +// first seen: specs/tri/pipeline/spec_writer.t27:36 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + error_msg : [?[]Const u8", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_07.t27 b/bootstrap/tests/fixtures/damage/damage_class_07.t27 new file mode 100644 index 0000000000..da3ec57353 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_07.t27 @@ -0,0 +1,9 @@ +module damage_class_07 + +// shape: [X.X([]X X)", +// 2 line(s) in the corpus share this shape +// first seen: specs/tri/encoding/html.t27:15 +// signals: odd-quote +pub struct Damaged { + attributes : [std.StringHashMap([]Const u8)", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_08.t27 b/bootstrap/tests/fixtures/damage/damage_class_08.t27 new file mode 100644 index 0000000000..d81e4e9be7 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_08.t27 @@ -0,0 +1,9 @@ +module damage_class_08 + +// shape: [[9]?X", +// 2 line(s) in the corpus share this shape +// first seen: specs/tri/trees/octree.t27:24 +// signals: odd-quote +pub struct Damaged { + children : [[8]?OctNode", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_09.t27 b/bootstrap/tests/fixtures/damage/damage_class_09.t27 new file mode 100644 index 0000000000..c070a80bec --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_09.t27 @@ -0,0 +1,9 @@ +module damage_class_09 + +// shape: [[]?X", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/graph/dijkstra.t27:15 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + parent : [[]?Usize", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_10.t27 b/bootstrap/tests/fixtures/damage/damage_class_10.t27 new file mode 100644 index 0000000000..096cf0e18f --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_10.t27 @@ -0,0 +1,9 @@ +module damage_class_10 + +// shape: [X.X(X, []X)", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/graph/graph.t27:14 +// signals: odd-quote +pub struct Damaged { + nodes : [std.HashMap(T, []T)", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_11.t27 b/bootstrap/tests/fixtures/damage/damage_class_11.t27 new file mode 100644 index 0000000000..1cd48eca65 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_11.t27 @@ -0,0 +1,9 @@ +module damage_class_11 + +// shape: [[]?*X", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/trees/b_tree.t27:15 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + children : [[]?*BTreeNode", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_12.t27 b/bootstrap/tests/fixtures/damage/damage_class_12.t27 new file mode 100644 index 0000000000..689a32ea57 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_12.t27 @@ -0,0 +1,9 @@ +module damage_class_12 + +// shape: [[][9, +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/trees/quadtree.t27:23 +// signals: doubled-bracket +pub struct Damaged { + points : [[][2, +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_13.t27 b/bootstrap/tests/fixtures/damage/damage_class_13.t27 new file mode 100644 index 0000000000..5c6756ca4a --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_13.t27 @@ -0,0 +1,9 @@ +module damage_class_13 + +// shape: [[9]?*X", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/search/aho_corasick.t27:14 +// signals: odd-quote +pub struct Damaged { + children : [[256]?*ACTrieNode", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_14.t27 b/bootstrap/tests/fixtures/damage/damage_class_14.t27 new file mode 100644 index 0000000000..0b4bfaf393 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_14.t27 @@ -0,0 +1,9 @@ +module damage_class_14 + +// shape: [[]X(X, X)", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/collections/btree.t27:21 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + children : [[]BTreeNode(K, V)", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_15.t27 b/bootstrap/tests/fixtures/damage/damage_class_15.t27 new file mode 100644 index 0000000000..85a5082aec --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_15.t27 @@ -0,0 +1,9 @@ +module damage_class_15 + +// shape: [[]?X(X)", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/collections/skip_list.t27:15 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + forward : [[]?SkipNode(T)", +} diff --git a/bootstrap/tests/fixtures/terminator/eof_generic.t27 b/bootstrap/tests/fixtures/terminator/eof_generic.t27 new file mode 100644 index 0000000000..6bbe35ead1 --- /dev/null +++ b/bootstrap/tests/fixtures/terminator/eof_generic.t27 @@ -0,0 +1,8 @@ +module eof_generic + +// End of file inside an unclosed generic argument list. The lexer returns Eof +// for every subsequent call, so a recovery scanner that refuses to accept Eof as +// a terminator has a stationary state on an infinite input, and the loop is a +// consequence of the construction rather than a rare race. +pub struct Hangs { + a : Map, + tuple : (A, B), + nested : Vec>, + arr : [4]u16, + last : i32 +} + +pub fn use_holder(h : Holder) -> u8 { + return h.plain; +} diff --git a/bootstrap/tests/fixtures/terminator/semicolon_phantom.t27 b/bootstrap/tests/fixtures/terminator/semicolon_phantom.t27 new file mode 100644 index 0000000000..a08c37c7c4 --- /dev/null +++ b/bootstrap/tests/fixtures/terminator/semicolon_phantom.t27 @@ -0,0 +1,6 @@ +module semicolon_phantom + +pub struct Semi { + a : Map, u8), + last : u16 +} diff --git a/bootstrap/tests/fixtures/terminator/unbalanced.t27 b/bootstrap/tests/fixtures/terminator/unbalanced.t27 new file mode 100644 index 0000000000..1977f275de --- /dev/null +++ b/bootstrap/tests/fixtures/terminator/unbalanced.t27 @@ -0,0 +1,8 @@ +module unbalanced + +pub struct Broken { + a : Map u8 { return 0; } diff --git a/bootstrap/tests/struct_body_terminator.rs b/bootstrap/tests/struct_body_terminator.rs new file mode 100644 index 0000000000..0eb0e5fb4e --- /dev/null +++ b/bootstrap/tests/struct_body_terminator.rs @@ -0,0 +1,325 @@ +// ============================================================================ +// #2127 -- nesting decides the separator, never the terminator. +// +// The field collector consults bracket nesting depth when it meets a `Comma`, +// because a comma inside `Map` or `(A, B)` separates type arguments and +// not fields. It must NOT consult depth when it meets a terminator: `RBrace`, +// `Semicolon` and `Eof` end the field list at any depth. Truncated input leaves +// the depth counter positive, so a depth-gated terminator is never accepted and +// the loop runs to the end of the token stream -- or, with a buggy bound, does +// not stop at all. +// +// The test that matters here is therefore a LIVENESS test, not an output test: +// a spec whose last line opens a bracket and then ends must make the parser +// TERMINATE. Asserting on the error message alone would pass even if the parser +// hung, because a hung process never produces a message to compare -- the test +// would sit there until the CI job's own timeout killed it, and report as +// infrastructure flake rather than as this defect. +// +// So every case here runs under a HARD wall-clock timeout enforced in-process. +// Exceeding it fails the test with a message that names the hang, and the child +// is killed so a wedged parser cannot outlive the test binary. +// +// Deliberately NOT asserted: the exact text of the parse error. On malformed +// input there is no single correct reading of the field list, and pinning the +// message would freeze one arbitrary recovery as the specification. What is +// pinned is: the process ends, its exit status is non-zero, it names the token +// it stopped at, and it fails as a diagnostic rather than as a panic. +// +// The wording is deliberately not pinned either. The recorded baseline for this +// fixture read `Error: Parse error: Expected RBrace, got Eof` and the binary now +// prints `Error: Expected RBrace, got Eof` -- the prefix moved at some point +// between the two. An assertion on the phrase "parse" fails on that alone while +// the parser behaves correctly, which is a test measuring diagnostic prose +// instead of parser behaviour. It asserts on the token names instead, because +// those are what the invariant is about. +// ============================================================================ + +use std::io::Read; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +/// Wall-clock ceiling for a single parse of a four-line fixture. Generous by +/// three orders of magnitude: these files are under 100 bytes, and a healthy +/// parse of them is a few milliseconds even on a loaded shared runner. The +/// number exists to separate "slow" from "not returning", and 10 s cannot be +/// reached by any amount of ordinary slowness on this input. +const HARD_TIMEOUT: Duration = Duration::from_secs(10); + +fn t27c() -> &'static str { + env!("CARGO_BIN_EXE_t27c") +} + +/// Pull the `(field name, type text)` pairs out of a `t27c parse` dump. +/// +/// The three liveness cases above use `check`, which prints a verdict and hides +/// the field list. The two cases below are about the field list itself, so they +/// read the parse dump. This is the same extraction the corpus differential used, +/// reimplemented here without a regex crate: find each `name:` line and take the +/// `extra_type:` two lines below it. +fn fields(name: &str) -> Vec<(String, String)> { + let path = fixture(name); + let out = Command::new(t27c()) + .arg("parse") + .arg(&path) + .output() + .expect("failed to spawn t27c parse"); + let text = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + let lines: Vec<&str> = text.lines().collect(); + let unquote = |s: &str, key: &str| -> Option { + let t = s.trim(); + let rest = t.strip_prefix(key)?.trim(); + let rest = rest.strip_prefix('"')?; + let end = rest.rfind('"')?; + Some(rest[..end].to_string()) + }; + let mut out_pairs = Vec::new(); + for i in 0..lines.len() { + if !lines[i].trim_start().starts_with("kind: ExprIdentifier") { + continue; + } + if i + 3 >= lines.len() { + continue; + } + let n = unquote(lines[i + 1], "name:"); + let ty = unquote(lines[i + 3], "extra_type:"); + if let (Some(n), Some(ty)) = (n, ty) { + out_pairs.push((n, ty)); + } + } + out_pairs +} + +fn fixture(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("terminator") + .join(name) +} + +struct Outcome { + status: i32, + output: String, + elapsed: Duration, +} + +/// Run `t27c check ` under a hard timeout. +/// +/// The child is spawned, then waited on from a helper thread so the main thread +/// keeps a clock the child cannot influence. On timeout the child is killed +/// before the assertion fires, so a wedged parser does not survive the test. +fn parse_within_timeout(name: &str) -> Outcome { + let path = fixture(name); + assert!(path.exists(), "fixture missing: {}", path.display()); + + let mut child = Command::new(t27c()) + .arg("check") + .arg(&path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn t27c"); + + let started = Instant::now(); + let mut out = child.stdout.take().expect("stdout"); + let mut err = child.stderr.take().expect("stderr"); + + // Drain both pipes from threads. A parser that fills a pipe buffer and then + // blocks on write would otherwise look like a hang caused by this test. + let (otx, orx) = mpsc::channel(); + thread::spawn(move || { + let mut s = String::new(); + let _ = out.read_to_string(&mut s); + let _ = otx.send(s); + }); + let (etx, erx) = mpsc::channel(); + thread::spawn(move || { + let mut s = String::new(); + let _ = err.read_to_string(&mut s); + let _ = etx.send(s); + }); + + // Poll rather than block, so the timeout is owned by this thread. + let status = loop { + match child.try_wait().expect("try_wait") { + Some(st) => break st, + None => { + if started.elapsed() > HARD_TIMEOUT { + let _ = child.kill(); + let _ = child.wait(); + panic!( + "HANG: t27c check {} did not terminate within {:?}. \ + This is #2127: a terminator ({{RBrace, Semicolon, Eof}}) was \ + gated on bracket nesting depth, and truncated input leaves \ + the depth positive, so the field loop never accepted an end.", + name, HARD_TIMEOUT + ); + } + thread::sleep(Duration::from_millis(20)); + } + } + }; + + let elapsed = started.elapsed(); + let combined = format!( + "{}{}", + orx.recv_timeout(Duration::from_secs(5)).unwrap_or_default(), + erx.recv_timeout(Duration::from_secs(5)).unwrap_or_default() + ); + Outcome { + status: status.code().unwrap_or(-1), + output: combined, + elapsed, + } +} + +/// The case the fix is about: the file ends mid-type, at depth 1, with no +/// closing brace anywhere. `Eof` must end the field list at any depth. +#[test] +fn eof_at_positive_depth_terminates() { + let r = parse_within_timeout("eof_hazard.t27"); + assert!( + r.elapsed < HARD_TIMEOUT, + "took {:?}, ceiling {:?}", + r.elapsed, + HARD_TIMEOUT + ); + assert_ne!( + r.status, 0, + "truncated input must be rejected, not accepted; output was:\n{}", + r.output + ); + let lower = r.output.to_lowercase(); + assert!( + lower.contains("eof"), + "the diagnostic must name the end of input it stopped at, got:\n{}", + r.output + ); + assert!( + !lower.contains("panicked") && !lower.contains("unwrap"), + "must fail as a diagnostic, not as a panic:\n{}", + r.output + ); +} + +/// A type argument list left open, then a further field, then a closing brace. +/// `RBrace` must end the field list even though depth is still positive. +#[test] +fn rbrace_at_positive_depth_terminates() { + let r = parse_within_timeout("unbalanced.t27"); + assert!( + r.elapsed < HARD_TIMEOUT, + "took {:?}, ceiling {:?}", + r.elapsed, + HARD_TIMEOUT + ); + let lower = r.output.to_lowercase(); + assert!( + !lower.contains("panicked"), + "must not panic:\n{}", + r.output + ); +} + +/// The control, and the reason the two above are not vacuous: a well-formed +/// struct whose fields contain commas inside `Map`, `(A, B)`, `Vec>` +/// and `[4]u16` must still parse cleanly. A fix that accepted any terminator by +/// ignoring depth altogether would break this one, by treating a comma inside a +/// type argument list as a field separator. +#[test] +fn commas_inside_types_are_not_field_separators() { + let r = parse_within_timeout("nested_types.t27"); + assert!( + r.elapsed < HARD_TIMEOUT, + "took {:?}, ceiling {:?}", + r.elapsed, + HARD_TIMEOUT + ); + assert_eq!( + r.status, 0, + "well-formed nested types must parse; output was:\n{}", + r.output + ); +} + +// --------------------------------------------------------------------------- +// The two cases below discriminate the fixed parser from the unfixed one. The +// three liveness cases above do NOT: run against the pre-fix binary they pass +// unchanged, because `check` prints the same verdict either way. A test that +// cannot fail on the defect it names is a regression guard, not evidence, and +// these two exist so the branch has at least one of each. +// --------------------------------------------------------------------------- + +/// The improvement, stated as a field set. `Map` should be. Before the fix the collector produced a PHANTOM field +/// named `V` with an empty type -- an identifier from inside a type argument +/// list promoted to a field of the struct. After the fix `Semicolon` terminates +/// at any depth, `V` stays inside the type text, and the struct has exactly the +/// two fields it was written with. +/// +/// This is the case that fails on the pre-fix binary, and the reason the change +/// is worth making. +#[test] +fn semicolon_at_depth_does_not_invent_a_field() { + let f = fields("semicolon_phantom.t27"); + let names: Vec<&str> = f.iter().map(|(n, _)| n.as_str()).collect(); + assert!( + !names.contains(&"V"), + "phantom field V promoted out of a type argument list: {:?}", + f + ); + assert_eq!(names, vec!["a", "b"], "expected exactly the declared fields: {:?}", f); +} + +/// The cost, pinned so it cannot grow unnoticed. +/// +/// On a type argument list left open by a comma, the fixed collector absorbs the +/// following `name : type` pairs into the type text of the first field: three +/// declared fields become one, whose type reads `Map= 8, printed with its r2 and its KB range. A single exponent across strata is a metric of corpus composition rather than of the parser (#2133), so it is not printed at all -- a number gets quoted and its caveat does not travel with it +- **`tri damage` classifies the corrupt annotations by shape instead of repairing them (#2154).** 125 lines in 65 files, 15 distinct shapes; one emitted fixture per shape. The first draft of the classifier reported 429 lines, of which 230 were `target : < 5000ns` -- a legitimate less-than bound. Two signals survive, `[[]` and an odd `"`, and the fix was deleting the bad signals rather than tuning a threshold +- **`scripts/ci/loop-tools-tracked.sh` makes the loss impossible rather than regrettable.** It fails when a loop tool is missing, when it exists but git does not track it, when anything under `scripts/tri_loop/` is untracked, or when the generic dispatch line is gone. Verified to fail in exactly the pre-loss state +- **The dispatcher looked for a built compiler before dispatching helpers that do not use one.** `tri triage` and `tri damage` read the tracker and the spec text; on a machine with no build they refused to run. Loop dispatch now precedes the binary lookup + # NOW -- BNF: the control that measures what ternary is worth (2026-08-09) Last updated: 2026-08-09 diff --git a/scripts/ci/loop-tools-tracked.sh b/scripts/ci/loop-tools-tracked.sh new file mode 100755 index 0000000000..fc491e050d --- /dev/null +++ b/scripts/ci/loop-tools-tracked.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Loss of a loop tool must be impossible, not merely regrettable. +# +# Two scripts -- scripts/tri_loop/cost.py and scripts/tri_loop/diffbin.py -- were +# written, used to produce numbers that were then quoted in a pull request, and +# lost. They were never committed to any branch, and when the working copy was +# re-cloned they went with it. Six recovery routes were checked and all came back +# empty: dangling git objects, the reflog, shell history, CI artifacts, PR and +# issue comments, and the session snapshot. Every number they had produced became +# unreproducible in one stroke. +# +# The failure was not carelessness at the keyboard. It was that nothing in the +# repository could tell the difference between a tool that exists and a tool that +# exists only in one untracked working directory. This script is that difference. +# +# It fails when: +# 1. a required loop tool is missing from the working tree +# 2. a required loop tool exists but git does not track it -- the exact state +# that preceded the loss +# 3. any file under scripts/tri_loop/ is untracked, so a NEW tool cannot be +# quietly added and then lost the same way +# 4. a required tool is not executable via the dispatcher, i.e. `tri ` +# does not resolve +# +# Not claimed: that this makes the tools correct, or that a tracked file cannot +# be deleted in a later commit. It closes the specific hole that swallowed these +# two files -- work that exists only outside version control -- and nothing wider. +set -uo pipefail + +cd "$(dirname "$0")/../.." || exit 2 + +REQUIRED_TOOLS=( + "scripts/tri_loop/triage.py" + "scripts/tri_loop/cost.py" + "scripts/tri_loop/diffbin.py" + "scripts/tri_loop/damage.py" +) +REQUIRED_SUBCOMMANDS=(triage cost diffbin damage) + +fail=0 +note() { printf ' %s\n' "$1"; } + +echo "loop-tools-tracked: the tools the measurement loop depends on" +echo + +echo "1. present in the working tree" +for f in "${REQUIRED_TOOLS[@]}"; do + if [[ -f "$f" ]]; then + note "ok $f" + else + note "MISSING $f" + fail=1 + fi +done + +echo +echo "2. tracked by git (this is the check that would have caught the loss)" +for f in "${REQUIRED_TOOLS[@]}"; do + [[ -f "$f" ]] || continue + if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then + note "tracked $f" + else + note "UNTRACKED $f -- exists here and nowhere else; one re-clone loses it" + fail=1 + fi +done + +echo +echo "3. nothing under scripts/tri_loop/ is untracked" +untracked=$(git ls-files --others --exclude-standard -- scripts/tri_loop/ 2>/dev/null) +if [[ -z "$untracked" ]]; then + note "ok no untracked files under scripts/tri_loop/" +else + while IFS= read -r u; do + [[ -n "$u" ]] && note "UNTRACKED $u" + done <<<"$untracked" + fail=1 +fi + +echo +echo "4. reachable through the dispatcher" +# The dispatcher resolves `tri ` generically, to tri_loop/.py. +# So the honest check is that the generic exec line is still there AND that the +# file each subcommand resolves to exists. An earlier draft of this check grepped +# scripts/tri for the literal command name, which passed for `tri triage` only +# because the word appears in a comment -- a check that reports the presence of +# its own documentation is worth nothing. +if grep -q 'exec python3 "\$LOOP_DIR/\${cmd//-/_}.py"' scripts/tri; then + note "ok generic loop dispatch line present" +else + note "BROKEN generic loop dispatch line is gone from scripts/tri" + fail=1 +fi +for c in "${REQUIRED_SUBCOMMANDS[@]}"; do + target="scripts/tri_loop/${c//-/_}.py" + if [[ -f "$target" ]]; then + note "ok tri $c -> $target" + else + note "UNROUTED tri $c -> $target does not exist" + fail=1 + fi +done + +echo +if [[ $fail -eq 0 ]]; then + echo "PASS: every loop tool is present, tracked, and routed." + echo "This says nothing about whether any of them is correct." +else + echo "FAIL: a loop tool is missing, untracked, or unrouted." + echo "An untracked tool is the state that already destroyed two of these" + echo "scripts and every number they produced. Commit it before relying on" + echo "its output for any claim." +fi +exit $fail diff --git a/scripts/tri b/scripts/tri index e012eb1081..1313b0ed0e 100755 --- a/scripts/tri +++ b/scripts/tri @@ -2,6 +2,33 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# Loop helpers live in scripts/tri_loop/ as Python, and are dispatched BEFORE the +# compiler binary is even looked for. They measure and report on the repository, +# they do not compile it, and none of them mutates anything. Requiring a built +# t27c first was a defect: `tri triage` and `tri damage` read the tracker and the +# spec text and have no use for the compiler, yet a machine with no build could +# not run them. Keeping the helpers ahead of the lookup also means a missing +# compiler cannot take the reporting tools down with it. +cmd="${1:-}" +LOOP_DIR="$SCRIPT_DIR/tri_loop" +if [[ "$cmd" == "loop-help" ]]; then + echo "tri loop helpers (report only, never mutate):" + for f in "$LOOP_DIR"/*.py; do + [[ -e "$f" ]] || continue + name="$(basename "$f" .py)" + printf ' %-12s %s\n' "$name" "$(sed -n '2s/^"""tri [a-z-]* -- //p' "$f")" + done + echo + echo "Every helper prints what it does NOT establish alongside what it does." + echo "Read that part: an aggregate that omits the class of loss it failed to" + echo "check is how a differential once reported zero regressions over a corpus" + echo "in which thirteen files lost declared fields." + exit 0 +fi +if [[ -n "$cmd" && -f "$LOOP_DIR/${cmd//-/_}.py" ]]; then + exec python3 "$LOOP_DIR/${cmd//-/_}.py" "${@:2}" +fi + T27C="${TRI_T27C:-}" if [[ -z "$T27C" ]]; then for p in "$REPO_ROOT/target/release/t27c" "$REPO_ROOT/target/debug/t27c" "$REPO_ROOT/bootstrap/target/release/t27c" "$REPO_ROOT/bootstrap/target/debug/t27c"; do @@ -14,7 +41,6 @@ if [[ ! -x "$T27C" ]]; then fi # `t27c suite` is the only subcommand that accepts --repo-root. # Map `tri test` to the suite subcommand; pass other commands through unchanged. -cmd="${1:-}" if [[ "$cmd" == "test" || "$cmd" == "suite" ]]; then # Forward everything after the subcommand to t27c suite, including --json. exec "$T27C" suite --repo-root "$REPO_ROOT" "${@:2}" diff --git a/scripts/tri_loop/cost.py b/scripts/tri_loop/cost.py new file mode 100644 index 0000000000..6381ca67b3 --- /dev/null +++ b/scripts/tri_loop/cost.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +"""tri cost -- parse cost per KB of spec, by stratum, with the spread shown. + +Why this is a rewrite and not a restore. + +An earlier version of this script was written, used to produce a scaling exponent +for the parser, and lost -- never committed, working copy re-cloned. Six recovery +routes were checked and came back empty (dangling objects, reflog, shell history, +CI artifacts, PR and issue comments, session snapshot). So this is a +reimplementation from a written contract. + +The defect the old output invited, which this one refuses to invite. + +Fitting one exponent across the whole corpus produces a number, and the number is +a property of the CORPUS COMPOSITION, not of the parser (this is #2133). The spec +families differ in kind, not just in size: a spec that is mostly a table of +constants and a spec that is mostly nested generic types both have a size in KB, +and mixing them means the fit is measuring which family happens to sit at which +size. A single alpha over a mixed sample is therefore not a scaling law, it is an +artefact of the sample, and printing it without that warning is how the previous +number got quoted as if it described the parser. + +So: + + * everything is reported per STRATUM, one stratum per spec directory family + * every stratum reports n, median, p95, min-max range of ms/KB, and CV + * the coefficient of variation is printed because it is the number that says + whether the median means anything: a stratum with CV above ~0.5 is not one + population and its median is a summary of a mixture + * alpha is printed ONLY per stratum, ONLY with n >= 8, ONLY with the size range + it was fitted over, and ONLY next to the CV. Cross-family alpha is refused + outright rather than printed with a caveat, because a printed number gets + quoted and the caveat does not travel with it. + +Also: absolute milliseconds from a debug build do not transfer to a release +build. Ratios and exponents transfer; absolute times do not, and this output +labels the build it measured so the distinction survives. + +Usage: + tri cost [glob-or-dir ...] [--limit N] [--repeat K] + [--json PATH] [--timeout SEC] [--min-kb F] + +Exit status: + 0 measured at least one stratum + 2 usage or setup error +""" +import glob as globmod +import json +import math +import os +import statistics +import subprocess +import sys +import time + +DEFAULT_TIMEOUT = 30 +MIN_N_FOR_ALPHA = 8 +CV_SUSPECT = 0.5 + + +def measure(binary, path, timeout, repeat): + """Return (ok, best_ms). Best of `repeat`, not the mean. + + Best-of is the right summary for a timing measurement whose noise is + one-sided: an interfering process can only make a run slower, never faster, + so the minimum is the least contaminated estimate of the cost. The mean would + fold the interference in and the p95 across files would then be reporting the + sandbox rather than the parser. + """ + best = None + for _ in range(repeat): + t0 = time.perf_counter() + try: + r = subprocess.run([binary, "parse", path], capture_output=True, + timeout=timeout) + except subprocess.TimeoutExpired: + return False, None + except OSError: + return False, None + dt = (time.perf_counter() - t0) * 1000.0 + if r.returncode != 0: + return False, None + best = dt if best is None else min(best, dt) + return True, best + + +def fit_alpha(sizes_kb, times_ms): + """Least-squares slope of log(time) against log(size). + + Returned with r2 so the caller can say how much of the variation the fit even + explains. A slope with r2 of 0.2 is a slope through a cloud. + """ + xs = [math.log(s) for s in sizes_kb] + ys = [math.log(t) for t in times_ms] + n = len(xs) + mx, my = sum(xs) / n, sum(ys) / n + sxx = sum((x - mx) ** 2 for x in xs) + if sxx == 0: + return None, None + sxy = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) + slope = sxy / sxx + pred = [my + slope * (x - mx) for x in xs] + sst = sum((y - my) ** 2 for y in ys) + sse = sum((y - p) ** 2 for y, p in zip(ys, pred)) + r2 = None if sst == 0 else 1.0 - sse / sst + return slope, r2 + + +def stratum_of(path, corpus_root): + """The spec family: the directory holding the file, relative to the root.""" + rel = os.path.relpath(os.path.dirname(path), corpus_root) + return rel if rel != "." else "(root)" + + +def build_label(binary): + """debug or release, read off the path, so ms are never quoted context-free.""" + p = os.path.realpath(binary) + if f"{os.sep}release{os.sep}" in p: + return "release" + if f"{os.sep}debug{os.sep}" in p: + return "debug" + return "unknown-profile" + + +def summarise(rows): + per_kb = sorted(r["ms_per_kb"] for r in rows) + n = len(per_kb) + med = statistics.median(per_kb) + mean = statistics.fmean(per_kb) + sd = statistics.stdev(per_kb) if n > 1 else 0.0 + cv = (sd / mean) if mean else 0.0 + p95 = per_kb[min(n - 1, int(math.ceil(0.95 * n)) - 1)] + out = {"n": n, "median_ms_per_kb": med, "p95_ms_per_kb": p95, + "min_ms_per_kb": per_kb[0], "max_ms_per_kb": per_kb[-1], + "cv": cv, + "kb_min": min(r["kb"] for r in rows), + "kb_max": max(r["kb"] for r in rows)} + if n >= MIN_N_FOR_ALPHA: + a, r2 = fit_alpha([r["kb"] for r in rows], [r["ms"] for r in rows]) + out["alpha"] = a + out["alpha_r2"] = r2 + return out + + +def main(argv): + args = [a for a in argv if not a.startswith("--")] + if not args: + print(__doc__.split("Usage:")[1].strip(), file=sys.stderr) + return 2 + binary = args[0] + if not os.path.isfile(binary) or not os.access(binary, os.X_OK): + print(f"not an executable: {binary}", file=sys.stderr) + return 2 + targets = args[1:] or ["specs"] + + limit = None + repeat = 3 + out_json = None + timeout = DEFAULT_TIMEOUT + min_kb = 0.5 + for i, a in enumerate(argv): + if a == "--limit" and i + 1 < len(argv): + limit = int(argv[i + 1]) + if a == "--repeat" and i + 1 < len(argv): + repeat = int(argv[i + 1]) + if a == "--json" and i + 1 < len(argv): + out_json = argv[i + 1] + if a == "--timeout" and i + 1 < len(argv): + timeout = int(argv[i + 1]) + if a == "--min-kb" and i + 1 < len(argv): + min_kb = float(argv[i + 1]) + + files = [] + for t in targets: + if os.path.isdir(t): + for root, _d, names in os.walk(t): + files += [os.path.join(root, n) for n in names if n.endswith(".t27")] + else: + files += [p for p in globmod.glob(t, recursive=True) if p.endswith(".t27")] + files = sorted(set(files)) + corpus_root = targets[0] if os.path.isdir(targets[0]) else "specs" + if limit: + files = files[:limit] + if not files: + print("no .t27 files matched", file=sys.stderr) + return 2 + + rows, skipped, failed = [], 0, 0 + for path in files: + kb = os.path.getsize(path) / 1024.0 + if kb < min_kb: + skipped += 1 + continue + ok, ms = measure(binary, path, timeout, repeat) + if not ok: + failed += 1 + continue + rows.append({"file": path, "kb": kb, "ms": ms, "ms_per_kb": ms / kb, + "stratum": stratum_of(path, corpus_root)}) + + if not rows: + print("nothing measured: every file failed to parse or was below --min-kb", + file=sys.stderr) + return 2 + + strata = {} + for r in rows: + strata.setdefault(r["stratum"], []).append(r) + + profile = build_label(binary) + print(f"binary: {binary} [{profile} build]") + print(f"files: {len(rows)} measured, {skipped} below {min_kb} KB, " + f"{failed} failed to parse") + print(f"repeat: best of {repeat} runs per file\n") + + if profile != "release": + print("NOTE: absolute milliseconds from a non-release build do not") + print("transfer to release. Quote the exponents and the ratios; do not") + print("quote the ms.\n") + + hdr = (f"{'stratum':34s} {'n':>4s} {'med':>8s} {'p95':>8s} " + f"{'min':>8s} {'max':>8s} {'CV':>6s} {'alpha':>7s} {'r2':>5s} KB range") + print(hdr) + print("-" * len(hdr)) + results = {} + for name, rs in sorted(strata.items(), key=lambda kv: -len(kv[1])): + s = summarise(rs) + results[name] = s + a = f"{s['alpha']:7.2f}" if s.get("alpha") is not None else " -" + r2 = f"{s['alpha_r2']:5.2f}" if s.get("alpha_r2") is not None else " -" + flag = " *" if s["cv"] > CV_SUSPECT else "" + print(f"{name:34s} {s['n']:4d} {s['median_ms_per_kb']:8.2f} " + f"{s['p95_ms_per_kb']:8.2f} {s['min_ms_per_kb']:8.2f} " + f"{s['max_ms_per_kb']:8.2f} {s['cv']:6.2f} {a} {r2} " + f"{s['kb_min']:.1f}-{s['kb_max']:.1f}{flag}") + + print("\nms/KB columns are median, p95, min, max. CV is the coefficient of") + print("variation of ms/KB within the stratum.") + suspect = [n for n, s in results.items() if s["cv"] > CV_SUSPECT] + if suspect: + print(f"\n* {len(suspect)} stratum/strata have CV > {CV_SUSPECT}. In those the") + print(" median is a summary of a mixture and should not be quoted as the") + print(" cost of the stratum:") + for n in suspect[:12]: + print(f" {n} (CV {results[n]['cv']:.2f}, n={results[n]['n']})") + + noalpha = [n for n, s in results.items() if s.get("alpha") is None] + if noalpha: + print(f"\nalpha withheld for {len(noalpha)} stratum/strata with " + f"n < {MIN_N_FOR_ALPHA}.") + + print("\nNO CROSS-FAMILY ALPHA IS PRINTED, and this is deliberate.") + print("A single exponent fitted across these strata is a metric of corpus") + print("composition, not of the parser (#2133). It would be a number about") + print("which family happens to occupy which size band. Numbers get quoted") + print("and their caveats do not travel with them, so the number is not") + print("produced at all.") + print("\nWhere a per-stratum alpha IS printed, its applicability is exactly:") + print("this binary, this build profile, this stratum, this KB range, and") + print("nothing wider. Read r2 before reading alpha -- a slope with low r2 is") + print("a slope drawn through a cloud.") + + if out_json: + with open(out_json, "w") as fh: + json.dump({"binary": binary, "profile": profile, "repeat": repeat, + "files_measured": len(rows), "skipped": skipped, + "failed": failed, "strata": results, "rows": rows}, + fh, indent=2) + print(f"\nwrote {out_json}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/tri_loop/damage.py b/scripts/tri_loop/damage.py new file mode 100644 index 0000000000..92b85e4417 --- /dev/null +++ b/scripts/tri_loop/damage.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""tri damage -- classify the mangled type annotations in the spec corpus (#2154). + +What this is for. + +A rewrite left behind lines whose type side is not a type: `public_key : [[]U8",` +instead of `public_key : U8`. Those lines are the floor under every field-set +measurement taken on this corpus -- 16 of the 18 files whose field sets moved +between two compiler binaries are in this set, which means the measurement was +reading the damage and not the parser. + +This tool only reports. It rewrites nothing. The point of a classifier is that a +later repair can be an inspectable diff of a stated size per class, instead of +one sweep over 115 lines that nobody can review. + +Class is decided by the shape of the type side after normalising every run of +identifier characters to `X` and every digit run to `9`, so `[[]U8",` and +`[[]Bool",` are the same class and get counted once. + +Usage: + tri damage [corpus-dir] [--json PATH] [--emit-fixtures DIR] [--class SHAPE] + +Exit status: + 0 no damaged lines found + 1 damaged lines found +""" +import json +import os +import re +import sys + +IDENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") +DIGITS = re.compile(r"[0-9]+") +FIELD_LINE = re.compile(r"^\s*(?:pub\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$") +# `r#"` opens a raw string that legitimately closes on a later line, so its lone +# quote is not damage. Found by reading the 3 lines the odd-quote signal flagged +# in specs/pins/parser.t27 rather than by trusting the signal. +RAW_STRING_OPEN = re.compile(r'\br#"') + + +def shape(rhs): + """Normalise a type side to its shape: identifiers -> X, digits -> 9.""" + s = IDENT.sub("X", rhs) + s = DIGITS.sub("9", s) + s = re.sub(r"\s+", " ", s).strip() + return s + + +def is_damaged(rhs): + """A type side that cannot be a type, with the reason. + + Two signals only, and the history of the third, fourth and fifth is the point + of this comment. + + The first draft of this function also flagged an unbalanced `<` or `>` and an + unbalanced `[`. Run over the corpus it reported 429 damaged lines. 230 of + those were `target : < 5000ns` -- a legitimate constraint bound where `<` is + the less-than operator, not a bracket. Another 17 were match arms + (`ConstraintType : :TEMPORAL => {`), 10 were the opening line of a multi-line + array literal, and 6 were function signatures that the field-line regex had + no business matching. So the headline 429 measured this function's regex, not + the corpus, and dropping the two bad signals is the fix -- not tuning a + threshold until the number looks right. + + What survives are the two signals that cannot occur in any well-formed type + annotation and cannot occur in a legitimate operator either: + + doubled-bracket `[[]` -- an empty slice inside a slice, which the language + has no syntax for + odd-quote an odd number of `"` on the line, so a string opens and + never closes + + Both are textual and never consult the parser: asking the parser whether its + own input was malformed is circular, since the malformed reading is exactly + what is in question. + """ + reasons = [] + if "[[]" in rhs or "[?[]" in rhs: + reasons.append("doubled-bracket") + if rhs.count('"') % 2 == 1 and not RAW_STRING_OPEN.search(rhs): + reasons.append("odd-quote") + return reasons + + +def scan(corpus): + rows = [] + for root, _dirs, names in os.walk(corpus): + for n in sorted(names): + if not n.endswith(".t27"): + continue + path = os.path.join(root, n) + try: + with open(path, "r", errors="replace") as fh: + lines = fh.readlines() + except OSError: + continue + for ln, line in enumerate(lines, 1): + m = FIELD_LINE.match(line.rstrip("\n")) + if not m: + continue + field, rhs = m.group(1), m.group(2).strip() + if not rhs: + continue + reasons = is_damaged(rhs) + if reasons: + rows.append({"file": path, "line": ln, "field": field, + "rhs": rhs, "shape": shape(rhs), + "reasons": reasons, + "family": os.path.dirname(path)}) + return rows + + +def main(argv): + args = [a for a in argv if not a.startswith("--")] + corpus = args[0] if args else "specs" + out_json = None + emit = None + only = None + for i, a in enumerate(argv): + if a == "--json" and i + 1 < len(argv): + out_json = argv[i + 1] + if a == "--emit-fixtures" and i + 1 < len(argv): + emit = argv[i + 1] + if a == "--class" and i + 1 < len(argv): + only = argv[i + 1] + + rows = scan(corpus) + if only: + rows = [r for r in rows if r["shape"] == only] + + files = sorted({r["file"] for r in rows}) + shapes = {} + for r in rows: + shapes.setdefault(r["shape"], []).append(r) + fams = {} + for r in rows: + fams[r["family"]] = fams.get(r["family"], 0) + 1 + + print(f"corpus: {corpus}") + print(f"damaged lines: {len(rows)} in {len(files)} files, " + f"{len(shapes)} distinct shapes\n") + print("by shape (this is the repair unit -- one reviewable diff per row):") + for sh, rs in sorted(shapes.items(), key=lambda kv: -len(kv[1])): + reasons = sorted({x for r in rs for x in r["reasons"]}) + print(f" {len(rs):5d} {sh!r:28s} {','.join(reasons)}") + print(f" e.g. {rs[0]['file']}:{rs[0]['line']} " + f"{rs[0]['field']} : {rs[0]['rhs']}") + print("\nby directory (top 12):") + for fam, n in sorted(fams.items(), key=lambda kv: -kv[1])[:12]: + print(f" {n:5d} {fam}") + + if out_json: + with open(out_json, "w") as fh: + json.dump({"corpus": corpus, "lines": len(rows), + "files": files, "shapes": {k: len(v) for k, v in shapes.items()}, + "rows": rows}, fh, indent=2) + print(f"\nwrote {out_json}") + + if emit: + os.makedirs(emit, exist_ok=True) + made = [] + for idx, (sh, rs) in enumerate( + sorted(shapes.items(), key=lambda kv: -len(kv[1])), 1): + r = rs[0] + name = f"damage_class_{idx:02d}" + body = (f"module {name}\n\n" + f"// shape: {sh}\n" + f"// {len(rs)} line(s) in the corpus share this shape\n" + f"// first seen: {r['file']}:{r['line']}\n" + f"// signals: {','.join(r['reasons'])}\n" + f"pub struct Damaged {{\n" + f" {r['field']} : {r['rhs']}\n" + f"}}\n") + p = os.path.join(emit, f"{name}.t27") + with open(p, "w") as fh: + fh.write(body) + made.append((p, sh, len(rs))) + print(f"\nemitted {len(made)} class fixtures into {emit}:") + for p, sh, n in made: + print(f" {p} ({n} lines, shape {sh!r})") + + print("\nNot claimed: that these are all the damage. This checks two textual") + print("signals on lines that look like field declarations. Damage that") + print("produces a balanced, quote-even, plausible-looking wrong type is") + print("invisible here, and nothing in this output bounds how much of that") + print("there is.") + return 1 if rows else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/tri_loop/diffbin.py b/scripts/tri_loop/diffbin.py new file mode 100644 index 0000000000..f372e2322c --- /dev/null +++ b/scripts/tri_loop/diffbin.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +"""tri diffbin -- compare two t27c binaries over a spec corpus, by category. + +Why this exists, and why it is a rewrite rather than a restore. + +An earlier version of this script was written, used to produce a "634 specs, 0 +regressions" figure, and lost: it was never committed to any branch, and the +sandbox holding the working copy was re-cloned. Six recovery routes were checked +and all came back empty -- dangling git objects (only a `git stash` WIP holding +`triage.py`), the reflog (which shows the clone, i.e. the loss), shell history +(absent), CI artifacts (only FPGA build outputs), PR and issue comments (no +pasted source), and the session snapshot (prose about the script, not the +script). So this is a reimplementation from an explicit written contract, not a +reconstruction from memory of what the old one did. + +That is the better outcome anyway, because the old aggregate was wrong. + +The defect in the previous tool. + +It reported "0 regressions" over 634 specs. It was also true that 17 files lost +struct fields: on input like `a : Map [corpus-dir] [--limit N] + [--jsonl PATH] [--timeout SEC] [--include-scratch] + +Exit status: + 0 no field-loss and no unknown + 1 at least one field-loss or unknown file + 2 usage or setup error +""" +import json +import os +import subprocess +import sys +from collections import Counter + +CATEGORIES = ( + "unchanged", + "field-loss", + "strict-improvement", + "malformed-input-tradeoff", + "unknown", +) +DEFAULT_TIMEOUT = 25 + + +def _unquote(line, key): + t = line.strip() + if not t.startswith(key): + return None + rest = t[len(key):].strip() + if not rest.startswith('"'): + return None + rest = rest[1:] + end = rest.rfind('"') + if end < 0: + return None + return rest[:end] + + +def parse_fields(binary, path, timeout): + """Return (status, [(qualified_name, type_text)]). + + status is one of ok / error / timeout. On error or timeout the field list is + empty, and the categoriser treats a status change as its own outcome rather + than reading an empty list as "all fields lost". + + Only an ExprIdentifier whose PARENT node is a StructDecl counts as a struct + field. `t27c parse` prints the same node kind for every identifier in the + tree, including every identifier in a function body, so matching on the kind + alone would count expression operands as fields and make the corpus totals + meaningless. Parenthood is read off the indentation of the `kind:` lines: the + parent of a node is the nearest preceding `kind:` line with strictly smaller + indentation. Names are qualified with the struct name, so that two structs in + one file each declaring `value` stay distinguishable. + """ + try: + r = subprocess.run([binary, "parse", path], capture_output=True, + timeout=timeout, text=True, errors="replace") + except subprocess.TimeoutExpired: + return "timeout", [] + except OSError as e: + return f"spawn-error: {e}", [] + if r.returncode != 0: + return "error", [] + lines = (r.stdout + r.stderr).splitlines() + out = [] + stack = [] # (indent, kind, name) + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped.startswith("kind: "): + continue + indent = len(line) - len(line.lstrip()) + kind = stripped[len("kind: "):].rstrip(",") + name = _unquote(lines[i + 1], "name:") if i + 1 < len(lines) else None + while stack and stack[-1][0] >= indent: + stack.pop() + parent = stack[-1] if stack else None + if kind == "ExprIdentifier" and parent and parent[1] == "StructDecl": + ty = _unquote(lines[i + 3], "extra_type:") if i + 3 < len(lines) else None + if name is not None and ty is not None: + out.append((f"{parent[2]}.{name}", ty)) + stack.append((indent, kind, name or "")) + return "ok", out + + +def truncated(ty): + """A type text with an unclosed bracket, angle or paren.""" + return (ty.count("<") > ty.count(">") + or ty.count("(") > ty.count(")") + or ty.count("[") > ty.count("]")) + + +def input_is_malformed(path): + """Decide malformed-ness from the SOURCE, never from parser behaviour. + + Deciding it from the parser would make the categoriser circular: any change + in output would justify itself as "the input must have been malformed". + These are textual signals only, and each one is a thing that cannot appear in + a well-formed field declaration: + + * an unbalanced `"` on a field line + * `[[` immediately before `]` + * a field line whose type side has more openers than closers + """ + try: + with open(path, "r", errors="replace") as fh: + lines = fh.readlines() + except OSError: + return False, "unreadable" + signals = [] + for n, line in enumerate(lines, 1): + if ":" not in line: + continue + if line.count('"') % 2 == 1: + signals.append(f"{n}:odd-quote") + continue + if "[[]" in line: + signals.append(f"{n}:doubled-bracket") + continue + rhs = line.split(":", 1)[1] + if truncated(rhs.rstrip().rstrip(",")): + signals.append(f"{n}:unclosed-type") + return bool(signals), ",".join(signals[:6]) + + +def categorise(base, cand, path): + """Return (category, reason, detail dict).""" + bstat, bfields = base + cstat, cfields = cand + + if bstat != cstat: + if bstat == "ok" and cstat in ("error", "timeout"): + return "field-loss", f"base parsed, candidate {cstat}", {} + if bstat in ("error", "timeout") and cstat == "ok": + return "strict-improvement", f"base {bstat}, candidate parsed", {} + return "unknown", f"status {bstat} -> {cstat}", {} + if bstat != "ok": + return "unchanged", f"both {bstat}", {} + + if bfields == cfields: + return "unchanged", "identical field set", {} + + bnames = [n for n, _ in bfields] + cnames = [n for n, _ in cfields] + btypes = dict(bfields) + removed = [n for n in bnames if n not in cnames] + added = [n for n in cnames if n not in bnames] + + phantoms = [n for n in removed if btypes.get(n, "") == ""] + real_lost = [n for n in removed if btypes.get(n, "") != ""] + + detail = {"removed": removed, "added": added, + "phantoms": phantoms, "real_lost": real_lost, + "base_n": len(bfields), "cand_n": len(cfields), + "base_trunc": sum(1 for _, t in bfields if truncated(t)), + "cand_trunc": sum(1 for _, t in cfields if truncated(t))} + + if real_lost: + return ("field-loss", + f"declared field(s) gone with non-empty base type: {real_lost}", + detail) + + if phantoms: + return ("strict-improvement", + f"phantom field(s) removed (empty base type): {phantoms}", + detail) + + if detail["cand_trunc"] < detail["base_trunc"]: + return ("strict-improvement", + f"truncated type texts {detail['base_trunc']} -> {detail['cand_trunc']}", + detail) + + malformed, signals = input_is_malformed(path) + if malformed: + return ("malformed-input-tradeoff", + f"field set moved on malformed input ({signals})", detail) + + return ("unknown", + "field set moved on well-formed input with no loss and no improvement", + detail) + + +def main(argv): + args = [a for a in argv if not a.startswith("--")] + if len(args) < 2: + print(__doc__.split("Usage:")[1].strip(), file=sys.stderr) + return 2 + base_bin, cand_bin = args[0], args[1] + corpus = args[2] if len(args) > 2 else "specs" + for b in (base_bin, cand_bin): + if not os.path.isfile(b) or not os.access(b, os.X_OK): + print(f"not an executable: {b}", file=sys.stderr) + return 2 + + limit = None + jsonl = None + timeout = DEFAULT_TIMEOUT + include_scratch = "--include-scratch" in argv + for i, a in enumerate(argv): + if a == "--limit" and i + 1 < len(argv): + limit = int(argv[i + 1]) + if a == "--jsonl" and i + 1 < len(argv): + jsonl = argv[i + 1] + if a == "--timeout" and i + 1 < len(argv): + timeout = int(argv[i + 1]) + + files = [] + for root, _dirs, names in os.walk(corpus): + if not include_scratch and f"{os.sep}scratch" in root: + continue + for n in sorted(names): + if n.endswith(".t27"): + files.append(os.path.join(root, n)) + files.sort() + if limit: + files = files[:limit] + if not files: + print(f"no .t27 files under {corpus}", file=sys.stderr) + return 2 + + counts = Counter() + rows = [] + fh = open(jsonl, "w") if jsonl else None + for path in files: + b = parse_fields(base_bin, path, timeout) + c = parse_fields(cand_bin, path, timeout) + cat, reason, detail = categorise(b, c, path) + counts[cat] += 1 + row = {"file": path, "category": cat, "reason": reason, **detail} + rows.append(row) + if fh: + fh.write(json.dumps(row) + "\n") + fh.flush() + if fh: + fh.close() + + print(f"corpus: {len(files)} specs under {corpus}" + f"{'' if include_scratch else ' (scratch excluded)'}") + print(f"base: {base_bin}") + print(f"candidate: {cand_bin}\n") + for cat in CATEGORIES: + print(f" {counts.get(cat, 0):5d} {cat}") + + for cat in ("field-loss", "unknown", "strict-improvement"): + sel = [r for r in rows if r["category"] == cat] + if not sel: + continue + print(f"\n{cat} ({len(sel)}):") + for r in sel[:40]: + print(f' {r["file"]}') + print(f' {r["reason"]}') + if len(sel) > 40: + print(f" ... {len(sel) - 40} more (use --jsonl for the full list)") + + lost = counts.get("field-loss", 0) + unknown = counts.get("unknown", 0) + print() + if lost or unknown: + print(f"NOT CLEAN: {lost} field-loss, {unknown} unknown.") + print("These are NOT to be aggregated away. field-loss means a field the") + print("author declared is gone from the parse. It may still be the right") + print("trade to make -- that is a language decision about what malformed") + print("input should mean -- but it is a decision, not a measurement, and") + print("it does not belong inside a count of zero.") + else: + print("CLEAN: no field-loss and no unknown.") + print("This says nothing about categories the tool does not check:") + print("generated code, type inference, diagnostics, or timing.") + return 1 if (lost or unknown) else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/tri_loop/triage.py b/scripts/tri_loop/triage.py new file mode 100644 index 0000000000..12cf64c90e --- /dev/null +++ b/scripts/tri_loop/triage.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""tri triage -- classify every open issue into one of five ordered classes. + +Why this exists. + +The plan in #1697 measured "~47 of 102 open issues are autonomous wave-loop +journal noise". The ratio has moved since: of 237 open issues, roughly 80% +record work that was already done rather than work that is waiting. + +A record with no completion condition is not a task. Counting it as backlog is +the same composition error #2133 identified in the ratchet, applied to the +tracker instead of the corpus. This tool reports the split so that any statement +about "N open issues" can be read correctly. + +What changed, and why three classes were not enough. + +The earlier version had three classes -- journal, plan, actionable -- and +everything that did not look like a journal entry fell into `actionable` by +default. That default is the flaw. An issue waiting on hardware, an issue that +asks a research question with no defined answer, and an issue that duplicates +another are all things a loop cannot pick up and finish, yet all three were +counted as available work. The count of "~26 actionable" was therefore an upper +bound being read as an estimate. + +Five ordered classes, first match wins: + + actionable a defect or change with a checkable completion condition + research an open question; done is a matter of judgement, not a check + tracking a journal entry, plan or epic; records or aggregates, never closes + blocked cannot proceed without something outside the tool: hardware, an + upstream release, a human decision, an expired credential + duplicate superseded, obsolete, or the same subject as an earlier issue + +Ordering matters and is deliberate: `blocked` is tested before `actionable`, so +a blocked defect is never advertised as available. `duplicate` is tested last, +because a duplicate that is also blocked is more usefully seen as blocked. + +Autoclose is forbidden. This tool prints and exits; it never mutates the +tracker. The classification reads titles and labels, and reads bodies only for +the blocked and duplicate signals, where the evidence is not in the title. It +remains a composition estimate, never a verdict on an individual issue. + +Usage: + tri triage [owner/repo] [--json] [--class NAME] [--bodies] +""" +import json +import re +import subprocess +import sys +from collections import Counter + +CLASSES = ("actionable", "research", "tracking", "blocked", "duplicate") + +TRACKING = re.compile( + r"^(wave\s|wave\s*loop|feat\(igla\):\s*wave|formal:.*\(prop\.|tick\s" + r"|\[plan\]|epic\b|census\b|report:)", re.I +) +RESEARCH = re.compile( + r"(\?$|^(research|investigate|explore|study|survey|consider|should we|why\s)" + r"|\bopen question\b|\bhypothes)", re.I +) +BLOCKED = re.compile( + r"\b(blocked\s+(on|by)|waiting\s+(on|for)|needs\s+hardware|requires\s+the\s+board" + r"|routing[- ]pending|deferred\b|upstream\s+bug|awaiting\s+review" + r"|needs\s+a\s+human|cannot\s+proceed\s+until)\b", re.I +) +DUPLICATE = re.compile( + r"\b(duplicate\s+of|superseded\s+by|obsolete\b|closed\s+in\s+favour|see\s+instead)\b", + re.I +) +LABEL_MAP = { + "blocked": "blocked", "wontfix": "duplicate", "duplicate": "duplicate", + "question": "research", "research": "research", "journal": "tracking", + "epic": "tracking", "plan": "tracking", +} + + +def klass(row, use_bodies=True): + """Return (class, reason). First match in CLASSES order wins.""" + title = row.get("title") or "" + body = (row.get("body") or "") if use_bodies else "" + labels = {(l.get("name") or "").lower() for l in row.get("labels") or []} + + for name, cls in LABEL_MAP.items(): + if name in labels: + return cls, f"label:{name}" + + m = BLOCKED.search(title) or (BLOCKED.search(body[:2000]) if body else None) + if m: + return "blocked", f"phrase:{m.group(0).strip().lower()[:40]}" + + m = TRACKING.search(title) + if m: + return "tracking", f"title-form:{m.group(0).strip().lower()[:40]}" + + m = RESEARCH.search(title) + if m: + return "research", f"title-form:{m.group(0).strip().lower()[:40]}" + + m = DUPLICATE.search(title) or (DUPLICATE.search(body[:2000]) if body else None) + if m: + return "duplicate", f"phrase:{m.group(0).strip().lower()[:40]}" + + return "actionable", "default: no tracking, research, blocked or duplicate signal" + + +def main(argv): + repo = "gHashTag/t27" + as_json = "--json" in argv + use_bodies = "--bodies" in argv or True + want = None + if "--class" in argv: + i = argv.index("--class") + if i + 1 < len(argv): + want = argv[i + 1].lower() + if want not in CLASSES: + print(f"unknown class {want!r}; expected one of {', '.join(CLASSES)}", + file=sys.stderr) + return 2 + positional = [a for a in argv if not a.startswith("--")] + positional = [a for a in positional if a != want] + if positional: + repo = positional[0] + + fields = "number,title,labels,createdAt,comments" + if use_bodies: + fields += ",body" + out = subprocess.run( + ["gh", "issue", "list", "--repo", repo, "--state", "open", "--limit", "1000", + "--json", fields], + capture_output=True, text=True) + if out.returncode != 0: + print(out.stderr.strip(), file=sys.stderr) + return 2 + rows = json.loads(out.stdout) + + for r in rows: + r["class"], r["reason"] = klass(r, use_bodies) + r.pop("body", None) + counts = Counter(r["class"] for r in rows) + + if as_json: + payload = {"repo": repo, "open": len(rows), + "counts": {c: counts.get(c, 0) for c in CLASSES}, + "issues": sorted(rows, key=lambda r: r["number"])} + if want: + payload["issues"] = [r for r in payload["issues"] if r["class"] == want] + print(json.dumps(payload, indent=2)) + return 0 + + total = len(rows) or 1 + print(f"{repo}: {len(rows)} open") + for c in CLASSES: + v = counts.get(c, 0) + print(f" {v:5d} {c:<11s} ({100 * v / total:.0f}%)") + + for c in ([want] if want else ["actionable", "blocked"]): + sel = sorted((r for r in rows if r["class"] == c), key=lambda r: r["number"]) + print(f"\n{c} ({len(sel)}):") + for r in sel: + print(f' #{r["number"]:<6d} {r["createdAt"][:10]} {r["title"][:88]}') + print(f' {r["reason"]}') + + print("\nNOTE: a composition estimate from titles, labels and body prefixes -- not a") + print(" verdict on any single issue. This tool never closes anything, and") + print(" nothing here licenses a bulk action. `blocked` is tested before") + print(" `actionable` on purpose, so no blocked item is advertised as available.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) From b1884f9566ab72b0a8d646d1a8c525c0b48eb493 Mon Sep 17 00:00:00 2001 From: gHashTag Date: Fri, 14 Aug 2026 19:46:13 +0000 Subject: [PATCH 2/2] corpus: freeze the damage, patch one class at a time, and say where one rule stops (Closes #2160) The mechanism is one character: the opening quote of the type string was replaced by '['. Substituting it back and asking whether the result is a closed string is a decision procedure, and it splits the 125 lines exactly on class boundaries -- 107 in 12 classes restorable, 18 in 3 classes truncated beyond recovery and held for a language decision rather than guessed at. Two validations per candidate, because deleting a line also makes a file parse: the field must return with a non-empty type and nothing previously present may vanish. Two measurement units, because our own first reading of six still-malformed classes (co-located destroyed lines) was checked and found false -- the cause was co-located damage from other restorable classes. Repairing those exposed a third defect that had been hidden behind them: ten files now fail on 'pub const Name(T) = struct', a parser gap on generic const struct declarations, not corpus damage. Re-run differential: 623 unchanged, 8 field-loss, 1 strict-improvement, 2 malformed-input-tradeoff, 0 unknown (was 616/13/1/4/0). All eight remaining field-loss files contain a destroyed line. field-loss is not zero, so #2151 stays undecided. Negative fixtures were tested for discriminating power, not assumed to have it: the reconstructed first signal set fires on 6 of 6, the current one on 0. No spec under specs/ is modified by anything here. --- .../fixtures/damage_classes/DC-06dafedd.t27 | 15 + .../fixtures/damage_classes/DC-546b13cd.t27 | 15 + .../fixtures/damage_classes/DC-550ec752.t27 | 15 + .../fixtures/damage_classes/DC-7247f52e.t27 | 15 + .../fixtures/damage_classes/DC-72bb7dcf.t27 | 15 + .../fixtures/damage_classes/DC-73be92fd.t27 | 15 + .../fixtures/damage_classes/DC-774471d4.t27 | 15 + .../fixtures/damage_classes/DC-801c2390.t27 | 15 + .../fixtures/damage_classes/DC-83e0cb30.t27 | 15 + .../fixtures/damage_classes/DC-ab5c5903.t27 | 15 + .../fixtures/damage_classes/DC-b8514836.t27 | 15 + .../fixtures/damage_classes/DC-c349b5d1.t27 | 15 + .../fixtures/damage_classes/DC-c580e907.t27 | 15 + .../fixtures/damage_classes/DC-d9efbc31.t27 | 15 + .../fixtures/damage_classes/DC-f918a784.t27 | 15 + .../damage_negative/neg_01_latency_bound.t27 | 7 + .../damage_negative/neg_02_match_arms.t27 | 8 + .../neg_03_multiline_array.t27 | 9 + .../damage_negative/neg_04_fn_signature.t27 | 7 + .../damage_negative/neg_05_raw_string.t27 | 6 + .../damage_negative/neg_06_intact_field.t27 | 8 + docs/NOW.md | 13 +- docs/corpus/damage_snapshot_2026-08-15.json | 3045 +++++++++++++++++ scripts/ci/loop-tools-tracked.sh | 4 +- scripts/tri_loop/damage_freeze.py | 155 + scripts/tri_loop/damage_repair.py | 449 +++ 26 files changed, 3934 insertions(+), 2 deletions(-) create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-06dafedd.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-546b13cd.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-550ec752.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-7247f52e.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-72bb7dcf.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-73be92fd.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-774471d4.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-801c2390.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-83e0cb30.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-ab5c5903.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-b8514836.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-c349b5d1.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-c580e907.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-d9efbc31.t27 create mode 100644 bootstrap/tests/fixtures/damage_classes/DC-f918a784.t27 create mode 100644 bootstrap/tests/fixtures/damage_negative/neg_01_latency_bound.t27 create mode 100644 bootstrap/tests/fixtures/damage_negative/neg_02_match_arms.t27 create mode 100644 bootstrap/tests/fixtures/damage_negative/neg_03_multiline_array.t27 create mode 100644 bootstrap/tests/fixtures/damage_negative/neg_04_fn_signature.t27 create mode 100644 bootstrap/tests/fixtures/damage_negative/neg_05_raw_string.t27 create mode 100644 bootstrap/tests/fixtures/damage_negative/neg_06_intact_field.t27 create mode 100644 docs/corpus/damage_snapshot_2026-08-15.json create mode 100644 scripts/tri_loop/damage_freeze.py create mode 100644 scripts/tri_loop/damage_repair.py diff --git a/bootstrap/tests/fixtures/damage_classes/DC-06dafedd.t27 b/bootstrap/tests/fixtures/damage_classes/DC-06dafedd.t27 new file mode 100644 index 0000000000..b102d2b1f3 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-06dafedd.t27 @@ -0,0 +1,15 @@ +// damage class DC-06dafedd +// shape: [[]?X", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/graph/dijkstra.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]?Usize", + + +// expected: parse-restored + +pub const DamageCase = struct { + parent : [[]?Usize", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-546b13cd.t27 b/bootstrap/tests/fixtures/damage_classes/DC-546b13cd.t27 new file mode 100644 index 0000000000..012c31775d --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-546b13cd.t27 @@ -0,0 +1,15 @@ +// damage class DC-546b13cd +// shape: [[]?*X", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/trees/b_tree.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]?*BTreeNode", + + +// expected: parse-restored + +pub const DamageCase = struct { + children : [[]?*BTreeNode", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-550ec752.t27 b/bootstrap/tests/fixtures/damage_classes/DC-550ec752.t27 new file mode 100644 index 0000000000..599ece469d --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-550ec752.t27 @@ -0,0 +1,15 @@ +// damage class DC-550ec752 +// shape: [X.X([]X X)", +// lines: 2 in the corpus, across 2 file(s) +// origin: specs/tri/encoding/html.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "std.StringHashMap([]Const u8)", + + +// expected: parse-restored + +pub const DamageCase = struct { + attributes : [std.StringHashMap([]Const u8)", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-7247f52e.t27 b/bootstrap/tests/fixtures/damage_classes/DC-7247f52e.t27 new file mode 100644 index 0000000000..87c6aacf04 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-7247f52e.t27 @@ -0,0 +1,15 @@ +// damage class DC-7247f52e +// shape: [[9]X", +// lines: 5 in the corpus, across 4 file(s) +// origin: specs/tri/crypto/hmac.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[64]U8", + + +// expected: parse-restored + +pub const DamageCase = struct { + opad : [[64]U8", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-72bb7dcf.t27 b/bootstrap/tests/fixtures/damage_classes/DC-72bb7dcf.t27 new file mode 100644 index 0000000000..8dd919ec6e --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-72bb7dcf.t27 @@ -0,0 +1,15 @@ +// damage class DC-72bb7dcf +// shape: [[]X [, +// lines: 10 in the corpus, across 3 file(s) +// origin: specs/tri/pipeline/spec_writer.t27:23 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: NONE -- substituting the delimiter yields '"[]Const [,', +// which is not a closed string. The type text was truncated as well, +// so the element type is gone and a repair would have to invent it. +// expected: needs-human-language-decision + +pub const DamageCase = struct { + steps : [[]Const [, +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-73be92fd.t27 b/bootstrap/tests/fixtures/damage_classes/DC-73be92fd.t27 new file mode 100644 index 0000000000..a9dbe1bab8 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-73be92fd.t27 @@ -0,0 +1,15 @@ +// damage class DC-73be92fd +// shape: [[]X(X, X)", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/collections/btree.t27:21 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]BTreeNode(K, V)", + + +// expected: parse-restored + +pub const DamageCase = struct { + children : [[]BTreeNode(K, V)", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-774471d4.t27 b/bootstrap/tests/fixtures/damage_classes/DC-774471d4.t27 new file mode 100644 index 0000000000..eb6f6d7cdf --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-774471d4.t27 @@ -0,0 +1,15 @@ +// damage class DC-774471d4 +// shape: [?[]X X", +// lines: 3 in the corpus, across 3 file(s) +// origin: specs/tri/pipeline/spec_writer.t27:36 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "?[]Const u8", + + +// expected: parse-restored + +pub const DamageCase = struct { + error_msg : [?[]Const u8", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-801c2390.t27 b/bootstrap/tests/fixtures/damage_classes/DC-801c2390.t27 new file mode 100644 index 0000000000..6a46376562 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-801c2390.t27 @@ -0,0 +1,15 @@ +// damage class DC-801c2390 +// shape: [[][9, +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/trees/quadtree.t27:23 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: NONE -- substituting the delimiter yields '"[][2,', +// which is not a closed string. The type text was truncated as well, +// so the element type is gone and a repair would have to invent it. +// expected: needs-human-language-decision + +pub const DamageCase = struct { + points : [[][2, +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-83e0cb30.t27 b/bootstrap/tests/fixtures/damage_classes/DC-83e0cb30.t27 new file mode 100644 index 0000000000..e0d03cd209 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-83e0cb30.t27 @@ -0,0 +1,15 @@ +// damage class DC-83e0cb30 +// shape: [[][, +// lines: 7 in the corpus, across 6 file(s) +// origin: specs/tri/encoding/mime.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: NONE -- substituting the delimiter yields '"[][,', +// which is not a closed string. The type text was truncated as well, +// so the element type is gone and a repair would have to invent it. +// expected: needs-human-language-decision + +pub const DamageCase = struct { + to : [[][, +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-ab5c5903.t27 b/bootstrap/tests/fixtures/damage_classes/DC-ab5c5903.t27 new file mode 100644 index 0000000000..dfe33d42c4 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-ab5c5903.t27 @@ -0,0 +1,15 @@ +// damage class DC-ab5c5903 +// shape: [[]?X(X)", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/collections/skip_list.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]?SkipNode(T)", + + +// expected: parse-restored + +pub const DamageCase = struct { + forward : [[]?SkipNode(T)", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-b8514836.t27 b/bootstrap/tests/fixtures/damage_classes/DC-b8514836.t27 new file mode 100644 index 0000000000..1cdbcdc65e --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-b8514836.t27 @@ -0,0 +1,15 @@ +// damage class DC-b8514836 +// shape: [[]X X", +// lines: 37 in the corpus, across 17 file(s) +// origin: specs/tri/pipeline/spec_writer.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]Const u8", + + +// expected: parse-restored + +pub const DamageCase = struct { + field_type : [[]Const u8", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-c349b5d1.t27 b/bootstrap/tests/fixtures/damage_classes/DC-c349b5d1.t27 new file mode 100644 index 0000000000..10847cfa82 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-c349b5d1.t27 @@ -0,0 +1,15 @@ +// damage class DC-c349b5d1 +// shape: [[9]?X", +// lines: 2 in the corpus, across 2 file(s) +// origin: specs/tri/trees/octree.t27:24 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[8]?OctNode", + + +// expected: parse-restored + +pub const DamageCase = struct { + children : [[8]?OctNode", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-c580e907.t27 b/bootstrap/tests/fixtures/damage_classes/DC-c580e907.t27 new file mode 100644 index 0000000000..6250e87dcd --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-c580e907.t27 @@ -0,0 +1,15 @@ +// damage class DC-c580e907 +// shape: [X.X(X, []X)", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/graph/graph.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "std.HashMap(T, []T)", + + +// expected: parse-restored + +pub const DamageCase = struct { + nodes : [std.HashMap(T, []T)", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-d9efbc31.t27 b/bootstrap/tests/fixtures/damage_classes/DC-d9efbc31.t27 new file mode 100644 index 0000000000..7c471a5ad6 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-d9efbc31.t27 @@ -0,0 +1,15 @@ +// damage class DC-d9efbc31 +// shape: [[]X", +// lines: 52 in the corpus, across 44 file(s) +// origin: specs/tri/pipeline/builder.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]T", + + +// expected: parse-restored + +pub const DamageCase = struct { + items : [[]T", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-f918a784.t27 b/bootstrap/tests/fixtures/damage_classes/DC-f918a784.t27 new file mode 100644 index 0000000000..0446698856 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-f918a784.t27 @@ -0,0 +1,15 @@ +// damage class DC-f918a784 +// shape: [[9]?*X", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/search/aho_corasick.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[256]?*ACTrieNode", + + +// expected: parse-restored + +pub const DamageCase = struct { + children : [[256]?*ACTrieNode", +}; diff --git a/bootstrap/tests/fixtures/damage_negative/neg_01_latency_bound.t27 b/bootstrap/tests/fixtures/damage_negative/neg_01_latency_bound.t27 new file mode 100644 index 0000000000..82cc908456 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_01_latency_bound.t27 @@ -0,0 +1,7 @@ +// NEGATIVE fixture: a latency bound, not damage. +// The first regex reported 230 of these as damaged, which made 429 a metric of +// the regex rather than of the corpus. tri damage must report zero here. +pub const Budget = struct { + target : < 5000ns, + ceiling : < 12us, +}; diff --git a/bootstrap/tests/fixtures/damage_negative/neg_02_match_arms.t27 b/bootstrap/tests/fixtures/damage_negative/neg_02_match_arms.t27 new file mode 100644 index 0000000000..854b93ffb4 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_02_match_arms.t27 @@ -0,0 +1,8 @@ +// NEGATIVE fixture: match arms. 17 false positives came from these. +fn classify(x: u8) -> u8 { + match x { + 0 : 1, + 1 : 2, + _ : 0, + } +} diff --git a/bootstrap/tests/fixtures/damage_negative/neg_03_multiline_array.t27 b/bootstrap/tests/fixtures/damage_negative/neg_03_multiline_array.t27 new file mode 100644 index 0000000000..870c3b80b5 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_03_multiline_array.t27 @@ -0,0 +1,9 @@ +// NEGATIVE fixture: an array literal opening a multi-line value. 10 false +// positives came from the opening line being read as a damaged type side. +pub const Table = struct { + rows : "[]u8", +}; +pub const DATA = [ + 1, 2, 3, + 4, 5, 6, +]; diff --git a/bootstrap/tests/fixtures/damage_negative/neg_04_fn_signature.t27 b/bootstrap/tests/fixtures/damage_negative/neg_04_fn_signature.t27 new file mode 100644 index 0000000000..59fa90449c --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_04_fn_signature.t27 @@ -0,0 +1,7 @@ +// NEGATIVE fixture: function signatures. 6 false positives came from these. +fn insert(key: []const u8, value: u32) -> bool { + return true; +} +fn find(needle: []const u8) -> ?usize { + return null; +} diff --git a/bootstrap/tests/fixtures/damage_negative/neg_05_raw_string.t27 b/bootstrap/tests/fixtures/damage_negative/neg_05_raw_string.t27 new file mode 100644 index 0000000000..741cc33ece --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_05_raw_string.t27 @@ -0,0 +1,6 @@ +// NEGATIVE fixture: a legal raw string whose body contains a bracket and an odd +// number of quotes. The odd-quote signal must not fire inside a raw string. +pub const Patterns = struct { + rx : r#"[a-z]+"[0-9]"#, + tpl : "[]u8", +}; diff --git a/bootstrap/tests/fixtures/damage_negative/neg_06_intact_field.t27 b/bootstrap/tests/fixtures/damage_negative/neg_06_intact_field.t27 new file mode 100644 index 0000000000..5e7108cf39 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_06_intact_field.t27 @@ -0,0 +1,8 @@ +// NEGATIVE fixture: the intact convention itself, including the slice and +// optional-slice forms that the damaged classes are corruptions of. +pub const Intact = struct { + children : "[4]?QuadNode", + parts : "[]Const u8", + name : "[]u8", + level : "usize", +}; diff --git a/docs/NOW.md b/docs/NOW.md index e2fcbab53a..745de66b01 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -1,7 +1,18 @@ -# NOW -- the loop tools are in the repository, and the differential now names the loss (2026-08-15) +# NOW -- the corpus damage is frozen, classed, and one rule's reach is measured (2026-08-15) Last updated: 2026-08-15 +## corpus: freeze the damage, patch one class at a time, and say where one rule stops (Closes #2160) + +- **The damage mechanism is fully determined, and it is one character.** An intact field in this corpus is `name : "TypeText",`. Every damaged line has the same single defect: the opening quote of the type string was replaced by `[`, so `children : "[4]?QuadNode",` became `children : [[4]?QuadNode",`. That one fact explains both signals `tri damage` detects -- the doubled bracket and the odd quote count -- and it makes the candidate patch a single substitution at a known offset whose inverse is itself +- **The split between repairable and destroyed is a decision procedure, not a judgement.** Substitute the delimiter and ask whether the result is a closed string. For 107 lines in 12 classes it is: every character of the type text survived and the repair only restores the delimiter. For 18 lines in 3 classes it is not, because the type text was TRUNCATED as well -- `[[]Const [,` becomes `"[]Const [,` and the element type is simply gone. `[]Const []Const u8` and `[]Const [N]u8` are both plausible readings and the file carries no evidence for either. Those 18 get no patch and are held as `needs-human-language-decision`; auto-repairing them would be a guess dressed as a fix, and unfalsifiable afterwards because the original is unrecoverable +- **Parsing is not evidence of repair, so every candidate is validated twice.** Deleting the offending line also makes a file parse. The second check requires the specific field to be back with a non-empty type AND no previously-present field to have vanished. Only then is the effect `parse-restored` +- **A hypothesis of ours was checked and found false, which is why there are two measurement units.** The per-class run left six classes `still-malformed`, and the first reading was co-located unrestorable damage. Not one of those seven files contains a destroyed line. The actual cause was co-located damage from OTHER restorable classes, which a single-class run leaves in place. The `--combined` mode repairs every restorable row per file and reports 49 files `parse-restored`, 11 `still-malformed`, 2 `ambiguous`, 3 `needs-human-language-decision`. Per-class answers whether one rule suffices for a shape; per-file answers what the rule achieves. Neither number replaces the other +- **Repairing two defects exposed a third that had been hidden behind them.** Ten of the eleven remaining `still-malformed` files now fail at `pub const Name(T) = struct {` with `Unexpected token in expression: KwStruct` -- a parser gap on generic const struct declarations, not corpus damage. It was unobservable while those files failed earlier for a different reason. The eleventh, `bitset.t27`, fails at `Expected LParen, got KwTest` and is separate and unclassified +- **The re-run differential improves but does not clear, and the residue is explained.** Over the repaired corpus: 623 `unchanged`, 8 `field-loss`, 1 `strict-improvement`, 2 `malformed-input-tradeoff`, 0 `unknown`, against 616 / 13 / 1 / 4 / 0 before. All eight remaining `field-loss` files contain a destroyed line -- eight of eight, an exact correlation. Because `field-loss` is not zero, the gate on #2151 is not met and #2151 stays undecided +- **The negative fixtures were tested for discriminating power rather than assumed to have it.** Six fixtures pin the false signals that once made a damage count a metric of its own regex: latency bounds of the form `target : < 5000ns`, match arms, multi-line array literals, function signatures, raw strings, and the intact convention itself. The reconstructed first signal set fires on six of six; the current one fires on none. On the real corpus that naive set reports 1378 lines where the current one reports 125 +- **Not claimed:** that 125 is all the damage. `tri damage` checks two textual signals, so damage that produces a balanced, quote-even, plausible-looking wrong type is invisible to it, and nothing here bounds how much of that exists. Nothing is claimed about performance either: 63 unparsed files are still excluded from `cost` and the full-corpus hang is undiagnosed + ## tooling: restore tri cost and tri diffbin, and stop aggregating field loss to zero (Closes #2158) - **Two measurement tools were lost, and with them every number they had produced.** `scripts/tri_loop/cost.py` and `diffbin.py` were written, quoted in a pull request, and never committed; the working copy was later re-cloned. Six recovery routes came back empty: dangling git objects held only a `git stash` WIP with `triage.py`, the reflog records the clone rather than the content, shell history is absent, CI artifacts hold only FPGA outputs, no PR or issue comment carries the source, and the session snapshot preserves prose about the scripts instead of the scripts. They are reimplemented here from a written contract, not reconstructed from memory diff --git a/docs/corpus/damage_snapshot_2026-08-15.json b/docs/corpus/damage_snapshot_2026-08-15.json new file mode 100644 index 0000000000..308506cdf8 --- /dev/null +++ b/docs/corpus/damage_snapshot_2026-08-15.json @@ -0,0 +1,3045 @@ +{ + "class_index": [ + { + "class_id": "DC-d9efbc31", + "count": 52, + "files": [ + "specs/tri/agent/faculty_board.t27", + "specs/tri/collections/bitmap.t27", + "specs/tri/collections/bitset.t27", + "specs/tri/collections/bitvector.t27", + "specs/tri/collections/btree.t27", + "specs/tri/collections/circular_buffer.t27", + "specs/tri/collections/deque.t27", + "specs/tri/collections/interval.t27", + "specs/tri/collections/lru.t27", + "specs/tri/collections/map.t27", + "specs/tri/collections/priority_queue.t27", + "specs/tri/collections/queue.t27", + "specs/tri/collections/ring_buffer.t27", + "specs/tri/collections/stack.t27", + "specs/tri/crypto/crypto.t27", + "specs/tri/encoding/csv.t27", + "specs/tri/encoding/html.t27", + "specs/tri/encoding/json.t27", + "specs/tri/encoding/markup.t27", + "specs/tri/encoding/msgpack.t27", + "specs/tri/encoding/xml.t27", + "specs/tri/graph/dijkstra.t27", + "specs/tri/graph/disjoint_set.t27", + "specs/tri/graph/graph.t27", + "specs/tri/graph/graph_bfs.t27", + "specs/tri/graph/graph_dfs.t27", + "specs/tri/graph/prims_mst.t27", + "specs/tri/graph/topological_sort.t27", + "specs/tri/io/compress.t27", + "specs/tri/math/bezier.t27", + "specs/tri/math/matrix.t27", + "specs/tri/math/polynomial.t27", + "specs/tri/pipeline/builder.t27", + "specs/tri/search/bloom_filter.t27", + "specs/tri/search/knuth_morris_pratt.t27", + "specs/tri/trees/b_tree.t27", + "specs/tri/trees/fenwick_tree.t27", + "specs/tri/trees/kd_tree.t27", + "specs/tri/trees/rtree.t27", + "specs/tri/trees/segment_tree.t27", + "specs/tri/trees/suffix_array.t27", + "specs/tri/utils/bytes.t27", + "specs/tri/utils/logger.t27", + "specs/tri/utils/template.t27" + ], + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "count": 37, + "files": [ + "specs/tri/agent/eternal_monitor.t27", + "specs/tri/collections/variant.t27", + "specs/tri/crypto/base32.t27", + "specs/tri/crypto/base64.t27", + "specs/tri/encoding/html.t27", + "specs/tri/encoding/markup.t27", + "specs/tri/encoding/mime.t27", + "specs/tri/encoding/msgpack.t27", + "specs/tri/encoding/xml.t27", + "specs/tri/net/url.t27", + "specs/tri/pipeline/spec_writer.t27", + "specs/tri/pipeline/workflow.t27", + "specs/tri/search/knuth_morris_pratt.t27", + "specs/tri/search/regex.t27", + "specs/tri/utils/logger.t27", + "specs/tri/utils/template.t27", + "specs/tri/utils/version.t27" + ], + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "shape": "[[]X X\"," + }, + { + "class_id": "DC-72bb7dcf", + "count": 10, + "files": [ + "specs/tri/agent/handoff.t27", + "specs/tri/pipeline/spec_writer.t27", + "specs/tri/pipeline/workflow.t27" + ], + "reasons": [ + "doubled-bracket" + ], + "shape": "[[]X [," + }, + { + "class_id": "DC-83e0cb30", + "count": 7, + "files": [ + "specs/tri/encoding/mime.t27", + "specs/tri/graph/graph_bfs.t27", + "specs/tri/io/fs.t27", + "specs/tri/search/aho_corasick.t27", + "specs/tri/search/regex.t27", + "specs/tri/search/regex_advanced.t27" + ], + "reasons": [ + "doubled-bracket" + ], + "shape": "[[][," + }, + { + "class_id": "DC-7247f52e", + "count": 5, + "files": [ + "specs/tri/crypto/hmac.t27", + "specs/tri/crypto/sha256.t27", + "specs/tri/search/boyer_moore.t27", + "specs/tri/utils/utf8.t27" + ], + "reasons": [ + "odd-quote" + ], + "shape": "[[9]X\"," + }, + { + "class_id": "DC-774471d4", + "count": 3, + "files": [ + "specs/tri/agent/eternal_monitor.t27", + "specs/tri/pipeline/spec_writer.t27", + "specs/tri/pipeline/workflow.t27" + ], + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "shape": "[?[]X X\"," + }, + { + "class_id": "DC-550ec752", + "count": 2, + "files": [ + "specs/tri/encoding/html.t27", + "specs/tri/encoding/xml.t27" + ], + "reasons": [ + "odd-quote" + ], + "shape": "[X.X([]X X)\"," + }, + { + "class_id": "DC-c349b5d1", + "count": 2, + "files": [ + "specs/tri/trees/octree.t27", + "specs/tri/trees/quadtree.t27" + ], + "reasons": [ + "odd-quote" + ], + "shape": "[[9]?X\"," + }, + { + "class_id": "DC-06dafedd", + "count": 1, + "files": [ + "specs/tri/graph/dijkstra.t27" + ], + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "shape": "[[]?X\"," + }, + { + "class_id": "DC-546b13cd", + "count": 1, + "files": [ + "specs/tri/trees/b_tree.t27" + ], + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "shape": "[[]?*X\"," + }, + { + "class_id": "DC-73be92fd", + "count": 1, + "files": [ + "specs/tri/collections/btree.t27" + ], + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "shape": "[[]X(X, X)\"," + }, + { + "class_id": "DC-801c2390", + "count": 1, + "files": [ + "specs/tri/trees/quadtree.t27" + ], + "reasons": [ + "doubled-bracket" + ], + "shape": "[[][9," + }, + { + "class_id": "DC-ab5c5903", + "count": 1, + "files": [ + "specs/tri/collections/skip_list.t27" + ], + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "shape": "[[]?X(X)\"," + }, + { + "class_id": "DC-c580e907", + "count": 1, + "files": [ + "specs/tri/graph/graph.t27" + ], + "reasons": [ + "odd-quote" + ], + "shape": "[X.X(X, []X)\"," + }, + { + "class_id": "DC-f918a784", + "count": 1, + "files": [ + "specs/tri/search/aho_corasick.t27" + ], + "reasons": [ + "odd-quote" + ], + "shape": "[[9]?*X\"," + } + ], + "classes": 15, + "corpus": "specs", + "corpus_sha256": "1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9", + "file_sha256": { + "specs/tri/agent/eternal_monitor.t27": "b5afb6170a5befb94ac33558fa795f236dd6075d9e05f6a0dd2e673d05aba069", + "specs/tri/agent/faculty_board.t27": "9e12c1f5268143af7f4026346e22ae9fcd63218aefbebbfe503316e9e295c32e", + "specs/tri/agent/handoff.t27": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "specs/tri/collections/bitmap.t27": "afd5d67ecd9d8c1810b896e0d522f3ef6fbbad00717a26b19edbf7d3b766e31d", + "specs/tri/collections/bitset.t27": "f6e28351c3de17a876cbe077207413866dafddb5b1251c5e6b54f1ed9d3bcb11", + "specs/tri/collections/bitvector.t27": "435342ddd91e9a68767defaeab6d9b67279c50dd763056187d89210081b4026e", + "specs/tri/collections/btree.t27": "0539a5ff094636b4c10db13d2246f53c1bf80f62a74e468107969e6f9f28020c", + "specs/tri/collections/circular_buffer.t27": "089c5a092639f361684384a25291fbecdee0b3ba5476d25cd1fc62c2456fabd5", + "specs/tri/collections/deque.t27": "57170d1f89805ebf6a9516326884d6d969770839fe4bc1c39193fbc4d0c50a07", + "specs/tri/collections/interval.t27": "b259aff814f1812bbee828b05567a71fdd57c7be838d4a85bd092b7b7c5a9e40", + "specs/tri/collections/lru.t27": "00755caba914623ad212f422ad2225f2de28144d0bb190788580e14ee87959ab", + "specs/tri/collections/map.t27": "7e0fdae860e6349fdb6e2951b1d7588b0861822f7675c669996d061899c95127", + "specs/tri/collections/priority_queue.t27": "9bab098fcc7065874c346143e85bec43b9966b0669b6647871e573c7a90ed6c9", + "specs/tri/collections/queue.t27": "3175c9ed4dcef7978d7d871df08836c40c88965cb9c45b83d6d9c53fa8a9d463", + "specs/tri/collections/ring_buffer.t27": "87c586fbef0a7b22b2f6e5f6741a345695bfd9ca5e36a7e510561632b37b7348", + "specs/tri/collections/skip_list.t27": "3cfdb7e154b5a0ec8d0a825f265b1f5e165af4ae3c19b5c499efc9f5a61a170b", + "specs/tri/collections/stack.t27": "d9c36841edfb5e75ee74a6e948fccf724656ad0d6bebc169bddd6dc3fbdc7f65", + "specs/tri/collections/variant.t27": "1280493ead77ca373c85b07e35b10a460fb7ad6f387b0820d9925ac38d4f1167", + "specs/tri/crypto/base32.t27": "088cbb8893f0019cef2ff3626aef421d06417126099cd17051ad591cff0d9c0d", + "specs/tri/crypto/base64.t27": "570efb578996b026b3572669478b17ce90e70aee9b0f304f2fce1a5f241ad84e", + "specs/tri/crypto/crypto.t27": "6b5d36b7cfc6f68858a34b7d78bfe043b0e9693f8ba3cf4ef4229781f2db521b", + "specs/tri/crypto/hmac.t27": "357ef72e7b4d971cc3711e664fe4fded7b6ab4514247831507baa0641e3d1eed", + "specs/tri/crypto/sha256.t27": "6433411437aec985707e377addd8284b33da7093824baba08d0ffaf31c3b14d8", + "specs/tri/encoding/csv.t27": "42da022525b9f341740cd36a50413176069c2afb5c31e63f4dcacf06fd6f400f", + "specs/tri/encoding/html.t27": "d483f9ea30569515431de6016f687dfae767aa8b47c211ed8910e46a7aa004f6", + "specs/tri/encoding/json.t27": "4d0a9fa0e6ec79369479b8add40308d46b32a438d2f255858abe8687ecb51574", + "specs/tri/encoding/markup.t27": "f60ffa66f95a923d551adbf6f40ef3fa81a2179d81e1e8d76fb9202dc1f8f8f4", + "specs/tri/encoding/mime.t27": "c273019708b1d133a70c19e70461253c641fc756ab623112a7071d6bf030be87", + "specs/tri/encoding/msgpack.t27": "3c5439b5f1e8cc7f55ea6684770f6e2213008609c07390f017a2fae37a5611e4", + "specs/tri/encoding/xml.t27": "1243df7ee550f3e946c9a36ddd98d797c479de0b7f15b10c1aa4aeb2902cb4b1", + "specs/tri/graph/dijkstra.t27": "44044c9787eb731200c48f4bdcd52daf48854b92a7273fba511aa5316ee88ad0", + "specs/tri/graph/disjoint_set.t27": "8d9a61646cad919fc95990648024ba90faa9ac2cd644cffc180bbe9222626a31", + "specs/tri/graph/graph.t27": "8f72ef2f6711c80cfc323ea331747896aa0fb996af8ed2d8585a65fc45619d82", + "specs/tri/graph/graph_bfs.t27": "5d0df89b1a1bde5f8d5fa4733b3ad09250d5600bd1d0d8f9c31966a2f1db3827", + "specs/tri/graph/graph_dfs.t27": "09b5968ba0895ab7842bfce9087e1733c4a0e4cef7d02ba04021aea8e776434e", + "specs/tri/graph/prims_mst.t27": "642d11214c510192057a915200d03aa793e184955d215b1c8a0152b627e1f8da", + "specs/tri/graph/topological_sort.t27": "618fcd45d00e8ffa4efbd5d1be35ae3a641b317f50f6344407e918064fa7f7bc", + "specs/tri/io/compress.t27": "9b0ebfbac8cbf32eb4c63fa1d8744577ec72d6f46fbfb0cd9dd369f4e44a1fd9", + "specs/tri/io/fs.t27": "c3a47ae15cf04f1ca3fb372da482d3fff460b1e2b6b544ede2d15f87c0ac6402", + "specs/tri/math/bezier.t27": "4f0ae1c180bc369bd06c7f4535ef6a201c37798184ff864bfef6ef436ee95562", + "specs/tri/math/matrix.t27": "d7cfa970509a60bdda062871dcd0abf5880616d6688656c59706fddc9e92c7fd", + "specs/tri/math/polynomial.t27": "12f3f821850c7aa6ffb4eb00599f8f0adeb53f0017808197e363d8a71014a288", + "specs/tri/net/url.t27": "6752afaf3b5a4c3b07b5cffcb548b67ae1681ffc5f1a4f9abddf97b509139aee", + "specs/tri/pipeline/builder.t27": "18d79f751cfe6da5030c5dad9c7c58c1313c1e9074355cceb2cf5398f69e71cf", + "specs/tri/pipeline/spec_writer.t27": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "specs/tri/pipeline/workflow.t27": "80b4e2cfda9e06a964846f8f622c9d752c54eb88d4a7d0cb44bd8f287d3d9c41", + "specs/tri/search/aho_corasick.t27": "366ade2de4cb17215a51999e65e62e5799de582a4438b2aec2ebd2763747dd3f", + "specs/tri/search/bloom_filter.t27": "e88d868bf244cdbf0a0312f6afcdd769f8cb119851c811e8fe4131db2f58b776", + "specs/tri/search/boyer_moore.t27": "baf22f7136a669ca3528ffa121bfb755ce02221a4ede00c7811e49500542dfbb", + "specs/tri/search/knuth_morris_pratt.t27": "fa851daf9a4c4b0e8cfb4a215dc741378e0756711cb98986e2f2c8000266a4e8", + "specs/tri/search/regex.t27": "4e0b80ad075dac6274040a5327d17244dc1e1206368598cccc5883d96de30ccb", + "specs/tri/search/regex_advanced.t27": "e8cdd8c7cc567ec42e6d72b5264bcabc59a1460a07e8957c36cdcf6f14edf837", + "specs/tri/trees/b_tree.t27": "421fe9ec4300007ac244ce16b0b57ef39b3401e494e08082defe73bb797997bc", + "specs/tri/trees/fenwick_tree.t27": "689d3e8dfffa09bc98609a75404dcfcb83cecbc18087b05b8a5a94e0bfe77ae0", + "specs/tri/trees/kd_tree.t27": "ac3052b6c5dab2c3ee103ce3f0cc6941865cd766c1fd12a1c4cc712c283b7c32", + "specs/tri/trees/octree.t27": "d2cf6df1b58bd432a6a45baf815781216cfad662bbbbb64e090248d8d00c80f3", + "specs/tri/trees/quadtree.t27": "0b9a0c9e250bda8456bac8e3609bd8a3294e0d902dad9c91dc10fd569f554b03", + "specs/tri/trees/rtree.t27": "ff560d230e72de0accc0dfc8ed0cfb464934225c77ed13e6cae4aa0ec303e68e", + "specs/tri/trees/segment_tree.t27": "dd8ebf2929c0c7d105676201c2afd5fac1315064085a50029c8472d3436934d8", + "specs/tri/trees/suffix_array.t27": "f9a84a52ce9586dd5e0ba1bc9a768a0e942871ab1cc63b61712304e9c48dfc55", + "specs/tri/utils/bytes.t27": "53bbef23b5864bc8ac792977e15c4f55f1c4db021acdb360b2c3a5005bdd5a60", + "specs/tri/utils/logger.t27": "58c449a8aac8f2fbae73fa7c67532d9f4cded68e794aa154862031f3e2893da1", + "specs/tri/utils/template.t27": "d0d82d46597a79fe8374c729404485f5c7933868c035726234c1d3d8cbcd22d2", + "specs/tri/utils/utf8.t27": "f478a091aadf9aa5cb78ce6aef9f4b1847c734db395a74151cab5f69b864b4b1", + "specs/tri/utils/version.t27": "18088d7e682e849ca9b7fe9827bc5d7927ab4f0a2332833c1999f4fec193caff" + }, + "files": 65, + "lines": 125, + "rows": [ + { + "class_id": "DC-d9efbc31", + "context_after": [ + " capacity : usize,", + " len : usize," + ], + "context_before": [ + "", + " pub const Builder(T) = struct {" + ], + "field": "items", + "file": "specs/tri/pipeline/builder.t27", + "file_sha256": "18d79f751cfe6da5030c5dad9c7c58c1313c1e9074355cceb2cf5398f69e71cf", + "line": 14, + "raw_line": " items : [[]T\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]T\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + "", + " pub const SpecField = struct {" + ], + "field": "field_type", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 14, + "raw_line": " field_type : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " output : [[]Const u8\",", + " steps : [[]Const [," + ], + "context_before": [ + "", + " pub const SpecBehavior = struct {" + ], + "field": "inputs", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 21, + "raw_line": " inputs : [[]Const SpecField\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const SpecField\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " steps : [[]Const [,", + " };" + ], + "context_before": [ + " pub const SpecBehavior = struct {", + " inputs : [[]Const SpecField\"," + ], + "field": "output", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 22, + "raw_line": " output : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " };", + "" + ], + "context_before": [ + " inputs : [[]Const SpecField\",", + " output : [[]Const u8\"," + ], + "field": "steps", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 23, + "raw_line": " steps : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " types : [[]Const SpecType\",", + " behaviors : [[]Const SpecBehavior\"," + ], + "context_before": [ + "", + " pub const SpecTemplate = struct {" + ], + "field": "module_name", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 27, + "raw_line": " module_name : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " behaviors : [[]Const SpecBehavior\",", + " };" + ], + "context_before": [ + " pub const SpecTemplate = struct {", + " module_name : [[]Const u8\"," + ], + "field": "types", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 28, + "raw_line": " types : [[]Const SpecType\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const SpecType\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " module_name : [[]Const u8\",", + " types : [[]Const SpecType\"," + ], + "field": "behaviors", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 29, + "raw_line": " behaviors : [[]Const SpecBehavior\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const SpecBehavior\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " generated_path : [[]Const u8\",", + " compile_ok : \"bool\"," + ], + "context_before": [ + "", + " pub const WriteResult = struct {" + ], + "field": "spec_path", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 33, + "raw_line": " spec_path : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " compile_ok : \"bool\",", + " error_msg : [?[]Const u8\"," + ], + "context_before": [ + " pub const WriteResult = struct {", + " spec_path : [[]Const u8\"," + ], + "field": "generated_path", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 34, + "raw_line": " generated_path : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-774471d4", + "context_after": [ + " };", + "" + ], + "context_before": [ + " generated_path : [[]Const u8\",", + " compile_ok : \"bool\"," + ], + "field": "error_msg", + "file": "specs/tri/pipeline/spec_writer.t27", + "file_sha256": "2e59db33b002aaef259e0a82e2f23b36995c7774acc5026d120de09c6e963416", + "line": 36, + "raw_line": " error_msg : [?[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[?[]Const u8\",", + "shape": "[?[]X X\"," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " condition : [?[]Const u8\",", + " };" + ], + "context_before": [ + " pub const WorkflowStep = struct {", + " command : String," + ], + "field": "depends_on", + "file": "specs/tri/pipeline/workflow.t27", + "file_sha256": "80b4e2cfda9e06a964846f8f622c9d752c54eb88d4a7d0cb44bd8f287d3d9c41", + "line": 15, + "raw_line": " depends_on : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-774471d4", + "context_after": [ + " };", + "" + ], + "context_before": [ + " command : String,", + " depends_on : [[]Const [," + ], + "field": "condition", + "file": "specs/tri/pipeline/workflow.t27", + "file_sha256": "80b4e2cfda9e06a964846f8f622c9d752c54eb88d4a7d0cb44bd8f287d3d9c41", + "line": 16, + "raw_line": " condition : [?[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[?[]Const u8\",", + "shape": "[?[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " pub const Workflow = struct {", + " version : String," + ], + "field": "steps", + "file": "specs/tri/pipeline/workflow.t27", + "file_sha256": "80b4e2cfda9e06a964846f8f622c9d752c54eb88d4a7d0cb44bd8f287d3d9c41", + "line": 21, + "raw_line": " steps : [[]Const WorkflowStep\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const WorkflowStep\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " rows : [[]CsvRow\",", + " delimiter : \"u8\"," + ], + "context_before": [ + "", + " pub const CsvDocument = struct {" + ], + "field": "headers", + "file": "specs/tri/encoding/csv.t27", + "file_sha256": "42da022525b9f341740cd36a50413176069c2afb5c31e63f4dcacf06fd6f400f", + "line": 17, + "raw_line": " headers : [[]CsvRow\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]CsvRow\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " delimiter : \"u8\",", + " };" + ], + "context_before": [ + " pub const CsvDocument = struct {", + " headers : [[]CsvRow\"," + ], + "field": "rows", + "file": "specs/tri/encoding/csv.t27", + "file_sha256": "42da022525b9f341740cd36a50413176069c2afb5c31e63f4dcacf06fd6f400f", + "line": 18, + "raw_line": " rows : [[]CsvRow\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]CsvRow\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " attributes : [std.StringHashMap([]Const u8)\",", + " children : [[]HtmlNode\"," + ], + "context_before": [ + "", + " pub const HtmlNode = struct {" + ], + "field": "tag", + "file": "specs/tri/encoding/html.t27", + "file_sha256": "d483f9ea30569515431de6016f687dfae767aa8b47c211ed8910e46a7aa004f6", + "line": 14, + "raw_line": " tag : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-550ec752", + "context_after": [ + " children : [[]HtmlNode\",", + " inner_text : [[]Const u8\"," + ], + "context_before": [ + " pub const HtmlNode = struct {", + " tag : [[]Const u8\"," + ], + "field": "attributes", + "file": "specs/tri/encoding/html.t27", + "file_sha256": "d483f9ea30569515431de6016f687dfae767aa8b47c211ed8910e46a7aa004f6", + "line": 15, + "raw_line": " attributes : [std.StringHashMap([]Const u8)\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[std.StringHashMap([]Const u8)\",", + "shape": "[X.X([]X X)\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " inner_text : [[]Const u8\",", + " };" + ], + "context_before": [ + " tag : [[]Const u8\",", + " attributes : [std.StringHashMap([]Const u8)\"," + ], + "field": "children", + "file": "specs/tri/encoding/html.t27", + "file_sha256": "d483f9ea30569515431de6016f687dfae767aa8b47c211ed8910e46a7aa004f6", + "line": 16, + "raw_line": " children : [[]HtmlNode\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]HtmlNode\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " attributes : [std.StringHashMap([]Const u8)\",", + " children : [[]HtmlNode\"," + ], + "field": "inner_text", + "file": "specs/tri/encoding/html.t27", + "file_sha256": "d483f9ea30569515431de6016f687dfae767aa8b47c211ed8910e46a7aa004f6", + "line": 17, + "raw_line": " inner_text : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + "", + " pub const JsonArray = struct {" + ], + "field": "items", + "file": "specs/tri/encoding/json.t27", + "file_sha256": "4d0a9fa0e6ec79369479b8add40308d46b32a438d2f255858abe8687ecb51574", + "line": 23, + "raw_line": " items : [[]JsonValue\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]JsonValue\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " content : [[]Const u8\",", + " children : [[]MarkdownNode\"," + ], + "context_before": [ + "", + " pub const MarkdownNode = struct {" + ], + "field": "type", + "file": "specs/tri/encoding/markup.t27", + "file_sha256": "f60ffa66f95a923d551adbf6f40ef3fa81a2179d81e1e8d76fb9202dc1f8f8f4", + "line": 14, + "raw_line": " type : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " children : [[]MarkdownNode\",", + " };" + ], + "context_before": [ + " pub const MarkdownNode = struct {", + " type : [[]Const u8\"," + ], + "field": "content", + "file": "specs/tri/encoding/markup.t27", + "file_sha256": "f60ffa66f95a923d551adbf6f40ef3fa81a2179d81e1e8d76fb9202dc1f8f8f4", + "line": 15, + "raw_line": " content : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + " type : [[]Const u8\",", + " content : [[]Const u8\"," + ], + "field": "children", + "file": "specs/tri/encoding/markup.t27", + "file_sha256": "f60ffa66f95a923d551adbf6f40ef3fa81a2179d81e1e8d76fb9202dc1f8f8f4", + "line": 16, + "raw_line": " children : [[]MarkdownNode\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]MarkdownNode\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " to : [[][,", + " subject : [[]Const u8\"," + ], + "context_before": [ + "", + " pub const Email = struct {" + ], + "field": "from", + "file": "specs/tri/encoding/mime.t27", + "file_sha256": "c273019708b1d133a70c19e70461253c641fc756ab623112a7071d6bf030be87", + "line": 14, + "raw_line": " from : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-83e0cb30", + "context_after": [ + " subject : [[]Const u8\",", + " body : [[]Const u8\"," + ], + "context_before": [ + " pub const Email = struct {", + " from : [[]Const u8\"," + ], + "field": "to", + "file": "specs/tri/encoding/mime.t27", + "file_sha256": "c273019708b1d133a70c19e70461253c641fc756ab623112a7071d6bf030be87", + "line": 15, + "raw_line": " to : [[][,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[][,", + "shape": "[[][," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " body : [[]Const u8\",", + " };" + ], + "context_before": [ + " from : [[]Const u8\",", + " to : [[][," + ], + "field": "subject", + "file": "specs/tri/encoding/mime.t27", + "file_sha256": "c273019708b1d133a70c19e70461253c641fc756ab623112a7071d6bf030be87", + "line": 16, + "raw_line": " subject : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " to : [[][,", + " subject : [[]Const u8\"," + ], + "field": "body", + "file": "specs/tri/encoding/mime.t27", + "file_sha256": "c273019708b1d133a70c19e70461253c641fc756ab623112a7071d6bf030be87", + "line": 17, + "raw_line": " body : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " bin_value : [[]Const u8\",", + " array_value : [[]MsgPackValue\"," + ], + "context_before": [ + " uint_value : \"u64\",", + " float_value : \"f64\"," + ], + "field": "str_value", + "file": "specs/tri/encoding/msgpack.t27", + "file_sha256": "3c5439b5f1e8cc7f55ea6684770f6e2213008609c07390f017a2fae37a5611e4", + "line": 22, + "raw_line": " str_value : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " array_value : [[]MsgPackValue\",", + " map_value : \"std.StringHashMap(MsgPackValue)\"," + ], + "context_before": [ + " float_value : \"f64\",", + " str_value : [[]Const u8\"," + ], + "field": "bin_value", + "file": "specs/tri/encoding/msgpack.t27", + "file_sha256": "3c5439b5f1e8cc7f55ea6684770f6e2213008609c07390f017a2fae37a5611e4", + "line": 23, + "raw_line": " bin_value : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " map_value : \"std.StringHashMap(MsgPackValue)\",", + " };" + ], + "context_before": [ + " str_value : [[]Const u8\",", + " bin_value : [[]Const u8\"," + ], + "field": "array_value", + "file": "specs/tri/encoding/msgpack.t27", + "file_sha256": "3c5439b5f1e8cc7f55ea6684770f6e2213008609c07390f017a2fae37a5611e4", + "line": 24, + "raw_line": " array_value : [[]MsgPackValue\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]MsgPackValue\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " attributes : [std.StringHashMap([]Const u8)\",", + " children : [[]XmlNode\"," + ], + "context_before": [ + "", + " pub const XmlNode = struct {" + ], + "field": "tag", + "file": "specs/tri/encoding/xml.t27", + "file_sha256": "1243df7ee550f3e946c9a36ddd98d797c479de0b7f15b10c1aa4aeb2902cb4b1", + "line": 14, + "raw_line": " tag : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-550ec752", + "context_after": [ + " children : [[]XmlNode\",", + " text : [[]Const u8\"," + ], + "context_before": [ + " pub const XmlNode = struct {", + " tag : [[]Const u8\"," + ], + "field": "attributes", + "file": "specs/tri/encoding/xml.t27", + "file_sha256": "1243df7ee550f3e946c9a36ddd98d797c479de0b7f15b10c1aa4aeb2902cb4b1", + "line": 15, + "raw_line": " attributes : [std.StringHashMap([]Const u8)\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[std.StringHashMap([]Const u8)\",", + "shape": "[X.X([]X X)\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " text : [[]Const u8\",", + " };" + ], + "context_before": [ + " tag : [[]Const u8\",", + " attributes : [std.StringHashMap([]Const u8)\"," + ], + "field": "children", + "file": "specs/tri/encoding/xml.t27", + "file_sha256": "1243df7ee550f3e946c9a36ddd98d797c479de0b7f15b10c1aa4aeb2902cb4b1", + "line": 16, + "raw_line": " children : [[]XmlNode\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]XmlNode\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " attributes : [std.StringHashMap([]Const u8)\",", + " children : [[]XmlNode\"," + ], + "field": "text", + "file": "specs/tri/encoding/xml.t27", + "file_sha256": "1243df7ee550f3e946c9a36ddd98d797c479de0b7f15b10c1aa4aeb2902cb4b1", + "line": 17, + "raw_line": " text : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " host : [[]Const u8\",", + " port : \"?u16\"," + ], + "context_before": [ + "", + " pub const Url = struct {" + ], + "field": "scheme", + "file": "specs/tri/net/url.t27", + "file_sha256": "6752afaf3b5a4c3b07b5cffcb548b67ae1681ffc5f1a4f9abddf97b509139aee", + "line": 14, + "raw_line": " scheme : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " port : \"?u16\",", + " path : [[]Const u8\"," + ], + "context_before": [ + " pub const Url = struct {", + " scheme : [[]Const u8\"," + ], + "field": "host", + "file": "specs/tri/net/url.t27", + "file_sha256": "6752afaf3b5a4c3b07b5cffcb548b67ae1681ffc5f1a4f9abddf97b509139aee", + "line": 15, + "raw_line": " host : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " query : [[]Const u8\",", + " fragment : [[]Const u8\"," + ], + "context_before": [ + " host : [[]Const u8\",", + " port : \"?u16\"," + ], + "field": "path", + "file": "specs/tri/net/url.t27", + "file_sha256": "6752afaf3b5a4c3b07b5cffcb548b67ae1681ffc5f1a4f9abddf97b509139aee", + "line": 17, + "raw_line": " path : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " fragment : [[]Const u8\",", + " };" + ], + "context_before": [ + " port : \"?u16\",", + " path : [[]Const u8\"," + ], + "field": "query", + "file": "specs/tri/net/url.t27", + "file_sha256": "6752afaf3b5a4c3b07b5cffcb548b67ae1681ffc5f1a4f9abddf97b509139aee", + "line": 18, + "raw_line": " query : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " path : [[]Const u8\",", + " query : [[]Const u8\"," + ], + "field": "fragment", + "file": "specs/tri/net/url.t27", + "file_sha256": "6752afaf3b5a4c3b07b5cffcb548b67ae1681ffc5f1a4f9abddf97b509139aee", + "line": 19, + "raw_line": " fragment : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " original_len : \"usize\",", + " };" + ], + "context_before": [ + "", + " pub const Compressed = struct {" + ], + "field": "data", + "file": "specs/tri/io/compress.t27", + "file_sha256": "9b0ebfbac8cbf32eb4c63fa1d8744577ec72d6f46fbfb0cd9dd369f4e44a1fd9", + "line": 14, + "raw_line": " data : [[]U8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]U8\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-83e0cb30", + "context_after": [ + " absolute : \"bool\",", + " };" + ], + "context_before": [ + "", + " pub const Path = struct {" + ], + "field": "parts", + "file": "specs/tri/io/fs.t27", + "file_sha256": "c3a47ae15cf04f1ca3fb372da482d3fff460b1e2b6b544ede2d15f87c0ac6402", + "line": 14, + "raw_line": " parts : [[][,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[][,", + "shape": "[[][," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " padding : bool,", + " };" + ], + "context_before": [ + "", + " pub const Base32 = struct {" + ], + "field": "alphabet", + "file": "specs/tri/crypto/base32.t27", + "file_sha256": "088cbb8893f0019cef2ff3626aef421d06417126099cd17051ad591cff0d9c0d", + "line": 14, + "raw_line": " alphabet : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " padding : bool,", + " };" + ], + "context_before": [ + "", + " pub const Base64 = struct {" + ], + "field": "alphabet", + "file": "specs/tri/crypto/base64.t27", + "file_sha256": "570efb578996b026b3572669478b17ce90e70aee9b0f304f2fce1a5f241ad84e", + "line": 14, + "raw_line": " alphabet : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " private_key : [[]U8\",", + " };" + ], + "context_before": [ + "", + " pub const KeyPair = struct {" + ], + "field": "public_key", + "file": "specs/tri/crypto/crypto.t27", + "file_sha256": "6b5d36b7cfc6f68858a34b7d78bfe043b0e9693f8ba3cf4ef4229781f2db521b", + "line": 14, + "raw_line": " public_key : [[]U8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]U8\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + " pub const KeyPair = struct {", + " public_key : [[]U8\"," + ], + "field": "private_key", + "file": "specs/tri/crypto/crypto.t27", + "file_sha256": "6b5d36b7cfc6f68858a34b7d78bfe043b0e9693f8ba3cf4ef4229781f2db521b", + "line": 15, + "raw_line": " private_key : [[]U8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]U8\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-7247f52e", + "context_after": [ + " inner : \"SHA256\",", + " };" + ], + "context_before": [ + "", + " pub const HMAC = struct {" + ], + "field": "opad", + "file": "specs/tri/crypto/hmac.t27", + "file_sha256": "357ef72e7b4d971cc3711e664fe4fded7b6ab4514247831507baa0641e3d1eed", + "line": 14, + "raw_line": " opad : [[64]U8\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[[64]U8\",", + "shape": "[[9]X\"," + }, + { + "class_id": "DC-7247f52e", + "context_after": [ + " buffer : [[64]U8\",", + " count : \"u64\"," + ], + "context_before": [ + "", + " pub const SHA256 = struct {" + ], + "field": "state", + "file": "specs/tri/crypto/sha256.t27", + "file_sha256": "6433411437aec985707e377addd8284b33da7093824baba08d0ffaf31c3b14d8", + "line": 14, + "raw_line": " state : [[8]U32\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[[8]U32\",", + "shape": "[[9]X\"," + }, + { + "class_id": "DC-7247f52e", + "context_after": [ + " count : \"u64\",", + " };" + ], + "context_before": [ + " pub const SHA256 = struct {", + " state : [[8]U32\"," + ], + "field": "buffer", + "file": "specs/tri/crypto/sha256.t27", + "file_sha256": "6433411437aec985707e377addd8284b33da7093824baba08d0ffaf31c3b14d8", + "line": 15, + "raw_line": " buffer : [[64]U8\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[[64]U8\",", + "shape": "[[9]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " degree : \"usize\",", + " };" + ], + "context_before": [ + "", + " pub const BezierCurve = struct {" + ], + "field": "control", + "file": "specs/tri/math/bezier.t27", + "file_sha256": "4f0ae1c180bc369bd06c7f4535ef6a201c37798184ff864bfef6ef436ee95562", + "line": 19, + "raw_line": " control : [[]Point\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Point\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " rows : \"usize\",", + " cols : \"usize\"," + ], + "context_before": [ + "", + " pub const Matrix = struct {" + ], + "field": "data", + "file": "specs/tri/math/matrix.t27", + "file_sha256": "d7cfa970509a60bdda062871dcd0abf5880616d6688656c59706fddc9e92c7fd", + "line": 14, + "raw_line": " data : [[]F64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]F64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " allocator : \"std.mem.Allocator\",", + " };" + ], + "context_before": [ + "", + " pub const Polynomial = struct {" + ], + "field": "coeffs", + "file": "specs/tri/math/polynomial.t27", + "file_sha256": "12f3f821850c7aa6ffb4eb00599f8f0adeb53f0017808197e363d8a71014a288", + "line": 14, + "raw_line": " coeffs : [[]F64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]F64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " parent : [[]?Usize\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + "", + " pub const DijkstraResult = struct {" + ], + "field": "distance", + "file": "specs/tri/graph/dijkstra.t27", + "file_sha256": "44044c9787eb731200c48f4bdcd52daf48854b92a7273fba511aa5316ee88ad0", + "line": 14, + "raw_line": " distance : [[]F64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]F64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-06dafedd", + "context_after": [ + " allocator : \"std.mem.Allocator\",", + " };" + ], + "context_before": [ + " pub const DijkstraResult = struct {", + " distance : [[]F64\"," + ], + "field": "parent", + "file": "specs/tri/graph/dijkstra.t27", + "file_sha256": "44044c9787eb731200c48f4bdcd52daf48854b92a7273fba511aa5316ee88ad0", + "line": 15, + "raw_line": " parent : [[]?Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]?Usize\",", + "shape": "[[]?X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " rank : [[]Usize\",", + " count : \"usize\"," + ], + "context_before": [ + "", + " pub const DisjointSet = struct {" + ], + "field": "parent", + "file": "specs/tri/graph/disjoint_set.t27", + "file_sha256": "8d9a61646cad919fc95990648024ba90faa9ac2cd644cffc180bbe9222626a31", + "line": 14, + "raw_line": " parent : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " count : \"usize\",", + " };" + ], + "context_before": [ + " pub const DisjointSet = struct {", + " parent : [[]Usize\"," + ], + "field": "rank", + "file": "specs/tri/graph/disjoint_set.t27", + "file_sha256": "8d9a61646cad919fc95990648024ba90faa9ac2cd644cffc180bbe9222626a31", + "line": 15, + "raw_line": " rank : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-c580e907", + "context_after": [ + " directed : \"bool\",", + " };" + ], + "context_before": [ + "", + " pub const Graph(T) = struct {" + ], + "field": "nodes", + "file": "specs/tri/graph/graph.t27", + "file_sha256": "8f72ef2f6711c80cfc323ea331747896aa0fb996af8ed2d8585a65fc45619d82", + "line": 14, + "raw_line": " nodes : [std.HashMap(T, []T)\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[std.HashMap(T, []T)\",", + "shape": "[X.X(X, []X)\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " cost : \"f64\",", + " };" + ], + "context_before": [ + "", + " pub const GraphPath = struct {" + ], + "field": "nodes", + "file": "specs/tri/graph/graph.t27", + "file_sha256": "8f72ef2f6711c80cfc323ea331747896aa0fb996af8ed2d8585a65fc45619d82", + "line": 19, + "raw_line": " nodes : [[]T\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]T\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-83e0cb30", + "context_after": [ + " allocator : \"std.mem.Allocator\",", + " };" + ], + "context_before": [ + "", + " pub const Graph = struct {" + ], + "field": "adj", + "file": "specs/tri/graph/graph_bfs.t27", + "file_sha256": "5d0df89b1a1bde5f8d5fa4733b3ad09250d5600bd1d0d8f9c31966a2f1db3827", + "line": 14, + "raw_line": " adj : [[][,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[][,", + "shape": "[[][," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " distance : [[]Usize\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + "", + " pub const BFSResult = struct {" + ], + "field": "order", + "file": "specs/tri/graph/graph_bfs.t27", + "file_sha256": "5d0df89b1a1bde5f8d5fa4733b3ad09250d5600bd1d0d8f9c31966a2f1db3827", + "line": 19, + "raw_line": " order : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " allocator : \"std.mem.Allocator\",", + " };" + ], + "context_before": [ + " pub const BFSResult = struct {", + " order : [[]Usize\"," + ], + "field": "distance", + "file": "specs/tri/graph/graph_bfs.t27", + "file_sha256": "5d0df89b1a1bde5f8d5fa4733b3ad09250d5600bd1d0d8f9c31966a2f1db3827", + "line": 20, + "raw_line": " distance : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " postorder : [[]Usize\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + "", + " pub const DFSResult = struct {" + ], + "field": "preorder", + "file": "specs/tri/graph/graph_dfs.t27", + "file_sha256": "09b5968ba0895ab7842bfce9087e1733c4a0e4cef7d02ba04021aea8e776434e", + "line": 14, + "raw_line": " preorder : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " allocator : \"std.mem.Allocator\",", + " };" + ], + "context_before": [ + " pub const DFSResult = struct {", + " preorder : [[]Usize\"," + ], + "field": "postorder", + "file": "specs/tri/graph/graph_dfs.t27", + "file_sha256": "09b5968ba0895ab7842bfce9087e1733c4a0e4cef7d02ba04021aea8e776434e", + "line": 15, + "raw_line": " postorder : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " total_weight : \"i64\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + "", + " pub const MSTResult = struct {" + ], + "field": "edges", + "file": "specs/tri/graph/prims_mst.t27", + "file_sha256": "642d11214c510192057a915200d03aa793e184955d215b1c8a0152b627e1f8da", + "line": 14, + "raw_line": " edges : [[]Edge\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Edge\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " has_cycle : \"bool\",", + " };" + ], + "context_before": [ + "", + " pub const TopologicalSort = struct {" + ], + "field": "order", + "file": "specs/tri/graph/topological_sort.t27", + "file_sha256": "618fcd45d00e8ffa4efbd5d1be35ae3a641b317f50f6344407e918064fa7f7bc", + "line": 14, + "raw_line": " order : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " children : [[]?*BTreeNode\",", + " leaf : \"bool\"," + ], + "context_before": [ + "", + " pub const BTreeNode = struct {" + ], + "field": "keys", + "file": "specs/tri/trees/b_tree.t27", + "file_sha256": "421fe9ec4300007ac244ce16b0b57ef39b3401e494e08082defe73bb797997bc", + "line": 14, + "raw_line": " keys : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-546b13cd", + "context_after": [ + " leaf : \"bool\",", + " count : \"usize\"," + ], + "context_before": [ + " pub const BTreeNode = struct {", + " keys : [[]Usize\"," + ], + "field": "children", + "file": "specs/tri/trees/b_tree.t27", + "file_sha256": "421fe9ec4300007ac244ce16b0b57ef39b3401e494e08082defe73bb797997bc", + "line": 15, + "raw_line": " children : [[]?*BTreeNode\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]?*BTreeNode\",", + "shape": "[[]?*X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " size : \"usize\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + "", + " pub const FenwickTree = struct {" + ], + "field": "data", + "file": "specs/tri/trees/fenwick_tree.t27", + "file_sha256": "689d3e8dfffa09bc98609a75404dcfcb83cecbc18087b05b8a5a94e0bfe77ae0", + "line": 14, + "raw_line": " data : [[]I64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]I64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " axis : \"usize\",", + " left : \"?KDNode\"," + ], + "context_before": [ + "", + " pub const KDNode = struct {" + ], + "field": "point", + "file": "specs/tri/trees/kd_tree.t27", + "file_sha256": "ac3052b6c5dab2c3ee103ce3f0cc6941865cd766c1fd12a1c4cc712c283b7c32", + "line": 14, + "raw_line": " point : [[]F64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]F64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-c349b5d1", + "context_after": [ + " data : \"?void\",", + " divided : \"bool\"," + ], + "context_before": [ + " pub const OctNode = struct {", + " bounds : \"BBox\"," + ], + "field": "children", + "file": "specs/tri/trees/octree.t27", + "file_sha256": "d2cf6df1b58bd432a6a45baf815781216cfad662bbbbb64e090248d8d00c80f3", + "line": 24, + "raw_line": " children : [[8]?OctNode\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[[8]?OctNode\",", + "shape": "[[9]?X\"," + }, + { + "class_id": "DC-c349b5d1", + "context_after": [ + " points : [[][2,", + " divided : \"bool\"," + ], + "context_before": [ + " pub const QuadNode = struct {", + " boundary : \"Rect\"," + ], + "field": "children", + "file": "specs/tri/trees/quadtree.t27", + "file_sha256": "0b9a0c9e250bda8456bac8e3609bd8a3294e0d902dad9c91dc10fd569f554b03", + "line": 22, + "raw_line": " children : [[4]?QuadNode\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[[4]?QuadNode\",", + "shape": "[[9]?X\"," + }, + { + "class_id": "DC-801c2390", + "context_after": [ + " divided : \"bool\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + " boundary : \"Rect\",", + " children : [[4]?QuadNode\"," + ], + "field": "points", + "file": "specs/tri/trees/quadtree.t27", + "file_sha256": "0b9a0c9e250bda8456bac8e3609bd8a3294e0d902dad9c91dc10fd569f554b03", + "line": 23, + "raw_line": " points : [[][2,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[][2,", + "shape": "[[][9," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " is_leaf : \"bool\",", + " };" + ], + "context_before": [ + " pub const RTreeNode = struct {", + " rect : \"Rect\"," + ], + "field": "children", + "file": "specs/tri/trees/rtree.t27", + "file_sha256": "ff560d230e72de0accc0dfc8ed0cfb464934225c77ed13e6cae4aa0ec303e68e", + "line": 22, + "raw_line": " children : [[]RTreeNode\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]RTreeNode\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " size : \"usize\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + "", + " pub const SegmentTree = struct {" + ], + "field": "data", + "file": "specs/tri/trees/segment_tree.t27", + "file_sha256": "dd8ebf2929c0c7d105676201c2afd5fac1315064085a50029c8472d3436934d8", + "line": 14, + "raw_line": " data : [[]I64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]I64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " allocator : \"std.mem.Allocator\",", + " };" + ], + "context_before": [ + "", + " pub const SuffixArray = struct {" + ], + "field": "data", + "file": "specs/tri/trees/suffix_array.t27", + "file_sha256": "f9a84a52ce9586dd5e0ba1bc9a768a0e942871ab1cc63b61712304e9c48dfc55", + "line": 14, + "raw_line": " data : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-f918a784", + "context_after": [ + " fail : \"*ACTrieNode\",", + " output : [[][," + ], + "context_before": [ + "", + " pub const ACTrieNode = struct {" + ], + "field": "children", + "file": "specs/tri/search/aho_corasick.t27", + "file_sha256": "366ade2de4cb17215a51999e65e62e5799de582a4438b2aec2ebd2763747dd3f", + "line": 14, + "raw_line": " children : [[256]?*ACTrieNode\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[[256]?*ACTrieNode\",", + "shape": "[[9]?*X\"," + }, + { + "class_id": "DC-83e0cb30", + "context_after": [ + " char : \"u8\",", + " };" + ], + "context_before": [ + " children : [[256]?*ACTrieNode\",", + " fail : \"*ACTrieNode\"," + ], + "field": "output", + "file": "specs/tri/search/aho_corasick.t27", + "file_sha256": "366ade2de4cb17215a51999e65e62e5799de582a4438b2aec2ebd2763747dd3f", + "line": 16, + "raw_line": " output : [[][,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[][,", + "shape": "[[][," + }, + { + "class_id": "DC-83e0cb30", + "context_after": [ + " allocator : \"std.mem.Allocator\",", + " };" + ], + "context_before": [ + " pub const ACAutomaton = struct {", + " root : \"*ACTrieNode\"," + ], + "field": "patterns", + "file": "specs/tri/search/aho_corasick.t27", + "file_sha256": "366ade2de4cb17215a51999e65e62e5799de582a4438b2aec2ebd2763747dd3f", + "line": 22, + "raw_line": " patterns : [[][,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[][,", + "shape": "[[][," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " hash_count : \"usize\",", + " size : \"usize\"," + ], + "context_before": [ + "", + " pub const BloomFilter = struct {" + ], + "field": "bits", + "file": "specs/tri/search/bloom_filter.t27", + "file_sha256": "e88d868bf244cdbf0a0312f6afcdd769f8cb119851c811e8fe4131db2f58b776", + "line": 14, + "raw_line": " bits : [[]Bool\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Bool\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-7247f52e", + "context_after": [ + " pattern_len : \"usize\",", + " };" + ], + "context_before": [ + "", + " pub const BMBadChar = struct {" + ], + "field": "table", + "file": "specs/tri/search/boyer_moore.t27", + "file_sha256": "baf22f7136a669ca3528ffa121bfb755ce02221a4ede00c7811e49500542dfbb", + "line": 14, + "raw_line": " table : [[256]Usize\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[[256]Usize\",", + "shape": "[[9]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " pattern : [[]Const u8\",", + " };" + ], + "context_before": [ + "", + " pub const KMPPrefix = struct {" + ], + "field": "table", + "file": "specs/tri/search/knuth_morris_pratt.t27", + "file_sha256": "fa851daf9a4c4b0e8cfb4a215dc741378e0756711cb98986e2f2c8000266a4e8", + "line": 14, + "raw_line": " table : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " pub const KMPPrefix = struct {", + " table : [[]Usize\"," + ], + "field": "pattern", + "file": "specs/tri/search/knuth_morris_pratt.t27", + "file_sha256": "fa851daf9a4c4b0e8cfb4a215dc741378e0756711cb98986e2f2c8000266a4e8", + "line": 15, + "raw_line": " pattern : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " compiled : \"bool\",", + " };" + ], + "context_before": [ + "", + " pub const Regex = struct {" + ], + "field": "pattern", + "file": "specs/tri/search/regex.t27", + "file_sha256": "4e0b80ad075dac6274040a5327d17244dc1e1206368598cccc5883d96de30ccb", + "line": 14, + "raw_line": " pattern : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-83e0cb30", + "context_after": [ + " };", + "" + ], + "context_before": [ + " start : \"usize\",", + " end : \"usize\"," + ], + "field": "groups", + "file": "specs/tri/search/regex.t27", + "file_sha256": "4e0b80ad075dac6274040a5327d17244dc1e1206368598cccc5883d96de30ccb", + "line": 21, + "raw_line": " groups : [[][,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[][,", + "shape": "[[][," + }, + { + "class_id": "DC-83e0cb30", + "context_after": [ + " start : \"usize\",", + " end : \"usize\"," + ], + "context_before": [ + " pub const RegexMatch = struct {", + " matched : \"bool\"," + ], + "field": "groups", + "file": "specs/tri/search/regex_advanced.t27", + "file_sha256": "e8cdd8c7cc567ec42e6d72b5264bcabc59a1460a07e8957c36cdcf6f14edf837", + "line": 19, + "raw_line": " groups : [[][,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[][,", + "shape": "[[][," + }, + { + "class_id": "DC-774471d4", + "context_after": [ + " };", + "" + ], + "context_before": [ + " max_alerts : usize,", + " auto_heal : bool," + ], + "field": "log_file", + "file": "specs/tri/agent/eternal_monitor.t27", + "file_sha256": "b5afb6170a5befb94ac33558fa795f236dd6075d9e05f6a0dd2e673d05aba069", + "line": 25, + "raw_line": " log_file : [?[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[?[]Const u8\",", + "shape": "[?[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " severity : Severity,", + " message : [[]Const u8\"," + ], + "context_before": [ + " pub const Alert = struct {", + " timestamp : i64," + ], + "field": "component", + "file": "specs/tri/agent/eternal_monitor.t27", + "file_sha256": "b5afb6170a5befb94ac33558fa795f236dd6075d9e05f6a0dd2e673d05aba069", + "line": 30, + "raw_line": " component : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " resolved : bool,", + " };" + ], + "context_before": [ + " component : [[]Const u8\",", + " severity : Severity," + ], + "field": "message", + "file": "specs/tri/agent/eternal_monitor.t27", + "file_sha256": "b5afb6170a5befb94ac33558fa795f236dd6075d9e05f6a0dd2e673d05aba069", + "line": 32, + "raw_line": " message : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + " build_broken : bool,", + " timestamp : i64," + ], + "field": "agents", + "file": "specs/tri/agent/faculty_board.t27", + "file_sha256": "9e12c1f5268143af7f4026346e22ae9fcd63218aefbebbfe503316e9e295c32e", + "line": 24, + "raw_line": " agents : [[]AgentStatus\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]AgentStatus\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " files : [[]Const [,", + " approach : String," + ], + "context_before": [ + " pub const PlannerOutput = struct {", + " issue_number : UInt32," + ], + "field": "subtasks", + "file": "specs/tri/agent/handoff.t27", + "file_sha256": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "line": 15, + "raw_line": " subtasks : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " approach : String,", + " spec_path : String," + ], + "context_before": [ + " issue_number : UInt32,", + " subtasks : [[]Const [," + ], + "field": "files", + "file": "specs/tri/agent/handoff.t27", + "file_sha256": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "line": 16, + "raw_line": " files : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " commits : [[]Const [,", + " lines_added : UInt32," + ], + "context_before": [ + " issue_number : UInt32,", + " branch : String," + ], + "field": "files_modified", + "file": "specs/tri/agent/handoff.t27", + "file_sha256": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "line": 28, + "raw_line": " files_modified : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " lines_added : UInt32,", + " lines_removed : UInt32," + ], + "context_before": [ + " branch : String,", + " files_modified : [[]Const [," + ], + "field": "commits", + "file": "specs/tri/agent/handoff.t27", + "file_sha256": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "line": 29, + "raw_line": " commits : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " iteration : UInt8,", + " max_iterations : UInt8," + ], + "context_before": [ + " issue_number : UInt32,", + " approved : Bool," + ], + "field": "feedback", + "file": "specs/tri/agent/handoff.t27", + "file_sha256": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "line": 41, + "raw_line": " feedback : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " timestamp : Int64,", + " cost_tokens_in : UInt64," + ], + "context_before": [ + " iteration : UInt8,", + " max_iterations : UInt8," + ], + "field": "files_reviewed", + "file": "specs/tri/agent/handoff.t27", + "file_sha256": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "line": 44, + "raw_line": " files_reviewed : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " regressions : [[]Const [,", + " timestamp : Int64," + ], + "context_before": [ + " tests_passed : UInt32,", + " tests_total : UInt32," + ], + "field": "benchmarks", + "file": "specs/tri/agent/handoff.t27", + "file_sha256": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "line": 55, + "raw_line": " benchmarks : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-72bb7dcf", + "context_after": [ + " timestamp : Int64,", + " };" + ], + "context_before": [ + " tests_total : UInt32,", + " benchmarks : [[]Const [," + ], + "field": "regressions", + "file": "specs/tri/agent/handoff.t27", + "file_sha256": "08f082e9497d687dbc081314395ecc56054ba89a30d0ba1c883f60e710d86a36", + "line": 56, + "raw_line": " regressions : [[]Const [,", + "reasons": [ + "doubled-bracket" + ], + "rhs": "[[]Const [,", + "shape": "[[]X [," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " capacity : usize,", + " };" + ], + "context_before": [ + "", + " pub const Bitmap = struct {" + ], + "field": "bits", + "file": "specs/tri/collections/bitmap.t27", + "file_sha256": "afd5d67ecd9d8c1810b896e0d522f3ef6fbbad00717a26b19edbf7d3b766e31d", + "line": 14, + "raw_line": " bits : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " size : \"usize\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + "", + " pub const Bitset = struct {" + ], + "field": "data", + "file": "specs/tri/collections/bitset.t27", + "file_sha256": "f6e28351c3de17a876cbe077207413866dafddb5b1251c5e6b54f1ed9d3bcb11", + "line": 14, + "raw_line": " data : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " length : usize,", + " };" + ], + "context_before": [ + "", + " pub const BitVector = struct {" + ], + "field": "bits", + "file": "specs/tri/collections/bitvector.t27", + "file_sha256": "435342ddd91e9a68767defaeab6d9b67279c50dd763056187d89210081b4026e", + "line": 14, + "raw_line": " bits : [[]Usize\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Usize\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " values : [[]V\",", + " children : [[]BTreeNode(K, V)\"," + ], + "context_before": [ + "", + " pub const BTreeNode(K, V) = struct {" + ], + "field": "keys", + "file": "specs/tri/collections/btree.t27", + "file_sha256": "0539a5ff094636b4c10db13d2246f53c1bf80f62a74e468107969e6f9f28020c", + "line": 19, + "raw_line": " keys : [[]K\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]K\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " children : [[]BTreeNode(K, V)\",", + " leaf : \"bool\"," + ], + "context_before": [ + " pub const BTreeNode(K, V) = struct {", + " keys : [[]K\"," + ], + "field": "values", + "file": "specs/tri/collections/btree.t27", + "file_sha256": "0539a5ff094636b4c10db13d2246f53c1bf80f62a74e468107969e6f9f28020c", + "line": 20, + "raw_line": " values : [[]V\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]V\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-73be92fd", + "context_after": [ + " leaf : \"bool\",", + " };" + ], + "context_before": [ + " keys : [[]K\",", + " values : [[]V\"," + ], + "field": "children", + "file": "specs/tri/collections/btree.t27", + "file_sha256": "0539a5ff094636b4c10db13d2246f53c1bf80f62a74e468107969e6f9f28020c", + "line": 21, + "raw_line": " children : [[]BTreeNode(K, V)\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]BTreeNode(K, V)\",", + "shape": "[[]X(X, X)\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " head : \"usize\",", + " tail : \"usize\"," + ], + "context_before": [ + "", + " pub const CircularBuffer = struct {" + ], + "field": "data", + "file": "specs/tri/collections/circular_buffer.t27", + "file_sha256": "089c5a092639f361684384a25291fbecdee0b3ba5476d25cd1fc62c2456fabd5", + "line": 14, + "raw_line": " data : [[]I64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]I64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " front : \"usize\",", + " back : \"usize\"," + ], + "context_before": [ + "", + " pub const Deque = struct {" + ], + "field": "data", + "file": "specs/tri/collections/deque.t27", + "file_sha256": "57170d1f89805ebf6a9516326884d6d969770839fe4bc1c39193fbc4d0c50a07", + "line": 14, + "raw_line": " data : [[]I64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]I64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + "", + " pub const IntervalSet = struct {" + ], + "field": "intervals", + "file": "specs/tri/collections/interval.t27", + "file_sha256": "b259aff814f1812bbee828b05567a71fdd57c7be838d4a85bd092b7b7c5a9e40", + "line": 20, + "raw_line": " intervals : [[]Interval\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Interval\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + " capacity : \"usize\",", + " entries : \"std.HashMap(K, V)\"," + ], + "field": "access_list", + "file": "specs/tri/collections/lru.t27", + "file_sha256": "00755caba914623ad212f422ad2225f2de28144d0bb190788580e14ee87959ab", + "line": 16, + "raw_line": " access_list : [[]K\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]K\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " values : [[]V\",", + " };" + ], + "context_before": [ + "", + " pub const Map(K, V) = struct {" + ], + "field": "keys", + "file": "specs/tri/collections/map.t27", + "file_sha256": "7e0fdae860e6349fdb6e2951b1d7588b0861822f7675c669996d061899c95127", + "line": 14, + "raw_line": " keys : [[]K\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]K\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + " pub const Map(K, V) = struct {", + " keys : [[]K\"," + ], + "field": "values", + "file": "specs/tri/collections/map.t27", + "file_sha256": "7e0fdae860e6349fdb6e2951b1d7588b0861822f7675c669996d061899c95127", + "line": 15, + "raw_line": " values : [[]V\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]V\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " size : \"usize\",", + " allocator : \"std.mem.Allocator\"," + ], + "context_before": [ + "", + " pub const PriorityQueue = struct {" + ], + "field": "data", + "file": "specs/tri/collections/priority_queue.t27", + "file_sha256": "9bab098fcc7065874c346143e85bec43b9966b0669b6647871e573c7a90ed6c9", + "line": 14, + "raw_line": " data : [[]I64\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]I64\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " back : [[]T\",", + " };" + ], + "context_before": [ + "", + " pub const Queue(T) = struct {" + ], + "field": "front", + "file": "specs/tri/collections/queue.t27", + "file_sha256": "3175c9ed4dcef7978d7d871df08836c40c88965cb9c45b83d6d9c53fa8a9d463", + "line": 14, + "raw_line": " front : [[]T\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]T\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + " pub const Queue(T) = struct {", + " front : [[]T\"," + ], + "field": "back", + "file": "specs/tri/collections/queue.t27", + "file_sha256": "3175c9ed4dcef7978d7d871df08836c40c88965cb9c45b83d6d9c53fa8a9d463", + "line": 15, + "raw_line": " back : [[]T\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]T\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " head : usize,", + " tail : usize," + ], + "context_before": [ + "", + " pub const Ring(T) = struct {" + ], + "field": "buffer", + "file": "specs/tri/collections/ring_buffer.t27", + "file_sha256": "87c586fbef0a7b22b2f6e5f6741a345695bfd9ca5e36a7e510561632b37b7348", + "line": 14, + "raw_line": " buffer : [[]T\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]T\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-ab5c5903", + "context_after": [ + " level : \"usize\",", + " };" + ], + "context_before": [ + " pub const SkipNode(T) = struct {", + " value : \"T\"," + ], + "field": "forward", + "file": "specs/tri/collections/skip_list.t27", + "file_sha256": "3cfdb7e154b5a0ec8d0a825f265b1f5e165af4ae3c19b5c499efc9f5a61a170b", + "line": 15, + "raw_line": " forward : [[]?SkipNode(T)\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]?SkipNode(T)\",", + "shape": "[[]?X(X)\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + "", + " pub const Stack(T) = struct {" + ], + "field": "items", + "file": "specs/tri/collections/stack.t27", + "file_sha256": "d9c36841edfb5e75ee74a6e948fccf724656ad0d6bebc169bddd6dc3fbdc7f65", + "line": 14, + "raw_line": " items : [[]T\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]T\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " value : T,", + " };" + ], + "context_before": [ + "", + " pub const Variant(T) = struct {" + ], + "field": "tag", + "file": "specs/tri/collections/variant.t27", + "file_sha256": "1280493ead77ca373c85b07e35b10a460fb7ad6f387b0820d9925ac38d4f1167", + "line": 14, + "raw_line": " tag : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " owned : bool,", + " };" + ], + "context_before": [ + "", + " pub const Bytes = struct {" + ], + "field": "data", + "file": "specs/tri/utils/bytes.t27", + "file_sha256": "53bbef23b5864bc8ac792977e15c4f55f1c4db021acdb360b2c3a5005bdd5a60", + "line": 14, + "raw_line": " data : [[]U8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]U8\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " timestamp : \"Instant\",", + " level : \"Level\"," + ], + "field": "message", + "file": "specs/tri/utils/logger.t27", + "file_sha256": "58c449a8aac8f2fbae73fa7c67532d9f4cded68e794aa154862031f3e2893da1", + "line": 20, + "raw_line": " message : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + " pub const Logger = struct {", + " min_level : \"Level\"," + ], + "field": "writers", + "file": "specs/tri/utils/logger.t27", + "file_sha256": "58c449a8aac8f2fbae73fa7c67532d9f4cded68e794aa154862031f3e2893da1", + "line": 25, + "raw_line": " writers : [[]LogWriter\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]LogWriter\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-d9efbc31", + "context_after": [ + " };", + "" + ], + "context_before": [ + "", + " pub const Template = struct {" + ], + "field": "parts", + "file": "specs/tri/utils/template.t27", + "file_sha256": "d0d82d46597a79fe8374c729404485f5c7933868c035726234c1d3d8cbcd22d2", + "line": 14, + "raw_line": " parts : [[]TemplatePart\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]TemplatePart\",", + "shape": "[[]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " variable : [[]Const u8\",", + " };" + ], + "context_before": [ + " pub const TemplatePart = struct {", + " is_literal : \"bool\"," + ], + "field": "text", + "file": "specs/tri/utils/template.t27", + "file_sha256": "d0d82d46597a79fe8374c729404485f5c7933868c035726234c1d3d8cbcd22d2", + "line": 19, + "raw_line": " text : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " is_literal : \"bool\",", + " text : [[]Const u8\"," + ], + "field": "variable", + "file": "specs/tri/utils/template.t27", + "file_sha256": "d0d82d46597a79fe8374c729404485f5c7933868c035726234c1d3d8cbcd22d2", + "line": 20, + "raw_line": " variable : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-7247f52e", + "context_after": [ + " len : \"u8\",", + " };" + ], + "context_before": [ + "", + " pub const Rune = struct {" + ], + "field": "bytes", + "file": "specs/tri/utils/utf8.t27", + "file_sha256": "f478a091aadf9aa5cb78ce6aef9f4b1847c734db395a74151cab5f69b864b4b1", + "line": 18, + "raw_line": " bytes : [[4]U8\",", + "reasons": [ + "odd-quote" + ], + "rhs": "[[4]U8\",", + "shape": "[[9]X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " build : [[]Const u8\",", + " };" + ], + "context_before": [ + " minor : \"usize\",", + " patch : \"usize\"," + ], + "field": "prerelease", + "file": "specs/tri/utils/version.t27", + "file_sha256": "18088d7e682e849ca9b7fe9827bc5d7927ab4f0a2332833c1999f4fec193caff", + "line": 17, + "raw_line": " prerelease : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + }, + { + "class_id": "DC-b8514836", + "context_after": [ + " };", + "" + ], + "context_before": [ + " patch : \"usize\",", + " prerelease : [[]Const u8\"," + ], + "field": "build", + "file": "specs/tri/utils/version.t27", + "file_sha256": "18088d7e682e849ca9b7fe9827bc5d7927ab4f0a2332833c1999f4fec193caff", + "line": 18, + "raw_line": " build : [[]Const u8\",", + "reasons": [ + "doubled-bracket", + "odd-quote" + ], + "rhs": "[[]Const u8\",", + "shape": "[[]X X\"," + } + ], + "tool": "tri damage-freeze" +} diff --git a/scripts/ci/loop-tools-tracked.sh b/scripts/ci/loop-tools-tracked.sh index fc491e050d..9ab0d52224 100755 --- a/scripts/ci/loop-tools-tracked.sh +++ b/scripts/ci/loop-tools-tracked.sh @@ -34,8 +34,10 @@ REQUIRED_TOOLS=( "scripts/tri_loop/cost.py" "scripts/tri_loop/diffbin.py" "scripts/tri_loop/damage.py" + "scripts/tri_loop/damage_freeze.py" + "scripts/tri_loop/damage_repair.py" ) -REQUIRED_SUBCOMMANDS=(triage cost diffbin damage) +REQUIRED_SUBCOMMANDS=(triage cost diffbin damage damage-freeze damage-repair) fail=0 note() { printf ' %s\n' "$1"; } diff --git a/scripts/tri_loop/damage_freeze.py b/scripts/tri_loop/damage_freeze.py new file mode 100644 index 0000000000..e2530b8ba2 --- /dev/null +++ b/scripts/tri_loop/damage_freeze.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""tri damage-freeze -- freeze the corpus damage list before any repair touches it (#2154). + +A repair that has no frozen "before" is not a repair, it is an edit. This writes an +archive that pins every damaged line to a stable class identifier, so a later patch +can cite the class ID and the snapshot digest instead of asserting that it changed +the right thing. + +What is recorded per row, and why each field is needed: + + class_id stable across runs and across changes to the class SET. Derived from + sha256 of the normalised shape, NOT from frequency rank -- ranking + by count means adding one damaged line silently renumbers every + class, and every earlier citation of "class 04" then points at + something else. + file, line location + field the field name whose type side is damaged + rhs the damaged type side, VERBATIM. This is the archive: no repair may + delete a damaged line without this record existing first + before/after two lines of context each side, so the struct the field belongs to + is identifiable without reopening the file at a moved line number + file_sha256 digest of the whole file at freeze time. A patch applied to a file + whose digest no longer matches the snapshot is applied to a + different file than the one that was surveyed, and must refuse + + corpus_sha256 digest over the sorted (file, sha256) pairs of every damaged file, + so the snapshot as a whole has one number to cite + +Usage: + tri damage-freeze [corpus-dir] [--out PATH] + +This tool writes the snapshot only. It never edits a spec. +""" + +import hashlib +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from damage import scan, shape # noqa: E402 (same directory, deliberate) + +CONTEXT = 2 + + +def class_id(shape_text): + """Stable ID for a damage class. + + Keyed on the shape text, so the ID of a class does not depend on how many + lines happen to be in it, nor on which other classes exist. + """ + return "DC-" + hashlib.sha256(shape_text.encode("utf-8")).hexdigest()[:8] + + +def file_digest(path): + h = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(65536), b""): + h.update(chunk) + return h.hexdigest() + + +def freeze(corpus): + rows = scan(corpus) + digests = {} + cache = {} + out = [] + for r in rows: + path = r["file"] + if path not in digests: + digests[path] = file_digest(path) + with open(path, "r", errors="replace") as fh: + cache[path] = fh.read().splitlines() + lines = cache[path] + i = r["line"] - 1 + out.append({ + "class_id": class_id(r["shape"]), + "shape": r["shape"], + "file": path, + "line": r["line"], + "field": r["field"], + "rhs": r["rhs"], + "raw_line": lines[i] if 0 <= i < len(lines) else None, + "context_before": lines[max(0, i - CONTEXT):i], + "context_after": lines[i + 1:i + 1 + CONTEXT], + "reasons": r["reasons"], + "file_sha256": digests[path], + }) + + # One number for the snapshot as a whole. Sorted so it does not depend on + # walk order. + agg = hashlib.sha256() + for path in sorted(digests): + agg.update(f"{path}:{digests[path]}\n".encode("utf-8")) + + classes = {} + for row in out: + c = classes.setdefault(row["class_id"], { + "class_id": row["class_id"], "shape": row["shape"], + "count": 0, "files": set(), "reasons": set(), + }) + c["count"] += 1 + c["files"].add(row["file"]) + c["reasons"].update(row["reasons"]) + for c in classes.values(): + c["files"] = sorted(c["files"]) + c["reasons"] = sorted(c["reasons"]) + + return { + "tool": "tri damage-freeze", + "corpus": corpus, + "lines": len(out), + "files": len(digests), + "classes": len(classes), + "corpus_sha256": agg.hexdigest(), + "file_sha256": {p: digests[p] for p in sorted(digests)}, + "class_index": [classes[k] for k in sorted(classes, key=lambda k: (-classes[k]["count"], k))], + "rows": out, + } + + +def main(argv): + args = [a for a in argv if not a.startswith("--")] + corpus = args[0] if args else "specs" + out_path = None + for i, a in enumerate(argv): + if a == "--out" and i + 1 < len(argv): + out_path = argv[i + 1] + if out_path is None: + out_path = "docs/corpus/damage_snapshot.json" + + snap = freeze(corpus) + + print(f"corpus: {snap['corpus']}") + print(f"damaged lines: {snap['lines']}") + print(f"damaged files: {snap['files']}") + print(f"classes: {snap['classes']}") + print(f"corpus_sha256: {snap['corpus_sha256']}\n") + print(f"{'class_id':12} {'n':>4} {'files':>5} shape") + print("-" * 74) + for c in snap["class_index"]: + print(f"{c['class_id']:12} {c['count']:4d} {len(c['files']):5d} {c['shape']!r}") + + os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True) + with open(out_path, "w") as fh: + json.dump(snap, fh, indent=1, sort_keys=True) + fh.write("\n") + print(f"\nwrote {out_path}") + print("Class IDs are keyed on the shape text, so they survive a change to the") + print("class set. Cite the class_id and corpus_sha256, never a rank.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/tri_loop/damage_repair.py b/scripts/tri_loop/damage_repair.py new file mode 100644 index 0000000000..7197623a75 --- /dev/null +++ b/scripts/tri_loop/damage_repair.py @@ -0,0 +1,449 @@ +#!/usr/bin/env python3 +"""tri damage-repair -- one candidate patch per damage class, with the effect measured (#2154). + +Nothing here rewrites the corpus. A candidate is applied to a COPY, validated +twice, and printed as a reversible unified diff. Landing it is a separate, +human-reviewed act. + +## The mechanism, established from the corpus and not from memory + +An intact field in this corpus is `name : "TypeText",` -- the type side is a +quoted string. Every damaged line has the SAME single defect: the OPENING quote +of that string was replaced by `[`. + + intact children : "[4]?QuadNode", + damaged children : [[4]?QuadNode", + +which explains both signals `tri damage` detects at once: the doubled bracket +(`"[]T"` -> `[[]T"`) and the odd quote count (the opening one is gone). So the +candidate patch is one character, at a known offset, and inverting it is the same +operation in reverse. That is the entire repair for the classes where it works. + +## Why the classes split, and why the split is the load-bearing result + +Applying the substitution and asking whether the result is a closed string is a +decision procedure, not a guess: + + RESTORABLE `[[]Const u8",` -> `"[]Const u8",` a closed string. Nothing was + invented: every character of the type text survived the damage + and the repair only puts the delimiter back + + DESTROYED `[[]Const [,` -> `"[]Const [,` NOT a closed string. The type + text was also TRUNCATED at its second `[`, and the element type + that followed is simply gone. `[]Const [` cannot be completed + without deciding what the element type was -- `[]Const []Const u8` + is a plausible reading and so is `[]Const [N]u8`. There is no + evidence in the file either way + +The second group therefore gets `needs-human-language-decision` and NO patch. An +auto-repair there would be a guess wearing the costume of a fix, and it would be +unfalsifiable afterwards because the original is unrecoverable. This is the +difference between repairing a delimiter and inventing a type. + +## Double validation, and why parsing alone is not enough + +A file can be made to parse by deleting the offending line. So each applied +candidate is checked twice: + + 1. SYNTACTIC `t27c parse` exits 0 on the repaired file + 2. SEMANTIC the specific damaged field is present in the parsed field set + with a NON-EMPTY type text, and no previously-present field + disappeared + +Check 2 is the one that distinguishes a restored declaration from a merely +parseable file. Reported effect is one of: + + parse-restored both checks pass + still-malformed syntactic check fails + ambiguous parses, but the field did not come back, or a + different field vanished + needs-human-language-decision no patch was attempted, information destroyed + +Usage: + tri damage-repair [--snapshot PATH] [--binary PATH] [--class DC-xxxxxxxx] + [--diff] [--apply-to DIR] [--json PATH] + +`--apply-to` writes repaired copies into a scratch tree; specs/ is never touched. +""" + +import difflib +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from diffbin import parse_fields # noqa: E402 (same directory, deliberate) + +CLOSED_STRING = re.compile(r'^"[^"]*",?$') +DEFAULT_SNAPSHOT = "docs/corpus/damage_snapshot_2026-08-15.json" +TIMEOUT = 25 + +EFFECTS = ( + "parse-restored", + "still-malformed", + "ambiguous", + "needs-human-language-decision", +) + + +def candidate_rhs(rhs): + """The one-character candidate patch, or None when it is not applicable. + + Returns (repaired_rhs, restorable). restorable is False when the substitution + does not yield a closed string, which means the type text itself was truncated + and no patch is proposed. + """ + if not rhs.startswith("["): + return None, False + cand = '"' + rhs[1:] + return cand, bool(CLOSED_STRING.match(cand)) + + +def sha256_file(path): + h = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(65536), b""): + h.update(chunk) + return h.hexdigest() + + +def repair_lines(lines, rows): + """Apply the candidate to the given rows of one file. Returns (new_lines, applied). + + Only the type side is touched, and only its first character. The field name, + the indentation and the trailing comma are left byte-identical, so the diff is + one character per line and reads as such. + """ + new = list(lines) + applied = [] + for r in rows: + i = r["line"] - 1 + if not (0 <= i < len(new)): + continue + cand, restorable = candidate_rhs(r["rhs"]) + if not restorable: + continue + old = new[i] + # Locate the type side by its recorded text rather than by re-parsing the + # line, so a patch cannot drift onto a different construct. + if r["rhs"] not in old: + continue + new[i] = old.replace(r["rhs"], cand, 1) + applied.append(r) + return new, applied + + +def validate(binary, orig_path, repaired_text, rows_applied, tmpdir): + """Two checks. Returns (effect, detail).""" + scratch = os.path.join(tmpdir, "validate.t27") + with open(scratch, "w") as fh: + fh.write(repaired_text) + + base_status, base_fields = parse_fields(binary, orig_path, TIMEOUT) + cand_status, cand_fields = parse_fields(binary, scratch, TIMEOUT) + + if cand_status != "ok": + return "still-malformed", f"parse status {cand_status} after repair" + + base_map = dict(base_fields) + cand_map = dict(cand_fields) + + # Semantic check 1: every repaired field must now be present with a non-empty + # type. Names are qualified Struct.field by parse_fields, and the snapshot + # records the bare field name, so match on the suffix. + missing = [] + for r in rows_applied: + want = "." + r["field"] + hits = [(k, v) for k, v in cand_map.items() if k.endswith(want)] + if not hits or not any(v.strip() for _k, v in hits): + missing.append(r["field"]) + + # Semantic check 2: nothing that used to be there may vanish. A repair that + # restores one declaration by losing another is not a repair. + vanished = sorted(k for k in base_map if k not in cand_map) + + if missing or vanished: + bits = [] + if missing: + bits.append("field(s) did not come back: " + ", ".join(sorted(missing))) + if vanished: + bits.append("field(s) vanished: " + ", ".join(vanished[:6])) + return "ambiguous", "; ".join(bits) + + gained = sorted(k for k in cand_map if k not in base_map) + return "parse-restored", (f"parse ok; {len(rows_applied)} field(s) repaired; " + f"{len(gained)} declaration(s) newly visible" + + (": " + ", ".join(gained[:6]) if gained else "")) + + +def combined(snap, rows, binary, have_binary, tmpdir, apply_to, json_out): + """Repair every restorable row of a file at once, then validate per file. + + The per-class run answers "is this class's patch sound", and it answers it with + a confound: a file carrying damage from four classes still fails to parse after + one class is repaired, and the class is then blamed for a neighbour's defect. + That is what happened on the first run -- six classes came back still-malformed, + and the first guess (co-located UNRESTORABLE damage) was checked and found + false: none of those files contained a destroyed line. The actual cause was + co-located damage from OTHER RESTORABLE classes, left untouched by a + single-class run. + + So per-file is the unit that answers "what does the one rule achieve", and the + per-class run stays as the unit that answers "is one rule enough for this + shape". Both are reported; neither replaces the other. + """ + per_file = {} + for r in rows: + per_file.setdefault(r["file"], []).append(r) + + counts = dict.fromkeys(EFFECTS, 0) + out = [] + print("combined mode: every restorable row per file, validated per file\n") + for path in sorted(per_file): + frows = per_file[path] + with open(path, "r", errors="replace") as fh: + orig = fh.read() + new_lines, applied = repair_lines(orig.splitlines(), frows) + new_text = "\n".join(new_lines) + ("\n" if orig.endswith("\n") else "") + held = [r for r in frows if not candidate_rhs(r["rhs"])[1]] + if not applied: + eff, det = "needs-human-language-decision", \ + f"all {len(frows)} damaged line(s) are destroyed, none restorable" + elif have_binary: + eff, det = validate(binary, path, new_text, applied, tmpdir) + if held and eff != "parse-restored": + det += f"; {len(held)} destroyed line(s) remain in this file" + else: + eff, det = "ambiguous", "validation skipped: no binary" + counts[eff] += 1 + out.append({"file": path, "applied": len(applied), "held": len(held), + "effect": eff, "detail": det}) + if apply_to: + dest = os.path.join(apply_to, path) + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w") as fh: + fh.write(new_text) + + for e in EFFECTS: + sel = [o for o in out if o["effect"] == e] + if not sel: + continue + print(f"--- {e}: {len(sel)} file(s) ---") + for o in sel[:8]: + print(f" {o['file']} (+{o['applied']} repaired, {o['held']} held) {o['detail'][:110]}") + if len(sel) > 8: + print(f" ... and {len(sel) - 8} more") + print() + + print("=" * 74) + print("per-file effect distribution (no aggregation across effects):") + for e in EFFECTS: + print(f" {e:32} {counts[e]:3d}") + lines_ok = sum(o["applied"] for o in out if o["effect"] == "parse-restored") + lines_held = sum(o["held"] for o in out) + print(f"\nrestorable lines inside parse-restored files: {lines_ok}") + print(f"lines held for a human language decision: {lines_held}") + print(f"snapshot total: {snap['lines']}") + print("\nNo spec under specs/ was modified by this run.") + if json_out: + with open(json_out, "w") as fh: + json.dump({"mode": "combined", "corpus_sha256": snap["corpus_sha256"], + "counts": counts, "files": out}, fh, indent=1, sort_keys=True) + fh.write("\n") + print(f"wrote {json_out}") + return 0 + + +def main(argv): + snapshot = DEFAULT_SNAPSHOT + binary = "/tmp/t27c.fixed" + only = None + apply_to = None + json_out = None + want_diff = "--diff" in argv + for i, a in enumerate(argv): + if a == "--snapshot" and i + 1 < len(argv): + snapshot = argv[i + 1] + elif a == "--binary" and i + 1 < len(argv): + binary = argv[i + 1] + elif a == "--class" and i + 1 < len(argv): + only = argv[i + 1] + elif a == "--apply-to" and i + 1 < len(argv): + apply_to = argv[i + 1] + elif a == "--json" and i + 1 < len(argv): + json_out = argv[i + 1] + + if not os.path.exists(snapshot): + print(f"no snapshot at {snapshot}", file=sys.stderr) + print("run: tri damage-freeze specs --out " + DEFAULT_SNAPSHOT, file=sys.stderr) + return 2 + snap = json.load(open(snapshot)) + + rows = snap["rows"] + if only: + rows = [r for r in rows if r["class_id"] == only] + if not rows: + print(f"no rows for class {only}", file=sys.stderr) + return 2 + + # A patch applied to a file that has changed since the freeze is applied to a + # different file than the one that was surveyed. Refuse rather than proceed. + stale = [] + for r in rows: + p = r["file"] + if not os.path.exists(p): + stale.append((p, "missing")) + elif sha256_file(p) != r["file_sha256"]: + stale.append((p, "digest differs from snapshot")) + stale = sorted(set(stale)) + if stale: + print("REFUSING: the corpus moved since the snapshot was frozen.") + for p, why in stale[:10]: + print(f" {p} ({why})") + print("\nRe-freeze, review the difference, and only then repair.") + return 1 + + by_class = {} + for r in rows: + by_class.setdefault(r["class_id"], []).append(r) + + tmpdir = "/tmp/tri_damage_repair" + os.makedirs(tmpdir, exist_ok=True) + if apply_to: + os.makedirs(apply_to, exist_ok=True) + + have_binary = os.path.exists(binary) + results = [] + counts = dict.fromkeys(EFFECTS, 0) + + print(f"snapshot: {snapshot}") + print(f"corpus_sha256: {snap['corpus_sha256']}") + print(f"binary: {binary}" + ("" if have_binary else " [MISSING -- validation skipped]")) + print(f"classes: {len(by_class)}\n") + + if "--combined" in argv: + return combined(snap, rows, binary, have_binary, tmpdir, apply_to, json_out) + + for cid in sorted(by_class, key=lambda c: (-len(by_class[c]), c)): + crows = by_class[cid] + shape = crows[0]["shape"] + _cand, restorable = candidate_rhs(crows[0]["rhs"]) + + if not restorable: + effect = "needs-human-language-decision" + counts[effect] += 1 + reading = [] + for r in crows[:3]: + reading.append(f"{r['file']}:{r['line']} {r['field']} : {r['rhs']}") + results.append({"class_id": cid, "shape": shape, "lines": len(crows), + "files": sorted({r["file"] for r in crows}), + "effect": effect, "patch": None, + "detail": ("the type text is truncated as well as unquoted; the element " + "type is not recoverable from the file"), + "owner": "language owner", + "decision_criterion": ( + "state what an unclosed element type in a slice position means, and " + "whether the corpus may carry a placeholder; only then can a value " + "be written in without guessing")}) + print(f"{cid} n={len(crows):3d} {shape!r}") + print(f" effect: {effect}") + print(" no patch proposed. Substituting the delimiter yields " + f"{_cand!r}, which is not a closed string:") + print(" the type text was truncated too, so a repair would have to invent the") + print(" element type. owner: language owner") + for line in reading: + print(f" {line}") + print() + continue + + # Group by file, apply, validate, diff. + per_file = {} + for r in crows: + per_file.setdefault(r["file"], []).append(r) + + effects_seen = [] + details = [] + diffs = [] + for path, frows in sorted(per_file.items()): + with open(path, "r", errors="replace") as fh: + orig = fh.read() + lines = orig.splitlines() + new_lines, applied = repair_lines(lines, frows) + new_text = "\n".join(new_lines) + ("\n" if orig.endswith("\n") else "") + if not applied: + effects_seen.append("ambiguous") + details.append(f"{path}: recorded text not found at the recorded line") + continue + diff = list(difflib.unified_diff( + orig.splitlines(keepends=True), new_text.splitlines(keepends=True), + fromfile=f"a/{path}", tofile=f"b/{path}", n=1)) + diffs.append("".join(diff)) + if have_binary: + eff, det = validate(binary, path, new_text, applied, tmpdir) + else: + eff, det = "ambiguous", "validation skipped: no binary" + effects_seen.append(eff) + details.append(f"{path}: {det}") + if apply_to: + dest = os.path.join(apply_to, path) + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w") as fh: + fh.write(new_text) + + # A class is only as good as its worst file. + for e in EFFECTS: + if e in effects_seen: + effect = e + break + else: + effect = "ambiguous" + counts[effect] += 1 + + results.append({"class_id": cid, "shape": shape, "lines": len(crows), + "files": sorted(per_file), + "effect": effect, + "patch": "replace the leading '[' of the type side with '\"'", + "reversible": True, + "per_file_effects": effects_seen, + "detail": details}) + print(f"{cid} n={len(crows):3d} {shape!r}") + print(f" patch: replace the leading '[' of the type side with '\"' (1 char, invertible)") + print(f" effect: {effect} [{effects_seen.count('parse-restored')}/{len(effects_seen)} files parse-restored]") + for d in details[:3]: + print(f" {d}") + if len(details) > 3: + print(f" ... and {len(details) - 3} more file(s)") + if want_diff and diffs: + print(" --- candidate diff (first file) ---") + for line in diffs[0].splitlines()[:12]: + print(" " + line) + print() + + print("=" * 74) + print("effect distribution over classes (no aggregation across effects):") + for e in EFFECTS: + print(f" {e:32} {counts[e]:3d}") + restor = sum(r["lines"] for r in results if r["effect"] == "parse-restored") + human = sum(r["lines"] for r in results if r["effect"] == "needs-human-language-decision") + print(f"\nlines covered by a validated candidate: {restor}") + print(f"lines held for a human language decision: {human}") + print(f"lines total in snapshot: {snap['lines']}") + print("\nNo spec under specs/ was modified by this run.") + + if json_out: + with open(json_out, "w") as fh: + json.dump({"snapshot": snapshot, "corpus_sha256": snap["corpus_sha256"], + "binary": binary, "counts": counts, "classes": results}, + fh, indent=1, sort_keys=True) + fh.write("\n") + print(f"wrote {json_out}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))