---
name: codebase-constitution
description: Writes a repo's architecture rules as a human contract plus the machine checks that enforce them — import boundaries, file and function budgets, naming — then baselines existing violations so CI fails only on an increase. Use when setting conventions for a new repo, imposing structure on one that has none, or turning a style document nobody follows into something enforced.
license: MIT
compatibility: TypeScript, Go and Python repos with a CI system. Requires permission to run the project's linter and write config files.
metadata:
  category: coding
  version: "1.0.0"
---

# 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 -20` tells 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`, or `import/no-restricted-paths` with a zone per feature.
  Routes may import features; features may not import routes; features may not import each other.
- **Go**: `depguard` in `.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-linter` contracts in `pyproject.toml` — a `layers` contract for vertical
  direction and an `independence` contract 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:

1. Run the checks, write the current violations to a committed baseline file.
2. CI compares against the baseline and **fails only when the count goes up**, or when a file not in
   the baseline violates a rule.
3. 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:arch` script 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:arch` runs clean against the baseline on a fresh clone.
- [ ] Every rule in `ARCHITECTURE.md` names 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.
