specification-first work

rule

agree on the user outcome and owning contract before substantial implementation. code implements an approved decision; it does not silently make one.

why

code written before the contract buries product decisions in code: shipped behavior becomes the only authority for questions nobody answered, and every later consumer reverse-engineers intent from an unrelated service instead of reading a specification. the invariant: code never silently resolves a material product choice.

change: introduce a `suspended` account state
spec-first: spec defines sign-in, invoice, restore transitions and acceptance
evidence → service implements the approved contract.
code-first: service adds the flag → behavior becomes the decision → later
agents reverse-engineer boundaries and make conflicting
assumptions.

consequence

a service adds a suspended flag without specifying whether suspended users can sign in, receive invoices, or be restored. the code's behavior becomes the only authority:

  • "can suspended users reset their password?" is answered by diving into an unrelated service's login logic, not a spec.
  • two agents fixing bugs in the same subsystem make different assumptions → silent incompatibilities.
  • a third team reverse-engineers the boundaries from code instead of approved intent.
  • with three incompatible implementations, refactoring requires coordination across every consumer — no shared specification exists to update once.

workflow

  1. update the owning specification, decision, handbook, schema, or other non-code artifact first — the visible handoff for every other agent. every Markdown or MDX artifact begins with parseable frontmatter.
  2. identify the owner: product, domain, API, data, or UI contract.
  3. state current fact, target, non-goals, and acceptance evidence.
  4. resolve material choices before irreversible work.
  5. add the active change to each code file's change ledger before editing its implementation.
  6. update the owner again with the behavior change and its final evidence.

do not

  • build a large slice from scattered chat history.
  • treat an agent draft as user approval.
  • leave an open decision hidden in code.
  • start by editing implementation while the contract or non-code change record is still absent or stale.

good and bad

bad

// account service - implementation without specification
async function suspendAccount(userId: string) {
await db.update(accounts).set({ status: "suspended" }).where(eq(accounts.id, userId));
// undefined: can suspended users sign in? receive invoices? be restored?
// the implementation accidentally becomes the product decision
}

good

# specifications/account-states.mdx
title: account state suspended
transitions:
- from: active
to: suspended
trigger: admin_request | fraud_detection
behavior:
signIn: false
invoiceDelivery: false
restoration: admin_only
non_goals:
- automatic restoration after period
- partial access during suspension
acceptance_evidence: |
POST /accounts/{id}/suspend → status=200, account.state=suspended
POST /auth/login with suspended account → status=403, error=account_suspended
// account service - implements approved specification
async function suspendAccount(userId: string) {
await db.update(accounts).set({ status: "suspended" }).where(eq(accounts.id, userId));
// verification links to named state transition
await logger.info("account_suspended", { userId, state: "suspended" });
}

the specification defines suspended, its transitions, sign-in/invoice behavior, non-goals, and acceptance evidence first; the service then implements that approved contract.

search pages

go to any page