Codebase Constitution
Most architecture documents are fiction. They describe a structure the code had eighteen months ago, nobody reads them, and the one person who enforced them has left. The failure is not that the rules were wrong — it is that a rule with no checker is a comment.
This skill writes rules and their enforcement together, and refuses to write one without the other.
The core rule
Every rule must be mechanically checkable. If you cannot express it as a lint rule, a script or a CI step, it does not go in the constitution — it goes in an appendix headed "conventions we chose not to enforce", so nobody mistakes an aspiration for a contract.
1. Read the repo before proposing anything
A 2,000-file monolith and a three-week-old prototype do not get the same rules. Establish:
- File count, language mix, framework, test runner, and what CI already runs.
- The lint and format config that exists. You are extending it, not replacing it — a constitution that rewrites someone's working ESLint setup gets reverted wholesale.
- The current shape: are there feature directories, layer directories, or neither?
- The distribution of file sizes.
find src -name '*.ts' | xargs wc -l | sort -n | tail -20tells you what the budget can realistically be.
2. Set budgets from that distribution, not from a blog post
Propose numbers the repo can nearly meet, then ratchet. Reasonable defaults:
| Budget | Default | Notes |
|---|---|---|
| Lines per file | 300 | The number everyone quotes. Treat it as a budget, not a cap. |
| Lines per function | 50 | Catches more real problems than the file rule does. |
| Parameters | 4 | Beyond this, pass an object. |
| Nesting depth | 3 | The strongest single predictor of a function nobody wants to touch. |
| Cyclomatic complexity | 10 | Pairs with nesting; catches the flat-but-branchy function. |
Every budget needs an escape hatch, and the hatch must cost something. A file over budget either
splits or carries a one-line header saying why it does not. A hard cap with no hatch produces
helpers2.ts — the same code, split at an arbitrary seam, now with an import between the halves.
That is worse than the file you started with.
3. Write the boundaries as checks
This is the part that matters most and is skipped most. Pick the tool for the stack:
- TypeScript:
eslint-plugin-boundaries, orimport/no-restricted-pathswith a zone per feature. Routes may import features; features may not import routes; features may not import each other. - Go:
depguardin.golangci.yml, one rule per feature package denying the sibling packages and allowing the shared platform packages. Most teams use depguard only to ban a dependency and never discover it does this. - Python:
import-lintercontracts inpyproject.toml— alayerscontract for vertical direction and anindependencecontract across feature packages.
4. Baseline, then ratchet
Turning the rules on in a repo that violates them produces a red build and a revert. Instead:
- Run the checks, write the current violations to a committed baseline file.
- CI compares against the baseline and fails only when the count goes up, or when a file not in the baseline violates a rule.
- Anyone touching a baselined file is expected to bring it into line; the baseline shrinks over time and can never grow.
This is the single thing that makes a constitution adoptable. Without it you are asking for a refactoring week nobody has budgeted.
5. Ship the artifacts
ARCHITECTURE.md— the human contract. Structure, the boundaries, the budgets, and for each one a line naming the check that enforces it.- The lint and boundary config, extending what was there.
- A
check:archscript the developer can run locally with the same command CI uses. - A CI job wired to that script.
- The escape-hatch convention documented where people will hit it.
Judgement calls
- Feature-driven or layer-driven? Decide from evidence: more than one contributor and several entry points favours features; a single-purpose library is fine flat. Do not impose feature directories on a codebase with four modules.
- How many rules? Fewer than fifteen. A constitution nobody can hold in their head gets skimmed, and the important rule is now buried among the trivia.
- Rule or code review? If it is mechanically checkable, make it a rule and stop spending human attention on it. If it needs judgement, leave it to review and do not pretend otherwise.
Verification gate
-
check:archruns clean against the baseline on a fresh clone. - Every rule in
ARCHITECTURE.mdnames the check that enforces it. Read the file and confirm — no unenforced rule is stated as though it were binding. - The baseline was generated from the current tree, not hand-written.
- CI fails on a deliberately introduced violation. Test this; an unverified gate is not a gate.
- The existing lint config still passes — nothing was silently dropped.
- Paste the command output rather than describing it.