fix(coding-dev): prefer a test over the build script for size limits

A shell check only runs where that script runs. A test travels with the repo,
needs no extra tooling, and fails with a message naming the offending files.
Enforcement is now a preference order: native linter rule > test > build
script as fallback, with a worked example and a note on why a per-compile
build hook usually is not worth the incremental-build cost.

Propagated per language: rust (clippy has no file-length lint -> workspace
root test), js-typescript (ESLint max-lines covers both natively, no test
needed), go/python/csharp (linter for functions, test for files), bash (don't
reimplement in the gate what a test can assert).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
BluscreamandClaude Opus 5 committed 2026-09-18 12:52:37 +02:00
1 parent 5d96e69d2b
commit 32b47b075b
7 files changed
+60 -8

No files matched your search

+43 -4
View File
@@ -142,10 +142,49 @@ depends on those.
| **Function parameters** | 3–4 | — | ~5, then pass a struct/object |
| **Nesting depth** | 2 | — | 3, then extract |
- **Enforce the function limit with the linter, not by eye** (e.g. Clippy's `too_many_lines`).
A function that trips it wants extracting, not a suppression.
- **Enforce the file limit in the build script** — a check that fails over the hard limit and
*names* anything past the soft split point, so the seam gets found while splitting is cheap.
### Enforce both mechanically, never by eye
A limit nobody checks is a suggestion. Pick the **highest** applicable option:
1. **A linter rule**, where one exists — the function limit is usually covered natively
(Clippy's `too_many_lines`, ESLint's `max-lines-per-function`, `golangci-lint`'s `funlen`,
Ruff's `PLR0915`). A function that trips it wants extracting, not a suppression.
2. **A test**, for anything the linter does not cover — file length almost always falls here.
A test is the best home for it: it runs with the suite everyone already runs, it needs no
extra tooling, it fails with a message naming the offending files, and it travels with the
repository rather than living in one machine's shell script.
3. **The build script**, only as a fallback when the project has no test suite to hang it on.
Whichever you use, it should **fail over the hard limit** and separately *name* anything past
the soft split point — so the seam gets found while splitting is still cheap, rather than at
the moment the hard limit finally trips.
```rust
#[test]
fn no_source_file_exceeds_limits() {
const HARD: usize = 1000;
const WARN: usize = 600;
let mut over = Vec::new();
for path in source_files() {
let lines = std::fs::read_to_string(&path).unwrap().lines().count();
if lines > HARD {
over.push(format!("{} — {lines} lines (limit {HARD})", path.display()));
} else if lines > WARN {
// Not a failure: surfaces the seam while splitting is still cheap.
eprintln!("note: {} is {lines} lines, approaching the {HARD} limit", path.display());
}
}
assert!(over.is_empty(), "files over the size limit:\n{}", over.join("\n"));
}
```
> [!NOTE]
> A test runs with the suite, not on every compile. If you genuinely need the check on every
> build, most toolchains have a pre-build hook for it (Cargo's `build.rs`, an MSBuild target,
> a bundler plugin) — but that cost is paid on every incremental build forever, and the suite
> is usually soon enough. Reach for the hook only if oversized files keep landing anyway.
- **Split by concern, never by line count.** A large `strip.rs` becomes
`strip/fader.rs`, `strip/knobs.rs`, `strip/routing.rs` — never `strip1.rs` / `strip2.rs`.
- One coherent responsibility per module. A module that draws widgets should not also own