spring-clean · diff
git:20260809.4511906 to git:20260813.85c983d
41 added, 72 removed. Audit F to F.
---
name: spring-clean
description: Deep-clean and simplify a whole repo or codebase, then leave guardrails so it stays clean. Delete dead surfaces, gitignore build artifacts, relocate dev docs, notes, clones and dumps to the project's workspace repo (the assistant-dev home base), split oversized files and functions, unify duplicated logic, draw package boundaries, run a clean-code pass (naming, function size, DRY, KISS, magic numbers, linter-warning burn-down), and encode the rules as architecture tests. Use when the user asks to clean up, declutter, tidy, simplify, restructure, or modularize a repo, asks for clean code or SOLID, wants dev docs moved out of the code tree, says "spring clean", "housekeeping", "خونه تکونی", "تمیزکاری", "کلین کد", "سادهسازی کدبیس", "ریپو رو مرتب کن", complains that the repo is a mess or a file is huge, or wants dead code removed and LoC reduced. Repo-scale work on the whole tree or a subtree, not a single diff.
---
- Spring-clean a repository the way خونه تکونی cleans a house before the new year: everything comes out, only what belongs goes back, and the house gets rules that keep it clean. The pass is net-negative and behavior-preserving. Success is fewer lines, fewer surfaces, identical behavior, and tests that stop the mess from regrowing. Calibration from one real three-day pass on a production TypeScript service: 409 files touched, 9,272 lines added, 70,666 deleted, entrypoint from 723 lines to 44.
+ Spring-clean a repository the way خونه تکونی cleans a house before the new year: everything comes out, only what belongs goes back, and the house gets rules that keep it clean. The pass is behaviour-preserving, and it is not a deletion contest. The one real pass on record was net **+5,185 lines**, and its single most valuable outcome was writing the first tests a security file ever had.
- ## Phase 0: measure, don't guess
+ ## Phase 0: frame it, before any edit
- Read-only inventory before any edit. On big repos, fan out explorer subagents for the scan; keep verdicts in the main context.
+ **Declare the scoreboard.** Pick the numbers this pass will be judged on, measure them now, re-measure at the end. A worked set: files over the cap, worst complexity ceiling, linter warnings, test count, architecture test files, root non-code files. **Two scoreboards, not one.** What got cleaner in the code repo is the score. What got relocated (workspace file counts, disk freed) is reported separately and is not cleaning, because a score that rewards moving files is gameable by shuffling.
- - Top 20 files by line count, plus any function past ~40 lines.
- - Checked-in build artifacts: anything CI or Docker also produces. The checkout test: nothing should deploy or run from a git checkout. If a pipeline builds it, git must not hold it.
- - Dead surfaces: routes, panels, feature gates, scripts, CI jobs with no live caller. Check triggers too: a job whose only trigger was removed is dead even though its YAML looks alive.
- - Vendored code that has an upstream home, and eval/test plumbing riding inside production code paths.
- - Companion material living in the code tree: engineering docs, plan archives, audit reports, notes, research, prompts, QA evidence, assistant config (`.claude/`, `CLAUDE.md`). It helps development but is not shipped code.
- - The untracked working tree, not just tracked files. **Gitignored is not clean**: reference clones, raw data dumps, generated reports, and scratch folders hiding behind `.gitignore` are still mess. Inventory them like everything else. The only untracked thing a code repo may keep is runtime scratch the code itself recreates on demand, in one intention-revealing dir (`var/`, not `local/` or `tmp/` or `stuff/`).
- - Duplicated tables: the same retry policy, URL map, or constant list maintained in two places.
- - Import-graph smells: cycles, `../` imports crossing package lines, deep imports bypassing entry points, god files everyone imports.
- - Clean-code offenders in the worst files: meaningless names, functions doing several things, magic numbers, commented-out code, comments that restate the code.
+ **Write the red lines**, now, not when you hit them. What must not change, each with its reason: vendored slices that stay diffable against upstream, guards whose branches only run under attack, a public surface that is a contract, byte budgets you must measure rather than quote, anything the repo declares law. Also: behaviour does not change, so a change that reddens an existing test is wrong and the test is right.
- Output: an inventory where every item carries one verdict.
+ **Name the gate and the other writers.** Write the exact command, and read **each exit code separately, never through a pipe**: a pipe throws the code away and `&&` hides every failure but the last. Assume a second writer in the tree: scope every commit to its own paths, stage and commit in one step, take a second reading before believing work is lost, and commit a green change immediately.
- ## Verdicts, one per item
+ ## Phase 1: survey once, then write the plan down
- - **Delete.** Dead surface: remove the whole thing, then cascade. Its auth gates, verify scripts, and CI jobs became dead the moment it went. A guard for a surface that cannot exist is pure tax.
- - **Ignore.** Built-elsewhere artifact: delete from git, add to `.gitignore` with a why comment stating who builds it now.
- - **Replace.** The surface is expensive but the capability matters: rebuild it in the cheapest form. A dev HTTP panel with thousands of lines can become a 90-line CLI over the same runners.
- - **Relocate.** Companion material: move it to the project's workspace repo (next section). It stays in git, versioned and synced, and leaves the code tree.
- - **Split.** File over the cap: cut along capability seams (what changes together), never at line numbers. Each piece gets a name that says what it owns.
- - **Unify.** Duplicated logic: one source-of-truth module, both callers import it.
- - **Keep.** Generated code and true vendor snapshots stay, exempt from size caps but marked as such.
+ Read-only, wide, one agent per slice, and **never repeated**. Look for: top files by line count and functions past ~40 lines; checked-in build artifacts (if a pipeline builds it, git must not hold it); dead surfaces including CI jobs whose only trigger was removed; companion material in the code tree; the **untracked** tree, because gitignored is not clean (reference clones, data dumps, scratch folders); duplicated tables; import-graph smells; clean-code offenders in the worst files.
- ## The workspace repo
+ **The output is a file, not an inventory in context.** Sixty-two commits do not fit a context window. Write a numbered unit list to the workspace repo, one line per unit with a `state` column, and put the resume contract at the top: *read this file and `git log` at the start of every session, take the next `todo`, verify it, commit it, tick it. Never re-plan from scratch, never ask what is next.* Every finding becomes a numbered unit with one verdict. This file is what survives a dead context, and without it the survey gets paid for twice.
- Every project gets one sibling repo named `<project>-workspace`. One per project, not one per code repo: a multi-repo product (backend, frontend, agent service) shares a single workspace. The workspace is not an archive: it is **the home base for assistant-driven development**. Sessions start there and reach into the code repos for code.
+ Record the **negative inventory** in the same file: what was checked and found clean, and every dead-code candidate rejected because a test, the manifest or a written decision reaches it. Without it the next pass re-derives all of it.
- - Moves there: engineering docs and handoffs, plan archives, audit reports, notes, research, references, prompts, QA evidence, generated reports, and assistant config that the build does not read. Design contracts, principles and living checklists move too — code comments cite their workspace path. Reference repo clones and raw data dumps also live under the workspace, gitignored **there** (re-clonable or PII-bearing, disk-only), never in the code tree.
- - Stays in the code repo: only what the build or CI literally reads, plus README, LICENSE, and deploy configs. The boundary test: does a machine consume this file? Then it stays. If only humans (or the assistant) read it, it moves — "the code cites it" is a pointer, not a residency permit. End state: the code repo is code, README, CI/deploy. Nothing else.
- - The full `CLAUDE.md` (project context, layout, conventions, gates) lives in the workspace. The code repo gets a thin `CLAUDE.md` that says what the repo is and points at the workspace. Leave the same one-line pointer in the main README.
- - Real pass: one commit removed the whole `docs/` tree (engineering handoffs, plan archives, done-plans) from a service repo; the markdown that stayed was exactly the code-locked set above. A second pass moved four reference clones (13k files) and a PII scratch dir out of the code repo's untracked tree into the workspace.
+ ## Verdicts, one per unit
- ## Phase 1: execute in slices
+ - **Delete.** Dead surface: remove the whole thing, then cascade. Its auth gates, verify scripts and CI jobs died with it.
+ - **Ignore.** Built elsewhere: delete from git, gitignore it, with a why comment naming who builds it now.
+ - **Replace.** The surface is expensive but the capability matters. Rebuild it in the cheapest form.
+ - **Relocate.** Companion material: move it to the workspace repo.
+ - **Split.** Cut along capability seams, never at line numbers. Each piece gets a name that says what it owns.
+ - **Unify.** One source-of-truth module, both callers import it.
+ - **Keep.** Generated code and true vendor snapshots, exempt from caps but marked as such.
+ - **Decided no, with the reason.** Leaving it alone was correct and the reason is on the record: the churn buys no failure mode, the rewrite ends byte-diffability, a test already fails on drift. An unsupervised cleaner does all of these; a recorded no is what stops it.
- Safest first: (1) dead deletions, (2) artifact ignores, (3) workspace relocations, (4) unifications, (5) splits and boundaries, (6) the clean-code pass. One branch per concern, one commit per slice, full gate (typecheck, tests, lint) between slices, merge fast. A long-lived refactor branch is a merge-conflict farm.
+ ## The workspace repo
- Defects found along the way get fixed in the same branch and named in the commit ("... and fix three defects"), never smuggled inside a refactor diff.
+ Every project gets one sibling repo named `<project>-workspace`, one per project rather than per code repo. It is the home base for assistant-driven development, not an archive. Engineering docs, plan archives, audit reports, notes, research, prompts, QA evidence and generated reports move there; reference clones and raw data dumps live there gitignored, never in the code tree.
- ## The clean-code pass
+ **The residency test is not who reads the file.** It is whether the file imports the code and runs from a fresh checkout. Prose about the code moves. Programs that touch the code stay, and get tracked: a tool that lives gitignored on one workstation makes the rule that orders its use unrunnable, and a rule you cannot run is not a rule. A document the repo declares law stays with the code for the same reason.
- Structure alone is not clean. After the moves and splits, apply the classic rules to the code the pass touched and to the worst offenders repo-wide, still behavior-preserving:
+ ## Phase 2: ledgers first, so a split has a finish line
- - **Names reveal intent**, for everything with a name: variables, functions, files, directories, env knobs. `w1-replay.sh` beats `replay.sh`; `var/` beats `local/`. If a name needs a comment, rename instead.
- - **Functions do one thing**, 20 to 40 lines; more than 3 parameters becomes one args object. Splitting a function means naming its stages, not just moving lines.
- - **DRY** one source of truth per fact; **KISS/YAGNI** the simplest thing that works, no speculative hooks (dormant "future fallback" code is a Delete verdict, not a Keep).
- - **No magic numbers or strings**: named constants next to their capability.
- - **Comments say why, never what.** Delete commented-out code and comments that restate the line below; keep the ones carrying a constraint or consequence.
- - **Linter warnings burn down like the size ledger**: fix every mechanically safe one, suppress the rest one by one with the reason inline, and record the remaining count so it only shrinks. A warning wall nobody reads is debt with no ledger.
- - **Boy-scout rule** as standing policy after the pass: every future edit leaves the file cleaner than it found it.
+ Land the size and complexity ledgers **before** the splits, pinned at today's numbers, each with the meta-check that the ledger cannot grow. Then a split is not done until its name has left the ledger. Without that, "shuffling, not cleaning" has no detector.
- ## The target shape
+ ## Phase 3: execute in slices
- Repo root first — these are defaults, not judgment calls:
+ Safest first: git hygiene, artifact ignores, workspace relocations, non-code out of the code tree, dead code, defects each in its own named commit, unifications, splits and boundaries, the clean-code pass, then tests for the untested surfaces that carry the most risk. That last one is a stage, not an afterthought: it is where the real defects are found.
- - **A single-service repo hosts the service at its root.** `server.ts`, `src`/`modules`, `package.json`, Dockerfile, compose all at top level. A repo whose only content sits inside `<repo>/service/` (or `app/`, `backend/`) is nesting for no reader. Only a genuinely multi-service repo gets per-service dirs.
- - **Deploy config is one directory.** CI file at root (the platform requires it); everything else deploy-shaped — runbook, helm charts, k8s manifests, terraform — under `deploy/`. Not `helm/` as a root sibling.
- - **Docs in the code repo default to zero.** README plus a thin `CLAUDE.md` pointer, and that is the target. Even "law" and "contract" docs live in the workspace; code comments cite the workspace path (`<project>-workspace/docs/...`). When the boundary test feels arguable, the doc moves.
- - **Source lives in `src/`, and structural directories get conventional names, not codenames.** `brain/`, `core/`, `engine/` are branding; `src/` is information. Map the import alias into it (`@/*` → `src/*`) so specifiers stay `@/modules/...` with no extra noise. Vendored-path mirroring with an upstream repo is a sed away, not a reason to keep the codename.
- - **Runtime content is not source.** Knowledge corpora, prompt files, and other data the code loads at runtime sit at the repo root (`knowledge/`, `prompts/`), not inside `src/`; every asset path anchors in one path module so a move is a one-file change.
+ **After every move or delete, grep the whole tree for the old path in the same commit** — comments, markdown, CI, Dockerfile, charts, `.gitignore` — then land that scan as a standing gate. A move is the one operation whose damage is invisible to every check that already exists, because no gate reads a path out of a comment.
- Inside the code:
+ Two shape targets are worth aiming at because they each produced real units: the entrypoint is boot only, with routing and handlers in their own package; and imports flow through package indexes with no `../` across package lines, which becomes a boundary test encoding a direction derived from what actually co-changes. Everything else about layout is a default for a repo with no shape yet, and against a repo whose layout is already load-bearing it argues for churn.
- - Entrypoint is boot only. Routing and handlers live in their own package; the real pass took `server.ts` from 723 lines to 44.
- - Capability packages: `modules/<name>/` with `index.ts` as the sole public entry. Big peers stay flat next to `modules/`, not nested inside.
- - Functions 20 to 40 lines. Files around 300 lines in the core, 500 in the periphery. Generated code exempt.
- - Imports flow through package indexes. No `../` across package lines. Pure layers (contracts, generated types, guard logic) never touch env, network, or storage.
+ **When a unit fails twice, change the approach, not the effort.** Revert, write the mechanism of the failure into the unit itself, then attack it differently. Two attempts at scripting a split that has no clean line boundaries is one attempt too many.
- ## Phase 2: guardrails, the part most cleanups skip
+ Every commit names the defect in plain words and carries the measurement, its date and the rejected alternative. That is what makes sixty commits auditable.
- Architecture lives in failing tests, not in docs or conventions that drift:
+ ## The clean-code pass
- - A size test with a debt ledger: hard cap for new files, every existing violator listed by path with its current line count as its personal ceiling, and a meta-check that the ledger itself cannot grow. Debt only burns down.
- - A boundary test: package-index-only imports, `../` ban, a deep-import allowlist holding only reviewed entries, cycle detection, purity scan for pure layers, and reachability analysis that prunes dev and script folders so eval-only code cannot fake liveness.
- - Waivers end at zero or inside the ledger. "Waivers cleared" is part of the definition of done.
- - Mirror the discipline per sub-project. A frontend gets its own structure test ("components render, adapters talk": components never fetch or touch storage directly).
+ Structure alone is not clean. Behaviour-preserving, on what the pass touched and the worst offenders repo-wide: names reveal intent, for everything with a name; functions do one thing, and splitting one means naming its stages; one source of truth per fact; the simplest thing that works, with speculative code a Delete rather than a Keep; every measured number named with its date and source; linter warnings burned down like the ledger. The rest is what the linter already does.
- ## Phase 3: prove it
+ ## Phase 4: prove it
- - Before/after table: file count, lines added and deleted, top five files then and now.
- - `ARCHITECTURE.md` (or the repo's equivalent) rewritten in the same branch. Docs that don't move with the refactor are the first new mess.
- - All gates green. Net line count down. Behavior identical: the pre-existing test suite passes unchanged, and any intended behavior change shipped as its own named commit.
+ Scoreboard re-measured, both halves reported separately. Ledgers shrunk. Every gate green **by exit code**. Negative inventory and decided-nos recorded in the plan file. Docs rewritten in the same branch.
+ **Ask about two things only**: a push to a shared remote or a history rewrite, and another person's credentials or environment. Decide everything else and do the work. On the one run this is drawn from, twenty-six of twenty-eight escalations were cold feet, and the owner said so. Anything genuinely not done says what stopped it, rather than pretending.
+
+ Hand the derived rule set to `workspace-rules`.
+
## Red flags
- - A "split" that moves lines without drawing an import boundary. That is shuffling, not cleaning.
- - Gating a dead surface instead of deleting it. The maintenance tax stays.
- - Deleting a surface but keeping its guards, verify scripts, or CI jobs.
- - A `.gitignore` line without a rationale comment.
- - Deleting knowledge that belongs in the workspace repo. If it would help the next developer, relocate it, don't trash it.
- - Behavior changes hiding inside refactor commits.
- - Caps enforced by review or convention instead of a failing test.
- - Calling the repo clean while gitignored piles (clones, dumps, scratch) still sit in its working tree.
- - A "cleanup" that reorganizes files but leaves 150-line functions, magic numbers, and dead knobs in place.
- - A single-service repo still nesting its code one directory deep, or deploy config scattered across root siblings (`helm/` next to `deploy/`).
- - Keeping a doc in the code repo because code comments cite it. Citations follow the doc to the workspace.
+ - A "split" that moves lines without drawing an import boundary. Shuffling, not cleaning.
+ - Gating or guarding a dead surface instead of deleting it, or deleting a surface and keeping its guards.
+ - Behaviour changes hiding inside refactor commits.
+ - Calling the repo clean while gitignored clones, dumps and scratch still sit in its working tree.