Skip to content

feat(ci): guard against source files that nothing declares (refs #2473) - #2501

Merged
noahgift merged 1 commit into
chore/delete-dead-duplicate-test-filesfrom
chore/guard-orphaned-source-files
Aug 15, 2026
Merged

feat(ci): guard against source files that nothing declares (refs #2473)#2501
noahgift merged 1 commit into
chore/delete-dead-duplicate-test-filesfrom
chore/guard-orphaned-source-files

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

Refs #2473. Stacked on #2498 — base is chore/delete-dead-duplicate-test-files, because this guard is RED on main until #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 .rs file under src/ that no mod / include!() / #[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. 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 .rs files across 12 crates.

Crate Orphans
aprender-serve 65
aprender-shell 7
aprender-test-cli 3
aprender-present-terminal 3
apr-cli 3
7 others 1 each

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, not inspection — invalid Rust appended, cargo check re-run, exit status captured directly to a variable:

File Exit Verdict
crates/aprender-test-cli/src/main_tests.rs 0 DEAD
crates/aprender-test-cli/src/commands_tests.rs 0 DEAD
crates/aprender-shell/src/cli_dispatch.rs 0 DEAD
src/bin/apr.rs 101 LIVE

That last row is why src/bin/** is exempt — auto-discovered binary targets are claimed by cargo, never by a mod line. 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.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 itself mutated three ways — dropping the regex's line-start anchor, removing the src/bin exemption, disabling the stale-baseline check. Each turns it RED with the correct specific message. The implementation was later restructured (associative arrays → sorted-file join), 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:

  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, not on the ratchet working.
  2. local scratch + 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

Refuses to pass unless it scanned ≥ 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.

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.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.

🤖 Generated with Claude Code

…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
noahgift merged commit 870ceb4 into chore/delete-dead-duplicate-test-files Aug 15, 2026
1 check passed
@noahgift
noahgift deleted the chore/guard-orphaned-source-files branch August 15, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant