codebase-architecture
Designs module contracts, deepens existing boundaries, and installs enforceable repository guardrails. Use when asked to "design the architecture", "simplify our modules", or "harden the repo". For one feature plan use planning; for diff cleanup use tidy; for tenancy use multi-te
Install
npx skills add https://github.com/mblode/agent-skills/tree/main/skills/codebase-architecture
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install mblode-agent-skills@llmmart
git clone https://github.com/mblode/agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole mblode/agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Codebase Architecture
Decide a TypeScript codebase's structure, improve it where change has become expensive, and make it hold. The target is a codebase a reader can hold in their head: few surfaces, one canonical way to do each job, and behaviour where you would first look for it.
- IS: folder structures, module contracts, request context and middleware pipelines, frontend/backend boundaries; architecture briefs; domain language and decision records; domain-informed deepening; guardrail tooling, CI gates, and agent wayfinding.
- IS NOT: scaffolding a new repo (
scaffold-nextjsfor a Next.js turborepo,scaffold-clifor a TypeScript CLI), multi-tenant domain/isolation/routing (multi-tenant-architecture), the content of AGENTS.md itself (agents-md), a plan for one feature (planning), a diff-scoped cleanup pass (tidy), or structural review of a local diff (pr-reviewer).
Contents
- Modes
- References
- Design mode (new codebase)
- Deepen mode (existing codebase)
- Harden mode (make it stick)
- Validation loop
- Output template
- Excuses
- Gotchas
- Related skills
Modes
Pick by the problem, not by the artifact, and say which you picked.
| Mode | You are here when | Output |
|---|---|---|
| Design | Starting a new app, service, or surface, and the structure is not decided yet | An architecture brief |
| Deepen | The code works, but change is expensive: concepts scattered, seams leaking, one idea under three names | Ranked opportunities, then one migrated slice |
| Harden | The structure is decided and keeps decaying, or agents keep doing the wrong thing in this repo | Wired checks, markers, and recipes |
Modes compose, and running more than one is normal. Design ends in Harden, because a contract with no check is a suggestion. Deepen ends in Harden, so the new seam cannot decay back. Harden runs alone when the structure is already right and only the enforcement is missing, which is the common case in a repo that agents work in.
When two look equally right, prefer Deepen. "Agents keep using the old pattern" sounds like Harden, but if the cause is one concept living in two places, quarantine only freezes the duplicate: Deepen deletes it and Harden holds the line until that lands. Harden alone is right when the old thing genuinely has to stay.
When you cannot write to the repo (no checkout, read-only request, or a question rather than a change), each mode's output degrades to its plan: the brief, the ranked opportunities, or the named checks with their rungs. Say which checks remain unproven, since none of them are wired.
As simple as possible, no simpler. Every mode cuts: surfaces in Design, concepts in Deepen, dual paths and dormant config in Harden. The floor does not get cut: validation at trust boundaries, error handling that prevents data loss, security, accessibility, observability on anything deployed, and whatever was explicitly asked for. A simplification that reaches one of those is a bug. Where a corner is cut on purpose, mark it with its ceiling and upgrade path rather than leaving the next reader to guess whether it is finished.
Copy this to track progress, and delete the lines for modes you are not running:
Codebase architecture progress:
- [ ] Modes chosen and stated (Design / Deepen / Harden)
- [ ] Design: assumptions stated, repo shape, every contract names its check, brief written
- [ ] Deepen: git hot spots, glossary, ranked opportunities with paths, one slice migrated
- [ ] Harden: existing checks surveyed, checks picked by failure, each landed green and proven to bite
- [ ] Validation loop run for the modes used; evidence recorded, N/A items named
References
Load only when the condition applies.
| Reference | Mode | Read when |
|---|---|---|
| references/stack-defaults.md | Design | Choosing libraries, tooling, or deploy targets |
| references/api-design.md | Design, Deepen | Designing endpoints, module contracts, request context, error shapes, or an agent-facing CLI/SDK surface |
| references/distributed-correctness.md | Design, Deepen | The work provably touches an external system, webhook, retry, audit trail, or money. In Deepen you can grep for it; in Design it is a question about requirements, so confirm before loading rather than inferring it from the product's domain |
| references/brief-conventions.md | Design | Writing the conventions, testing, quality-bar, or rollout and rollback sections of the brief |
| references/deepening-existing.md | Deepen | Running Deepen: vocabulary, opportunity patterns, output template |
| references/domain-language.md | Deepen | Writing or fixing a glossary, resolving naming divergence, recording a decision |
| references/enforcement-ladder.md | Harden | Adding any check to a repo that already violates it |
| references/guardrail-tooling.md | Harden | Choosing and wiring the actual checks: dead code, duplication, cycles, module and package boundaries, file size, staleness gates |
| references/wayfinding.md | Harden | Agents cannot find things, or keep re-deriving the same path |
| references/contagion-markers.md | Harden | The repo has legacy, generated, dual-path, or deliberately simplified code |
| references/verification-tiers.md | Harden | Defining which commands an agent should run, and when |
| references/agent-runtime.md | Harden | Configuring session hooks, permissions, or review gating |
| references/evaluation-scenarios.md | none | Changing this skill. Never loads during a user task; it is the author's rubric |
Design mode (new codebase)
Before any of this, ask whether each surface needs to exist. A module, service, app, or entrypoint that could be a folder in something that already ships is the cheapest architecture decision available, and the only one that stays cheap. Every surface you do accept pays the relationship cost in the surface-area budget (brief-conventions.md): name its owner, tests, observability, and deletion path before it goes in the brief.
- Constraints first: product scope, team size, compliance/security, expected scale, deploy targets, required integrations, and quality bar. A one-line request supplies none of these, so assume the common case, state every assumption in the brief's first section, and invite correction. Ask outright only where a wrong guess would restructure the brief rather than extend it, which in practice is multi-tenancy and whether the API is public.
- Choose repo shape:
apps/for deployable surfaces (api,web,admin).packages/for shared libraries (shared,ui,icons,auth,proto).
- Define backend module contracts, each naming its enforcement (import-boundary lint or type check):
handler: transport only.service: business orchestration.dao: database access only.mapper: DB/proto/domain transformations.constantsandtypes: module-local contracts.
- Define request context and middleware:
- Carry
tenantId,userId, andtraceIdin an AsyncLocalStorage-backedRequestContext, initialized in every entrypoint (RPC, HTTP, jobs, CLI) and read viagetContext(). A threadedctxparameter grows every signature, and adding one field later touches every call site. Implementation in references/api-design.md. - Require an explicit auth policy per RPC method at registration; a method without one fails registration rather than defaulting to open.
- Keep auth, logging, errors, and context in shared middleware, not per-handler code.
- Carry
- Define frontend boundaries (Next.js App Router default):
app/holds routing files only (page,layout,loading,error,route). Domain code lives insrc/modules/<name>/behind its root files; UI private to one route goes in a_components/folder beside its page. A page that grows logic moves it to a module, not to a sibling file inapp/.- Server Components by default;
"use client"at the interactive leaves. Where a client wrapper needs server-rendered content, pass it in aschildren. - Server state in TanStack or Connect Query; client state in component state; MobX only for cross-cutting client state that fits neither. Each piece of data has one owner: server data mirrored into
useState, or two stores synced withuseEffect, is the sign that ownership is unclear. proxy.ts(Next 16's name formiddleware.ts) handles redirects, rewrites, and headers. Authorization is decided inside each route handler and Server Function, because a matcher-excluded path skips the proxy and Server Functions post to their page's route.
- Testing and release:
- Unit tests stay DB-free; integration/E2E run in parallel with dynamically generated IDs so runs never collide on fixtures.
- Release in small, complete, reversible vertical slices with a rollback plan per change.
- A slice is complete only when reliability, error paths, observability, and user-facing states are covered; deferring them to a polish pass is how they never ship.
- Every contract in the brief names the lint rule, type check, or test that catches its violation, then continue into Harden mode to wire them.
Deepen mode (existing codebase)
Goal: domain-informed deepening, not a rewrite. Load references/deepening-existing.md for the analysis method, opportunity patterns, and output template.
- Map the domain language and decisions. Read
CONTEXT.md,docs/adr/, or local equivalents if present, then read the code for entities, actions, and contexts as the team names them. Note divergence (one concept, three names; or one name, three concepts). Format and ADR rules in references/domain-language.md. - Scope the scan by where change lands. Deepening pays off on code that keeps changing, so
git log --onelineover a good stretch of history first and weight the files that keep coming up. An unscoped scan drifts into speculative cleanup. - Find deepening opportunities. Look for anemic concepts, shallow modules, leaking seams, naming divergence, duplicated concepts, primitive obsession, misplaced logic, and tests forced past the public interface. Record each with file paths, never a vague smell. Check deletion first on every candidate: a concept with no live caller, a flag whose branch never runs, a layer with one implementation. Deleting it is the deepening, and it is the only move that cannot make the codebase harder to read.
- Rank by leverage. Prefer opportunities that pass the deletion test, localize named future changes, have low churn, meet a current requirement, and have a viable testing seam. Rank candidates before designing target interfaces; drop speculative cleanups.
- Migrate one vertical slice first. Prove the highest-leverage move end to end through one slice before generalizing.
- Enforce the new seam with lint, type, or test checks so it cannot decay, then roll out module by module. Continue into Harden mode for the enforcement rung and the check-bites test.
Harden mode (make it stick)
Two halves: guardrails stop the wrong thing landing, wayfinding makes the right thing cheap to find. Both exist because agents arrive by grep, not by reading docs, so the warning has to live where they land and the rule has to be an exit code rather than a sentence someone might recall.
Steps 1 to 3 always run. Steps 4 to 6 run only when their condition holds, and a request to add one check stops at step 3. Running all six for every request loads most of the bundle and is the failure this mode is most prone to.
- Survey what exists. Package scripts, CI steps, hook config, lint config, the instruction file, the docs index. Find three things: checks that run locally but do not gate the merge, checks that run in CI but cannot fail (
verification-tiers.md, "Commands that lie"), and dormant config nobody invokes. Wire the first, fix the second, delete the third (references/contagion-markers.md). - Choose checks by the failure they prevent, never by tool popularity. Categories and tools in references/guardrail-tooling.md. Pick the two or three failures this repo actually exhibits; installing the full set at once forces the weakest enforcement rung on all of them.
- Install each check: pick an enforcement rung for the violations that already exist (references/enforcement-ladder.md), ship it green, then prove it bites (run it, break it on purpose, watch it fail with a message naming the fix, revert). Wire it into both a pre-commit hook and CI.
- Wayfinding per references/wayfinding.md: naming and locality, the add-a-new-X recipe file, the trust-labeled docs index, one canonical instruction file.
- Contagion markers per references/contagion-markers.md: anything an agent must not copy or must not edit gets a greppable marker at the code site naming what to use instead.
- Runtime ergonomics: verification tiers in references/verification-tiers.md; session hooks, permission allowlists, and review gating in references/agent-runtime.md.
Validation loop
Run the items matching the modes you ran, and record results in the output. Each needs evidence; "looks consistent" is not a pass. An item that cannot execute yet, because nothing is installed or the repo is not writable, is recorded N/A with that reason. Silently passing it is how an unenforced contract ships looking verified.
- Consistency (Design, Deepen): naming, module contracts, and middleware rules read the same across every service. Evidence: a contradiction scan with zero findings.
- Enforceability (all): every contract names its lint rule, type check, or test. Evidence: an enforcement note per contract, and for any check actually installed, the pass, then fail on a deliberate violation, then pass after revert.
- Operability (Design): observability, health checks, and a rollback path per deployable surface. Evidence: the rollout section names each.
- Quality gates (whenever code changed): the repo's lint, type-check, and targeted tests (
npm run lint,npm run check-types,npm run test --workspace=<pkg>or equivalents). Evidence: passing output, quoted. - CI and local agree (Harden): the CI step invokes the same umbrella command a developer runs, or the difference is deliberate and stated.
- No dangling pointers, and a recipe works cold (Harden): a grep proving every path named in the docs index and instruction file exists, plus a fresh-context agent following one add-a-new-X recipe end to end with no further guidance.
- Net simplicity (all): the result leaves a reader less to hold, not more. Evidence: the net change in files, surfaces, and exported names, with every increase named and paid for by what it removed elsewhere; plus, for each layer, port, or indirection introduced, the second caller or implementation that made it real. An architecture pass that only adds has failed this check even when every other item passes.
On failure: fix the brief, the conventions, or the wiring, then re-run the loop.
Output template
Design mode produces this brief. Deepen mode's ranked-opportunity template is in references/deepening-existing.md. Harden mode's output is the wiring itself plus the loop's evidence, not a document.
# Architecture brief
## Context and constraints
## Repo shape
## Backend module contracts
## Request context and middleware policy
## Frontend boundaries
## Testing strategy
## Quality bar and surface-area budget
## Rollout and rollback plan
## Open risks and follow-ups
Size the brief to the decisions, not to the template. Drop any heading the project does not face rather than filling it: a single-tenant internal service with no frontend does not owe you a Frontend boundaries section. Each section carries the decision and the constraint that forced it, not a restatement of the conventions in the references. A brief that pads to nine sections costs the review attention that the two contested decisions needed.
Excuses
Each rebuttal redirects to the step being skipped.
| Excuse | Rebuttal |
|---|---|
| "The check is obviously configured right." | You have not watched it fail. A misconfigured gate passes on everything and reads as coverage. |
| "There are too many existing violations to fix." | That is what the enforcement ladder is for. Pick a rung and land it green today rather than a perfect rule next quarter. |
| "AGENTS.md already says not to do that." | A prompt rule decays under context pressure. If a static tool can check it, it belongs in tooling. |
| "We'll add the enforcement in a follow-up." | The follow-up is the deadline's first casualty, and the contract decays from the day it ships unenforced. |
| "CI already runs that tool." | Running is not gating. Read the step: a tool with no threshold, a warn-only rule, or a job behind a stale path filter is green on every PR. |
Gotchas
Design and Deepen
- Microservices for a team under 5 buy a deploy pipeline, contract versioning, and an on-call surface per service. Start with a modular monorepo; split when a boundary is proven by team or scale pressure.
- App-level deps in a monorepo's root
package.jsonhoist silently, so an app builds locally and breaks when deployed alone. Each app owns its deps. - A
handler/service/daocontract with no import-boundary rule decays at the first deadline. Add the rule (daomay not importhandler) the day you write the contract. "use client"at page or layout level converts the whole subtree to client rendering and forfeits streaming and direct server data access. Push it to leaves.- Extracting to
packages/before 3+ apps need the code couples release cycles for nothing. The exception is the contract two surfaces already share (generated types, the RPC schema, branded IDs): that is the interface between them, and it belongs in a package at two apps. - Dual-writing to a database and a queue or webhook without an outbox (or CDC) loses or fabricates a notification whenever one side commits and the other fails. See
references/distributed-correctness.md. - An externally-forceable invariant enforced by construction (unsigned type, hard CHECK) crashes or clamps when the outside world forces the state. Represent it, detect it post-factum, recover explicitly.
- A whole-codebase deepening scan without
git loghot-spot scoping fills the list with modules nobody touches, and every entry on it is speculative by definition. - Relying on
proxy.tsas the only authorization layer: a matcher-excluded path skips it, and Server Functions post to their page's route, so a matcher change silently removes coverage. Check authorization in the handler or Server Function itself.
Harden
jscpdwithout--thresholdexits 0 on any duplication, andknipwith too many declaredentryfiles hides real dead code behind them. A green step is not a gate until you have watched it fail.- Pick cycle tooling that resolves this repository's aliases and workspace edges. Prefer a configured linter rule or dependency-cruiser before adding another graph tool; verify an intentional cycle fails.
- dependency-cruiser without
options.tsConfigcannot resolve path aliases, drops those edges, and passes every rule on a graph with half its imports missing. turbo boundarieschecks cross-package imports and undeclared dependencies only; it sees nothing inside a package, so it does not replace the module boundary rule.- A hand-rolled shrink-only baseline (
*-ratchet.mjsplus*.baseline.json) reimplements theignore, allowlist, andwarnmechanisms knip, the linter, dependency-cruiser, and jscpd already ship, and the baseline becomes the file people edit for a green run. - Dormant config or an unused devDep reads to an agent as live convention; a config pointing at a renamed file yields a confident empty result instead of an error.
- A docs index entry or add-a-new-X recipe written from memory names a moved file, and the agent follows the pointer with full confidence rather than doubting the doc. Grep-verify every path before publishing it.
- Pre-commit hooks alone are not installed on a fresh clone or in a worktree, which is exactly where agents run. CI is the gate; the hook is the fast signal.
- A LEGACY marker only in
docs/legacy.mdis never seen by an agent that arrived by grep. The marker goes at the top of the frozen file.
Related skills
agents-md: the AGENTS.md / CLAUDE.md file itself. This skill owns the checks and docs tree that file points at; a rule a linter can enforce goes here as an exit code, not there as prose.tidy: the diff-scoped cleanup that Harden's guardrails keep small;pr-reviewer: read-only review of a local diff.planning: a plan for one feature; architecture briefs from Design mode feed into it.scaffold-nextjs,scaffold-cli: creating the repo this skill then structures.multi-tenant-architecture: tenant identification, isolation, and routing; this skill supplies the module layout underneath.dx-audit: the developer-facing surface a package ships outward;api-design.mdhere covers only the contract shape.
Maintenance only: evals/evals.json contains regression scenarios for changes to this skill; it does not load during a user task.
Files (agent-skills)
-
evals
-
evals.json 1.4 KB
{ "skill_name": "codebase-architecture", "evals": [ { "id": 1, "prompt": "Harden a repository whose CI runs jscpd without a failure threshold. A seeded duplicate currently exits zero. Keep scope to that gate.", "expected_output": "Configure an effective gate and demonstrate a deliberate violation fails.", "files": [], "assertions": [ "Checks the process exit status", "Shows pass/fail evidence using a disposable violation", "Does not install unrelated architecture tools" ] }, { "id": 2, "prompt": "Design a plan only for an internal service. Existing code already supplies an auth policy registry and request context.", "expected_output": "Reuse existing contracts in a bounded architecture brief.", "files": [], "assertions": [ "Names existing auth and context ownership", "Does not introduce a second registry", "Does not claim guardrails were installed" ] } ], "routing": { "should_trigger": [ "Harden a repository whose CI runs jscpd without a failure threshold. A seeded duplicate currently exits zero. Keep scope to that gate.", "Design a plan only for an internal service. Existing code already supplies an auth policy registry and request context." ], "near_miss": [ { "prompt": "Simplify only the current diff before I open a PR.", "expected": "tidy" } ] } }
-
-
references
-
agent-runtime.md 3.2 KB
# Agent Runtime Configuration that makes a session productive from its first turn, and merge gating that matches the risk of the change. Load when configuring session hooks, permissions, or review gating. ## Hooks Every manual setup step is a failed or slow session. Three hooks cover most of it: - **Session start:** install dependencies when they are missing. A fresh worktree becomes productive with zero instructions, and the agent never spends a turn diagnosing a missing module as a code error. - **After a write or edit:** run the formatter and autofixer on the file just written. The tree stays clean by construction rather than by the agent remembering, and a formatting gate can never fail on agent-authored code. - **Before a destructive command:** block what should never run unattended, where the repo has such commands. The post-edit hook largely subsumes a pre-commit formatting hook, and catches the same failure earlier and cheaper. Keep pre-commit for what a per-file hook cannot see: whole-repo checks, cross-file rules, and anything needing the staged set. The harness owns the settings file's schema and matcher syntax. Decide what belongs in it and why; do not restate mechanics the harness already documents. ## Permission allowlist Allowlist the read-only commands an agent runs constantly (status, log, diff, typecheck, lint, test, ripgrep, ls). Each approval prompt is a stall, and a session spent approving `git status` twenty times trains everyone toward blanket approval, which is the outcome the prompts exist to prevent. Allowlist by command shape, not by prefix breadth. `git diff` is read-only; `git` is not. ## Worktree bootstrap For parallel agent fleets, a bootstrap script that copies environment files, reuses dependency and codegen artifacts from the main checkout when lockfiles match, and offsets ports so worktrees never collide. Without the port offset, the second agent's dev server fails in a way that looks like a code bug. ## Blast-radius review rubric A checked-in rubric, read from the base ref, telling the automated reviewer which changes it may approve and which must escalate to a human. Without one, the reviewer applies generic defaults: uniformly cautious, so nothing merges unattended, or uniformly permissive, so nothing is gated. Two explicit lists, not a severity score: **Auto-approve:** features, bug fixes, refactors, tests, documentation, styling, copy and translation additions, analytics events, feature-flag default changes. **Escalate to a human:** billing and payments, authentication and authorization, data deletion, migrations touching stored data, build, signing, and release configuration, permission and entitlement changes, anything altering a public contract. The escalation list is the one worth arguing over, and its shape generalises: money, identity, destructive data operations, persisted-data shape, and anything that ships to users outside the normal deploy path. Everything else is reversible by the rollback path, which is why it can merge unattended. Verify the rubric the same way as any other gate: open a documentation-only change (auto-approves) and a migration (escalates). A rubric nobody has watched escalate is not known to work. -
api-design.md 5.4 KB
# API and Interface Design Contract-first patterns for REST APIs, module boundaries, request context, and TypeScript interfaces. Load when designing endpoints, defining module contracts, wiring request context, or reviewing API surface changes. ## Contents - Core principles - Format contracts - Request context - Errors as a contract - Agent-facing surfaces ## Core Principles ### Hyrum's Law Every observable behavior will be depended on by someone, regardless of the documented contract. Be intentional about what you expose; implementation details leak into de facto contracts. ### Contract first Define the interface before any handler. The interface, not prose about it, is the contract: ```ts interface TaskAPI { createTask(input: CreateTaskInput): Promise<Task>; listTasks(query: ListTasksQuery): Promise<PaginatedResult<Task>>; getTask(id: TaskId): Promise<Task>; updateTask(id: TaskId, patch: Partial<CreateTaskInput>): Promise<Task>; deleteTask(id: TaskId): Promise<void>; } ``` `CreateTaskInput` is client-supplied and stays distinct from `Task`, which adds the server-owned `id`, `createdAt`, `updatedAt`, and `createdBy`. ### Branded IDs Identifiers in the contract are branded, not bare strings: `type TaskId = string & { readonly __brand: 'TaskId' }`, so a `UserId` cannot be passed where a `TaskId` is expected. ### Consistent error semantics One error shape across all endpoints: `{ error: { code: string; message: string; details?: unknown } }`. Status codes: 400 invalid input, 401 unauthenticated, 403 unauthorized, 404 not found, 409 conflict, 422 validation failure, 500 server error (never expose internals). ### Validate at boundaries only Validate at API route handlers, form submissions, external service response parsing, and environment variable loading. Between internal functions the type contract is the validation; re-checking there adds a second failure surface without adding a trust boundary. ### Prefer addition over modification Extend interfaces with optional fields. Never modify or remove existing fields without a migration path. ## Format Contracts Standard REST naming needs no instruction; these two choices do, because both deviate from what a handler author would reach for. - Enum values are `UPPER_SNAKE` (`"IN_PROGRESS"`), even though response fields stay camelCase. - Every list endpoint returns `{ data: [...], pagination: { page, pageSize, totalItems, totalPages } }`. A list endpoint without this envelope is a breaking change waiting to happen. ## Request Context Read ambient request state through an `AsyncLocalStorage` store, never as a threaded parameter: ```ts import { AsyncLocalStorage } from "node:async_hooks"; type RequestContext = { tenantId: string; userId: string; traceId: string }; const store = new AsyncLocalStorage<RequestContext>(); export const getContext = () => store.getStore()!; export const runWithContext = (ctx: RequestContext, fn: () => void) => store.run(ctx, fn); ``` Initialize it in every entrypoint: RPC, HTTP, jobs, and CLI. Forgetting jobs and CLI makes `getContext()` throw far from the cause. ## Errors as a Contract Whoever debugs a failure works from the output alone, and a structured failure is the difference between one fix loop and five. - Structured error classes carrying a machine-readable code. Callers and tests assert on class and code; message strings are not API and change without warning. - Two audiences by construction: a caller-safe message plus a developer-only guidance field, with user-facing copy registered separately from internal error identity so internals never leak to users. - Domain errors stay transport-agnostic. Middleware owns the mapping to HTTP or RPC status, so services never pick status codes ad hoc. - Log the raw error object and let serializers extract type, stack, and cause. Never catch-log-rethrow: middleware already logs unhandled errors once, and the duplicate sends whoever is debugging after two failures that are one. - The ambient request context above auto-enriches every log line with request and correlation IDs, so a failure is traceable from log output with no per-call-site work. ## Agent-Facing Surfaces A CLI, SDK, or MCP server that agents drive needs the contract to be discoverable and the output to be machine-parseable, not just human-readable. - **Self-describing spec.** Expose a no-auth command that emits the interface as a progressive, token-budgeted JSON tree: a top-level overview (commands, global flags, output shape) drills into a subcommand summary, then a full per-command spec (arguments, options, output schema, examples). The agent orients from the contract itself instead of scraping `--help` or docs. - **Scriptable output contract.** Make the same flags work on every command: `--json` for structured output, `--format text|json|csv`, `--dry-run` to preview a mutation without applying it, `--quiet` implies JSON. Auto-switch to JSON when stdout is not a TTY, and suppress interactive prompts when piped, so an agent gets structured output by default. - **Forward the caller's credential.** When one surface calls another (an assistant calling your API, a gateway forwarding to a service), forward the requesting user's scoped credential, never a service credential. Reject a request on any transport that cannot enforce the credential's scope (for example, a scope-restricted key hitting a WebSocket path that cannot narrow it) rather than silently widening access. -
brief-conventions.md 5.3 KB
# Brief Conventions and Rollout The entry format for the brief's conventions, testing, and quality-bar sections, the convention shapes that recur, and the rollout and rollback section. Load when writing any of those sections of an architecture brief. Make conventions enforceable; leave generic style advice out. ## Contents - Entry format - Convention shapes - Testing shapes - Rollout and rollback ## Entry format Each convention needs four fields: - **Boundary:** the files, modules, package, or entrypoint the rule applies to. - **Failure mode:** the bug, drift, or operational failure the rule prevents. - **Enforcement:** the lint rule, type check, test, generator, or review gate that catches violations. - **Owner:** the package, team, or module that owns exceptions. A convention that cannot name all four is a preference, and preferences do not go in the brief. ## Convention shapes - **Quality bar:** the product qualities the architecture protects, usually reliability, speed, clarity, efficacy, and efficiency. Enforcement: release checklist plus targeted tests, monitoring, and rollback gates. - **Surface-area budget:** every new module, route, job, entrypoint, feature flag, setting, and deployable surface adds relationship cost. Enforcement: the brief names the new relationships, ownership, tests, observability, and deletion or sunset path before the surface is accepted. - **Complete vertical slices:** a slice ships with its contracts, error paths, user-facing states, observability, and rollback path. Enforcement: PR template or release gate rejects happy-path-only slices. - **Entropy control:** old flows, duplicate paths, and low-value features are deleted, sunset, or marked legacy with an owner and review date. Enforcement: legacy registry, deprecation grep, or scheduled cleanup check. - **Domain language:** one canonical name per business concept; aliases listed only for migration. Enforcement: domain glossary plus tests or lint for generated API and schema names. - **Layer imports:** handlers import services; services import DAOs and clients; DAOs import neither handlers nor request objects. Enforcement: import-boundary lint. - **Context initialization:** every RPC, HTTP, job, worker, and CLI entrypoint initializes `RequestContext` before shared services run. Enforcement: entrypoint tests or a fail-closed bootstrap helper. - **Auth policy registration:** each route or RPC method declares an auth policy at registration. Enforcement: type-level registry or startup validation. - **Monorepo dependency ownership:** each deployable app declares its runtime dependencies; root manifests hold workspace tooling only. Enforcement: package-manager constraints or dependency lint. - **Dependency-version single source:** one place defines each shared dependency's version across the monorepo; apps reference it rather than pinning their own. On pnpm that is `catalog:`; on npm workspaces (which have no catalogs) it is `syncpack` version groups. Enforcement: `syncpack lint` or the package manager's own check in CI. - **Tool-owned ordering:** append-only ordered artifacts (DB migrations, changelog entries) are generated by the CLI, never hand-authored; a hand-typed future-dated migration blocks every one after it. Enforcement: CI check that new entries are tool-generated and monotonic. ## Testing shapes - **Test data isolation:** integration and E2E tests generate unique tenant, user, and resource IDs per run. Enforcement: fixture helper plus a test for hard-coded shared IDs. - **Invariant testing:** core invariants hold for any generated input and are asserted after every step of a generated operation sequence, not only at the end. Enforcement: property-based tests plus a harness that injects between-step assertions. - **Idempotency testing:** every operation that touches the outside world produces no second effect when replayed. Enforcement: a test middleware that repeats each declared operation and asserts no change from the second call. - **Crash and resume testing:** long multi-step flows survive dying between any two steps. Enforcement: tests that inject a failure at each step and assert the flow resumes to a consistent state. - **Round-trip testing:** serialize/deserialize and convert/convert-back land where they started, or within a known tolerance. Enforcement: generative round-trip tests over the boundary types. - **Backward compatibility:** current code still reads records written by old code. Enforcement: a corpus of real old-format payloads asserted to deserialize and project correctly. ## Rollout and rollback Prefer instant rollback over perfect pre-merge hygiene: treat PRs as broadcast rather than permission, and buy safety with the reversal path instead of the gate. - Pin every deploy to a commit SHA (build arg or tag) so "what is running" is always answerable. - Ship a one-click rollback workflow: inputs are the target SHA, the environment, and a mandatory free-text reason; it validates the SHA exists before deploying and emits a summary of what moved. - End the workflow with a post-rollback checklist: watch error rates filtered by the deployed version, then investigate the root cause. A rollback without a follow-up reschedules the incident. Enforcement: the workflow itself. A deploy path that cannot name its running SHA, or a rollback that runs without a recorded reason, fails the Operability check in the validation loop. -
contagion-markers.md 5.3 KB
# Contagion Markers Agents mimic whatever code they read first, and they arrive by grep rather than by reading documentation. Anything that must not be copied, or must not be edited, needs a marker at the code site. Load when the repo has legacy, generated, or dual-path code. ## Legacy quarantine Mixed-quality code teaches the wrong conventions, and deleting legacy is not always an option. Quarantine what stays, in three layers: 1. **A greppable marker in the frozen file itself**, at the top: ```ts // LEGACY: do not use as a reference or extend. See docs/legacy.md ``` This is the load-bearing layer. An agent that greps for a symbol and lands in the middle of an old module never opens the doc, so the warning has to be where it lands. 2. **A short `docs/legacy.md`** naming each frozen area, why it is frozen, and **what to use instead**. The replacement is the part that matters: "react-final-form is frozen, use react-hook-form" redirects, "this is legacy" only discourages. 3. **A lint rule** on the deprecated import or package, so the redirect fires at the moment of temptation rather than in review. Pick its severity from the enforcement ladder like any other check rather than defaulting to `warn`: a handful of importers is a rung-1 fix-and-block, and `warn` is right only when the list is too long to clear now. State the marker convention once in the instruction file, so agents know what it means before they hit one, and add a CI grep asserting every file under a quarantined path carries the marker. Without it the convention is true on the day you write it and decays from the next file added. Two further rules: - **Never leave an unmarked old/new dual path.** A deprecated endpoint next to its replacement, or two ways to fetch the same data, reads as two valid conventions. Delete the old path or mark it. - **Quarantine whole directories where that is the natural seam.** A `test-legacy/` folder marked "archived, do not add to, not a source of truth" keeps an old suite runnable without teaching its patterns. ## Deprecation greps Removed APIs, commands, and packages must not reappear in active code or docs. A grep check per removed item, where each hit prints the sanctioned replacement, catches the reintroduction that a linter cannot express. Exclude changelogs and historical docs: they are the record, and failing on them trains people to stop writing them. ## Generated contracts Anything derivable from a schema (API clients, GraphQL types, protobuf messages, DB models) is generated, never hand-written, so no copy can drift: - **Make the schema the single source of truth** and state the rule in the instruction file: import generated types, never hand-write a shape the codegen already owns. - **Commit the generated output.** Agents then read real types on any checkout without knowing how to run codegen. - **Banner every generated file, from the generator itself**, not by hand: ```ts // GENERATED FILE. DO NOT EDIT. Run `npm run codegen` to regenerate. ``` Emit it from the generator's config (`prepend`, `afterOneFileWrite`, or the equivalent) so it cannot be lost on the next regeneration. Naming the regeneration command matters as much as the warning: it puts the agent's next action in the message rather than sending it to look one up. - **Gate contract changes in CI:** a regenerate-and-diff check that the committed output still matches the schema, plus a breaking-change check against the published schema. The banner is the same contagion defense as the LEGACY marker, pointed at the opposite failure: one says do not copy this, the other says do not edit this. ## Deliberate simplifications The third marker, and the one most repos lack. Code that is knowingly simpler than the problem (the in-memory queue that will need a real one, the O(n²) loop that is fine at current volume, the single-region assumption) reads to an agent as either finished work to extend or a bug to fix. Both are wrong, and both are expensive. Mark it at the code site with the ceiling and the upgrade path, so the next reader learns when it stops being correct rather than whether it is: ```ts // SIMPLIFIED: in-memory, single process. Fine to ~10k queued items. // Move to the outbox table (see docs/adr/0012) before multi-instance deploy. ``` The ceiling is the load-bearing half. "This is a simplification" invites a rewrite nobody asked for; "fine to 10k, then do X" is a decision the reader can check against reality. Two rules keep it honest: the marker is for corners cut on purpose, never for a shortcut you would be embarrassed to name, and it is not a substitute for the things that are never simplified (validation at trust boundaries, error handling that prevents data loss, security, accessibility). A simplification that cuts one of those is a bug wearing a marker. ## Dormant config A config file, script, or devDependency nobody invokes is a contagion source in its own right. An agent reads it as live convention and extends it, or runs it and trusts the result. Worse when it points at something that does not exist: a lint config aimed at a `tsconfig` that was renamed produces a confident empty result rather than an error, so the check appears to pass while covering nothing. Delete dormant config and its dependency together. If it is meant to be revived, that is a ticket, not a file left in the tree. -
deepening-existing.md 8.2 KB
# Deepening an existing codebase Find domain-informed deepening opportunities in existing code. "Deepening" means making the design express the domain more faithfully so future changes stay local; not adding layers, not a rewrite. Load during Deepen mode. ## Contents 1. [Vocabulary](#vocabulary) 2. [Map the domain language](#map-the-domain-language) 3. [Deepening opportunity patterns](#deepening-opportunity-patterns) 4. [Module-depth screen](#module-depth-screen) 5. [Dependency and testing checks](#dependency-and-testing-checks) 6. [Rank by leverage](#rank-by-leverage) 7. [Output template](#output-template) 8. [Anti-patterns](#anti-patterns) ## Vocabulary Use these words exactly. Substituting a synonym is not a style slip: it splits one concept across two names in the output, which is the exact failure this analysis exists to find. - **Module**: anything with an interface and an implementation. Scale-agnostic on purpose: a function, a class, a package, or a slice spanning tiers. _Avoid_: component, service, unit. - **Interface**: everything a caller must know to use the module correctly. Not just the type signature: also invariants, ordering constraints, error modes, required configuration. _Avoid_: API, signature (both name only the type-level surface). - **Seam**: the place a module's interface lives, where behavior can be altered without editing in that place. _Avoid_: boundary (reserved here for import and layer rules, and for trust boundaries where input is validated). - **Depth**: leverage at the interface, meaning how much behavior a caller or a test exercises per unit of interface it has to learn. - **Locality**: what maintainers get from depth. Change, bugs, and verification concentrate in one place instead of spreading across callers. **Rejected framing:** depth as the ratio of implementation lines to interface lines. It is the common definition and the one to drift back toward, and it rewards padding the implementation. A module that grew 200 lines of duplicated branching did not get deeper. Depth is leverage at the interface; measure it by what a caller stops having to know. ## Map the domain language Read `CONTEXT.md`, `docs/adr/`, or local equivalents if present. Existing decisions are constraints, not stale obstacles; only challenge them when the current code shows real friction. Recover the ubiquitous language the code uses before proposing changes: - **Entities and values:** domain nouns (Order, Subscription, Payout). Where do they live? Real types, or `any`/loose objects? - **Actions:** verbs (settle, refund, suspend). Methods on a domain object, or free functions scattered across handlers? - **Contexts:** where one part stops caring about another's internals (billing vs catalog vs identity). - **Naming divergence:** one concept named three ways, or one name meaning three things. The strongest signal the model is unclear. Capture a short glossary so opportunities reference real names, not invented ones. Its format and the rules for when a decision is worth recording are in `domain-language.md`. ## Deepening opportunity patterns Each is a concrete, nameable issue, not a vague "this could be cleaner". | Pattern | What it looks like | Why it matters | |---|---|---| | Anemic domain concept | Data in one place, its rules scattered across handlers/services | Changing the rule means hunting every call site; the model doesn't own its invariants | | Shallow module | Public interface nearly matches the implementation, or callers must know internal ordering/invariants | The module adds little leverage; tests and callers still carry the complexity | | Leaking seam | One context reaches into another's tables, internals, or private helpers | Couples contexts; a change in one silently breaks the other | | Naming divergence | Same concept, different names per module, or one name for several concepts | Names can't be trusted; refactors miss instances | | Duplicated concept | Same domain idea reimplemented in parallel | Fixes and rules drift between copies | | Primitive obsession | Core concepts as bare strings/numbers (a `string` userId everywhere) | Nowhere to centralize validation; easy to mix up arguments | | Misplaced logic | Business rule in a transport/handler/UI layer | Untestable without the transport; not reusable | ## Module-depth screen Use this screen to keep the review from becoming generic cleanup advice: - A candidate must hide more behavior behind a smaller public surface, improve locality, or make tests cross one stable interface. - Deletion test: if deleting the module only moves identical complexity elsewhere, it is a pass-through; if deleting it spreads behavior across callers, the module is earning its place and may be worth deepening. - Friction prompts: understanding one concept requires opening many small files; callers need private sequencing knowledge; pure helpers were extracted only to make tests possible while orchestration bugs remain elsewhere; tests cannot exercise behavior through the public surface. - One adapter means a hypothetical seam; two means a real one. A port with a single implementation is indirection you pay for and nothing varies across it. (Distinct from the rule of three for duplicated code below: that one counts copies, this one counts things that differ.) - Do not propose a new seam only because it is aesthetically tidy. A seam needs current variation, a real test adapter, or a named future change it makes local. ## Dependency and testing checks Classify dependencies before suggesting the new shape: | Dependency | Good move | Test shape | |---|---|---| | In-process | Collapse shallow modules and expose one smaller interface | Test directly through the new interface | | Local stand-in exists | Keep the dependency behind an internal seam | Run the stand-in in the test suite | | Owned remote system | Define a port at the network seam | Production adapter plus in-memory test adapter | | True external system | Inject the provider behind a port | Fake or mock adapter, with idempotency and reconciliation for effects | Testing rule: the deepened interface is the test surface. Keep old shallow-module tests until replacement coverage is green, then delete the tests that only preserve the old structure. Do not expose internal seams just because tests use them. ## Rank by leverage Score each by evidence: - Does a current requirement become easier or safer? - Which named future changes become local from this move? - How much churn is required? - Is the duplication proven by 3+ real instances, or only speculated? - Which dependency category applies, and what test seam proves the behavior? Prefer the opportunity that localizes the most future changes for the least churn. Defer or drop the rest. Record every dropped or deferred opportunity in the output's "Out of scope (deferred)" section with its reason. The list is load-bearing: a future audit reads it first so rejected ideas aren't re-evaluated from scratch, and a stale reason ("no current requirement") signals the item to promote. ## Output template ```markdown # Deepening opportunities ## Domain glossary - <concept>: <where it lives, what names it goes by> ## Opportunities (ranked by leverage) 1. [<pattern>] <concept/module> - Observation: <what the code does today, with file paths> - Domain rationale: <how this diverges from the domain model> - Leverage: High | Medium | Low, <which future changes become local> - Depth rationale: <how the move shrinks the interface or improves locality> - Dependency/testing: <in-process | local stand-in | owned remote | external; how behavior will be tested> - Suggested move: <the smallest change that fixes it; name the slice to migrate first> ## Out of scope (deferred) - <opportunity>: <why deferred: speculative / low leverage / no current requirement> ``` ## Anti-patterns - Big-bang rewrite. Migrate one vertical slice first, always. - Renaming for taste, not to match the domain. Every rename must reduce divergence. - Extracting an abstraction from two instances. Wait for three real consumers. - Listing smells without a suggested move and leverage score. Not actionable until both exist. - Inventing domain terms the team doesn't use. Recover language from the code; don't impose new vocabulary. - Designing full target interfaces for every candidate before choosing one. Rank first, then deepen one selected slice. -
distributed-correctness.md 5.7 KB
# Distributed Correctness Patterns Load when designing flows that call external systems, consume webhooks, retry, need an audit trail, or move money. Three principles, each enforced by a test, barrier, or constraint: - **No invented data.** A retry or duplicate must not double-apply: dedupe, idempotency, reconciliation. - **No lost data.** What happened survives a crash: durable progress, at-least-once delivery, append-only history. - **No trust.** Providers, components, and the world fail or lie: verify at the boundary, fail loud, cross-check. ## Idempotency Retried calls must collapse into one effect. - Explicit key scoped to operation and client; deriving one from the payload is fragile. - The check-and-record barrier must be atomic, or concurrent duplicates both pass. - Replay the stored result (including a stored error); do not reprocess. - A step that already advanced the state re-runs as a no-op, not an error. - **Derive the row id from the key** for creates: hash the idempotency key into the new record's primary id (and any batch/transaction ids). A retried create then targets the same id and collapses on the existing unique constraint, so no separate dedupe table is needed. ## Deterministic ids across clients Offline-first and multi-client systems cannot round-trip to the server to mint an id before writing. - Compute the id on the client from stable inputs: a UUIDv5 over a frozen namespace constant plus the logical key (for a join row, the two parent ids). Every client derives the same id for the same logical entity, so concurrent inserts of the same edge converge instead of colliding. - The namespace constant is load-bearing: changing it regenerates every id in the system. Freeze it and treat a change as a data migration. - Keep cross-language ports (a Swift or Kotlin client and the server) in lockstep on the same algorithm and namespace, with a shared test vector, or clients silently disagree on ids. ## Full resumability A flow can die between any two steps; assume it will. - Durable state machine; commit each step before the next. - An independent driver resumes incomplete flows so a crashed orchestrator cannot strand them. - Every step safe to re-run (see Idempotency). - External effects do not roll back: roll forward (retry) or compensate (a saga of undo actions). ## Reliable notification (dual-write problem) A DB change and an event publish share no transaction: publish-then-commit can lose or fabricate notifications. - **Outbox:** write the publish intent in the change's transaction; a relay retries until delivered. CDC and the event log are alternatives. - Delivery is at-least-once; consumers dedupe on a stable event id. ## Reconciliation External-fed data drifts. - Missing records are easy; differing ones (same id, different value) are hard. Bake timing in so in-flight items are not flagged, and match on a stored external id. - Fix each discrepancy with a correction or reprocess, never a silent overwrite. ## Consuming external APIs You control none of a third party's schema, quality, or uptime. - Validate only the fields you use, and fail loud on those; validating ignored fields turns a harmless provider change into an outage. - Persist every request and response: audit trail and reprocessing material. - Sandboxes diverge from production; verify critical paths live (canary, small volume). ## Webhooks are hints, not truth - No guarantees on ordering, validity, delivery, or single delivery. - Verify the signature over the raw received bytes; re-serialization breaks it. - Acknowledge fast (2xx after storing the raw payload), then process asynchronously. - Query the provider's API for authoritative state instead of trusting the payload; retry, since the API can lag. Back delivery with reconciliation. ## Invariants Enforce three layers together: **by construction** (invalid states unrepresentable via types or constraints; cannot express cross-system rules), **at runtime** (assert at the point of violation), **post-factum** (jobs that catch what shipped). - Forbidden is not unrepresentable: do not encode an externally-forceable invariant ("balance never negative") as an unsigned type or hard constraint, or the system crashes or clamps when the world forces the state. Represent it, detect it, recover. ## Money - Never floats; store integer minor units, compute chained math in arbitrary precision. In JSON, money is a string or a minor-unit integer, never a bare number (a JSON number is a double). - Pair amount with currency in one type and forbid cross-currency arithmetic; conversion is explicit, at a controlled rate. - Derive balances from a ledger of movements; a stored mutable balance is a cache, never the source of truth. Book fees explicitly, never leave them as rounding residue. - Round late and once, at the boundary. Splitting then rounding breaks the sum; track the residual explicitly instead of dropping it. - Reserve before spending: check-and-reserve against available balance (total minus reserved) must be linearizable, or two concurrent flows back their spends with the same funds. ## Immutable audit trails Keep the history, not just the latest value. - Append-only. Capture **what** happened, **when**, **who** triggered it, and **why**. - Audit every create/update/delete as one structured record: operation, entity, entity id, and before+after state on updates. Attach the actor from ambient request context so a call site cannot forget it. - Corrections are new records linked both ways to the original, never edits or deletes. - Record event-time and record-time separately; one `created_at` loses information you cannot reconstruct. - For erasure: keep PII in a separate store keyed by opaque id, or encrypt per-user and delete the key (crypto-shredding), so erasure never rewrites history. -
domain-language.md 5.4 KB
# Domain Language and Decision Records The two artifacts that keep a codebase's vocabulary and its non-obvious choices from rotting: a glossary and a set of short decision records. Load when writing or fixing a glossary, resolving naming divergence, or recording an architecture decision. ## Glossary format One file, conventionally `CONTEXT.md` at the repo root. Each entry: ```markdown **Payout**: Money leaving the platform to a seller, after fees and holds are applied. One payout covers many orders. _Avoid_: transfer, disbursement, settlement ``` - **Define what the term IS, not what it does.** One or two sentences. A definition that describes behavior becomes wrong the first time the behavior changes. - **Be opinionated.** When several words name one concept, pick one and list the rest under `_Avoid_`. The Avoid list is the working half of the entry: it is what makes a reviewer or an agent catch the wrong word. - **Only terms this project argues about.** General programming concepts (timeout, retry, cache, event) do not belong even when the codebase uses them constantly. - **Record unresolved ambiguity too**, in a short list at the bottom. "We use 'account' for both Customer and User and have not decided" is more useful than silence, and it names the next decision to make. ## The glossary is a glossary and nothing else No implementation details, no spec content, no scratch notes, no architecture decisions. A general "project context" file accumulates whatever nobody had a home for and stops being read; a single-purpose one stays short enough to stay true. Architecture decisions go in decision records. Implementation detail goes in the code. ## Working practices - **Write it inline, not batched.** When a term resolves mid-conversation, record it there and then. Batched glossary updates never happen. - **Challenge against the glossary as you go.** "The glossary defines cancellation as voiding the whole order, but you seem to mean removing one line item. Which is it?" Naming divergence surfaces in conversation long before it surfaces in code. - **Cross-reference with the code.** "The code cancels whole Orders, but you just said partial cancellation is possible." One of the two is wrong and finding out which is the point. - **Proceed silently when it does not exist.** If the repo has no glossary and no decision records, do not flag their absence or propose scaffolding them upfront. Create the file when there is something to write in it. - **A missing term is a signal.** If the concept you need is not in the glossary, either you are inventing language the project does not use, or there is a real gap. Both are worth a sentence; neither is worth inventing vocabulary over. ## Multi-context repos Where the repo spans several contexts (billing, catalog, identity), each gets its own glossary and the root gets a map: the list of contexts, and their relationships named by the events that cross between them. ```markdown ## Relationships - Ordering to Fulfillment: emits `OrderPlaced` - Billing to Ordering: emits `PaymentCaptured`, `PaymentFailed` ``` The relationship list is the thing worth maintaining. Two contexts with no named event between them are either independent or coupled through something nobody has admitted to. ## When a decision is worth recording Offer a decision record only when all three hold: 1. **Hard to reverse.** If it is cheap to undo, skip it; you will just undo it. 2. **Surprising without context.** If the choice is obvious, nobody will wonder why. 3. **The result of a real trade-off.** If there was no viable alternative, there is nothing to record beyond "we did the obvious thing". The categories that pass most often: deliberate deviations from the obvious path (these stop the next engineer from "fixing" something intentional), constraints not visible in the code (a vendor limit, a contractual obligation, a migration deadline), and choices made under information that has since disappeared. ## Decision record format ```markdown # Enum values are UPPER_SNAKE, response fields stay camelCase Our first two API consumers both hand-wrote switch statements over enum values and both got bitten by casing drift. UPPER_SNAKE makes enum values visually distinct from fields at the call site. The inconsistency is deliberate. ``` A title and one to three sentences of context, decision, and why. That is the whole template. Status, Considered Options, and Consequences are optional and most records will not need them. Add Considered Options only when a rejected alternative is likely to be proposed again; the value of the record is that a decision was made and why, not the completeness of the form. Number them (`docs/adr/0007-enum-casing.md`) so they can be cited, and never edit one to reflect a new decision. Write a new record that supersedes it; the old reasoning is the record's whole point. ## Anti-patterns - A glossary entry with no `_Avoid_` list, when synonyms are in active use. The entry documents the winner without retiring the losers, so both keep appearing. - Inventing domain terms the team does not use. Recover the language from the code and the conversation; a glossary that reads as an outsider's vocabulary gets ignored. - A decision record written as a template with every section filled in. The length signals importance the decision does not have, and nobody reads the third one. - Editing an existing record when the decision changes. Supersede it instead; a record rewritten to match the present cannot explain the past. -
enforcement-ladder.md 6.2 KB
# Enforcement Ladder How to introduce a check into a codebase that already violates it. Load when adding any guardrail to an existing repo. ## The ladder Take the first rung that holds. 1. **Fix the violations and block.** Correct whenever the count is small enough to fix in the same change. The cleanest outcome and more often reachable than it looks: run the tool before assuming otherwise. 2. **Scope with the tool's own config.** Every tool in this category ships one: `knip.json` `ignore` and `ignoreDependencies`, jscpd `ignore` globs, dependency-cruiser `pathNot`. The exclusion sits next to the rule, so anyone reading the config sees what is exempt. **Not the linter's `ignorePatterns`** (ESLint and Oxlint both have one). It removes those files from *every* rule, not the one you are adding, so trading 400 long files for 400 unlinted files leaves the repo worse while looking like you followed the ladder. Rung 2 holds for the linter only when the violations sit in directories that should be unlinted anyway (generated output, vendored code); carve those out first and they come off the count before you pick a rung for the rest. 3. **Allowlist or downgrade in the linter.** An `overrides` entry (Oxlint's `.oxlintrc.json`, or an ESLint flat-config object scoped with `files`) naming the current offenders, or start the rule at `warn` and promote to `error` once burned down. Use this when the violations are a known finite list you intend to shrink. Prefer `error` plus an allowlist over a blanket `warn`: `warn` fails to block the next new violation, which is the whole point of adding the rule. List explicit paths, never globs, so the exemption cannot silently cover a file written tomorrow, and so growing it shows up as added lines in a diff a reviewer reads. That review is the only thing holding the list down. An allowlist has the same pull as the baseline file below (under deadline, the cheapest green is appending your path), and it does not even fail when it grows; what it has instead is that every addition is visible, attributable, and in the same file as the rule it defeats. 4. **Report-only, non-blocking CI.** The rule runs and prints, nothing fails. Lowest value, but it beats not running: the number is visible and the wiring is done for the day someone burns the list down. Whichever rung you pick, **write down which and why** in the config file or the CI step itself. The next person needs to know whether they are looking at a deliberate exemption or an accident. ## Never hand-roll a baseline Do not write a custom guard script plus a committed baseline file (`*-ratchet.mjs` and `*.baseline.json`) that records the current violation count, fails when it grows, and rewrites itself downward when it shrinks. It is an appealing design and it does not survive contact. It was built and deleted for two reasons: - **Every tool in the category already ships the mechanism.** Rungs 2 and 3 are native features of knip, the linter, dependency-cruiser, and jscpd. The custom layer reimplements them and adds a file that must be regenerated, reviewed, and merged. - **The baseline becomes the thing people edit.** Under deadline the cheapest green is a bigger number, and a baseline that only shrinks by convention does not only shrink. The exception is narrow: a genuinely bespoke invariant no tool expresses (a naming rule derived from file paths, a ban on a specific cast shape, registry completeness). Write that as a check, and even then use rung 1 or 3 for the existing violations rather than a count file. Structural specs that walk the filesystem cover most of this ground and ride the existing test command. ## Ship it green Land the rule and the fix for its existing violations in the same change. A rule that ships red teaches everyone, agent and human, that this particular check is noise to be worked around. That lesson generalises to the next check you add. The ladder exists precisely so you never have to choose between shipping red and not shipping. ## Prove it bites The completion criterion for installing any guardrail, and the step most often skipped because the config "obviously" works: 1. Run the check. It must **pass**. 2. Introduce a violation on purpose (a deep import, a duplicated block, an unused export, a misnamed directory). It must **fail**, and the message must name the fix. 3. Revert. It must **pass** again. A check nobody has watched fail is not known to work. The common failure is silent: a glob that matches nothing, a path alias the tool cannot resolve, a rule registered under a config section the runner never reads. All three produce a green run that proves nothing. ## Self-explaining failures Every violation message states why the invariant exists and how to fix it, not just where it fired: ``` src/modules/billing/lib/invoice.ts is imported from src/modules/orders/checkout.ts. Modules are reachable only through their index.ts, so internals can be refactored without breaking other modules. Import from ~modules/billing instead. ``` An agent that gets this self-corrects on the spot. An agent that gets `no-restricted-imports` and a path guesses, or asks, or reverts something unrelated. Boundary-lint messages should also name the rule and link the convention doc, so the failure teaches the convention it enforces. Some rules cannot carry a custom message: `max-lines` in both Oxlint and ESLint emits a fixed string with no why and no fix, and takes no `message` option. Where that is the case, put the explanation in a comment above the rule in the config, which is where anyone debugging the failure looks next, and do not let the gap talk you out of the rule. ## Graduated enforcement Two variants of the same check, wired at different strictness: - **Pre-commit:** fast and warn-only where the check is slow or noisy. Fast signal without blocking a work-in-progress commit. When a hook does fail an agent's commit, it self-corrects immediately; that is the cheapest QA round available. - **CI:** blocking. This is the merge gate, and an invariant that is not gated here decays silently. Wire both, always. Hooks are not guaranteed installed on a fresh clone or in a worktree, which is exactly where agents run; CI alone gives feedback long after the agent's edit loop has moved on. -
evaluation-scenarios.md 5.4 KB
# Codebase Architecture Evaluation Scenarios Run these when changing the skill. This file never loads during a user task; it is a rubric for the author, not guidance for the agent. Evaluate the observable workflow, not whether the answer repeats the skill's wording. Scenarios 1 to 3 test mode routing, which is the property this skill was restructured to get right, so a regression there matters more than anything else here. ## Contents - 1. Greenfield structure (Design) - 2. Change has become expensive (Deepen) - 3. Agents copy the wrong pattern (Harden) - 4. A check the repo already fails - 5. Modes compose - 6. Sibling boundary - 7. A gate that is already green - Ablation notes ## 1. Greenfield structure (Design) **Prompt:** "We're starting a new API and an admin dashboard. How should I structure this?" **Expected behavior:** - Enters Design mode and says so; opens no Deepen or Harden reference. - Asks about constraints (team size, scale, deploy targets, compliance) before proposing a repo shape. - Each module contract it proposes names the lint rule, type check, or test that catches a violation. - Produces the architecture brief template. - Does not run `git log` for hot spots; that step belongs to Deepen and there is no history to read. ## 2. Change has become expensive (Deepen) **Prompt:** "Adding one discount type touched nine files. Something is wrong with how this is organised." **Expected behavior:** - Enters Deepen mode and loads `deepening-existing.md`. - Runs `git log` to find hot spots **before** listing opportunities, and weights those paths. - Records each opportunity with file paths and a named pattern, never a vague smell. - Ranks by leverage before designing any target interface. - Proposes one vertical slice to migrate first. - Does not emit the full architecture brief; the artifact is the ranked opportunity list. ## 3. Agents copy the wrong pattern (Harden) **Prompt:** "Claude keeps extending our old Redux store instead of the new one. Fix the repo so it stops." **Expected behavior:** - Enters Harden mode and reaches `contagion-markers.md`. - Proposes a greppable marker **in the frozen files themselves**, plus the doc entry naming the replacement, plus a lint rule on the deprecated import at the rung the ladder picks (fix-and-block when the importer list is short, `warn` only when it is not). - Does not answer with a docs entry or an AGENTS.md line alone; an agent that arrived by grep never reads either. - Does not open a dead-code or duplication tool, which addresses a different failure. - Opens no Design reference. ## 4. A check the repo already fails **Prompt:** "Add a file-size limit. About 400 files are over any cap we'd pick." **Expected behavior:** - Loads `enforcement-ladder.md` and names which rung it picked and why. - **Never proposes a custom guard script with a committed baseline file.** This is the regression test for the doctrine the skill previously had wrong; any `*-ratchet.mjs` or `*.baseline.json` in the answer is a failure regardless of how good the rest is. - Lands the rule green, by fixing or by scoping with the tool's own config, rather than shipping it red. - Names the pass, break, revert observation as the completion criterion rather than declaring the config correct. ## 5. Modes compose **Prompt:** "Design the module structure for our new billing service, and make sure it stays that way." **Expected behavior:** - Runs Design then Harden, and states that it is doing both. - Carries each Design contract into Harden as a specific check, rather than restating the contracts. - Does not satisfy "stays that way" with prose in AGENTS.md when a static tool can check it. ## 6. Sibling boundary **Prompt:** "Review my diff and clean up the architecture while you're at it." **Expected behavior:** - Routes the diff review to `pr-reviewer` and diff-scoped fixes to `tidy`. - Applies this skill only to what is outside the diff, or asks which is wanted. - Does not launch a repo-wide deepening scan in response to a diff-shaped request. ## 7. A gate that is already green **Prompt:** "CI already runs jscpd and knip, but duplicated code keeps merging. Why?" **Expected behavior:** - Runs Harden step 1 (survey) before proposing any new tool, and reads the actual CI step and config. - Identifies that `jscpd` without `--threshold` exits 0 regardless of findings, and checks whether knip's step gates the merge or only prints. - Fixes the wiring (a threshold, a blocking step) and proves each bites with pass, break, revert, rather than adding a third tool. - Does not propose madge or any tool with no release in the last two years; for cycles it reaches for `import/no-cycle` or dependency-cruiser. ## Ablation notes Rules whose absence has been observed to regress a scenario, so they should not be cut in a future density pass: - The `git log` hot-spot step in Deepen (scenario 2). Without it the opportunity list fills with modules nobody touches. - The explicit "never hand-roll a baseline" wording in `enforcement-ladder.md` (scenario 4). The baseline design is intuitively appealing and is what the model reaches for unprompted, which is why the prohibition is stated as an absolute here rather than as an outcome. - The "exits 0 without `--threshold`" fact in `guardrail-tooling.md` (scenario 7). Without it the model adds a bare `jscpd` step and reports the gate as wired. Everything else in the bundle is unablated and should be treated as a guess until a scenario proves otherwise. -
guardrail-tooling.md 11.2 KB
# Guardrail Tooling The checks worth wiring, what each catches, and how to scope it. TypeScript-first and Oxlint-first, since the stack default runs Ultracite over Oxlint; each rule names its ESLint or Biome form where a repo runs those instead. The categories are language-agnostic, so swap the tool per ecosystem. Load when choosing and wiring the actual checks. ## Contents - Pick by failure, not by tool - The standard set - Module public-interface boundary - Layering and package boundaries - Structural lint that reflects the filesystem - Regenerate-and-diff staleness gates - The environment contract - Scheduled cleanup passes - Convention entries for the brief ## Pick by failure, not by tool Name the failure this repo actually exhibits, then pick the check. Installing the full set in one pass produces a wall of violations, forces the weakest enforcement rung on all of them, and trains everyone to skip hooks. ## The standard set | Category | Tool | Catches | Native scoping | |---|---|---|---| | Dead code, unused exports and deps | `knip` | Orphaned helpers agents leave behind, then later read as live convention | `ignore`, `ignoreDependencies`, per-workspace `entry` in `knip.json` | | Copy-paste duplication | `jscpd --threshold <percent>` | Near-duplicate blocks where a fix lands in one copy and the others drift | `ignore` globs and `minTokens` in `.jscpd.json`; `threshold` is the part that fails the run | | Import cycles | `import/no-cycle` (Oxlint; ESLint via `eslint-plugin-import-x`), or dependency-cruiser's `no-circular` where the repo already runs it | A tangled module graph where a change breaks something non-local | Oxlint's `ignoreTypes` defaults on, so type-only edges do not count; `maxDepth` bounds the walk | | Module and layer boundaries | `no-restricted-imports` with `patterns` (Oxlint, ESLint, and Biome all take `group` or `regex` plus `message`) | Deep imports across modules, a DAO importing a handler | The linter's `overrides` allowlist naming current offenders | | Package boundaries in a monorepo | `turbo boundaries` (experimental) | A package importing a sibling's files by relative path, or using a dependency its `package.json` does not declare | `boundaries.tags` in `turbo.json` with `allow` and `deny` per tag | | File size and complexity | `max-lines` with `max` set explicitly (Oxlint's default is 300), `complexity` | Files too big to read in one pass, so agents chunk-read and revisit | Per-file override requiring a justifying comment | Two defaults that produce green runs proving nothing: - `jscpd` exits 0 whatever it finds until `--threshold` (or `threshold` in `.jscpd.json`) is set. A bare `jscpd src` step in CI is a report wearing a gate's clothing. - `knip` is zero-config for anything its plugins recognise: Next.js routes, Vitest, `package.json` `main` and `bin`, and most tool configs. Declare `entry` only for what no plugin sees (ad-hoc scripts, codegen inputs, custom tool configs). Leave those out and knip reports them and everything only they reach as unused; declare too many and real dead code hides behind them. Madge is the tool most answers reach for on cycles. It has had no release since 2024. Prefer the linter rule, which already runs in the edit loop, or dependency-cruiser, which is maintained and covers layering too. ## Module public-interface boundary Where the repo has feature or domain modules, the rule that pays for itself: a module is reachable from outside only through its root files, while relative imports inside a module stay legal. ```json // .oxlintrc.json (the same object goes under `rules` in an ESLint flat config) { "rules": { "no-restricted-imports": ["error", { "patterns": [{ "regex": "^~modules/[^/]+/[^/]+/", "message": "Modules are reachable only through their root files (index.ts, client.ts, server.ts), so internals can be refactored without breaking other modules. Import from ~modules/<name> instead." }] }] } } ``` Public versus private is then decided by depth rather than by a list: a module's root files are its interface, anything in a subfolder is private, and a new subfolder never needs a config change. A module may expose several small entry points (`index.ts`, `client.ts`, `server.ts`) instead of funnelling everything through one barrel that re-exports a whole subtree. Biome takes the same idea as a gitignore-style `group` (`~modules/*/*/**`) rather than a regex; Oxlint's regex engine has no lookarounds, and this pattern needs none. Which module may depend on which is a separate concern from what is reachable. Leave layering as its own rule; conflating the two produces a config nobody can reason about. ## Layering and package boundaries Three shapes, picked by how many relationships the rule has to express: - **A few layers** (handler, service, dao): `no-restricted-imports` per layer directory, one `overrides` entry per layer with the imports it may not take. Readable at three layers, unreadable at ten modules. - **A dependency matrix across many modules**: dependency-cruiser. `npx depcruise --init` writes `.dependency-cruiser.cjs`; each `forbidden` rule carries a `comment` the reporter prints, so the failure explains itself. Point `options.tsConfig` at the repo's tsconfig or path aliases fail to resolve, the edge is silently dropped, and the rule passes on nothing. ```js // .dependency-cruiser.cjs module.exports = { forbidden: [ { name: "no-circular", severity: "error", from: {}, to: { circular: true } }, { name: "dao-below-handler", severity: "error", comment: "DAOs know nothing about transport. Move the logic into the service, or pass the data down.", from: { path: "^src/modules/[^/]+/dao/" }, to: { path: "^src/modules/[^/]+/handler/" }, }, ], options: { tsConfig: { fileName: "tsconfig.json" } }, }; ``` Run it as `depcruise --config .dependency-cruiser.cjs src`. - **Across packages in a turborepo**: `turbo boundaries`. It checks the two package-level failures in the table above by default and enforces `turbo.json` tags transitively. It is experimental and sees nothing inside a package, so it complements the import rule rather than replacing it. Escape hatches, for repos that already made the choice: on Nx, `@nx/enforce-module-boundaries` with `scope:` and `type:` tags; on Feature-Sliced Design, `steiger` with `@feature-sliced/steiger-plugin` rather than hand-written import rules for the layer order. ## Structural lint that reflects the filesystem The highest-leverage check for a repo with a naming or shape convention, and the one no off-the-shelf tool provides. A test spec walks the tree and asserts the invariants: - Directory naming (kebab-case under `src/modules`, no exceptions list). - File naming and required files per module. - Generated-file banners present wherever generated output lands. - Registry completeness: where every X must be registered (tools in a manifest, tables in a deletion sweep, routes with an auth policy), the spec reflects over the real code and fails when an item is missing from the registry. Nothing can be added without declaring it. Two properties make this worth writing by hand: - **It rides the existing test command**, so it needs no extra CI wiring, no new script, and no separate failure surface to explain. - **The assertion is the doc.** A prose convention drifts from the code silently; a spec that walks the same filesystem cannot. Ship it with its existing violation fixed in the same change, including the import and alias updates a directory rename implies. ## Regenerate-and-diff staleness gates Anything derived from a source of truth gets a CI step that re-derives it and fails on a dirty diff: ```bash npm run codegen git diff --exit-code src/generated ``` Applies to codegen output, API and RPC clients, docs generated from code, lockfiles, and schema snapshots. It is the only check that catches the specific failure of someone editing generated output by hand, or changing the schema and committing without regenerating. When the upstream schema is not reachable on pull requests, gate against a committed snapshot instead and state the reduced coverage in the step's own name or comment, so nobody later reads a passing gate as full coverage. ## The environment contract `.env.example` is the canonical list of environment variables, and a static check keeps it honest: scan source for environment reads (`process.env`, the config library's accessor) and fail when a referenced key is not listed, naming the missing key. Prefer the static scan over boot-time validation. It runs in CI without booting the app, and it works in repos with no dependency-injection boot to hook. Where the app does boot in CI, a boot check that constructs the wiring while loading `.env.example` covers both at once. ## Scheduled cleanup passes Agent-written code accretes single-use helpers, stale dual paths, and bloated files even with every check above wired. Small scheduled passes beat waiting for a rewrite: 1. Run the dead-code and duplication tools; delete what they flag. 2. Split any file over the size cap along its natural seams. 3. One slice at a time, verified by the existing suite. Never big-bang. Refactor safety equals test coverage: a thin suite caps how aggressive a pass can be, so growing coverage is part of staying agent-ready rather than a separate track. Run `tidy` for the diff-scoped version of this. ## Convention entries for the brief Four fields, per the entry format in `brief-conventions.md`: **Boundary** (what it applies to), **Failure mode** (what it prevents), **Enforcement** (what catches a violation), **Owner** (who owns exceptions). A convention that cannot name all four does not belong in the brief. - **Dead code:** Boundary: all packages. Failure mode: agents grep dead helpers, treat them as live conventions, and extend them. Enforcement: `knip` in pre-commit and CI. Owner: platform/tooling. - **File size:** Boundary: all source files. Failure mode: agents burn tokens on chunked reads and revisit the file per task. Enforcement: `max-lines` at ~400, per-file overrides requiring a comment. Owner: each package. - **Duplication:** Boundary: all packages. Failure mode: fixes land in one copy and drift from the others. Enforcement: `jscpd --threshold` in CI. Owner: platform/tooling. - **Module interface:** Boundary: cross-module imports. Failure mode: a refactor inside one module silently breaks another. Enforcement: `no-restricted-imports` banning subfolder paths, message naming the fix. Owner: each module. - **Enforcement rung:** Boundary: every check with pre-existing violations. Failure mode: a rule lands red, gets ignored, and the next check inherits the habit. Enforcement: the rung is recorded in the config or CI step, and each check has been observed failing. Owner: platform/tooling. - **Generated contracts:** Boundary: schema files and committed generated output. Failure mode: agents hand-edit generated files or hand-write shapes the codegen owns. Enforcement: generator-emitted banner plus a CI regenerate-and-diff check. Owner: the schema-owning package. - **Legacy marker:** Boundary: paths listed in the legacy doc. Failure mode: agents copy deprecated patterns into new code. Enforcement: CI grep asserting every file under a listed path carries the marker. Owner: the team that owns the migration. -
stack-defaults.md 1.1 KB
# Stack Defaults Default baseline; deviate only when a project constraint requires it, and record the deviation in the brief. This is the most perishable file in the bundle: it is a preference, not a finding, and library choices rot faster than anything else here. **Last reviewed: 2026-09.** Treat it as stale if that date is more than a year old, and confirm the choice against the ecosystem before writing it into a brief. - Turborepo + npm workspaces. - Next.js App Router + React. Request interception lives in `proxy.ts` (Next 16 renamed `middleware.ts`; the codemod is `npx @next/codemod@canary middleware-to-proxy .`). - Tailwind CSS + shadcn/ui. - React Hook Form + TanStack Query (Connect Query where the API is ConnectRPC). - ConnectRPC + protobuf types. - Prisma + Postgres. - Supabase (auth/storage), Stripe (payments), Resend (email), Twilio (SMS). - Ultracite preset over Oxlint + Oxfmt, hooks via Lefthook. Guardrail rules in `guardrail-tooling.md` name the Oxlint form first and the ESLint or Biome equivalent where a repo runs those instead. - Vitest. - Deploy: Vercel (web) + Fly.io (API). -
verification-tiers.md 4.3 KB
# Verification Tiers An agent that knows exactly which check to run wastes no tokens running the wrong one, and a command that passes while proving nothing costs more than one that fails. Load when defining which commands an agent should run, and when. ## Contents - The tier ladder - Latency budgets and file-scoped variants - Commands that lie - CI runs the same command - The boot check - Tests that survive parallelism ## The tier ladder Name the tiers, publish the routing, and state in the instruction file which tier gates a commit. | Tier | Contents | Runs when | |---|---|---| | `check` | lint, typecheck, format check | Continuously during an edit loop | | `verify` | `check` plus unit tests | Before a commit | | `verify:full` | `verify` plus integration, boot check, staleness gates | Before a push or in CI | Three tiers is the useful number. Two collapses the edit loop into the test suite; four leaves the agent guessing which to pick. ## Latency budgets and file-scoped variants Encode the budget in test filenames so "run the narrowest relevant tier" is executable rather than a judgment call: `*.test.ts` under 3s, `*.integration.test.ts` under 10s, `*.e2e.test.ts` unbounded and excluded from `verify`. Publish file-scoped variants too (`lint:file`, typecheck one project, test one path). The agent knows exactly which files it touched, so a per-file check is the fastest loop available to it, and the one it will actually run between edits. ## Commands that lie The sharpest thing to write down, and the one nobody thinks to. Enumerate this repo's commands that pass while proving nothing, because a green-but-lying command is worse than a red one: it ends the investigation. Recurring shapes worth checking for: - **A flag that hides findings.** `--quiet` suppressing lint warnings, a reporter that swallows a category, a threshold set so high nothing trips it. - **A tool that reports but never fails.** `jscpd` without `--threshold` exits 0 on any amount of duplication; a boundary tool run without `--validate` or with every rule at `info`. The step is present, green, and gating nothing. - **A filter matching zero files.** A test path pattern, a workspace filter, or a glob that silently matches nothing and exits 0. This is the most common one and the most convincing. - **A stale incremental cache.** `tsc --incremental` or a build cache returning a result computed against code that has since changed. - **A step skipped by condition.** A CI job gated on a path filter or a branch condition that no longer matches, so the gate is nominally present and never runs. Publish the list in the instruction file or a verify doc, with what to run instead. It is per-repo, so the generalisable move is making the list a step rather than the list itself. ## CI runs the same command CI invokes the same umbrella command a developer runs (`verify:full`), rather than a hand-maintained list of individual steps. A separately-maintained CI list drifts: a check gets added locally and never wired, or a step is dropped during a refactor and nobody notices because the absence of a failure looks like success. Where CI must diverge, make the difference explicit and stated rather than emergent. ## The boot check A check that constructs the app's wiring (dependency container, module graph, route registration) without serving traffic. It catches the class of error typecheck and unit tests both miss, which is exactly the class agents introduce when they add a dependency or register a new module: everything compiles, every unit test passes, and the app cannot start. Have it load `.env.example` so the environment contract is exercised at the same time. ## Tests that survive parallelism Guardrails only hold if the suite behind them is trustworthy: - Unique IDs per test, generated at runtime, never shared fixtures. Shared fixtures collide the moment the suite runs in parallel, and the resulting flake teaches everyone to rerun rather than read. - Seeded randomness and a frozen-clock helper, so a failure reproduces. - Unit tests stay database-free; anything needing a database is an integration test and named as one. - A pluggable interface ships its behavioral spec as an importable contract test suite, so a new adapter (often agent-written) proves conformance by calling one function rather than reimplementing the expectations. -
wayfinding.md 6.7 KB
# Wayfinding Making the right thing cheap to find. Load when agents cannot locate things, keep re-deriving the same path, or cite documentation that is no longer true. ## Contents - Why this pays - Naming and locality - Add-a-new-X recipes - One canonical instruction file - The docs index and trust labels - Docs that stay true ## Why this pays Code cleanliness does not change an agent's pass rate; it changes the cost of every task. Across 660 Claude Code trials over repo pairs matched on architecture, dependencies, and behavior but differing on rule violations and cognitive complexity, the clean side used 7 to 8% fewer tokens and revisited already-edited files 34% less often, with pass rate unchanged within noise (Sonar, [arXiv:2605.20049](https://arxiv.org/abs/2605.20049)). Two mechanisms drive it: - **Traversal cost.** Agents rebuild context per task by grepping and reading. Predictable names and small files mean the first guess lands; bloated files mean chunked reads and repeated visits. - **Convention contagion.** Agents mimic whatever code they read first. A legacy pattern sitting unmarked next to the current one gets copied even when the instruction file says otherwise. ## Naming and locality - Name files for what someone would grep first: `invoice-refunds.ts`, not `utils2.ts` or `helpers.ts`. - Keep files small enough to read in one pass. The ~400-line lint cap doubles as a traversal budget. - Co-locate code that changes together. A feature spread across six directories is six reads before the first edit. - One canonical name per concept. Naming divergence, one concept with three names, is the strongest confusion signal for agents and humans alike. Deepen mode recovers the vocabulary; the glossary format is in `domain-language.md`. ## Add-a-new-X recipes The single highest-value wayfinding artifact, and the one most repos lack. One file (`docs/knowledge/common-workflows.md` or local equivalent) holding numbered recipes for the additions this codebase makes over and over: a new module, a new screen and route, a new query and its generated hook, a new store, a new feature flag, a new translated string, a new locale. Format rules that make it work: - **Ten steps or fewer per recipe.** Longer means the thing itself needs simplifying, and the recipe is documenting the problem. - **Name exact files and exact commands.** "Register it in the module manifest" is not a step; `src/modules/index.ts`, plus the line to add, is. - **Link out for depth, never duplicate.** The recipe is the path; the deeper doc holds the reasoning. Duplicated prose drifts within a quarter. - **Trace every recipe against real code before writing it.** A recipe written from memory names files that moved, and an agent follows a stale pointer with full confidence rather than an error. - **Index it from the instruction file**, or it will not be found by the agents that need it most. Acceptance is behavioral, not editorial: a fresh-context agent follows one recipe end to end with no further guidance. If it stalls or asks a question, the recipe is missing a step. ## One canonical instruction file Two overlapping instruction files (AGENTS.md and CLAUDE.md, or per-tool variants) read inconsistently and drift apart, and the agent has to reconcile them before it can act. - Merge into one canonical file and symlink the other to it. One file to update, every tool reads the same content. - Never create a second one when the first exists. - Update it in the same change that changes the convention, not in a later docs pass. - Keep it hand-curated. Hand-written context files measurably beat LLM-generated ones (p=0.038), while generating one raises cost 20 to 23% for no significant accuracy change either way. Generated files mostly parrot documentation the repo already has: they only beat having no file at all once the README and docs are deleted ([arXiv:2602.11988](https://arxiv.org/abs/2602.11988)). - Write the requirements the agent cannot discover, not an overview of what it can. Directory enumerations and codebase tours appeared in every generated file in that study and did not reduce the steps taken to reach the relevant code. A specific instruction does land: agents told to use a particular tool used it 1.6 times per task on average, against fewer than 0.01 times when it went unmentioned, so instruction-following is literal enough that a wrong line is as load-bearing as a right one. ## The docs index and trust labels Stale docs are worse than no docs, because an agent cites them confidently. Make the trust level explicit rather than implied. Index every agent-facing doc with two pieces of metadata: - **A trust label.** *Live* (maintained, believe it), *Reference* (stable background, still true but not actively tended), *Historical* (point-in-time artifact, do not treat as current). - **A one-line consult-when scope**, so the agent knows whether to open a doc before paying to read it. ```markdown | Doc | Trust | Consult when | |---|---|---| | docs/knowledge/common-workflows.md | Live | Adding a new module, screen, query, flag, or locale | | docs/knowledge/auth.md | Live | Touching session handling or a protected route | | docs/adr/ | Reference | A decision looks arbitrary and you want the reason | | docs/migrations/2024-mongo-to-postgres.md | Historical | Reading old code that still assumes Mongo shapes | ``` **A dangling index entry is worse than a missing one.** An index that points at files which do not exist sends the agent looking, and the absence reads as "I have the wrong path" rather than "this does not exist". When an entry names something unwritten, either write it or delete the entry, then grep-verify that every path in the index and the instruction file resolves. ## Docs that stay true - **Anchor to domain concepts over file paths** where possible. Paths go stale silently. Link-check the pointers that remain in CI, including the ones inside the instruction file. - **Validate what can be validated:** code snippets compile, frontmatter parses, any registry that mirrors docs into code stays in sync. - **Ship a copy-paste template file** next to the prose for any pattern agents must reproduce. A working file teaches more reliably than a description of one. - **Test doc examples against the real interface.** A drift test that extracts every command invocation from the docs, resolves each against the live command tree (command path and flags both exist), and fails the build on a mismatch turns "examples must stay runnable" into a gate. Reject past-dated examples in the same test so a stale snippet fails instead of misleading. - **Pair each non-obvious claim with the command that re-proves it.** A note shipping its own repro lets the reader re-verify rather than trust a claim that may have rotted.
-
-
SKILL.md 22 KB
--- name: codebase-architecture description: Designs module contracts, deepens existing boundaries, and installs enforceable repository guardrails. Use when asked to "design the architecture", "simplify our modules", or "harden the repo". For one feature plan use planning; for diff cleanup use tidy; for tenancy use multi-tenant-architecture. --- # Codebase Architecture Decide a TypeScript codebase's structure, improve it where change has become expensive, and make it hold. The target is a codebase a reader can hold in their head: few surfaces, one canonical way to do each job, and behaviour where you would first look for it. - **IS:** folder structures, module contracts, request context and middleware pipelines, frontend/backend boundaries; architecture briefs; domain language and decision records; domain-informed deepening; guardrail tooling, CI gates, and agent wayfinding. - **IS NOT:** scaffolding a new repo (`scaffold-nextjs` for a Next.js turborepo, `scaffold-cli` for a TypeScript CLI), multi-tenant domain/isolation/routing (`multi-tenant-architecture`), the content of AGENTS.md itself (`agents-md`), a plan for one feature (`planning`), a diff-scoped cleanup pass (`tidy`), or structural review of a local diff (`pr-reviewer`). ## Contents - Modes - References - Design mode (new codebase) - Deepen mode (existing codebase) - Harden mode (make it stick) - Validation loop - Output template - Excuses - Gotchas - Related skills ## Modes Pick by the problem, not by the artifact, and say which you picked. | Mode | You are here when | Output | |------|-------------------|--------| | **Design** | Starting a new app, service, or surface, and the structure is not decided yet | An architecture brief | | **Deepen** | The code works, but change is expensive: concepts scattered, seams leaking, one idea under three names | Ranked opportunities, then one migrated slice | | **Harden** | The structure is decided and keeps decaying, or agents keep doing the wrong thing in this repo | Wired checks, markers, and recipes | **Modes compose, and running more than one is normal.** Design ends in Harden, because a contract with no check is a suggestion. Deepen ends in Harden, so the new seam cannot decay back. Harden runs alone when the structure is already right and only the enforcement is missing, which is the common case in a repo that agents work in. **When two look equally right, prefer Deepen.** "Agents keep using the old pattern" sounds like Harden, but if the cause is one concept living in two places, quarantine only freezes the duplicate: Deepen deletes it and Harden holds the line until that lands. Harden alone is right when the old thing genuinely has to stay. **When you cannot write to the repo** (no checkout, read-only request, or a question rather than a change), each mode's output degrades to its plan: the brief, the ranked opportunities, or the named checks with their rungs. Say which checks remain unproven, since none of them are wired. **As simple as possible, no simpler.** Every mode cuts: surfaces in Design, concepts in Deepen, dual paths and dormant config in Harden. The floor does not get cut: validation at trust boundaries, error handling that prevents data loss, security, accessibility, observability on anything deployed, and whatever was explicitly asked for. A simplification that reaches one of those is a bug. Where a corner is cut on purpose, mark it with its ceiling and upgrade path rather than leaving the next reader to guess whether it is finished. Copy this to track progress, and delete the lines for modes you are not running: ```text Codebase architecture progress: - [ ] Modes chosen and stated (Design / Deepen / Harden) - [ ] Design: assumptions stated, repo shape, every contract names its check, brief written - [ ] Deepen: git hot spots, glossary, ranked opportunities with paths, one slice migrated - [ ] Harden: existing checks surveyed, checks picked by failure, each landed green and proven to bite - [ ] Validation loop run for the modes used; evidence recorded, N/A items named ``` ## References Load only when the condition applies. | Reference | Mode | Read when | |-----------|------|-----------| | [references/stack-defaults.md](references/stack-defaults.md) | Design | Choosing libraries, tooling, or deploy targets | | [references/api-design.md](references/api-design.md) | Design, Deepen | Designing endpoints, module contracts, request context, error shapes, or an agent-facing CLI/SDK surface | | [references/distributed-correctness.md](references/distributed-correctness.md) | Design, Deepen | The work provably touches an external system, webhook, retry, audit trail, or money. In Deepen you can grep for it; in Design it is a question about requirements, so confirm before loading rather than inferring it from the product's domain | | [references/brief-conventions.md](references/brief-conventions.md) | Design | Writing the conventions, testing, quality-bar, or rollout and rollback sections of the brief | | [references/deepening-existing.md](references/deepening-existing.md) | Deepen | Running Deepen: vocabulary, opportunity patterns, output template | | [references/domain-language.md](references/domain-language.md) | Deepen | Writing or fixing a glossary, resolving naming divergence, recording a decision | | [references/enforcement-ladder.md](references/enforcement-ladder.md) | Harden | Adding any check to a repo that already violates it | | [references/guardrail-tooling.md](references/guardrail-tooling.md) | Harden | Choosing and wiring the actual checks: dead code, duplication, cycles, module and package boundaries, file size, staleness gates | | [references/wayfinding.md](references/wayfinding.md) | Harden | Agents cannot find things, or keep re-deriving the same path | | [references/contagion-markers.md](references/contagion-markers.md) | Harden | The repo has legacy, generated, dual-path, or deliberately simplified code | | [references/verification-tiers.md](references/verification-tiers.md) | Harden | Defining which commands an agent should run, and when | | [references/agent-runtime.md](references/agent-runtime.md) | Harden | Configuring session hooks, permissions, or review gating | | [references/evaluation-scenarios.md](references/evaluation-scenarios.md) | none | Changing this skill. Never loads during a user task; it is the author's rubric | ## Design mode (new codebase) Before any of this, ask whether each surface needs to exist. A module, service, app, or entrypoint that could be a folder in something that already ships is the cheapest architecture decision available, and the only one that stays cheap. Every surface you do accept pays the relationship cost in the surface-area budget (`brief-conventions.md`): name its owner, tests, observability, and deletion path before it goes in the brief. 1. Constraints first: product scope, team size, compliance/security, expected scale, deploy targets, required integrations, and quality bar. A one-line request supplies none of these, so assume the common case, state every assumption in the brief's first section, and invite correction. Ask outright only where a wrong guess would restructure the brief rather than extend it, which in practice is multi-tenancy and whether the API is public. 2. Choose repo shape: - `apps/` for deployable surfaces (`api`, `web`, `admin`). - `packages/` for shared libraries (`shared`, `ui`, `icons`, `auth`, `proto`). 3. Define backend module contracts, each naming its enforcement (import-boundary lint or type check): - `handler`: transport only. - `service`: business orchestration. - `dao`: database access only. - `mapper`: DB/proto/domain transformations. - `constants` and `types`: module-local contracts. 4. Define request context and middleware: - Carry `tenantId`, `userId`, and `traceId` in an AsyncLocalStorage-backed `RequestContext`, initialized in every entrypoint (RPC, HTTP, jobs, CLI) and read via `getContext()`. A threaded `ctx` parameter grows every signature, and adding one field later touches every call site. Implementation in [references/api-design.md](references/api-design.md). - Require an explicit auth policy per RPC method at registration; a method without one fails registration rather than defaulting to open. - Keep auth, logging, errors, and context in shared middleware, not per-handler code. 5. Define frontend boundaries (Next.js App Router default): - `app/` holds routing files only (`page`, `layout`, `loading`, `error`, `route`). Domain code lives in `src/modules/<name>/` behind its root files; UI private to one route goes in a `_components/` folder beside its page. A page that grows logic moves it to a module, not to a sibling file in `app/`. - Server Components by default; `"use client"` at the interactive leaves. Where a client wrapper needs server-rendered content, pass it in as `children`. - Server state in TanStack or Connect Query; client state in component state; MobX only for cross-cutting client state that fits neither. Each piece of data has one owner: server data mirrored into `useState`, or two stores synced with `useEffect`, is the sign that ownership is unclear. - `proxy.ts` (Next 16's name for `middleware.ts`) handles redirects, rewrites, and headers. Authorization is decided inside each route handler and Server Function, because a matcher-excluded path skips the proxy and Server Functions post to their page's route. 6. Testing and release: - Unit tests stay DB-free; integration/E2E run in parallel with dynamically generated IDs so runs never collide on fixtures. - Release in small, complete, reversible vertical slices with a rollback plan per change. - A slice is complete only when reliability, error paths, observability, and user-facing states are covered; deferring them to a polish pass is how they never ship. 7. Every contract in the brief names the lint rule, type check, or test that catches its violation, then continue into Harden mode to wire them. ## Deepen mode (existing codebase) Goal: domain-informed deepening, not a rewrite. Load [references/deepening-existing.md](references/deepening-existing.md) for the analysis method, opportunity patterns, and output template. 1. **Map the domain language and decisions.** Read `CONTEXT.md`, `docs/adr/`, or local equivalents if present, then read the code for entities, actions, and contexts as the team names them. Note divergence (one concept, three names; or one name, three concepts). Format and ADR rules in [references/domain-language.md](references/domain-language.md). 2. **Scope the scan by where change lands.** Deepening pays off on code that keeps changing, so `git log --oneline` over a good stretch of history first and weight the files that keep coming up. An unscoped scan drifts into speculative cleanup. 3. **Find deepening opportunities.** Look for anemic concepts, shallow modules, leaking seams, naming divergence, duplicated concepts, primitive obsession, misplaced logic, and tests forced past the public interface. Record each with file paths, never a vague smell. Check deletion first on every candidate: a concept with no live caller, a flag whose branch never runs, a layer with one implementation. Deleting it is the deepening, and it is the only move that cannot make the codebase harder to read. 4. **Rank by leverage.** Prefer opportunities that pass the deletion test, localize named future changes, have low churn, meet a current requirement, and have a viable testing seam. Rank candidates before designing target interfaces; drop speculative cleanups. 5. **Migrate one vertical slice first.** Prove the highest-leverage move end to end through one slice before generalizing. 6. **Enforce the new seam** with lint, type, or test checks so it cannot decay, then roll out module by module. Continue into Harden mode for the enforcement rung and the check-bites test. ## Harden mode (make it stick) Two halves: **guardrails** stop the wrong thing landing, **wayfinding** makes the right thing cheap to find. Both exist because agents arrive by grep, not by reading docs, so the warning has to live where they land and the rule has to be an exit code rather than a sentence someone might recall. Steps 1 to 3 always run. Steps 4 to 6 run only when their condition holds, and a request to add one check stops at step 3. Running all six for every request loads most of the bundle and is the failure this mode is most prone to. 1. **Survey what exists.** Package scripts, CI steps, hook config, lint config, the instruction file, the docs index. Find three things: checks that run locally but do not gate the merge, checks that run in CI but cannot fail (`verification-tiers.md`, "Commands that lie"), and dormant config nobody invokes. Wire the first, fix the second, delete the third ([references/contagion-markers.md](references/contagion-markers.md)). 2. **Choose checks by the failure they prevent**, never by tool popularity. Categories and tools in [references/guardrail-tooling.md](references/guardrail-tooling.md). Pick the two or three failures this repo actually exhibits; installing the full set at once forces the weakest enforcement rung on all of them. 3. **Install each check:** pick an enforcement rung for the violations that already exist ([references/enforcement-ladder.md](references/enforcement-ladder.md)), ship it green, then prove it bites (run it, break it on purpose, watch it fail with a message naming the fix, revert). Wire it into both a pre-commit hook and CI. 4. **Wayfinding** per [references/wayfinding.md](references/wayfinding.md): naming and locality, the add-a-new-X recipe file, the trust-labeled docs index, one canonical instruction file. 5. **Contagion markers** per [references/contagion-markers.md](references/contagion-markers.md): anything an agent must not copy or must not edit gets a greppable marker at the code site naming what to use instead. 6. **Runtime ergonomics:** verification tiers in [references/verification-tiers.md](references/verification-tiers.md); session hooks, permission allowlists, and review gating in [references/agent-runtime.md](references/agent-runtime.md). ## Validation loop Run the items matching the modes you ran, and record results in the output. Each needs evidence; "looks consistent" is not a pass. An item that cannot execute yet, because nothing is installed or the repo is not writable, is recorded N/A with that reason. Silently passing it is how an unenforced contract ships looking verified. 1. **Consistency** (Design, Deepen): naming, module contracts, and middleware rules read the same across every service. Evidence: a contradiction scan with zero findings. 2. **Enforceability** (all): every contract names its lint rule, type check, or test. Evidence: an enforcement note per contract, and for any check actually installed, the pass, then fail on a deliberate violation, then pass after revert. 3. **Operability** (Design): observability, health checks, and a rollback path per deployable surface. Evidence: the rollout section names each. 4. **Quality gates** (whenever code changed): the repo's lint, type-check, and targeted tests (`npm run lint`, `npm run check-types`, `npm run test --workspace=<pkg>` or equivalents). Evidence: passing output, quoted. 5. **CI and local agree** (Harden): the CI step invokes the same umbrella command a developer runs, or the difference is deliberate and stated. 6. **No dangling pointers, and a recipe works cold** (Harden): a grep proving every path named in the docs index and instruction file exists, plus a fresh-context agent following one add-a-new-X recipe end to end with no further guidance. 7. **Net simplicity** (all): the result leaves a reader less to hold, not more. Evidence: the net change in files, surfaces, and exported names, with every increase named and paid for by what it removed elsewhere; plus, for each layer, port, or indirection introduced, the second caller or implementation that made it real. An architecture pass that only adds has failed this check even when every other item passes. On failure: fix the brief, the conventions, or the wiring, then re-run the loop. ## Output template Design mode produces this brief. Deepen mode's ranked-opportunity template is in `references/deepening-existing.md`. Harden mode's output is the wiring itself plus the loop's evidence, not a document. ```markdown # Architecture brief ## Context and constraints ## Repo shape ## Backend module contracts ## Request context and middleware policy ## Frontend boundaries ## Testing strategy ## Quality bar and surface-area budget ## Rollout and rollback plan ## Open risks and follow-ups ``` **Size the brief to the decisions, not to the template.** Drop any heading the project does not face rather than filling it: a single-tenant internal service with no frontend does not owe you a Frontend boundaries section. Each section carries the decision and the constraint that forced it, not a restatement of the conventions in the references. A brief that pads to nine sections costs the review attention that the two contested decisions needed. ## Excuses Each rebuttal redirects to the step being skipped. | Excuse | Rebuttal | |--------|----------| | "The check is obviously configured right." | You have not watched it fail. A misconfigured gate passes on everything and reads as coverage. | | "There are too many existing violations to fix." | That is what the enforcement ladder is for. Pick a rung and land it green today rather than a perfect rule next quarter. | | "AGENTS.md already says not to do that." | A prompt rule decays under context pressure. If a static tool can check it, it belongs in tooling. | | "We'll add the enforcement in a follow-up." | The follow-up is the deadline's first casualty, and the contract decays from the day it ships unenforced. | | "CI already runs that tool." | Running is not gating. Read the step: a tool with no threshold, a `warn`-only rule, or a job behind a stale path filter is green on every PR. | ## Gotchas ### Design and Deepen - Microservices for a team under 5 buy a deploy pipeline, contract versioning, and an on-call surface per service. Start with a modular monorepo; split when a boundary is proven by team or scale pressure. - App-level deps in a monorepo's root `package.json` hoist silently, so an app builds locally and breaks when deployed alone. Each app owns its deps. - A `handler`/`service`/`dao` contract with no import-boundary rule decays at the first deadline. Add the rule (`dao` may not import `handler`) the day you write the contract. - `"use client"` at page or layout level converts the whole subtree to client rendering and forfeits streaming and direct server data access. Push it to leaves. - Extracting to `packages/` before 3+ apps need the code couples release cycles for nothing. The exception is the contract two surfaces already share (generated types, the RPC schema, branded IDs): that is the interface between them, and it belongs in a package at two apps. - Dual-writing to a database and a queue or webhook without an outbox (or CDC) loses or fabricates a notification whenever one side commits and the other fails. See `references/distributed-correctness.md`. - An externally-forceable invariant enforced by construction (unsigned type, hard CHECK) crashes or clamps when the outside world forces the state. Represent it, detect it post-factum, recover explicitly. - A whole-codebase deepening scan without `git log` hot-spot scoping fills the list with modules nobody touches, and every entry on it is speculative by definition. - Relying on `proxy.ts` as the only authorization layer: a matcher-excluded path skips it, and Server Functions post to their page's route, so a matcher change silently removes coverage. Check authorization in the handler or Server Function itself. ### Harden - `jscpd` without `--threshold` exits 0 on any duplication, and `knip` with too many declared `entry` files hides real dead code behind them. A green step is not a gate until you have watched it fail. - Pick cycle tooling that resolves this repository's aliases and workspace edges. Prefer a configured linter rule or dependency-cruiser before adding another graph tool; verify an intentional cycle fails. - dependency-cruiser without `options.tsConfig` cannot resolve path aliases, drops those edges, and passes every rule on a graph with half its imports missing. - `turbo boundaries` checks cross-package imports and undeclared dependencies only; it sees nothing inside a package, so it does not replace the module boundary rule. - A hand-rolled shrink-only baseline (`*-ratchet.mjs` plus `*.baseline.json`) reimplements the `ignore`, allowlist, and `warn` mechanisms knip, the linter, dependency-cruiser, and jscpd already ship, and the baseline becomes the file people edit for a green run. - Dormant config or an unused devDep reads to an agent as live convention; a config pointing at a renamed file yields a confident empty result instead of an error. - A docs index entry or add-a-new-X recipe written from memory names a moved file, and the agent follows the pointer with full confidence rather than doubting the doc. Grep-verify every path before publishing it. - Pre-commit hooks alone are not installed on a fresh clone or in a worktree, which is exactly where agents run. CI is the gate; the hook is the fast signal. - A LEGACY marker only in `docs/legacy.md` is never seen by an agent that arrived by grep. The marker goes at the top of the frozen file. ## Related skills - `agents-md`: the AGENTS.md / CLAUDE.md file itself. This skill owns the checks and docs tree that file points at; a rule a linter can enforce goes here as an exit code, not there as prose. - `tidy`: the diff-scoped cleanup that Harden's guardrails keep small; `pr-reviewer`: read-only review of a local diff. - `planning`: a plan for one feature; architecture briefs from Design mode feed into it. - `scaffold-nextjs`, `scaffold-cli`: creating the repo this skill then structures. - `multi-tenant-architecture`: tenant identification, isolation, and routing; this skill supplies the module layout underneath. - `dx-audit`: the developer-facing surface a package ships outward; `api-design.md` here covers only the contract shape. Maintenance only: `evals/evals.json` contains regression scenarios for changes to this skill; it does not load during a user task.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.