feat(ci): guard against source files that nothing declares (refs #2473) - #2501
Merged
noahgift merged 1 commit intoAug 15, 2026
Conversation
…defect class was invisible to every gate we run #2473 deleted 15 orphaned files from aprender-test-lib. Nothing had noticed them for 15 months, because nothing looks. A .rs file under src/ that no mod, include!() or #[path] declares is never compiled, and every gate we run is blind to it: fmt skips it, clippy skips it, the test count does not move, and coverage cannot report on a file that was never built. The only signal is that mutating it does not turn the build RED, which no gate was asking. This adds that gate, and it surfaces that the class is much wider than #2473. WHAT IT FOUND 91 orphaned .rs files across 12 crates, concentrated in aprender-serve (65), aprender-shell (7), aprender-test-cli (3), aprender-present-terminal (3), apr-cli (3). Eight are *_tests.rs; one is literally named tests_conversion_stats_orphan.rs. These are recorded as a baseline, not fixed here: each needs its own read to decide wire-up vs delete, and bulk-deleting 91 files on inference is exactly the move this guard exists to prevent. A sample was verified by mutation rather than inspection, appending an invalid Rust line and re-running cargo check, exit status captured directly to a variable: crates/aprender-test-cli/src/main_tests.rs exit 0 DEAD crates/aprender-test-cli/src/commands_tests.rs exit 0 DEAD crates/aprender-shell/src/cli_dispatch.rs exit 0 DEAD src/bin/apr.rs exit 101 LIVE That last one is why src/bin/** is exempt: auto-discovered binary targets are claimed by cargo, never by a mod line. The exemption is not an assumption, it is a measurement. HOW IT IS PROVEN Against the pre-deletion tree the guard reports exactly the 15 files of #2473 and nothing else. Restoring them turns it RED; the state this PR sits on is GREEN. The self-test is an 11-case must-flag / must-not-flag table over a hermetic fixture, covering every claim form (mod, pub(crate) mod, pub (in path) mod, include!, #[path], Cargo.toml path=, mod.rs, src/bin) and the regex's known confusables: a commented-out // mod ghost; and an inline mod ghost { } with no semicolon must NOT claim their files. Plus three ratchet mutations proving the baseline comparison fails on a missing entry and on a stale one. The self-test was then itself mutated three ways: dropping the regex's line-start anchor, removing the src/bin exemption, and disabling the stale-baseline check. Each turns the self-test RED with the correct specific message. The implementation was later restructured from associative arrays to sorted-file join, so all three mutations were re-run against the new code rather than assuming the old proof transferred. Two bugs were caught this way and would not have been caught by reading: 1. The first version re-invoked itself as "$0" after scan_tree had cd'd elsewhere, and the script was not executable. Two of the three ratchet mutations were passing on "Permission denied" rather than on the ratchet working. Fixed by an absolute SCRIPT_PATH and invoking via bash. 2. A `local scratch` plus `trap ... EXIT` produced "scratch: unbound variable", the exact pitfall documented in check_contract_test_binding.sh. Now a checked global, with the note repeated at the site. VACUITY The guard refuses to pass unless it scanned at least MIN_FILES files across MIN_CRATES crates, so a collapsed universe cannot read as a clean tree. Its universe is built by find over the filesystem, and the defect (a missing mod line) cannot remove a file from that universe, which is the correct side to build it from. WIRING ci.yml guard-runner-labels job, self-test first then the tree; and make tier3. Text-only, no build, ~1.5s on 6965 files across 93 crates. bashrs lint: 0 errors. Refs #2473 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
noahgift
merged commit Aug 15, 2026
870ceb4
into
chore/delete-dead-duplicate-test-files
1 check passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #2473. Stacked on #2498 — base is
chore/delete-dead-duplicate-test-files, because this guard is RED onmainuntil #2498 lands (it reports exactly those 15 files). Merge #2498 first.Why
#2473 deleted 15 orphaned files. Nothing had noticed them for 15 months, because nothing looks.
A
.rsfile undersrc/that nomod/include!()/#[path]declares is never compiled — and every gate we run is blind to it.fmtskips it.clippyskips it. The test count does not move. Coverage cannot report on a file that was never built. The only signal is that mutating it does not turn the build RED, which no gate was asking.This adds that gate.
What it found
The class is much wider than #2473: 91 orphaned
.rsfiles across 12 crates.Eight are
*_tests.rs. One is literally namedtests_conversion_stats_orphan.rs.These are recorded as a baseline, not fixed here. Each needs its own read to decide wire-up vs. delete, and bulk-deleting 91 files on inference is exactly the move this guard exists to prevent.
A sample was verified by mutation, not inspection — invalid Rust appended,
cargo checkre-run, exit status captured directly to a variable:crates/aprender-test-cli/src/main_tests.rscrates/aprender-test-cli/src/commands_tests.rscrates/aprender-shell/src/cli_dispatch.rssrc/bin/apr.rsThat last row is why
src/bin/**is exempt — auto-discovered binary targets are claimed by cargo, never by amodline. The exemption is a measurement, not an assumption.How it is proven
Against the real defect: on the pre-deletion tree the guard reports exactly the 15 files of #2473 and nothing else. Restoring them turns it RED; the state this PR sits on is GREEN.
Self-test — an 11-case must-flag / must-not-flag table over a hermetic fixture, covering every claim form (
mod,pub(crate) mod,pub (in path) mod,include!,#[path], Cargo.tomlpath=,mod.rs,src/bin) and the regex's known confusables: a commented-out// mod ghost;and an inlinemod ghost { }with no semicolon must not claim their files. Plus three ratchet mutations proving the baseline comparison fails on a missing entry and on a stale one.The self-test was itself mutated three ways — dropping the regex's line-start anchor, removing the
src/binexemption, disabling the stale-baseline check. Each turns it RED with the correct specific message. The implementation was later restructured (associative arrays → sorted-filejoin), so all three were re-run against the new code rather than assuming the old proof transferred.Two bugs were caught this way that reading would not have caught:
"$0"afterscan_treehadcd'd elsewhere, and the script was not executable. Two of the three ratchet mutations were passing onPermission denied, not on the ratchet working.local scratch+trap … EXITproducedscratch: unbound variable— the exact pitfall documented incheck_contract_test_binding.sh. Now a checked global, with the note repeated at the site.Vacuity
Refuses to pass unless it scanned ≥
MIN_FILESfiles across ≥MIN_CRATEScrates, so a collapsed universe cannot read as a clean tree. Its universe is built byfindover the filesystem, and the defect — a missingmodline — cannot remove a file from that universe, which is the correct side to build it from.Known limits, stated plainly
This is a syntactic reachability check, one level deep. It proves a file is named somewhere, not that the namer is itself reachable from the crate root — a cluster of orphans that all declare each other would still pass. Deliberate trade: cheap, runs on every PR, catches the class that actually occurs. True reachability needs the compiler, and the compiler's answer is the mutation test, which cannot run in a gate.
Wiring
ci.ymlguard-runner-labelsjob (self-test first, then the tree) andmake tier3. Text-only, no build, ~1.5s on 6965 files across 93 crates.bashrs lint: 0 errors.🤖 Generated with Claude Code