METHODOLOGY · VERSION 4

Domain-Rules-Driven Development

On March 30, 2026 I ran a 20-minute QA audit on software that had 4,800 passing tests in CI. The audit found over 100 critical bugs in production. Each bug had a passing test sitting on top of it.

I’d generated those tests over the previous 10 weeks from a Markdown rules file, using a variant of TDD I was writing at the time. Before the TDD version, I’d written plain prose specifications: PRDs, long methodology documents, a Constitutional file I was pretty proud of. All of it read well. Agents ignored it past a certain context length.

The prose version failed in a specific way. An agent session that starts with a 3,000-word spec in its context ends with a fraction of that spec in its working memory. I could be twelve hours into a build, point at the document, point at the code that contradicted it, and hear back a coherent explanation for why the contradiction was intentional. Context length is the proximate cause. The deeper problem is that prose is an artifact an agent translates into code, and the translation has no mechanical check.

The tests version failed harder, because it shipped. A test asserts at a call site. A user journey is a sequence of things that happen between call sites: a page load, waiting for data to populate, a click, a navigation away, and coming back five minutes later to see if the record stuck. None of that is a function return value. You can simulate the sequence inside the test runner, and the simulation will pass reliably while the real sequence breaks. That’s what happened in the 4,800-test suite.

What replaced both was a different artifact. A domain rule is a specification with an assertion (machine-checkable), an enforcement path (a hook or ast-grep pattern that blocks violation at commit), and an origin (the specific incident that made the rule necessary). The rest of this document is what I’ve figured out about the shape of that artifact and the stack around it.

Four commitments

Four decisions hold the methodology together. The rules have to be machine-testable, because prose specifications decay past a certain session length and I haven’t found another fix. Enforcement runs mechanically; I’ve stopped trusting agent compliance with policy statements, which is reliable for the first few tool calls and drifts after. Third, source code has no authority of its own. Source is the artifact being constrained, and giving it final say collapses the system. And the rule set is monotonic. When I find a gap, I add a rule; when a rule turns out to be under-specified, I strengthen it. I don’t delete rules. I’ve been tempted: there are three rules in the current set where the first agent to read them complains they’re annoying to satisfy. I keep them anyway, because every rule in the set is there to close a specific thing that already broke.

What a rule looks like

Here’s a rule from the rebuild. AUTH-5 governs entity reads across organizational boundaries. I locked it on April 7, 2026 after discovering the prior error-code pattern let cross-organization probes distinguish “entity exists elsewhere” from “entity doesn’t exist.”

AUTH-5

Cross-organization access

adversarial · Locked 2026-04-07

Origin. Resolution of audit finding HC-1 (2026-04-07 authority-hierarchy lockdown). Supersedes the earlier FORBIDDEN "Access denied" pattern. The prior pattern leaked entity existence to cross-organization probes. An attacker could distinguish “entity exists in another organization” from “entity doesn’t exist” by the error code returned, enabling enumeration attacks across organizational boundaries.

Rule text. Every single-entity fetch MUST include organizationId = ctx.session.activeOrganizationId in the SQL WHERE clause. If the query returns zero rows, throw NOT_FOUND "<Entity> not found". Never use FORBIDDEN "Access denied". That error leaks entity existence.

Testable assertion. expect(error.code).toBe("NOT_FOUND"); expect(error.message).toBe("<Entity> not found") for any request crossing an organization boundary, regardless of whether the entity exists, is soft-deleted, or belongs to another organization.

Enforcement.

  • Write-time: hookify rule blocks the string FORBIDDEN "Access denied" in any procedure file.
  • Gate-time: static-analysis rule requires organizationId in every WHERE clause touching an organization-scoped table.
  • Runtime: middleware verifies session and active organization before procedure body executes.

Violation closed. Three distinct failure cases (entity does not exist, entity was soft-deleted, entity belongs to another organization) collapse into a single response. A probing client observes identical output for all three. Enumeration across organizational boundaries ceases to be possible.

Every rule in the set has the same six components: a layer classifier (spec, consistency, or adversarial); an origin incident; the rule text stated as a MUST; a testable assertion the reviewer could write without reading the source; the enforcement path or paths, usually some mix of write-time hook, commit gate, and runtime check; and a description of what failure mode is now closed. If a rule is missing one of these, it’s underspecified. I’ve written plenty of those. They read like policy, and an agent who doesn’t want to satisfy them has an easy out.

The three layers

Every rule belongs to one of three layers, and a rule set that skips a layer ships with the class of defect that layer was there to prevent.

Spec

The spec layer defines canonical behavior: what inputs a procedure accepts, how it authenticates, which error codes it returns, what side effects it causes, what its response looks like. A typical spec rule in the rebuild is that money columns are stored as integer cents in PostgreSQL bigint, never integer. The integer type caps at $21,474,836.47 in cents and silently overflows once a property value crosses that threshold. Most API documentation already covers this layer reasonably well, so spec rules tend to be the shortest and the most numerous in my rule set.

Consistency

The consistency layer enforces invariants that span procedures. These are the rules that only matter if you can check them against the whole surface at once: every SELECT on a soft-deletable table goes through notDeleted(); mutations always write an audit entry; error messages come from a canonical string table rather than whatever the agent decided sounded right. The hardest rule I’ve written at this layer is about soft-delete cascade behavior. It enumerates 18 child entity types individually and specifies what the application layer does when a parent goes soft-deleted: children keep their own state, never get cascaded to, and every read path joins back through the parent with notDeleted(), so orphaned children become invisible without being destroyed. That rule is 70 lines. It has to enumerate every entity by name, because a consistency invariant that holds for 17 of 18 cases is not actually an invariant.

Adversarial

The adversarial layer covers what happens when the system gets probed. Wildcard injection. Cross-scope access attempts. Race conditions around concurrent writes. Third-party service failure and the cascade that follows. Prompt injection against fields that feed an agent. Timing channels. AUTH-5 above is an adversarial rule. It’s about what an attacker learns from a probe that deliberately goes off-path. A domain that accepts external input without adversarial rules is a domain I haven’t finished specifying yet.

Each layer catches a class of defect the other two don’t. I’ve caught defects in the rebuild that only fell out under consistency-layer rules while spec-layer rules were green. Adversarial rules have caught probes in places where consistency rules had no coverage. The three-way split isn’t theoretical. It came from finding things late and having to name the category that would have caught them earlier.

How rules get enforced

A rule without an enforcement path is a policy statement, and I’ve learned to distrust those. Agents comply with policy statements for the first several tool calls of a session. After that, compliance falls off in a way I can reproduce across model families. Every rule in my set is paired with the strongest mechanical enforcement its shape admits, across five layers.

Write-time

Hookify rules fire on every Edit or Write tool call. If an agent tries to write FORBIDDEN "Access denied" into a procedure file, the edit is blocked before it lands, and the incident that produced the rule is attached to the block so the agent can see why:

id: block-access-denied-forbidden
matches: 'FORBIDDEN "Access denied"'
reason: AUTH-5. Leaks cross-org entity existence.

Gate-time

ast-grep patterns run before every commit. A write-time hook catches a single tool call; the gate catches drift that accumulated across a whole session without any single call crossing a hook. Every SELECT on an organization-scoped table must include organizationId = ctx.session.activeOrganizationId in its WHERE predicate, and pnpm gate fails the commit if it doesn’t.

Runtime

Middleware enforces what static analysis cannot see: whether the session is valid, what the active organization resolves to, whether the request exceeds rate limits, whether it’s being replayed. All checked before the procedure body runs, with no procedure-level code required to invoke the checks.

Lifecycle

Shell hooks react to tool events themselves. PreToolUse:Bash is the one I rely on most: it blocks destructive git commands (reset —hard, push —force to main, branch -D) and every known pattern for bypassing the test runner. SubagentStart injects the relevant contract registries into the subagent’s initial context so a subagent cannot hallucinate a specification for the code it’s about to write. SessionStart reads the current state of the rebuild and injects a list of domain-coverage gaps.

Memory

Every incident that produced a new rule is saved as a feedback memory. When a new session starts, the memory file loads before any tool call. I’ve watched this catch the case where a fresh agent session, with no knowledge of the last month of work, proposes to reintroduce a pattern the rule set was written to stop.

The five layers are not redundant. Each one catches something the other four miss. Starting with one layer, a hook leaked; with two, the agents learned to negotiate around the pair. Five is the count I’ve stopped at. I haven’t found a violation in the last six weeks that a sixth layer would have caught. If I do find one, the methodology says to add a layer, not revisit the existing five. When a layer catches something, the finding becomes the origin of a new rule, and the rule set grows.

The pipeline from rules to shipped code

The enforcement stack is the defense; the pipeline is what I do when writing code I want the enforcement stack to have nothing to catch from. It has six stages, and I run every domain through all of them.

1. Rules are the specification

Every domain file references seven shared authority files (_shared/auth.md, _shared/permissions.md, _shared/persistence.md, _shared/errors.md, _shared/audit-log.md, _shared/security.md, _shared/identifiers.md), and a cross-domain registry tracks which domain depends on which. Source code has no authority. When the rule and the source disagree, the source is wrong; it’s the thing being verified against the rule.

2. /contract extracts layers

The contract skill is the first thing I run for a domain. It reads the domain rules file in full, all seven shared authority files, and the domain registry. It verifies every shared infrastructure dependency the rules reference against what’s actually on disk, because rules name utilities that sometimes have stale import paths. Then it groups the procedures into three to six procedure layers, using six heuristics (shared consistency rules first, then entity lifecycle, permission gates, sub-entity tables, external integrations, and last the base procedure type). The output is a Domain Preamble plus one Layer Brief per layer, with rules quoted verbatim rather than paraphrased. The contract is regenerated every time I run /contract; the authoritative artifact is the rules file, which the contract reads from.

3. /build-source implements one layer at a time

Each procedure runs through a write-verify loop. The pre-flight is three lookup tables: canonical error-message strings checked character-by-character, the audit log call signature for this domain, and an ENTITY_TYPES pre-flight grep against audit.ts to confirm the entity type is already registered. The procedure itself is written using Zod field templates pulled from a fixed list rather than composed ad hoc, because ad hoc composition is where agents invent validation that doesn’t match the canonical inputs. After the write, 15 mechanical grep checks run against the procedure, and any failure gets fixed before the next procedure starts. After a full layer is written, four cross-procedure consistency checks run. The hard gate at the end of the stage is gate:static, which runs ESLint under zero-warnings mode, ast-grep against the 41 constitutional patterns, dep-cruiser against monorepo boundary rules, and the TypeScript strict typecheck. If any of the four fails, the layer isn’t done.

4. /review-source runs a 9-pass adversarial review

Pass 0 is the Extraction-Then-Verification protocol, applied against every shared infrastructure file the domain depends on. Phase A is extraction: each behavioral expression gets pulled into a numbered table. The second phase runs a rule-by-rule negation audit, with mandatory COMPLIANT and VIOLATING columns for every row. Phase C mechanically greps the source against the forbidden-patterns registry, which currently holds 19 patterns (FP-1 through FP-19). Passes 1 through 8 walk the domain procedure by procedure, covering inventory, input validation, query correctness, error handling, audit logs, response shape, security surface, and engineering quality in that order. Every finding carries an actionClass (FIX, SPEC-GAP, or RISK) and an auditSeverity (CRITICAL, HIGH, MEDIUM, or LOW). SPEC-GAP findings can only add to the rules. They cannot remove a requirement, and they cannot weaken MUST to SHOULD. A finding from reviewing source cannot change the rule it was supposed to check against. Output includes a structured JSON block the control plane reads mechanically for phase gating.

5. QA auditor exercises real user journeys

The QA auditor is a bash orchestrator that spins up an isolated Next.js build on port 3011, seeds authentication state along with cross-tenant fixtures, loads the YAML flow definitions for the domain under test, and dispatches a claude -p subprocess per flow with Playwright MCP attached. Each flow runs in desktop (1440x900) and mobile (375x812) viewports, captures console and network output, injects axe-core 4.11.x against the rendered DOM, and verifies persistence by reloading the list after a create and by navigating away and coming back. Findings land in a SQLite database, identified by stable hashes so a finding that recurs across runs is recognized as the same finding. A finding that persists across multiple runs ages in severity. A finding that appears once and never again is flagged as a flake. The timezone sweep runs the whole suite twice, under TZ=America/Los_Angeles and then TZ=Europe/London, which has caught date-handling bugs that in-process tests almost never surface.

6. Regression tests written after QA

Once a domain’s QA auditor run is clean, regression tests get written for each bug the audit surfaced. The tests use createCaller because the existing ast-grep rules fail any test that doesn’t, exercise production code directly, and cannot be silenced without disabling the gate. Each test carries a finding ID from the SQLite database, so the test and the incident that motivated it stay linked in the record.

Each stage is doing something the other five don’t. The enforcement stack blocks rule violations that would otherwise reach commit or production. The pipeline is how I try to produce code where the stack has nothing to catch in the first place. Tests land at stage 6 because they function as regression locks for bugs the QA auditor already surfaced.

DRDD is not TDD. In classic TDD the failing test is the specification, and behavior that can’t be captured by a failing test is implicitly out of scope. In DRDD the specification sits in a rules file, and tests exist only as regression locks for bugs the QA auditor surfaces. When I tried the tests-first variant with an agent in the implementation role (10 weeks of Domain-Rules TDD, written up in a separate essay), the agent produced a passing test suite faster than it produced correct code, because call-site assertions miss everything that matters about a user journey: persistence across page reload, multi-viewport rendering, state after navigation return, timezone handling across servers, accessibility against the rendered DOM. Gaps that can’t be specified by call-site assertions become defects that ship anyway.

Spec-driven development is the closer cousin, but DRDD differs in scope. SDD treats specifications as inputs to code generation, so the better the spec the better the generated code. DRDD treats specifications as the thing the code gets checked against after it’s been written. SDD doesn’t care whether the spec is still true post-generation. DRDD treats post-generation conformance as the only thing worth checking at commit.

Design by Contract is closer still, at least at the runtime layer. DbC attaches pre- and post-conditions to function boundaries at runtime. DRDD attaches assertions at five places instead of one: tool call, commit, runtime path, lifecycle event, and session-start memory read. The runtime layer in DRDD is basically DbC. The other four exist because an agent can violate a spec in places DbC has no view of.

Formal methods sit on the other side of a cost threshold most projects can’t cross. They buy provable correctness. DRDD is informal in its specification language (Markdown rules, not Coq) and mechanical at the enforcement layers. The tradeoff is proof-for-coverage. You give up certainty and gain a mechanism that catches new classes of failure as they’re discovered, without stopping the project to rewrite the spec in a theorem prover every time.

What this costs

DRDD is overkill on a small project. The enforcement stack is an upfront investment: hookify rules, ast-grep patterns, lifecycle hooks, MCP servers, and a rule corpus big enough to be worth checking against. The pipeline adds four skills (contract, build-source, review-source, qa-audit) plus the QA runner orchestrator and the SQLite finding store. On a weekend prototype I’d write one hook and ship. The methodology pays off when the project is large enough that agent drift across many sessions and many domains starts silently compounding, which in my experience is somewhere around the point where a codebase has more than one product sharing a backend and more than a few engineers’ worth of rules to keep track of, whether those engineers are human or not.

It also costs discipline. Rules are tempting to delete when a reviewer finds one inconvenient, and that is the hardest habit for me to hold. The rule set has to grow monotonically. Findings add rules, strengthen existing rules, or introduce additional enforcement layers for rules that were one-layer enforced before. Findings never remove rules or weaken language from MUST to SHOULD. A finding that looks like “this rule is wrong” is almost always a rule that’s incomplete and got tested against an unanticipated case. The move is to strengthen the rule, adding a clause that covers the case the source hit. I keep an explicit rule about when a rule can be deleted: when the code that made the rule necessary no longer exists in any domain, and every downstream rule that cited it has been updated or retired. I haven’t deleted one yet.

Provenance

The methodology is extracted from the specification system I’ve been building for a rebuild in progress. The numbers as of April 2026: 42 domain-rules files, roughly 3,500 rule statements across 24 domains. The enforcement stack is 48 hookify rules, 41 ast-grep patterns, 26 lifecycle hooks, 20 custom skills (the pipeline stages plus ancillary tooling), and 5 custom review agents. The shared tier carries rules for authentication, persistence, identifiers, error handling, audit logging, permissions, and security, and every product domain imports from it. Rule pages go up on the public rules explorer after I strip anything that identifies the codebase they came from. What’s published here is a working subset; the full corpus is in the rebuild, evolving.