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
69 changes: 69 additions & 0 deletions .claude/skills/ci-gates/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11440,3 +11440,72 @@ lines changed, 20 of them deletions, for a two-line wiring change.
**The check that catches it is the shape of the diff, not the list of files.**
A wiring change that adds a subcommand has no deletions; when the stat shows
some, the question is what else the formatter decided while it was in there.

## 448. Subtract sets, not strings -- and the gap was an undocumented threshold

§446 shipped **121** where an independent probe said **123**, and said the gap
was not resolved. Four attempts to locate it had failed, and every one failed
the same way: the comparison matched **truncated titles**. That is a defect in
the comparison, not in either reader, and it survives four tries because a
title-prefix match is *almost* right -- it finds most rows and quietly misses
the ones whose text was cut differently.

The fix is one flag. `tri skill claims --numbers` prints one
`<skill>:<number>` per counted section and nothing else, so the comparison is

```sh
comm -13 <(sort -n rust.txt) <(sort -n probe.txt)
```

Two lines of output, first try: sections **54** and **303**.

**When two readers of one population disagree, compare the IDENTITIES they
counted, not renderings of them.** A count has an index; use it. Every failed
attempt here was a string comparison standing in for a set operation.

**And the cause was worth the hunt.** Both sections carry a *single* digit --
"the gate that exited 0", "Typecheck FAILED, exit 0" -- and `carries()` requires
a run of **two or more**. That threshold was in the code with no comment, doing
work the documented rule (a word boundary on both sides) does not claim.

Measured on 485 open issues: the threshold excludes **20 titles**, and they are
not one kind of thing.

* About a dozen state a **count**: *"`implies` appears 9 times in live source
and 0 times in the compiler"*, *"MAX_SORRY counts 5 admitted proofs; 4 are in
files nothing compiles"*, *"4 of 7 passes have no precondition"*.
* The rest state a **value**: an exit code (`seal exits 0`), a literal (`the
lexer turns 0o777 into 0`), arithmetic (`-3/2 is -1, -3>>1 is -2`).

So the threshold is a crude proxy for *not a value*: wrong in one direction, and
dropping it takes the population **288 → 308** while admitting about eight
titles that count nothing. It stays -- and it is now **printed**, with
`--single` to list what it removes.

**A silent threshold makes a population read as complete.** The repair for a
rule you cannot cleanly justify is not always to delete it; where the
alternative is measurably worse, state it, size it, and let the reader see the
set it removed.

## 449. Widening a population is safe exactly when you measured it first

§446 named three tracked `SKILL.md` files outside `.claude/skills` and did not
read them, and the option written for the next pass said widening was blocked on
a question: *are they copies, forks, or the originals?* -- because counting
copies would double every figure.

Measured, in two commands: `.agents/skills/phi-loop/SKILL.md` and
`.agents/skills/tri-pipeline/SKILL.md` are **byte-identical** to their
`.claude/skills` counterparts, and all three unread files carry **zero**
numbered sections. So the worry was about an empty set: widening the walk to
every tracked `SKILL.md` leaves the figure count **unchanged at 123**.

The widening shipped, and so did the guard the worry deserved: byte-identical
files are detected, **named in the output**, and counted once. They contribute
nothing today; the day one of them gains a section, the count would otherwise
double it in silence.

**The pattern: a blocked option is often blocked on a measurement rather than a
decision.** Two `shasum` calls and a `grep -c` turned "this needs an owner's
call about which directory is authoritative" into "this changes nothing, and
here is the guard for the day it would".
144 changes: 142 additions & 2 deletions cli/tri/src/issues.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ pub enum IssuesCmd {
},
/// Open issues that state a COUNT in the title, and a reproducible sample.
Numbers {
/// Print the titles excluded by the two-digit rule alone.
#[arg(long)]
single: bool,
/// Print a systematic sample of this size. 0 prints only the population.
#[arg(long, default_value_t = 0)]
sample: usize,
Expand Down Expand Up @@ -164,6 +167,48 @@ pub fn named_workflows(text: &str, keys: &BTreeMap<String, String>) -> Vec<Strin
out
}

/// Titles excluded from the population by the two-digit rule alone.
///
/// The digit rule requires a run of **two or more** digits. That threshold was
/// never documented and it does real work: 20 open titles carry a single-digit
/// figure and nothing else, and they are not one kind of thing. Roughly a dozen
/// state a count -- *"`implies` appears 9 times in live source and 0 times in
/// the compiler"*, *"MAX_SORRY counts 5 admitted proofs; 4 are in files nothing
/// compiles"*, *"4 of 7 passes have no precondition"*. The rest state a VALUE:
/// an exit code (`seal exits 0`), a literal (`the lexer turns 0o777 into 0`),
/// or arithmetic (`-3/2 is -1, -3>>1 is -2`).
///
/// So the threshold is a crude proxy for *not a value*, wrong in one direction,
/// and removing it takes the population from **288 to 308** while adding about
/// eight titles that state no count. It is kept -- and it is now **printed**.
/// A silent threshold makes 288 read as the whole population; a stated one
/// makes it read as 288 plus a named 20 that a reader can judge.
pub fn single_digit_only(title: &str) -> bool {
let t = strip_addresses(title);
let c: Vec<char> = t.chars().collect();
let (mut i, mut one, mut two) = (0usize, false, false);
while i < c.len() {
if !c[i].is_ascii_digit() {
i += 1;
continue;
}
let start = i;
while i < c.len() && c[i].is_ascii_digit() {
i += 1;
}
let left = start == 0 || !c[start - 1].is_alphanumeric();
let right = i >= c.len() || !c[i].is_alphanumeric();
if left && right {
if i - start >= 2 {
two = true;
} else {
one = true;
}
}
}
one && !two && !NUMERALS.iter().any(|w| has_word(&t, w))
}

/// What kind of number a title carries.
///
/// The distinction is the whole point. `Wave Loop 369` and `#2841` and `Prop. 65`
Expand Down Expand Up @@ -341,6 +386,9 @@ pub fn carries(title: &str) -> Carries {
if i - start >= 2 && left && right {
found = true;
}
// A single digit with the boundary satisfied is NOT counted here.
// The threshold is deliberate and measured; `single_digit_only`
// carries the reason and the twenty titles it excludes.
}
found
};
Expand All @@ -365,7 +413,7 @@ pub fn carries(title: &str) -> Carries {
/// sample is SYSTEMATIC -- every k-th issue by ascending number -- rather than
/// chosen. Nothing here is random: run it next month and the overlap is exact
/// wherever the backlog has not moved.
fn numbers(sample: usize, limit: usize) -> Result<()> {
fn numbers(sample: usize, limit: usize, single: bool) -> Result<()> {
let lim = limit.to_string();
let raw = gh(&[
"issue",
Expand Down Expand Up @@ -413,6 +461,12 @@ fn numbers(sample: usize, limit: usize) -> Result<()> {
c(Carries::QuantifierOnly)
);
println!(" no figure {}", c(Carries::None));
let singles: Vec<&(u64, String, Carries)> =
rows.iter().filter(|r| single_digit_only(&r.1)).collect();
println!(
" single-digit only, excluded {} (--single prints them)",
singles.len()
);

println!(
"\n An ADDRESS is not a count. `#2841`, `Wave Loop 369`, `Prop. 65`, `w699`\n \
Expand All @@ -428,6 +482,26 @@ fn numbers(sample: usize, limit: usize) -> Result<()> {
c(Carries::Words)
);

println!(
"\n The digit rule requires TWO or more digits, and that threshold was\n \
never written down. Measured: it excludes {} titles, and they are not\n \
one kind of thing. Roughly a dozen state a count -- \"`implies` appears\n \
9 times in live source and 0 times in the compiler\" -- and the rest\n \
state a VALUE: an exit code, a literal, arithmetic. Dropping the\n \
threshold takes the population to {} and admits about eight titles that\n \
count nothing. It is kept, and now it is PRINTED: a silent threshold\n \
makes this population read as complete.",
singles.len(),
pop.len() + singles.len()
);

if single {
println!("\n EXCLUDED BY THE TWO-DIGIT RULE ALONE:\n");
for (n, t, _) in singles.iter().copied() {
println!(" #{n} {}", &t[..t.len().min(88)]);
}
}

if sample > 0 {
let k = pop.len().checked_div(sample).unwrap_or(1).max(1);
let picked: Vec<&&(u64, String, Carries)> = pop.iter().step_by(k).take(sample).collect();
Expand Down Expand Up @@ -491,7 +565,11 @@ struct Row {

pub fn run(cmd: &IssuesCmd) -> Result<()> {
let limit = match cmd {
IssuesCmd::Numbers { sample, limit } => return numbers(*sample, *limit),
IssuesCmd::Numbers {
sample,
limit,
single,
} => return numbers(*sample, *limit, *single),
IssuesCmd::Dated { limit, list } => return dated(*limit, *list),
IssuesCmd::Stale { limit } => limit,
};
Expand Down Expand Up @@ -1103,3 +1181,65 @@ mod dated_tests {
assert_eq!(anchor_of("plain prose", 1), Anchor::Answered);
}
}

#[cfg(test)]
mod single_digit_tests {
use super::*;

/// The twenty titles the two-digit threshold removes, in miniature.
#[test]
fn a_lone_digit_is_excluded_and_said_so() {
// Real ones from this backlog: counts the population does not carry.
assert!(single_digit_only(
"t27c seal exits 0 on a spec every backend rejects"
));
assert!(single_digit_only("4 of 7 passes have no precondition"));
assert!(single_digit_only("parser-fix blocker drops 6 -> 5"));
}

#[test]
fn a_two_digit_run_anywhere_takes_it_out_of_the_excluded_set() {
// It is IN the population, so it is not what this reports.
assert!(!single_digit_only("5 of 36 gates pass an empty tree"));
assert!(!single_digit_only("283 titles state a count"));
}

#[test]
fn a_numeral_word_takes_it_out_too() {
// Already counted as `Words`; reporting it as excluded would double it.
assert!(!single_digit_only("Nine live sites and 0 in the compiler"));
}

#[test]
fn an_address_is_not_a_lone_digit() {
// `#2841` is stripped first; what remains states nothing.
assert!(!single_digit_only(
"Grep before you file -- #2964 duplicated #2822"
));
assert!(!single_digit_only("Wave Loop 369 is an address"));
// A SINGLE-digit address is the case that actually exercises the
// stripping here: without it, `#7` reads as a lone figure. The
// four-digit examples above pass either way, which is why they are
// not a control on their own.
assert!(!single_digit_only("The gate refuses an empty tree -- #7"));
assert!(!single_digit_only("Prop. 5 is an address, not a count"));
}

/// The two sets must not overlap, or the printed totals double-count.
#[test]
fn the_population_and_the_excluded_set_are_disjoint() {
for t in [
"t27c seal exits 0 on a spec every backend rejects",
"4 of 7 passes have no precondition",
"5 of 36 gates pass an empty tree",
"Nine live sites and 0 in the compiler",
"#2964 duplicated #2822",
] {
let in_pop = matches!(carries(t), Carries::Digits | Carries::Words | Carries::Both);
assert!(
!(in_pop && single_digit_only(t)),
"{t:?} is in both the population and the excluded set"
);
}
}
}
10 changes: 8 additions & 2 deletions cli/tri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ enum SkillAction {
/// Print every section in the free population, one line each.
#[arg(long)]
list: bool,
/// Print `<skill>:<number>` for every counted section and nothing else.
#[arg(long)]
numbers: bool,
},
Begin {
#[arg(long)]
Expand Down Expand Up @@ -880,8 +883,11 @@ fn main() -> Result<()> {
SkillAction::Check { gaps } => {
skillnum::run(&skillnum::SkillCmd::Check { gaps: *gaps })?
}
SkillAction::Claims { list } => {
skillnum::run(&skillnum::SkillCmd::Claims { list: *list })?
SkillAction::Claims { list, numbers } => {
skillnum::run(&skillnum::SkillCmd::Claims {
list: *list,
numbers: *numbers,
})?
}
SkillAction::Begin { issue, desc } => cmd_skill_begin(&root, *issue, desc)?,
SkillAction::End => cmd_skill_end(&root)?,
Expand Down
Loading
Loading