git.lucas.co / cce-data-editor
structured data editor
git clone https://git.lucas.co/cce-data-editor.git

tests/doc_claims.rs (6.3K)

  1 //! CLAUDE.md, checked against the crate it describes.
  2 //!
  3 //! An integration test on purpose: anything under `src/` would be counted by
  4 //! its own scans — a `#[cfg(test)]` block added here to measure which modules
  5 //! carry tests promptly made this module one of them, and the line-count
  6 //! figures would drift by however long this file happens to be.
  7 //!
  8 //! Deliberately MIRRORED per crate rather than shared from a helper: each is
  9 //! its own git repository that must build standalone, and this needs nothing
 10 //! but std.
 11 
 12 /// CLAUDE.md's `(~Nk lines)` figures, checked against the files they describe.
 13 ///
 14 /// Those numbers exist to set expectations before opening a file — "this is
 15 /// the big one" — and they drift in total silence, because a stale number
 16 /// reads exactly like a fresh one. A workspace sweep on 2026-09-19 found
 17 /// EVERY size figure in every CLAUDE.md stale, all of them undercounts, the
 18 /// worst by 49% (cce-designer's app.rs, written ~5.4k at 8046 lines).
 19 ///
 20 /// Tolerance is 10%: loose enough that ordinary work does not trip it, tight
 21 /// enough that a file cannot quietly double. When it fails, write the number
 22 /// it reports — that is the whole fix.
 23 ///
 24 /// Deliberately MIRRORED into each crate that carries such a figure rather
 25 /// than shared from a helper: every crate here is its own git repository and
 26 /// must build standalone, and this needs nothing but `std`. Same call
 27 /// `ramp.rs` makes about its cce-ui parser.
 28 pub mod size_claims {
 29     use std::path::{Path, PathBuf};
 30 
 31     /// One `(~Nk lines)` claim: the name as CLAUDE.md spells it, the figure,
 32     /// and whether the claim also calls it the largest file.
 33     fn claims(doc: &str) -> Vec<(String, f64, bool)> {
 34         const TAIL: &str = " lines)";
 35         let mut out = Vec::new();
 36         let mut i = 0;
 37         while let Some(p) = doc[i..].find(TAIL) {
 38             let end = i + p;
 39             i = end + TAIL.len();
 40             let Some(open) = doc[..end].rfind('(') else { continue };
 41             let inner = &doc[open + 1..end];
 42             // "~8k", or "largest file, ~6.9k" — take the last word.
 43             let largest = inner.contains("largest file");
 44             let word = inner.rsplit([' ', ',']).next().unwrap_or("").trim();
 45             let digits = word.trim_start_matches('~');
 46             let value = match digits.strip_suffix('k') {
 47                 Some(k) => k.parse::<f64>().ok().map(|v| v * 1000.0),
 48                 None => digits.parse::<f64>().ok(),
 49             };
 50             // The backticked name immediately before the parenthetical.
 51             let before = &doc[..open];
 52             let name = before.rfind('`').and_then(|e| {
 53                 before[..e].rfind('`').map(|s| before[s + 1..e].to_string())
 54             });
 55             if let (Some(v), Some(n)) = (value, name) {
 56                 if v > 0.0 {
 57                     out.push((n, v, largest));
 58                 }
 59             }
 60         }
 61         out
 62     }
 63 
 64     /// Every `.rs` file under `src/`, as (path, line count).
 65     fn sources(root: &Path) -> Vec<(PathBuf, usize)> {
 66         fn walk(dir: &Path, out: &mut Vec<(PathBuf, usize)>) {
 67             let Ok(entries) = std::fs::read_dir(dir) else { return };
 68             for e in entries.flatten() {
 69                 let p = e.path();
 70                 if p.is_dir() {
 71                     walk(&p, out);
 72                 } else if p.extension().and_then(|x| x.to_str()) == Some("rs") {
 73                     if let Ok(s) = std::fs::read_to_string(&p) {
 74                         out.push((p, s.lines().count()));
 75                     }
 76                 }
 77             }
 78         }
 79         let mut v = Vec::new();
 80         walk(&root.join("src"), &mut v);
 81         v
 82     }
 83 
 84     #[test]
 85     fn test_claude_md_line_counts_match_the_source() {
 86         let root = Path::new(env!("CARGO_MANIFEST_DIR"));
 87         let doc = std::fs::read_to_string(root.join("CLAUDE.md"))
 88             .expect("CLAUDE.md is missing next to Cargo.toml");
 89         let files = sources(root);
 90         let claims = claims(&doc);
 91         assert!(
 92             !claims.is_empty(),
 93             "no `(~N lines)` figure found in CLAUDE.md — either the syntax changed \
 94              and this scan needs updating, or the figures were removed and so \
 95              should this test"
 96         );
 97 
 98         let mut bad: Vec<String> = Vec::new();
 99         for (name, claimed, largest) in &claims {
100             // A path relative to the crate root, else a unique basename.
101             let hits: Vec<&(PathBuf, usize)> = if root.join(name).is_file() {
102                 files.iter().filter(|(p, _)| *p == root.join(name)).collect()
103             } else {
104                 files
105                     .iter()
106                     .filter(|(p, _)| p.file_name().and_then(|x| x.to_str()) == Some(name.as_str()))
107                     .collect()
108             };
109             let [(path, actual)] = hits[..] else {
110                 bad.push(format!("`{name}`: names {} files under src/, cannot check", hits.len()));
111                 continue;
112             };
113             let actual = *actual as f64;
114             let drift = (actual - claimed) / claimed;
115             if drift.abs() > 0.10 {
116                 bad.push(format!(
117                     "`{name}` is documented as ~{} lines but has {} ({:+.0}%) — write ~{}",
118                     round_k(*claimed),
119                     actual as usize,
120                     drift * 100.0,
121                     round_k(actual)
122                 ));
123             }
124             if *largest {
125                 if let Some((big, n)) = files.iter().max_by_key(|(_, n)| *n) {
126                     if big != path {
127                         bad.push(format!(
128                             "`{name}` is called the largest file, but {} has {n} lines",
129                             big.strip_prefix(root).unwrap_or(big).display()
130                         ));
131                     }
132                 }
133             }
134         }
135         assert!(bad.is_empty(), "CLAUDE.md size claims are stale:\n  {}", bad.join("\n  "));
136     }
137 
138     /// "~8k" for 8046, "~3.1k" for 3134, "~950" for 950 — the spelling the
139     /// docs already use, so the failure message can be pasted straight in.
140     fn round_k(n: f64) -> String {
141         if n < 1000.0 {
142             return format!("{}", n.round() as usize);
143         }
144         let k = n / 1000.0;
145         if (k - k.round()).abs() < 0.05 {
146             format!("{}k", k.round() as usize)
147         } else {
148             format!("{k:.1}k")
149         }
150     }
151 }