Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .claude/skills/ci-gates/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8633,3 +8633,35 @@ somebody already transcribed one wrong, and no instrument has told
anybody, because nothing builds these proofs. A medium where a wrong
artefact is indistinguishable from a right one, and no build ever runs,
does not become safe by adding more artefacts to it.

## 343. A fix learned in one command does not travel to its neighbour

Last pass I found that a census of "specs the compiler cannot read" was counting
type errors and lexer errors as parse failures, and I split the stages -- in
`report`. This pass I opened `locate`, the command sitting beside it in the same
file, and **8 of its 40 answers were typecheck failures**.

The tell was not a red gate. It was setting out to fix the located items and
finding that the first family -- `t *= 2.0;`, `float` parameters, `f32` returns
-- all COMPILE in isolation. The work I had queued was not work.

**When you fix a category error, grep for every other place that asks the same
question.** The fix lived one function away and did not walk there by itself.

## 344. Make the tool state the claim you verified by hand

Having located 37 items, I checked by hand whether each one, wrapped in a bare
module, fails on its own -- the difference between "here is your bug in four
lines" and "here is roughly where it starts". All 37 did.

That number was true when I ran it and would have gone into a report as prose.
It is now a line the command prints, computed every run:

```
located AND causally confirmed 37
... the item ALONE reproduces 37 <- a minimal case, not a coordinate
```

An answer that does not reproduce alone prints `(only in context)` beside it.
**A property you checked once by hand is a property the tool should assert every
time**, or the next reader has your prose and no measurement.
104 changes: 87 additions & 17 deletions cli/tri/src/unparsed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ pub fn run(cmd: &UnparsedCmd, root: PathBuf) -> Result<()> {
.filter(|s| root.join(s).is_file() && !is_fixture(s))
.collect();
let (mut found, mut refused, mut silent) = (Vec::new(), Vec::new(), 0usize);
let (mut tc, mut lex, mut sem) = (0usize, 0usize, 0usize);
for spec in &specs {
let p = root.join(spec);
let quick = std::process::Command::new(&t27c)
Expand All @@ -603,31 +604,49 @@ pub fn run(cmd: &UnparsedCmd, root: PathBuf) -> Result<()> {
continue;
}
match locate_one(&t27c, &root, &p) {
Located::Item(a, b) => found.push((spec.clone(), a, b)),
Located::Item(a, b, alone) => found.push((spec.clone(), a, b, alone)),
Located::Refuted(a, b) => refused.push((spec.clone(), a, b)),
Located::WrongStage(Stage::Typecheck) => tc += 1,
Located::WrongStage(Stage::Lex) => lex += 1,
Located::WrongStage(_) => sem += 1,
Located::None(_) => silent += 1,
}
}
let alone = found.iter().filter(|(_, _, _, a)| *a).count();
println!(" located AND causally confirmed {}", found.len());
println!(" ... the item ALONE reproduces {alone} <- a minimal case, not a coordinate");
println!(" candidate REFUTED by causality {}", refused.len());
println!(" nothing claimed {silent}");
if tc + lex + sem > 0 {
println!();
println!(" not a PARSE failure, so not this command's question:");
if tc > 0 {
println!(
" {tc:>4} typecheck (the error already names its line AND reason)"
);
}
if lex > 0 {
println!(" {lex:>4} lex (unterminated string)");
}
if sem > 0 {
println!(" {sem:>4} semantics");
}
}
println!();
println!(" A confirmed item is one whose removal MOVES the reported error.");
println!(" Prefix bisection alone is unsound -- a truncated prefix can fail");
println!(" for a reason the whole file does not have -- so a candidate that");
println!(" does not survive that check is not an answer.");
if !found.is_empty() {
println!();
for (s, a, b) in found.iter().take(40) {
let n = b - a + 1;
println!(
" {s}:{a}{}",
if n > 1 {
format!("..{b}")
} else {
String::new()
}
);
for (s, a, b, alone) in found.iter().take(40) {
let span = if b > a {
format!("..{b}")
} else {
String::new()
};
let note = if *alone { "" } else { " (only in context)" };
println!(" {s}:{a}{span}{note}");
}
if found.len() > 40 {
println!(" ... and {} more", found.len() - 40);
Expand Down Expand Up @@ -1046,6 +1065,24 @@ mod tests {
assert_eq!(end, 5, "the closer is the last line, not the nested `}};`");
}

// `report` learned the stage split and `locate` did not: 8 of its first 40
// answers were typecheck failures. A type error already names its line AND
// its reason, so a bisection has nothing to add.
#[test]
fn locate_answers_only_for_parse_failures() {
assert!(stage_of("Typecheck FAILED (1 errors, 0 warnings):") != Stage::Parse);
assert!(stage_of("Error: unterminated string literal opened at line 5:1") != Stage::Parse);
assert!(stage_of("Error: parse error at module level near line 2") == Stage::Parse);
}

// A file with no module wrapper still has a body: the whole file.
#[test]
fn split_module_handles_a_bare_file() {
let src = ["fn a() { }", "fn b() { }"];
let (start, end) = split_module(&src);
assert_eq!((start, end), (0, 2));
}

// Every probe must be distinct: two rows sharing a source would report the
// same reading twice and hide one of them.
#[test]
Expand Down Expand Up @@ -1239,10 +1276,21 @@ fn split_module(lines: &[&str]) -> (usize, usize) {
}

enum Located {
/// Item [a, b] (1-based, inclusive), causality confirmed.
Item(usize, usize),
/// Item [a, b] (1-based, inclusive), causality confirmed. The flag says
/// whether the item ALONE -- wrapped in a bare module -- reproduces a
/// failure, which is the difference between "here is your bug, in four
/// lines" and "here is where it starts".
Item(usize, usize, bool),
/// A candidate the causality check refuted, with the line it named.
Refuted(usize, usize),
/// The file does not fail at PARSE, so there is no item to find.
///
/// `report` learned this and `locate` did not: 8 of its first 40 answers
/// were typecheck failures, where "the item whose presence causes the
/// failure" is a category error. A type error already names its line AND
/// its reason -- `cannot assign F64 to F32` -- so there is nothing for a
/// bisection to add.
WrongStage(Stage),
/// Nothing claimed, and why.
None(&'static str),
}
Expand All @@ -1259,6 +1307,24 @@ fn locate_one(t27c: &Path, root: &Path, path: &Path) -> Located {
let Some(orig) = orig else {
return Located::None("error names no line");
};
// Same stage split `report` uses. Without it this command answers a
// question about parsing with evidence from type checking.
let stage = {
let out = std::process::Command::new(t27c)
.arg("check")
.arg(path)
.current_dir(root)
.output();
match out {
Ok(o) => {
stage_of(&(String::from_utf8_lossy(&o.stderr) + String::from_utf8_lossy(&o.stdout)))
}
Err(_) => Stage::Parse,
}
};
if stage != Stage::Parse {
return Located::WrongStage(stage);
}
let (bstart, bend) = split_module(&lines);
if bstart == 0 || bend <= bstart {
return Located::None("no module body");
Expand Down Expand Up @@ -1335,9 +1401,13 @@ fn locate_one(t27c: &Path, root: &Path, path: &Path) -> Located {
// satisfied by arithmetic the way "the error moved past the item" was.
let (parsed, moved) = check_text(t27c, root, &muted.join("\n"));
let progressed = parsed || moved.is_some_and(|m| m > orig);
if progressed {
Located::Item(a, b)
} else {
Located::Refuted(a, b)
if !progressed {
return Located::Refuted(a, b);
}
// Does the item alone reproduce? Measured over the first 37 confirmed
// answers: all 37 did. That is what makes the output a set of minimal
// reproducers rather than a set of coordinates.
let item = lines[a - 1..b.min(lines.len())].join("\n");
let (alone_ok, _) = check_text(t27c, root, &format!("module m {{\n{item}\n}}"));
Located::Item(a, b, !alone_ok)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# NOW -- The fix I set out to do was not work, and the tool said so (2026-08-30)

## The fix I set out to do was not work, and the tool said so (Refs #2864)

- Took the 45 located items as 'work without research'. Probed the first family -- t *= 2.0, float parameters, f32 returns -- and every one COMPILES in isolation. The items were not defects.
- Cause: locate did not filter by stage. report learned that split last pass and locate did not, so 8 of its 40 answers were TYPECHECK failures, where 'the item whose presence causes the failure' is a category error. A type error already names its line AND its reason.
- Fixed: locate now answers only for parse failures and prints what it excluded -- 10 typecheck, 4 lex. 37 confirmed, 37 refuted, 9 nothing claimed; 37+37+9+10+4 = 97.
- Added a per-answer claim the tool makes itself: does the item ALONE, wrapped in a bare module, reproduce a failure? 37 of 37 do. That is the difference between a minimal case and a coordinate, and it is now measured rather than observed by me once.
Loading