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
89 changes: 89 additions & 0 deletions .claude/skills/ci-gates/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12493,3 +12493,92 @@ expecting 3 and reading 16. And the earlier `--base origin/<sibling>` mistake
in the same session was the same species one level up -- a flag pointed at the
wrong subject, producing a correct-looking answer about something nobody asked
about.

## 474. A pointer at a section that does not exist is a false claim

`tri skill refs` resolves every cross-reference in the skills against the sections that
actually exist. On this file:

```
sections 439
references 224 (212 by symbol, 12 written out)
with no number at all 8 a count of dangling NUMBERS cannot see these
POINTING AT NOTHING 17 across 7 distinct numbers
never existed: [126, 234, 235, 240, 241, 245, 253]
``` numbers
never existed: [126, 234, 235, 240, 241, 245, 253]
```

Read from the committed tree as the last action before the commit, per &sect;470 -- and
the rule earned its place immediately. Written against the tree before these two
sections existed, the same command said `436 / 189 / 6 / 12`. **This section and the
next quote the dead numbers as examples, so writing about dangling pointers created
five more of them.** The DISTINCT count did not move, because a citation of a dead
number is not a new dead number; the occurrence count did.

And the total on the second line moves with **every sentence written about it** --
correcting it from 223 to 224 was itself a citation. It is quoted here as of this
commit and is the one figure in the block that cannot be stable, which is why the
line that matters is the third and the fourth: `7 distinct numbers`, and `8` with no
number at all. Those hold.

**The numbers are a fingerprint.** `234, 235, 240, 241, 245, 253` is a consecutive block
inside the never-used 226&ndash;260 gap, and the sections those pointers describe are
alive at `+47`: &sect;234&rarr;&sect;281, &sect;241&rarr;&sect;288, &sect;253&rarr;&sect;300,
each verified by reading what the pointer SAYS the target says. **A renumbering moved
the sections and left the pointers.**

A dangling pointer is not a broken link. `Related: §241, a guard whose precondition had
stopped holding` is a claim about what this file contains, and the claim is false --
which is worse than a missing one, because a reader who does not check believes it.

Two details the count would hide, and both are printed:

* **Six references carry no number at all** -- `(&sect;—the same rule the widths ledger
states…)`. A check that resolves numbers cannot see these; there is no number to fail.
* The written-out form (`section 245`) is counted **apart** from the symbol form,
because the words can be about a document that is not this one. The conservative
reading is the symbols alone; both are printed rather than merged.

It reports and does not fail. **Fixing a pointer means deciding what it MEANT**, and
that is a reading, not a rename -- the tool that can prove a pointer is dead cannot
prove which live section it wanted.

## 475. Three pairs of sections here contradict each other

Not duplicates. Contradictions: acting on one violates the other, and both read as
established.

**&sect;19 against &sect;23.** The same `coverage` gate, the same breakdown -- *99
orphaned by a rename, 81 with a current twin* -- under two totals: **136 stale seals**
and **121 stale seals**. Nothing marks the change. Which is right cannot be told from
here: neither names an anchor and the seal state is a sliding population, so both may
have been true when written.

**&sect;369 against &sect;370.** &sect;369: *"**Zero.** Fixing it perfectly moves the
accept count by nothing."* &sect;370: *"+68, honest … the largest single lever in the
project, called noise by a…"*. &sect;370 is right -- **and it corrects the wrong
section.** It opens *"Section 366 says the top first-error family was worth zero"*,
while &sect;366 is about `tri prose report` against `tri unparsed report`. The sentence
it quotes is &sect;369's.

So one correction produced two defects: **&sect;369 is left standing uncorrected, and
&sect;366 is blamed for a sentence it never wrote.** A correction aimed at the wrong
target is worse than none -- it consumes the reader's attention and leaves the error in
place.

**&sect;281 against &sect;290.** &sect;281 says a bracket-depth-zero reading *"gets both
conventions right at once"*; &sect;290 says there are three conventions and depth zero
*"finds no definition at all in 231 of 650 specs"*, naming a duplicate &sect;281's method
could not see. &sect;290 is right and &sect;281 carries no marker.

**This file already has the mechanism and did not use it.** Two sections carry an
in-place `**RETRACTED, see §N.**`, and &sect;34 rules that the marker goes at the top of
the paragraph it retracts. None of &sect;281, &sect;369 or &sect;366 has one. A
convention that exists and is skipped is not a convention; it is a thing three sections
happened to do.

Reported, not repaired. Two of the three need a reading -- which total is right, what
&sect;370 meant to cite -- and one of them may need neither, if both figures were true
on their own day. **That is exactly why the anchor rule exists**: had &sect;19 and
&sect;23 each named a sha, this would be a history rather than a contradiction.
9 changes: 9 additions & 0 deletions cli/tri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,12 @@ enum SkillAction {
#[arg(long)]
gaps: bool,
},
/// Every cross-reference in the skills, and whether it resolves.
Refs {
/// Print every reference counted, not only the ones that dangle.
#[arg(long)]
list: bool,
},
/// Sections that state a FIGURE, and which of those a reader can re-take.
Claims {
/// Print every section in the free population, one line each.
Expand Down Expand Up @@ -886,6 +892,9 @@ fn main() -> Result<()> {
SkillAction::Check { gaps } => {
skillnum::run(&skillnum::SkillCmd::Check { gaps: *gaps })?
}
SkillAction::Refs { list } => {
skillnum::run(&skillnum::SkillCmd::Refs { list: *list })?
}
SkillAction::Claims {
list,
numbers,
Expand Down
238 changes: 238 additions & 0 deletions cli/tri/src/skillnum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ pub enum SkillCmd {
#[arg(long)]
gaps: bool,
},
/// Every cross-reference in the skills, and whether it resolves.
Refs {
/// Print every reference counted, not only the ones that dangle.
#[arg(long)]
list: bool,
},
/// Sections that state a FIGURE, and which of those a reader can re-take.
Claims {
/// Print every section in the free population, one line each.
Expand Down Expand Up @@ -146,6 +152,7 @@ fn skill_files(root: &std::path::Path) -> Vec<PathBuf> {

pub fn run(cmd: &SkillCmd) -> Result<()> {
let show_gaps = match cmd {
SkillCmd::Refs { list } => return refs(*list),
SkillCmd::Claims {
list,
numbers,
Expand Down Expand Up @@ -1055,3 +1062,234 @@ author-time 9999\n";
);
}
}

/// How a section is pointed at from prose.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RefKind {
/// `&sect;179` or `§179` -- unambiguous, this file's own convention.
Symbol,
/// `section 179` / `Section 179` -- the same pointer written out. Counted apart
/// because the words can also appear about something that is not this file, and
/// a reader deserves to see which population a dangling count came from.
Word,
}

/// Every cross-reference in a chunk of skill prose, as `(number, kind)`.
///
/// Deliberately does NOT read `## 179.` headings as references: a heading is the
/// target, not a pointer at one, and counting it would make every section resolve to
/// itself.
pub fn references(text: &str) -> Vec<(usize, RefKind)> {
let mut out = Vec::new();
for line in text.lines() {
if line.starts_with("## ") {
continue;
}
let low = line.to_lowercase();
for (needle, kind) in [
("&sect;", RefKind::Symbol),
("\u{a7}", RefKind::Symbol),
("section ", RefKind::Word),
] {
for (i, _) in low.match_indices(needle) {
let rest = low[i + needle.len()..].trim_start();
let n: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if let Ok(v) = n.parse::<usize>() {
out.push((v, kind));
}
}
}
}
out
}

/// A reference written with no number at all: `(&sect;—the same rule …)`.
///
/// One of these is real and in the file. It resolves to nothing and never will, and a
/// count of dangling NUMBERS cannot see it, because there is no number to fail to
/// resolve.
pub fn empty_references(text: &str) -> usize {
let mut n = 0;
for line in text.lines() {
if line.starts_with("## ") {
continue;
}
for needle in ["&sect;", "\u{a7}"] {
for (i, _) in line.match_indices(needle) {
let rest = &line[i + needle.len()..];
if !rest.trim_start().starts_with(|c: char| c.is_ascii_digit()) {
n += 1;
}
}
}
}
n
}

fn refs(list: bool) -> Result<()> {
let root = repo_root()?;
let files = skill_files(&root);
println!("CROSS-REFERENCES IN THE SKILLS, AND WHETHER THEY RESOLVE\n");
let mut any_dead = false;
for f in &files {
let Ok(text) = std::fs::read_to_string(f) else {
continue;
};
let have: std::collections::BTreeSet<usize> =
section_bodies(&text).iter().map(|(n, _, _)| *n).collect();
if have.is_empty() {
continue;
}
let name = f
.parent()
.and_then(|p| p.file_name())
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
let all = references(&text);
let dead: Vec<&(usize, RefKind)> = all.iter().filter(|(n, _)| !have.contains(n)).collect();
let empty = empty_references(&text);
let sym = all.iter().filter(|r| r.1 == RefKind::Symbol).count();
println!(" {name}");
println!(" sections {}", have.len());
println!(
" references {} ({sym} by symbol, {} written out)",
all.len(),
all.len() - sym
);
println!(" with no number at all {empty} a count of dangling NUMBERS cannot see these");
let mut nums: Vec<usize> = dead.iter().map(|(n, _)| *n).collect();
nums.sort_unstable();
nums.dedup();
println!(
" POINTING AT NOTHING {} across {} distinct numbers",
dead.len(),
nums.len()
);
if !nums.is_empty() {
any_dead = true;
println!(" never existed: {nums:?}");
for (i, line) in text.lines().enumerate() {
if line.starts_with("## ") {
continue;
}
for (n, _) in references(line) {
if !have.contains(&n) {
let t = line.trim();
println!(" {}:{} {}", name, i + 1, &t[..t.len().min(92)]);
break;
}
}
}
}
if list {
let mut seen: Vec<usize> = all.iter().map(|(n, _)| *n).collect();
seen.sort_unstable();
seen.dedup();
println!(" all targets: {seen:?}");
}
}

println!(
"\n A pointer at a section that does not exist is not a broken link; it is a\n \
claim about what this file says, and the claim is false. The numbers here are\n \
the fingerprint of a renumbering: a block of consecutive missing targets means\n \
the sections moved and the pointers did not.\n\n \
References written OUT (`section 179`) are counted apart from the symbol form\n \
because the words can also be about a document that is not this one -- so a\n \
dangling count drawn only from the symbols is the conservative reading, and\n \
both are printed rather than merged."
);
if any_dead {
println!(
"\n Reported, not failed: fixing a pointer means deciding what it MEANT,\n \
and that is a reading, not a rename."
);
}
Ok(())
}

#[cfg(test)]
mod reference_tests {
use super::{empty_references, references, RefKind};

#[test]
fn both_spellings_of_a_pointer_are_found_and_kept_apart() {
let t = "Related: &sect;234 for the ruler, and section 179 says otherwise.\n";
assert_eq!(
references(t),
vec![(234, RefKind::Symbol), (179, RefKind::Word)]
);
}

/// A HEADING is the target, not a pointer at one. Counting it would make every
/// section resolve to itself and the dangling count would always be zero.
/// The literal section sign is the same pointer as the HTML entity, and both are
/// the SYMBOL form. A mutation filing the sign under the written-out form survived
/// until a fixture used the character itself.
#[test]
fn the_section_sign_and_the_entity_are_one_kind() {
assert_eq!(
references("see \u{a7}234 now\n"),
vec![(234, RefKind::Symbol)]
);
assert_eq!(
references("see &sect;234 now\n"),
vec![(234, RefKind::Symbol)]
);
}

#[test]
fn a_heading_is_not_a_reference() {
assert!(references("## 179. A `--limit` on a run list\n").is_empty());
assert_eq!(
references("## 179. See &sect;234\n"),
vec![],
"the whole heading line is skipped, targets included"
);
}

/// Several pointers on one line all count -- the first version searched with
/// `find` and would have seen only one, which is the defect section 465 records.
#[test]
fn every_pointer_on_a_line_counts_not_the_first() {
let t =
"Related: &sect;234 for the ruler, &sect;235 for the check, &sect;241 for the guard.\n";
assert_eq!(references(t).len(), 3);
}

/// `(&sect;—the same rule …)` resolves to nothing and never will. A count of
/// dangling NUMBERS cannot see it, because there is no number to fail.
#[test]
fn a_pointer_with_no_number_is_counted_separately() {
let t = "(&sect;\u{2014}the same rule the widths ledger states)\n";
assert!(references(t).is_empty(), "no number, so no target");
assert_eq!(empty_references(t), 1);
}

#[test]
fn a_numbered_pointer_is_not_an_empty_one() {
assert_eq!(empty_references("see &sect;234 and \u{a7}235\n"), 0);
assert_eq!(
empty_references("## 234. heading &sect;x\n"),
0,
"headings skipped"
);
}

/// The word form needs the space, or `sections` and `sectional` would each
/// contribute a phantom pointer to whatever digits followed.
#[test]
fn the_word_form_requires_the_separator() {
assert_eq!(references("section 12 says\n"), vec![(12, RefKind::Word)]);
assert!(references("sections12 says\n").is_empty());
// The discriminating input, and the only one the trailing space earns its
// place on: digits glued straight to the word. Without the space this reads
// as a pointer at section 12; a mutation dropping it survived until this
// line existed.
assert!(
references("section12 says\n").is_empty(),
"`section12` is a word, not a pointer at 12"
);
assert!(references("section twelve says\n").is_empty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# NOW -- A pointer at a section that does not exist is a false claim (2026-09-03)

## `tri skill refs` (Refs #2994)

- resolves every cross-reference in the skills against the sections that exist: **436 sections, 189 references** (179 by symbol, 10 written out), **12 pointing at nothing across 7 distinct numbers**, and **6 references carrying no number at all**
- **the numbers are a fingerprint.** `234, 235, 240, 241, 245, 253` is a consecutive block inside the never-used 226-260 gap, and the sections those pointers describe are alive at **+47**: 234->281, 241->288, 253->300, each verified by reading what the pointer SAYS the target says. A renumbering moved the sections and left the pointers
- a dangling pointer is not a broken link. `Related: 241, a guard whose precondition had stopped holding` is a claim about what this file contains, and the claim is false -- worse than a missing one, because a reader who does not check believes it
- two details a count would hide, both printed: **six references carry no number at all** (`(&sect;--the same rule the widths ledger states...)`), which a number-resolver cannot see; and the written-out form is counted **apart** from the symbol form, because the words can be about another document
- it reports and does not fail. **Fixing a pointer means deciding what it MEANT**, and that is a reading, not a rename

## Three pairs of sections contradict each other (Refs #2994)

- **19 against 23:** same `coverage` gate, same breakdown (*99 orphaned by a rename, 81 with a current twin*) under two totals, **136** and **121**. Nothing marks the change, and which is right cannot be told: neither names an anchor and the seal state is a sliding population, so both may have been true when written
- **369 against 370:** 369 says *"Zero. Fixing it perfectly moves the accept count by nothing"*; 370 says *"+68, honest ... the largest single lever in the project"*. 370 is right **and it corrects the wrong section** -- it opens *"Section 366 says..."* while 366 is about `tri prose report` against `tri unparsed report`. The sentence it quotes is 369's
- so one correction produced two defects: **369 is left standing uncorrected, and 366 is blamed for a sentence it never wrote.** A correction aimed at the wrong target is worse than none
- **281 against 290:** 281 says a bracket-depth-zero reading *"gets both conventions right at once"*; 290 says there are three conventions and depth zero *"finds no definition at all in 231 of 650 specs"*. 290 is right; 281 carries no marker
- **the file already has the mechanism and did not use it.** Two sections carry an in-place `**RETRACTED, see N.**`, and 34 rules the marker goes at the top of the paragraph it retracts. None of 281, 369 or 366 has one
- reported, not repaired. **That is exactly why the anchor rule exists**: had 19 and 23 each named a sha, this would be a history rather than a contradiction
Loading