Architecture Fitness Functions
These tests encode the codebase's architectural invariants as executable checks.
They fail the build when the architecture erodes — so the patterns we've paid to
establish can't silently regress one commit at a time. "Beautiful code" here is
not a matter of taste; it's whatever these guards let compile.
Run them like any test — they're part of bun run test and gate every commit:
bun test src/__tests__/architecture/
They are pure file-scanners (no DB, no network) and finish in well under a second.
The guards
| File | Invariant | What it prevents |
|---|---|---|
no-console.test.ts |
All console.* goes through lib/logger. |
Log drift after the 213-call logger sweep. |
enum-ssot.test.ts |
No file hand-declares a union named after a contract/ enum. |
Frontend/backend enum copies that silently diverge. |
dispatch-exhaustiveness.test.ts |
reMatchAndValidate branches every SubmissionType. |
The #1 add-a-flow hazard: a new type falls through and wipes ValidationResults on edit. |
layering.test.ts |
lib imports nothing upward; services don't import routes. |
The tangle that made flows impossible to reason about. |
no-new-forks.test.ts |
No same-layer file pair exceeds 0.40 similarity. | The root-cause smell: copy-fork-per-flow. |
body-validation.test.ts |
Routes read bodies via validateBody, not raw c.req.json(). |
Unvalidated any bodies creeping back. |
file-size.test.ts |
New files ≤ 800 lines; existing monsters frozen at current size. | New 1500-line god-files (the other half of a fork). |
The ratchet philosophy
Every guard is a ratchet: it grandfathers the current reality and fails only
on new violations. This lets a guard land on a codebase that still has debt —
and alongside in-flight work on other branches — without blocking anything.
The grandfathered lists (GRANDFATHERED, BUDGET, GRANDFATHERED_LARGE,
SERVICES_TO_ROUTES) are the tidy-up's to-do list. Each entry is a known
debt we owe a fix. The rule is one-directional:
Shrink these lists as debt is paid down. Never grow one to dodge a guard.
If a change legitimately needs an exception (a genuinely-new optional-body
endpoint, say), adding to a list is a deliberate, reviewable act with a comment
explaining why — not a silent workaround.
Adding a guard
- Write the invariant as a scan over
walkTs(SRC, …)from./walk.ts. - Grandfather current violations into an allowlist so it passes on today's tree.
- Mutation-test it: temporarily empty the allowlist (or inject a violation)
and confirm the guard goes red, then revert. A guard you've only ever seen
pass is a guard you don't know works — this repo has already shipped one
off-by-one that only the mutation run caught. - Add a row to the table above.
Not yet built (candidates)
- Contract DTO SSOT — extend
enum-ssotfrom enums to shared request/response
DTOs, killing the "types duplicated backend↔frontend" root cause outright. - No raw god-table writes outside a repository layer — lands with Phase-2 of
the persistence split (satellites become the read path). - Flow-completeness — once
FLOW_REGISTRYexists, one test that every
SubmissionTypehas a registered processor + route + review descriptor.