use std::path::{Path, PathBuf};
fn notes() -> String {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("CLAUDE.md");
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("reading {}: {e}", path.display()))
.replace("\r\n", "\n")
}
fn sources() -> String {
fn walk(dir: &Path, into: &mut String) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, into);
} else if path.extension().is_some_and(|e| e == "rs")
&& let Ok(text) = std::fs::read_to_string(&path)
{
into.push_str(&text);
into.push('\n');
}
}
}
let mut all = String::new();
walk(&Path::new(env!("CARGO_MANIFEST_DIR")).join("src"), &mut all);
all
}
fn cited_test_names(notes: &str) -> Vec<String> {
let mut found = Vec::new();
for chunk in notes.split('`').skip(1).step_by(2) {
let looks_like_a_name = !chunk.is_empty()
&& chunk.starts_with(|c: char| c.is_ascii_lowercase())
&& chunk
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
if looks_like_a_name && chunk.matches('_').count() >= 4 {
found.push(chunk.to_string());
}
}
found.sort();
found.dedup();
found
}
fn cited_paths(notes: &str) -> Vec<PathBuf> {
let roots = ["src/", "assets/", "docs/", ".github/"];
let mut found = Vec::new();
for chunk in notes.split('`').skip(1).step_by(2) {
if roots.iter().any(|r| chunk.starts_with(r))
&& chunk
.chars()
.all(|c| c.is_ascii_alphanumeric() || "._/-".contains(c))
{
found.push(PathBuf::from(chunk));
}
}
found.sort();
found.dedup();
found
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_test_the_notes_name_still_exists() {
let sources = sources();
let missing: Vec<String> = cited_test_names(¬es())
.into_iter()
.filter(|name| !sources.contains(&format!("fn {name}")))
.collect();
assert!(
missing.is_empty(),
"CLAUDE.md names {} test(s) that do not exist: {missing:?}\n\n\
Either the test was renamed and the notes still cite the old name, \
or the claim around it was never true. Fix whichever it is — do not \
delete the citation to make this pass, because the sentence around \
it is asserting that something is pinned.\n\n\
If one of these is not a test at all but a long snake_case name \
this check mistook for one, rename it in the notes or widen the \
filter deliberately.",
missing.len()
);
}
#[test]
fn the_released_version_in_the_notes_matches_the_manifest() {
let notes = notes();
let claimed = notes
.lines()
.find_map(|line| {
let rest = line.strip_prefix("- **`")?;
let (version, tail) = rest.split_once('`')?;
tail.starts_with(" is released**")
.then(|| version.to_string())
})
.expect(
"CLAUDE.md must carry a line of the form \"- **`X.Y.Z` is released**\"; \
if that housekeeping note was reworded, reword this check with it",
);
assert_eq!(
claimed,
env!("CARGO_PKG_VERSION"),
"CLAUDE.md says {claimed} is released; Cargo.toml says {}. \
The version bump and the note belong in the same commit.",
env!("CARGO_PKG_VERSION")
);
}
#[test]
fn every_repository_path_the_notes_cite_exists() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let missing: Vec<PathBuf> = cited_paths(¬es())
.into_iter()
.filter(|path| !root.join(path).exists())
.collect();
assert!(
missing.is_empty(),
"CLAUDE.md cites {} path(s) that are not there: {missing:?}\n\n\
Something was moved or renamed and the notes were not brought along.",
missing.len()
);
}
#[test]
fn every_released_version_has_a_changelog_link() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("CHANGELOG.md");
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("reading {}: {e}", path.display()))
.replace("\r\n", "\n");
let versions: Vec<String> = text
.lines()
.filter_map(|line| line.strip_prefix("## ["))
.filter_map(|rest| rest.split(']').next())
.filter(|v| v.starts_with(|c: char| c.is_ascii_digit()))
.map(str::to_string)
.collect();
assert!(
versions.len() > 10,
"only {} version headings found — the format probably changed and \
this check has stopped looking at anything",
versions.len()
);
let missing: Vec<&String> = versions
.iter()
.filter(|v| !text.contains(&format!("\n[{v}]: http")))
.collect();
assert!(
missing.is_empty(),
"{} released version(s) have no link definition: {missing:?}\n\n\
Add `[X.Y.Z]: <compare url>` at the foot of CHANGELOG.md beside \
the others. Without it the heading renders as literal brackets.",
missing.len()
);
let newest = versions.first().expect("at least one version");
assert!(
text.contains(&format!(
"[Unreleased]: https://github.com/jchultarsky/mirador/compare/v{newest}...HEAD"
)),
"[Unreleased] should compare against v{newest}, the newest release. \
A stale one presents shipped work as unreleased, which is worse \
than a missing link."
);
}
#[test]
fn every_module_is_named_in_the_architecture_map() {
fn directory_entry(map: &str, directory: &str) -> String {
let heading = format!("{directory}/");
let mut entry = String::new();
let mut found = false;
for line in map.lines() {
if line.starts_with(&heading) {
found = true;
entry.push_str(line);
} else if found && line.starts_with(' ') {
entry.push(' ');
entry.push_str(line.trim());
} else if found {
break;
}
}
entry
}
fn names_module(entry: &str, file: &str) -> bool {
let stem = file.strip_suffix(".rs").unwrap_or(file);
entry
.split(|character: char| {
!(character.is_ascii_alphanumeric() || matches!(character, '_' | '.'))
})
.any(|word| word == file || word == stem)
}
fn walk(
root: &Path,
dir: &Path,
map: &str,
inspected: &mut usize,
missing: &mut Vec<String>,
) {
for entry in std::fs::read_dir(dir)
.expect("source directory must be readable")
.flatten()
{
let path = entry.path();
if path.is_dir() {
walk(root, &path, map, inspected, missing);
continue;
}
if path.extension().is_none_or(|extension| extension != "rs") {
continue;
}
let relative = path
.strip_prefix(root)
.expect("walked source stays below src")
.to_string_lossy()
.replace('\\', "/");
if relative == "main.rs" {
continue;
}
*inspected += 1;
let Some((directory, file)) = relative.split_once('/') else {
if !map.contains(&relative) {
missing.push(relative);
}
continue;
};
let entry = directory_entry(map, directory);
if entry.is_empty() || (file != "mod.rs" && !names_module(&entry, file)) {
missing.push(relative);
}
}
}
let notes = notes();
let map = notes
.split("## Architecture")
.nth(1)
.and_then(|rest| rest.split("```").nth(1))
.expect("CLAUDE.md must carry an ```-fenced map under `## Architecture`");
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut missing = Vec::new();
let mut inspected = 0usize;
walk(&root, &root, map, &mut inspected, &mut missing);
missing.sort();
assert!(
inspected > 40,
"the architecture sweep saw only {inspected} modules; it must recurse into \
config/, plugin/ and widgets/ rather than pass on top-level files alone"
);
assert!(
missing.is_empty(),
"the architecture map does not mention {}: {missing:?}\n\n\
Add a line for each. The map is what a reader consults first, and \
a module missing from it is a module they will not know exists — \
which is exactly how three of them went unlisted at once.",
missing.len()
);
}
#[test]
fn the_check_is_actually_looking_at_something() {
let names = cited_test_names(¬es());
assert!(
names.len() >= 10,
"only {} test name(s) recognised in CLAUDE.md — the filter has \
probably stopped matching, and a check that inspects nothing \
passes for the wrong reason: {names:?}",
names.len()
);
assert!(
!cited_paths(¬es()).is_empty(),
"no repository paths recognised in CLAUDE.md; same concern"
);
}
}