think
16px
820px

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

  1. Write the invariant as a scan over walkTs(SRC, …) from ./walk.ts.
  2. Grandfather current violations into an allowlist so it passes on today's tree.
  3. 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.
  4. Add a row to the table above.

Not yet built (candidates)

  • Contract DTO SSOT — extend enum-ssot from 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_REGISTRY exists, one test that every
    SubmissionType has a registered processor + route + review descriptor.