Files that cannot be read in one piece force re-reads and partial edits, which is the largest avoidable cost in an LLM-assisted workflow. This adds the enforcement side of a 250-line cap covering code AND prose. - file_length/config.py: single source of truth for the cap and its exemptions (generated, markup, vendored, data-ish txt), shared by both the gate and the survey so the two cannot silently disagree. - file_length/check.py: the check itself. Exits 1 and lists violations; adjudication is an exit code, not a judgement call. - scripts/check_file_length.sh: thin wrapper pre-commit calls with staged files. Verified against six fixtures: over-cap code fails, under-cap passes, generated l10n (1417 lines) exempt, vendored pdfjs exempt, the 174800-line wordlist exempt as data, and a 1101-line CLAUDE.md correctly fails since prose is capped too. Baseline at time of writing: 657 files over 250 lines across 29 repos. Each repo carries a refactor_claude_todo.md prompt with its own list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7yZz45ezmzCB89Cv5gAtZ
4.3 KiB
Refactor TODO — enforce the 250-line file cap
This file is a ready-to-use prompt. Paste it to Claude, or open this repo and say "do refactor_claude_todo". It is self-contained: everything needed to execute is below. Generated 2026-08-14 from a measured survey of every repo.
Goal
Every file in this repo must be at most 250 lines — source, tests, and
prose (.md/.txt/.rst/.tex) alike — and must stay that way forever,
enforced by a gate that fails the commit, not by a note anyone can ignore.
Why: a file that cannot be read in one piece forces re-reads and partial edits, which is the single largest avoidable cost in an LLM-assisted workflow. Aim by churn, not size alone — refactoring pays where code is read and changed often (Fowler, refactoring economic benefit).
Scope in this repo
- 39 files currently exceed 250 lines (of 139 eligible files).
- 14,348 lines sit in violation; longest file is 525 lines.
Exempt (do NOT split these):
- generated files —
*.g.dart,*.freezed.dart,*.gr.dart,**/l10n/generated/**, anything with aGENERATEDheader - markup —
.html,.css,.scss - data files —
.json,.yaml,.csv, wordlists and other data-ish.txt(mean line length under 25 chars)
Violations, highest ROI first
ROI = lines x commits in the last year. Work top-down; a long file nobody edits has near-zero payoff and should not be first.
| lines | commits/yr | kind | file |
|---|---|---|---|
| 515 | 6 | code | gatelock/gatelock/_window.py |
| 477 | 3 | code | gatelock/gatelock/tests/test_recovery.py |
| 259 | 5 | code | crdt_sync_dart/lib/src/sync.dart |
| 406 | 3 | code | crdt-sync/crdt_sync/_firebase_auth.py |
| 393 | 3 | code | crdt-sync/crdt_sync/tests/test_firebase_auth.py |
| 363 | 3 | code | crdt-sync/crdt_sync/_sync.py |
| 525 | 2 | code | crdt_sync_dart/test/firebase_auth_rest_test.dart |
| 483 | 2 | code | gatelock/gatelock/_scrollable.py |
| 321 | 3 | code | gatelock/gatelock/_recovery.py |
| 457 | 2 | code | sync_settings_ui/test/sync_settings_screen_test.dart |
| 452 | 2 | code | crdt_sync_dart/test/mirror_store_test.dart |
| 451 | 2 | code | crdt_sync_dart/test/sync_revisions_test.dart |
| 439 | 2 | code | aur-publish/publish.sh |
| 291 | 3 | code | crdt_sync_dart/test/github_client_test.dart |
| 418 | 2 | code | gatelock/gatelock/tests/test_outputs.py |
(24 further files over 250 lines not listed — re-run the survey for the full set.)
How to split
- Python — extract cohesive helpers into sibling modules; keep the public API and imports stable.
- Shell — split into
lib/*.shsourced by a thin entry script. Keepset -euo pipefailin each. - Dart / TypeScript — extract widgets/components into their own files.
- Tests — split by test-group into sibling files
(
foo.test.ts->foo.parsing.test.ts,foo.render.test.ts). Coverage must not drop. - Docs — split into topic files under
docs/with an index. For an oversizedCLAUDE.md, move detail into referenced docs so the always-loaded part shrinks.
Do not game the cap: no one-lining, no deleting tests, no moving code into
an exempt extension, no # noqa-style suppressions.
Make it permanent (required — this is the point)
A refactor without a gate silently regrows. Before this task is done:
- Wire the shared gate
~/utils/scripts/check_file_length.shinto this repo's.pre-commit-config.yamlas a local hook. If the repo has no pre-commit config, add a minimal one. - The hook checks files in the commit (not the whole tree), so unrelated commits never break, and it fails — exit 1, not a warning.
- No baseline file and no allowlist. Those are suppressions.
- If this repo has CI (
.github/workflows), add the same check there so it also fails on push.
Done condition
bash ~/utils/scripts/check_file_length.sh --allfrom this repo root exits 0.- The repo's own test suite and coverage bar are still green.
pre-commit run --files <changed files>passes.- A deliberately over-250-line test file, staged, makes
git commitfail. - For a deployed daemon/app: the entry point still actually runs.
Verify
Run the repo's own suite, then run the program itself and confirm the behaviour is unchanged. Testing is the last step, never the first.