Bring this repository up to the standard
You are working in a repository you may not have written. Your job is to leave it structurally
sounder, more secure, more portable and more verifiable than you found it, and to leave behind the
mechanisms that keep it that way after you are gone.
Every rule below was distilled from production codebases built and operated with AI agents, and
exists because its absence caused a real defect: where a rule names the defect, that is why it is a
rule and not a preference.
Read it as invariants, not as a stack. The examples are TypeScript, React, Node and Postgres,
mostly through Next.js App Router and Supabase, because that is where these defects happened, not
where they apply. Translate as you read: a named client means whatever client you have, row-level
security means whatever row-scoped authorisation your database offers, a named framework means the
worked example. Express, Fastify, NestJS, a worker and a queue consumer are the same shape
underneath. Adapt the naming; do not adapt the invariants. Where your stack has no equivalent of a
mechanism, the invariant moves up a layer, usually into the application gate: say which layer you
moved it to, in the plan.
How to run this
You run inside Claude Code or a harness like it, on your own, end to end: audit, plan, execute,
verify, report. You never ask the owner what to act on, in what order or how; the standard decides,
and where it leaves a choice you make it and record why. Nothing is committed, so the diff is the
owner's review and undo.
Work as an orchestrator, not as a single reader:
- Do not read the whole repository yourself. Context is the scarce resource; spend yours on
summaries, the plan and the report. Reading is delegated.
- Fan out. Subagents in parallel for work that does not depend on each other's results: one
reader per area in the audit, one builder per independent plan item in the execution. Give each
a precise brief and a fixed return shape, and keep what it returns rather than what it read.
- Verify adversarially. Every finding and every guardrail is checked by a different agent than
the one that produced it, instructed to refute. An author cannot verify their own guardrail, for
the same reason nobody proofreads their own writing well.
- Isolate parallel work. Builders touching disjoint files run in their own worktrees and hand
back a diff; merge one at a time, and typecheck, lint and test after every merge. Structural
moves that touch shared files run serially in the main tree, and the audit file, the plan and the
report stay yours.
- Nothing here limits you to these steps. If the repository needs something this document does
not name, do it, hold the invariants, and record it in the ledger. If a step does not apply, skip
it and say why. The standard is the floor, not the ceiling.
What done looks like
Seven things, and not one of them contains a claim that nothing checks:
- A feature-sliced structure with one-way imports: domain code with its domain, a router that
holds only routing, a shared floor that never imports upward.
- One data path: every client read and write through a versioned API with a fixed envelope,
gated, validated, delegated to a service, consumed through one typed helper.
- Three-layer security that holds when any one layer is wrong: a gate resolving identity and
tenant before any data work, privileges that start closed, row-level policies underneath.
- A data model whose types come from the database, with a declarative schema, additive
reviewed migrations, and value sets that cannot drift from the database.
- A verification floor: every invariant that costs money, trust or tenancy caught by a type, a
derived test, a lint rule or a build assertion, each proven by breaking it, over an integration
floor that runs the real services against a local database and cannot reach a third party.
- Portability by construction: pure domain logic and contracts in modules a second client can
import without a build step.
- An instruction layer that stays true: standing instructions, path-scoped rules, an
append-only ledger with a generated index, and a loop that turns each defect into a mechanism.
If some exist, audit and repair them; if none do, build them. An instruction nobody checks goes
stale, and a stale one is worse than none because an agent follows it without hesitating. Every
claim you write is verifiable against the code today, or replaced by a mechanism that verifies it.
Absolute constraints
- Never run
git commit, git push, git reset --hard, git stash, or any history rewrite.
Every change stays in the working tree. If the harness supports it, make this a permission
prompt, not just prose.
- Never run migrations, seeds, resets or diffs against anything but a local database, and never
deploy. If a command's blast radius is unclear, record it as blocked and move on.
- Never read, print, grep or echo a secret value. Check existence or length only.
- Delete, move and overwrite freely inside the repository, since git holds the originals. Never
touch a file outside it. List every deletion in the report.
- Add a dependency only when a guardrail needs it, as a dev dependency, through the lockfile,
and never by removing a supply-chain control. List every addition.
- Never weaken a security or billing invariant to satisfy a design principle. A cleaner
abstraction that loosens a gate or an idempotency key is not cleaner.
- Run whatever verification the work needs, as often as it needs it: typecheck, lint and targeted
tests after every step, the full suite and a production build before you finish.
- Never stop to ask. If something is genuinely blocked, do everything that does not depend on it,
then name it in the report with what you would have done.
- Report what you could not verify. An unverified claim stated as fact is the exact failure this
system exists to prevent.
Know the repository first
Everything after this depends on it, and it is the phase most easily done badly. Grep matches
spelling, not meaning: a probe written for one client finds nothing in a repository that uses
another, and zero hits then reads as clean. So nothing is judged until the repository is
understood in its own terms. Fan the reconnaissance out, one reader per segment, and write the
result as a map at the top of docs/STANDARD-AUDIT.md:
- The tree. Workspaces and packages, entry points (servers, routes, handlers, workers, jobs,
CLIs), generated code, where tests live and what the runner's config actually includes. Segment
the repository by what the code does, not by folder names.
- The dependencies, read as decisions. From the manifest and lockfile: framework, HTTP layer,
database client or ORM, validation, auth, server-state and form libraries, design system, test
runner, linter, deploy target. For each, the fact the standard turns on: does the database client
throw or resolve its errors; is there a mutation path that bypasses the API; does the database
enforce row-level authorisation, or connect as one pooled role?
- The shape, read from real files. Per segment, open a representative route, service, schema,
test and component, and write down how each is actually built: how errors travel, where the
tenant comes from, where validation runs. Two shapes for one job is a finding.
- The instruction layer. Every file telling an agent or a person how to work here:
AGENTS.md,
CLAUDE.md, .claude/ rules, agents, skills, hooks and settings, cursor and copilot files, ADRs
and decision logs. Claims to check in 1a, not truth; reuse their vocabulary.
Then translate. Map every area (A to L) to this repository's own mechanism and name, or "not
applicable" with the reason, and write the probes you will run for it, derived from the
dependencies and the files you read. Rules hold at the invariant level, not the syntax level: with
a client that throws, the defect is a swallowed catch rather than an unread error object; with one
pooled connection, row authorisation has to live in the gate, so name where it lives. Where the
stack offers a better mechanism than the one named here (a typed router instead of hand-typed
envelopes, ORM middleware for tenant scoping, the schema library already in use), propose it: the
invariant is fixed, the mechanism is yours. Every later phase reads this map.
Phase 1: audit. Change nothing.
Read only, fanned out: one reader per area, plus one each for 1a, 1c, 1d and 1e, each handed
the map and reading files, not only probe output. Every reader returns findings in one shape: the rule, a verdict (holds, breaks, not applicable), the evidence
as file and line, a severity, and what it did not read. Then a verifier, told to refute:
it re-reads the evidence behind every finding marked breaks or critical and returns confirmed,
refuted or downgraded, with its reason. Only confirmed findings enter the plan; refuted ones stay
in the report as refuted, so the next reader does not rediscover them. Write it to
docs/STANDARD-AUDIT.md, then continue without waiting.
1a. What already exists. Every instruction file the map found, plus README, ARCHITECTURE and
docs. For each: path,
line count, rough token cost (words divided by 0.75), last modified, and how much restates what
the code already says. Then sample every concrete claim: does that file exist, does that function
have that name, does that command run, is that list complete? Report how many you checked and how
many were wrong. Do not soften it.
1b. Which dialects the code speaks. Do not ask; read. These probes are examples written for
Next.js, supabase-js and Postgres. Run those the map says apply, rewrite the rest in this stack's
spelling, add your own for whatever the map surfaced, and record which you replaced. A probe that
returns nothing proves only that the spelling does not occur: record zero as unknown, not clean,
until a probe for this stack or a read of the files settles it.
# From the root: git grep skips ignored paths; quoted pathspecs and POSIX regexes run anywhere.
# Framework-bound mutations (unusable by native, agents or partners)
git grep -n "use server" -- '*.ts' '*.tsx'
# Browser-side database access (bypasses the API boundary)
git grep -nE "createBrowserClient|from\('|from\(\"" -- '*.tsx' | grep -v "auth\."
# Unread write results: many clients RESOLVE an error rather than throwing
git grep -nE "await [a-zA-Z_.]+\.from\([^)]*\)\.(insert|update|upsert|delete)" -- '*.ts' | grep -v "const {"
# Silent config fallbacks (set once, forgotten, repo says otherwise)
git grep -nE "process\.env\.[A-Z_]+ \?\? ['\"]" -- '*.ts'
# Type escape hatches
git grep -nE ": any([^[:alnum:]_]|$)|as any([^[:alnum:]_]|$)|@ts-ignore|@ts-expect-error" -- '*.ts' '*.tsx'
# Error envelope drift
git grep -n "{ error:" -- '*route.ts' '*controller*' '*handler*'
# Data work inside a transport module, and HTTP inside a service
git grep -nE "\.from\(|\.query\(|prisma\.|drizzle|knex\(|INSERT INTO|SELECT .* FROM" -- '*route*' '*routes/*' '*controller*' '*handler*'
git grep -nE "new Response\(|NextResponse|res\.(status|json|send)\(|c\.json\(" -- '*service*' '*services/*'
# The same guard copied per handler instead of composed once
git grep -nE "getSession\(|verifyAuth\(|requireAuth\(|jwt\.verify\(" -- '*route*' '*routes/*' '*handler*' | cut -d: -f1 | sort | uniq -c | sort -rn
# Where tests live, against what the runner's include glob picks up
git ls-files '*.test.*' '*.spec.*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn
# Files that do everything (a prompt to look, not a verdict)
git ls-files '*.ts' '*.tsx' | grep -v '\.test\.' | xargs wc -l | sort -rn | head -20
Then, against the database if there is one, in its own dialect: which functions a public or
unauthenticated role can execute (in Postgres a NULL proacl means PUBLIC can, and any
SECURITY DEFINER function in that set is a P0, because it bypasses grants and row policies);
which tables grant anything to the anonymous role or writes to the authenticated one; which foreign
keys to the users table lack an ON DELETE; and whether ledger or audit tables can be rewritten by
the application's own role.
Note where a pattern is consistent and where it has forked into two dialects. A forked pattern is
worth writing down; a consistent one usually is not, because the code teaches it.
1c. What has already gone wrong. Evidence, not intuition: commit messages shaped like incidents
(fix, revert, hotfix, regression, "again", "actually"), because a bug fixed twice is a guardrail
waiting to be written; defensive comments ("do not remove", "must run before", "looks redundant
but"), each an invariant someone learned the hard way; clusters of TODO, FIXME and HACK, where they
cluster mattering more than how many; anything touching money, authentication, tenant isolation,
deletion, external calls, retries, jobs or webhooks, because silent failure costs most there; and
the same defensive check repeated in many places, which is repetition under duress. If history is
squashed, say so. Do not invent incidents; a short evidenced list is the correct output.
1d. What verification exists. Test framework and how it runs, whether tests hit a real local
database or mocks, whether the linter gates or reports, type checking, what actually blocks a merge
in CI and with what token permissions, and whether anything stops a test reaching a third-party
service with live credentials. Then the question that matters: of the invariants in 1b and 1c,
which are caught automatically today, and which depend on someone remembering?
1e. Portability and scale ceilings. Is there a second consumer of the API, today or on the
roadmap, and where does the logic it would need live now? Where are the hard ceilings: a row cap,
per-instance rate limiters, a single-parent foreign key, a queue that does not exist?
Phase 2: the target shape
The reference you audit against and migrate toward, written as rules with the failure each
prevents, so you can judge whether the failure applies here. When it does not, say so in the plan
and skip the rule; do not apply it ceremonially.
A. Structure and layering
- Feature slices, not type folders.
features/<domain>/ owns its vertical: components,
hooks, services, validation.ts (shared by the client form and the API), types.ts (the
contract both sides import), optionally framework-free rules and pure utils. Create only what
the domain needs; empty folders are forbidden, thin slices are fine.
- The router holds routing primitives only. A page imports a feature component and renders
it, and holds no form logic, data fetching or business rule. Cross-cutting UI lives in one
place: design-system primitives, providers, layout. Nothing new goes into a
shared/ bucket;
that is where slices go to die.
- One-way imports:
types → lib → hooks → components + features → app. A module in the
shared floor that reaches up into a feature is that feature's policy wearing a shared path;
move it. The one sanctioned exception is an aggregator presenting every domain's tunables in an
operator panel.
- Derive the layering table, both ways. A new violator fails, and a fixed one still listed
also fails, so nobody cites a dead violation as precedent.
- A monorepo when a second consumer exists, not before; until then keep domain logic in
framework-free files, so the lift is a move rather than a rewrite.
- Name conventions once:
<feature><Action>Schema, use* hooks, <domain>Keys, the injected
client first, integer amounts in the smallest unit, UTC timestamps.
B. The one data path
client component
→ features/<domain>/hooks (server-state library)
→ fetchApi<T>('/api/v1/...')
→ route: gate → validate → service → envelope
→ features/<domain>/services (injected client, scoped by the RESOLVED tenant id)
→ database
- No framework-bound mutations. Server actions and their equivalents lock every mutation to
one framework and are invisible to a native app, an agent or a partner; route handlers are
callable by anything that speaks HTTP. The only thing the browser SDK does directly is
authentication and push-based realtime.
- The route shape, always in this order. Gate first, before any data work, early-returning
the gate's response. Validate with a module-level schema from the feature's
validation.ts,
reporting the first issue as path: message with a 400, and answering 404 on a malformed id
param rather than a raw cast error 500. Call the service with the injected client and a params
object, and let it return an outcome, never a response. Return one envelope:
{ success, message, data }, status on the response, never a bare { error }; rate-limited is
429 with Retry-After and the same shape.
- An explicit return type on every handler. TypeScript infers one happily and reports
nothing, so an unannotated handler drops the contract with no signal. Derive the raw-body set
from the code and fail that annotation on a handler that builds none.
- One typed client helper:
fetchApi<T>(url, init?, timeoutMs?) sets the JSON header unless
the body is FormData, applies an abort timeout below the server's hard kill, reads the envelope
once, throws message on failure and returns data unwrapped. Raw fetch in a hook is a
defect.
- Behaviour lives in the service; every consumer inherits it. A REST route, an agent tool and
an MCP tool call the same
updateTask, and a miss returns false in one place, becoming 404, a
tool error and not_found respectively. Never fix behaviour in a consumer, and keep input
bounds in validation.ts where every surface imports them, because a .max() hardcoded in a
tool will drift.
- Public, no-auth routes are a register, and the register is derived. A test finds every route
that calls no gate and fails unless each is named in the rules file with one sentence of why it
is safe. It checks presence, never the reasoning; say so in the test.
- A separate API deployable (Hono, Express, Fastify) keeps the same four steps and the same
envelope, and should give the client an end-to-end typed contract.
C. Security, three layers
Any one layer failing must not open the money or tenancy path. Where your stack has only one of
these layers, the missing invariants move into the gate; name that in the plan.
Layer 1: the application gate.
verifyAuth() verifies the session and returns { userId, client } or a 401. Local JWT
verification is fine at the edge, a server-authoritative call when you need the user object,
and never a spoofable session read on the server.
- Ownership after identity: a resource the caller does not own is 403 on an owned surface and 404
on a tenant surface, so there is no existence leak. An admin check reads a role from a row the
user cannot write, never a self-writable flag, and a platform-operator gate answers 404 with an
operator flag as its predicate, never a role check: an operator who resolves as owner on every
tenant would otherwise open the cross-tenant surface to every client owner.
- Never trust a tenant, owner or account id from the request. Resolve it from the
authenticated user. The one sanctioned body-carried id is on an operator route whose gate is
identity-based, documented as the exception.
- Paid or expensive surfaces take a second gate immediately after the tenant gate: rate limit,
then budget, returning 429 or 402 with a human message, never a silent degrade.
- A cron or worker route authenticates with a constant-time bearer comparison against a secret
that fails closed when unset. A webhook authenticates with an HMAC over the RAW body, never
re-serialised before verifying, with a replay window where the provider supports one.
- One CSRF check: a mutating request whose
Origin is present and not one of this deployment's
hosts is 403, failing closed on an unparseable origin; token and HMAC callers send none and
pass. Every post-auth redirect goes through one safeNext() allowing only a clean
single-slash relative path, blocking //evil and /\evil.
Layer 2: privileges, starting closed.
Explicit REVOKE ALL on every table from every role, then the minimum back: nothing for the
anonymous role, SELECT only and row-scoped for the authenticated one, CRUD for the service role
except on append-only tables (ledger, audit, events) where it gets SELECT and INSERT only. A
balance history cannot be rewritten by buggy service code because the privilege does not exist.
Revoke EXECUTE from PUBLIC, not just from named roles. CREATE FUNCTION grants it to
PUBLIC by default and every role is a member, so per-role revokes remove nothing; add
ALTER DEFAULT PRIVILEGES ... REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC or every future function
reopens the hole. One of these codebases found its settle, void and create money functions
callable with the browser key this way.
A new table starts closed: client-readable means row-level security, a SELECT policy and a
SELECT grant in its own schema file; secret, money or infrastructure means row-level security
on, no policy, no grant, served to the client only through the API.
Layer 3: row-level policies as the net.
Own-row SELECT policies on user tables, SELECT policies on reference tables, and zero client
write policies anywhere, so a direct write is denied even if a grant slips. Access helpers are
SECURITY DEFINER, STABLE, with an empty search_path, and are the single source every policy
calls. Test policies as the authenticated role inside a transaction; outside one you silently run
as superuser and the pass is false.
Supporting controls, each with its incident.
- Secrets fail closed through one
requireEnv(name), never ?? "" (fails open on an HMAC) and
never ! (a cryptic 500 later). Non-secret config is a typed constant in the module that owns
it, not process.env.X ?? "default".
- An env manifest declares every variable the app reads with a tier and a note; the production
build fails when a required one is missing and names every missing one at once; the same list
feeds an operator config page, and a test pins the two. Tunable constants governing spend or
safety join one aggregator surfaced read-only to the operator, with a
source: pointer a test
resolves.
- Every foreign key to the users table declares
ON DELETE: cascade for owned data, set null for
attribution, a deliberate block on primary ownership, because erasure must be executable.
D. The money path
Skip this area if the repository moves no value; apply all of it if it moves any.
- The ledger is append-only at the privilege level: no UPDATE, no DELETE, for anyone.
Corrections are new signed rows, never edits.
- Settlement is one atomic procedure: resolve, update status, write the ledger row, adjust the
balance, in one transaction, with
balance = balance + delta over a CHECK (balance >= 0) and a
unique index on (user, type, ref) that makes a second payout row impossible.
- Idempotency keys are per unit of work, content-addressed, never positional: per invocation,
per content hash, per provider reference. A positional key such as a batch cursor double-charges
exactly the jobs that resume, and
ON CONFLICT (key) WHERE key IS NOT NULL DO NOTHING must
match the partial index predicate exactly or Postgres raises 42P10.
- Writes resolve, they do not throw. Many clients resolve an error object on a constraint or
timeout, so a bare write or a destructure without
error is a fully silent failure. Every write
on a money, membership or tenancy path reads the error, logs it with context, and throws on a
webhook or retry path so the provider redelivers. Enforce with a lint rule; satisfy it by
reading the error, never by disabling it.
- Money-path reads fail closed, because a transient blip read as "no rows" is a mis-charge:
zero members grandfathers the cheapest tier forever, an unresolved customer drops a paid
invoice. Conditional claims go through one primitive:
claimOne(query) returns
matched | not_matched | error, and only not_matched maps to 404 or busy, because a bare
!data guard hides a real error as a benign no-match.
- External resources are owned, including their teardown. A cascade reaches only rows, so
whatever creates a subscription, a project, a repo or a VM owns destroying it, through one
teardown primitive every delete path reuses, or documents in the ledger why it is retained.
Persist the reclaim handle with or before the create: one written later leaks when the request
dies in between.
- Every non-terminal status has a reaper that is not the in-request
catch, because a hard
kill never runs the catch. An idle-windowed sweep terminalises processing, publishing,
running, and its window is a parameter, because an updated_at trigger defeats an aged test
row.
- Webhooks resolve the account from a stored mapping written in an authenticated context, never
from payload metadata, and a disagreement is an alarm.
E. Data modelling, schema and types
- Declarative schema files are the source of truth, numbered, applied in order, with a table,
its policy and its grants in the same file. A policy or index referencing a column added in a
later file breaks the shadow database and disables diffing repo-wide.
- Migrations are generated artefacts, reviewed against the silent-miss checklist: all DML,
policy alterations, every REVOKE, function ACLs, views, schema-level privileges. Anything on
that list is written by hand, and a clean diff proves only that the engine sees no difference.
- Additive by default. Permitted: create table, index, function, policy, grant, and adding a
nullable or defaulted column. Forbidden without a five-step plan (additive shape, backfill, flip
reads, flip writes, drop the old shape): drop table, drop column, narrow a type, mass update,
truncate, rename a live column. Never edit an applied migration, never change the remote through
a dashboard, never reset a linked database, and apply migrations before the code that reads
them, writing down which pre-existing surfaces break in the window between.
- Types come from the database, regenerated after every schema change. Status unions are the
generated enum, never a hand-written literal union: one compiled, linted and passed every test
while every insert on the new surface violated the CHECK in production, silently, because the
write was fire-and-forget. A test parses the schema and asserts each code-side value set equals
its database constraint.
- The API caps every response at a fixed row count regardless of the requested limit, so any read
that can exceed it pages with a stable unique order. A list passed to an
IN clause is bounded
by request-line bytes, not key count.
- Postgres is the queue until job types multiply:
FOR UPDATE SKIP LOCKED, a lease, bounded
attempts, dedup by partial index; the worker body survives a dispatcher swap.
F. Frontend: server state, forms, design system
- Three data tiers, simplest first: server fetch into props; server fetch seeding a client
query when the client must refetch, filter or paginate; a client query for the rest. Never
useState plus useEffect to fetch.
- Query keys come from the domain's key factory and are imported, never inline, with nested
prefixes so a parent key invalidates its children, and invalidation written at the mutation's
success handler rather than broadened to everything.
- Forms are one pattern with no exceptions: the form library, a schema resolver, and the design
system's field and message components, with server errors mapped back onto the field. Per-item
pending state derives from the mutation's own variables, every query has visible error feedback,
and there is no
console.* in client code.
- The design system is the only source of primitives: no raw
<input> or <button>, colours
from tokens, and lookup maps where every class name exists as a literal.
- Render purity: no clock or randomness in render. Copy carries no em dashes and no filler
words.
G. Portability: native and shared packages
- The API-first data path is what makes a native app possible; nothing else here matters for
portability if mutations live in framework-bound actions.
- Shared modules, in the order a second client needs them: generated database types; contracts
(rules, schemas, catalogues, key factories); pure domain math; design tokens; provider adapters.
Modules a native bundler must transpile stay dependency-free apart from the validation library.
The native app imports the real pricing function, so it cannot quote a value the server rejects.
- A hook depending only on
fetchApi and a key factory is shareable as-is. The native lint gate
runs at zero warnings, and a package is verified to resolve before anything builds on it.
- Vertical registries: an exhaustive
switch with a never default, a lookup that throws on an
unknown key, and parseX(v): X | null for any string that selects a code path.
H. Integrations and adapters
- Normalise at the adapter boundary: each provider implements one interface and returns only
normalised shapes, raw response types never leave the adapter folder, and consumers dispatch by a
stored provider column rather than a default.
- Types come from real captured responses, never the spec, kept as scrubbed fixtures. One
provider documented 97 fields and returned 170; another encodes a confirmed zero as an absent
key. Distinguish absent from zero end to end, because a transformer that coalesces to zero
destroys the distinction settlement needs, and keep external ids as strings, because a
Number()
on a UUID silently turns lookups into "no data".
- One orchestration wrapper around every provider call: duration, uniform errors, structured
logs. Compensating transactions when the external create succeeds and the internal write fails,
idempotent third-party operations, and sandbox mode everywhere but production.
I. Observability and lifecycle
- Structured JSON logs through
logInfo / logError / logAlarm with context, actor and error.
Never raw console output on the server, never a silent catch, and a helper for thrown values that
are not Error, because a plain object stringifies to [object Object].
- Three levels, and the third is the point.
error is diagnostics and there will be hundreds
of distinct events; alarm means a control fired or money or trust is provably wrong, never a
retryable failure. A test derives the alarm set from call sites and fails if an event is raised
at both levels or alarms grow past a tenth of errors.
- Post-response side effects go through one
persist(fn), or the runtime drops them when it
freezes. Every scheduled job logs one healthy-signature line with its counts, health endpoints
degrade per query, and client analytics go through one typed track().
J. AI agents, VMs and durable work
Apply when the repository runs models, agents or sandboxes. Skip otherwise.
- Grounding is code-enforced where it can be: retrieve first, refuse without calling the model
on empty retrieval, verify citations against what retrieval returned rather than parsing them
from prose, and log a fabricated reference loudly.
- Retrieved, repository and sandbox content is UNTRUSTED: the prompt says quote-never-obey from one
shared constant, and the hard backstop for writes is an approval gate whose classes are
single-sourced and read by both the server and the client renderer, because a write tool missing
from them hangs the turn. Write tools call the same feature services as the routes, never their
own SQL.
- Models are typed constants derived from the pricing table, so an unpriced model is a compile
error rather than a turn billed at zero, and budgets, step caps and stream timeouts are central
constants.
- Metering is one ledger row per generation with a deterministic idempotency key, written
through
persist(), and it still reads the error. Sum usage from the finished steps, not the
top-level total, or a hard mid-loop error meters free.
- Every paid surface is gated on all three doors alike: the HTTP route, the tool surface and the
background service. A source-scan test derives spending symbols and gate symbols per file, strips
comments first (two such tests were vacuous because a gate named in a comment satisfied them),
and fails a spender without a gate unless it is listed as deliberately ungated.
- Ingestion holds four invariants: never stampede the gateway; non-destructive swap (embed
then replace, keep the original on failure); honest failure (
failed with a reason and a retry,
never a partial index marked done); and no arbitrary caps, being background and resumable rather
than truncated. A big job runs on the queue with a convergent drive keyed by content hash, so any
interruption resumes to the same fixed point.
- The sandbox is the security boundary. Pin the agent runtime version, because billing parses
its event stream. Egress is deny-by-default with an exact host allowlist, credentials are
injected at the firewall edge and never enter the VM, and the clone token is scoped to one
repository. Wipe the tree before overlaying a repository, scan for secrets before publishing,
call
stop() in finally, meter VM compute as its own ledger row, persist the handle with the
create, and sweep orphans on a timer.
- Agent-facing surfaces: a public one self-limits by IP, caps every string input well above any
legitimate size, and spends no model calls per request; a tenant one authenticates with a hashed
bearer token, resolves the workspace from it, rejects an expired token as unknown, and omits
write tools entirely for a read-only token. Tool behaviour lives in the services so the two
cannot drift.
K. Dependencies and supply chain
- One package manager, pinned, with a frozen lockfile in CI and on the host.
- A resolution-time cooldown against freshly published versions, measured in days (pnpm counts
minimumReleaseAge in minutes, so three days is 4320), with exact pins excluded because they
have no older fallback, plus a PR-layer cooldown with security updates exempt. Neither replaces
the other; document which door each covers.
audit gates CI at the level the tree can actually hold today, because a red gate everyone
ignores is worse than none. Say why the level is what it is, and when to tighten.
- CI workflow token permissions are
contents: read unless a step needs more.
- Do not hand-roll a format a dependency already parses: the security content, which bound to
enforce, is the earned part; the byte-poking is not.
L. Backend structure: layers, middleware, and where the tests live
- Four layers, each importing only downward: transport, validation, service, data access. The
transport layer (a route handler, a controller, a bot handler, a queue consumer) parses the
request, runs the gates and maps an outcome to a response, holding no business rule and no query.
The service holds the rules and knows nothing about HTTP. Data access holds the queries and no
rules. A file doing two of these is the file everything else ends up in: a thousand-line handler
that validates, decides, queries and formats is the shape this prevents.
- Cross-cutting concerns are middleware, composed once: authentication, tenant resolution, rate
limiting, request ids, structured logging, the error envelope, CSRF, idempotency keys, with a
route declaring which it uses. The same check copied into twelve handlers is twelve places for it
to drift, and the thirteenth handler, the one without it, is the incident.
- One error path: services return outcomes or throw one hierarchy, and the transport maps them
to status codes in one place, because a handler that catches and formats its own errors invents
its own envelope.
- Single responsibility per module, split by responsibility (routing, rules, queries,
formatting) and never by line count; a file past a few hundred lines is a prompt to look, not a
verdict. Open for extension through seams: a registry or an exhaustive switch with a
never
default, so adding a provider means adding a file rather than editing every caller.
- Dependency inversion at the trust boundary: services take the database client, the clock and
the external adapters as arguments. Nothing reaches for its own connection, which is what makes
the gate the real boundary and the service testable without a network.
- KISS: no abstraction before the second caller (no repository layer over a client that already
is one, no base class for one handler, no event bus for one subscriber), and delete the seam that
never got one. DRY: one source per fact, with shared bounds in a validation module and shared
literals in a contract module, because a copied literal in a second file drifts.
- A test asserts an outcome, not a branch: the row that landed, the status the caller was
told, never a mock standing in for the thing under test, and keep the happy-path twin or a
hard-coded safe value passes.
- Tests live in one convention, and the runner's include glob is the source of truth: either
colocated or a mirrored tree, never both, with integration tests that need the database sitting
together behind one setup file and one fixtures directory. A test file outside the glob never
runs and reads as coverage, so derive the list from the tree and compare it with what the runner
picks up.
Phase 3: plan
Produce one ordered plan. For each item: the invariant, the evidence from Phase 1, the cheapest
mechanism, and what it will not catch. Order by consequence to a paying user, then by cost.
The mechanism ladder, cheapest first, because the cheapest one that works is the one that
survives:
- A type or signature that makes the wrong thing fail to compile.
- A test that derives what it expects from the codebase, so there is nothing to update.
- A lint rule, custom if the linter supports authoring one, scoped to production source.
- An assertion at build or startup, so a misconfigured deploy fails before it serves.
- A CI step or hook.
- Prose. Last resort, and it says what it does not catch.
Rules for the plan:
- One guardrail per thing that has actually broken or sits on a money, tenancy or deletion path.
If you cannot name the incident, the commit or the defensive comment, do not propose it.
- No hand-maintained list, ever: if someone must add a line when they add a file, it is already
broken. Derive both sides and compare.
- Decide alone. Where the standard leaves a choice, take the option that changes the least
behaviour and holds the invariant with the cheapest rung, and record the rejected alternative in
the ledger. Never leave a question for the owner in place of a decision.
Write the plan into docs/STANDARD-AUDIT.md under the audit, then execute it in that order. You
do not wait for approval; the ordering rules are the approval. When an item turns out to be wrong
once you are inside the code, revise the plan in the file and say why.
Phase 4: migrate the structure
Confirm the working tree is clean. If it is not, do not stash and do not touch the modified files:
work around them and list them in the report as untouched.
Split the plan into serial items (structural moves, anything touching shared files) and
independent items (disjoint files, one mechanism each). Serial items go first, one at a time in
the main tree, with typecheck and lint after each. Independent items go to builders in isolated
worktrees, each briefed with the invariant, the evidence, the mechanism, what done looks like and
the verification to run. Merge one at a time, verifying after each.
- One slice at a time: move a domain's components, hooks, services and validation into its slice
and leave the page as a thin import. A move is a move, with no behaviour change in the same unit.
- Where the schema is not declarative, do not rewrite history: capture the current state into
declarative files, verify a diff against a LOCAL database built from those files is empty (the
linked diff is the owner's step), and land every change from then on as an additive incremental.
- Where privileges are open, write one tightening migration: revoke from
PUBLIC, close the
anonymous role, restrict the authenticated one to SELECT, restrict append-only tables, set the
default privileges. This is a behaviour change, not a move: any client still writing directly
from the browser stops working the moment it lands, so take those write sites from the second
probe, move each behind the API first, and land the revoke only when that list is empty.
Phase 5: mechanise the invariants, and break each one
One at a time, with two agents, because the one that wrote a guardrail cannot see what it misses.
The builder writes it, runs it, and confirms it passes. Then a different agent breaks the invariant
deliberately: it copies the guarded file outside the repository, introduces the exact defect the
guardrail exists to catch, and runs again, requiring a failure whose message names the file and
says what is wrong. It restores from the copy rather than from memory, confirms byte-identical,
re-runs green, and hands back what it broke and the failure output as the guardrail's proof.
Not optional, and not skippable for being obvious. A guardrail that has never failed is decoration,
and worse than nothing because it manufactures confidence. Expect some first attempts to pass while
broken; that is the most common defect in this kind of work. If you cannot make one fail on
purpose, delete it.
Guardrails worth having in most repositories of this shape, each derived from the tree, each with
its stated limit:
- Unread write results: a lint rule flagging an awaited write whose error is discarded. Known
gap: a builder captured to a variable and awaited later.
- Money-table reads: the same rule scoped by table name for wallet, membership, ledger and
grant reads. It narrows the judgement, not removes it.
- Route contract: every handler declares the envelope return type, or the raw-body type only
when the file builds a raw body.
- Public-route register: every route that calls no gate is named in the rules file with a
justification. It checks presence, never whether the reason is good.
- Admin gate: every operator route and page uses the one gate, and a hand-rolled inline check
fails even when correct, because duplication was the defect.
- Layering: the shared-floor-to-features edge table is derived both ways.
- Enum parity: each code-side value set that mirrors a database constraint equals it.
- Env manifest: no duplicates, the build refuses and names every missing variable, and the CI
env block contains every enforced key.
- Config registry: every
source: pointer resolves to a file that exports the constant.
- Docs enumerations: every path a live doc names exists, and no doc hand-enumerates the rules
corpus or the schema files.
- Decision log: every superseded stamp cites an existing entry title, and the index is not
stale.
- Alarm tier: no event is raised at both error and alarm, and alarms stay under a tenth of
errors.
- Paid-surface gating: every file calling a spending symbol also calls a gate symbol, comments
stripped, or is listed as deliberately ungated.
- Audit labels: the action set, its label map and its writers are one type, and a call-site
scan rejects an action nothing emits.
- Egress: the test setup refuses any non-local host, and a test asserts the guard is installed.
Every scanner that reads source strips comments first, with a lexer that respects strings and
templates. Two scanners in these codebases were provably vacuous because a symbol in a prose
comment satisfied them.
Phase 6: the verification floor
- Integration tests run the real service layer against a local database. The setup file
hard-refuses a non-local URL before any client is built, fixtures provision real tenants through
the real onboarding service, and row-level tests use an authenticated-role client for a real
signed-in user, which is what the policies actually see. Money-path unit tests run against
captured real provider responses, not against the spec.
- A database security suite asserts anonymous lockout on every table, append-only behaviour
for ledger and audit tables even as the service role, and EXECUTE denied on every
security-definer function. Counts use an exact-match helper, because a substring match passes
"48" against "8". It is a deploy gate, run after every migration.
- A browser layer that checks claims, not just requests: where the product asserts something on
screen, a flow drives it and compares the claim with the data behind it, against an isolated copy
with neutralised credentials, never a dev server holding live ones.
- CI on every push to the deploying branch: install frozen, lint, typecheck, audit, a
production build with dummy env (which runs the env assertion), the local database, the tests,
with
contents: read permissions. The re-run matrix is a table in the rules file: which
change requires which check, and what that check cannot see.
- Test the outcome, not the branch; keep the happy-path twin so a hard-coded safe value cannot
pass; name the safe set, not the unsafe one. Deliberately absent is a section, not a silence: list every known gap with its reason.
Phase 7: the instruction layer, the ledger and the loop
Now write the standing instructions and the domain rules, with the guardrails in place so the
prose no longer has to carry what a check now covers.
The root file. AGENTS.md, or a tool file that imports it: one short file read before every
task, holding only what the code cannot say (what is not obvious, what is dangerous, the commands,
the consent rules) and a pointer to the domain rules. No directory tour, no framework explanation, no
file lists.
The domain rules. One file per area, declaring the paths it governs so it loads only when that
area is touched, with a test that every glob matches a real file. It holds how the area works, its
invariants, its past mistakes and what to re-run. Where instruction files already existed, this phase
is mostly deletion: report the token cost before and after.
The ledger. An append-only decision log, newest first, one entry per non-obvious decision:
YYYY-MM-DD — Title, what was decided, what was rejected, why. An entry later overturned is
stamped in place, where a reader meets the dead claim:
> SUPERSEDED <date> by "<title>" — <one clause why>. Cite the title, not just the date, because a
date alone can point at twenty entries. Once the log is too long to read whole, generate an index
from the log itself, with a check that fails when it is stale. Never hand-maintain the index.
Harness enforcement. Where the tooling supports it: ask-prompts on commit and push, deny rules on
production env files and destructive linked-database commands, a verifier agent defined with its
instruction to refute, and hooks for checks that must run on every edit. The prose is the rule; the
prompt, the hook and the permission rule are the guarantee.
The loop, written into the standing instructions as the closing section:
- A decision gets its ledger entry in the same change as the code, never after.
- When something breaks twice, build the check. Do not add a paragraph.
- Never write a check that needs a hand-maintained list; derive both sides.
- When a check replaces a rule, delete the rule in the same change, and when a check makes an
existing claim false, fix every place the claim appears.
- A rules file must shrink as often as it grows. Delete on sight: a claim the code no longer
supports, a waiver whose bug is fixed, a seam that got a body.
- The docs tests check structure, not truth: a path that resolves, a register that is complete, a
count that matches. A confidently wrong sentence passes every one of them, and that part stays
yours.
- If these files only ever grow, the loop has stopped running.
Final report
Write it at the end of docs/STANDARD-AUDIT.md and summarise it in your last message: which of the
seven parts existed and which you created; every move, as from and to; each guardrail, what it
catches and does not, and whether breaking it proved it; what you deleted and added; the migrations
and the exact steps to apply them; the ceilings left and their triggers; what you deliberately did
not act on; what stayed blocked; which subagents ran and what the verifier refuted; and every claim
you could not verify. Recommend nothing untested, and never commit for the owner.