// DOS · spec-driven code generation
We compile a written specification into a running service.
The spec is a plain-English Markdown document. A compiler turns it into TypeScript operations, schemas and both test suites. Coding agents write the functions that remain. Roughly 60% of a finished service is generated deterministically and never edited by anyone — human or model.
This is not prompt-and-check. The high-volume, error-prone parts of an API service — input validation, output schemas, permission wiring, pagination, CRUD, test scaffolding — come out of a deterministic compiler, so they cannot drift from the spec. Agents work on the remainder, and the gates decide what ships.
// the pipeline
One path for a change, and no side door.
spec/ composer gates ┌─────────────┐ ┌─────────────┐ ┌────────────────────────┐ │ documents │ │ validate │ │ tsc · eslint │ │ operations │ ─────▶ │ build │ ────▶ │ unit · scenario │ ───▶ ship │ permissions │ └──────┬──────┘ │ coverage 100% │ │ scenarios │ │ │ runtime output check │ └─────────────┘ │ └───────────┬────────────┘ plain English │ │ human-approved ▼ │ ┌──────────────────┐ │ │ ~60% generated │ │ │ ~40% by agent │ ◀──── repair ─┘ └──────────────────┘
Specification
Work starts as a cross-linked wiki in docs/spec/ describing every document, operation, permission and test scenario in plain English. It is the design source of truth: when spec and code disagree, the spec wins. Agents help draft it; an engineer approves it; a product or operations lead can read it without reading code.
Validation
composer validate checks the spec's internal consistency — every type reference resolves, every operation's input and output attributes exist on its document, every scenario names a real operation and a real role.
Compilation
composer build generates the service: operations, schemas, validation, permission wiring, pagination, CRUD, and both test suites.
Generation
Coding agents write what the compiler cannot infer — business helpers, document methods, security policy. By this point that has been reduced to a set of small, isolated, individually described functions.
Verification
Every change runs the full stack of gates. Nothing merges by being waved through.
Repair
When a gate fails, the agent gets the failure and works it until the checks pass. No human babysitting the retry cycle.
Judgment
Where a failure raises a question the gates can't settle, an engineer decides. Engineer time goes to specs and judgment calls, not line-by-line review of generated code.
// the question everyone asks first
Why is your AI-written code better than ours?
The honest answer isn't a better model or a cleverer prompt. It's that the hard part was settled by a compiler before the model was asked anything.
An agent told to "build an activities service" is being asked to make a hundred coupled decisions at once — naming, structure, validation, error handling, tenancy, tests — with nothing to check itself against until the whole thing exists. That is the hardest possible shape of task to hand a model, and it's what ad-hoc prompting does. It's also why quality depends on who was driving.
Here the compiler has already made those decisions. What's left isn't a service. It's a set of individual functions, each arriving with:
- A fixed signature, because the call site is already generated.
- A one-sentence description of its job, taken from the spec that declared it.
- Types on both sides, generated from the same schemas, so a wrong shape is a compile error.
- A unit test already written, asserting it's invoked where the spec said it would be.
Here is a real example, complete. The spec declared a before step reading "Ensures that specified operation has a template," and this is the entire unit of work the agent was handed:
/** Validates that a template exists for the specified operation ID. */ const validateOperationId = async (context: Context, parameters: unknown): Promise<void> => { const operationId = got(parameters, 'mutation.operationId') as string; if (!config.has(`templates.${operationId}`)) { throw new InvalidParametersError(`Template for operation ID "${operationId}" is not found`); } };
Thirteen lines, one responsibility, nothing to get architecturally wrong. Most agent-written units in our services are this size; the largest are well under a hundred lines. Work decomposed this far is close to the easiest thing current models do — and it's verified immediately, not after integration.
// verification
Eight gates. Build failures, not review comments.
| Gate | Catches |
|---|---|
composer validate | Broken spec — before any code exists |
| Spec coverage | An operation no scenario exercises end to end |
tsc | Drift between operation I/O and document shape |
| ESLint | Style, documentation, complexity |
| Generated unit tests | Per-operation security, schema shape, before logic |
| Generated scenario tests | End-to-end behaviour against a real database, multi-role, multi-tenant |
| Coverage threshold | Any untested line, branch, or function — set at 100% |
| Runtime output validation | A response that doesn't match its declared schema — in production |
The last row is worth pausing on. Output schemas are enforced at request time, not only in CI. A service that returns something it never promised fails loudly rather than shipping malformed data downstream.
// why the tests can be trusted
The assertion doesn't live in the test file.
Tests are compiled from the spec, not written alongside the code. An agent cannot make a failing test pass by weakening the assertion. To change what is verified, it has to change the specification — which is a reviewed diff in a document a human approved.
### 4. Platform activity is not returned to another organization user - Role: USER2 - Operation: IndexActivities - Assertions: - result does not include item where `id` equals `activityId`
it('Platform activity is not returned to another organization user', async () => { const result = await t.request('IndexActivities', {}, t.USER2()); t.ensureNoItem(result as Record<string, unknown>[], { id: t.get(context, 'activityId') }); });
The tenant-isolation guarantee is stated once, in a sentence a non-engineer can check, and enforced mechanically forever after. Test-weakening becomes visible instead of buried.
// the quality standard
100% coverage, and a scenario for every operation.
- Two suites, two jobs. Unit tests cover every operation, helper and document method in isolation — operation tests are generated, asserting the security model, tags, mutation schema shape, output schema shape and each
beforestep. Integration tests are compiled from spec scenarios and run end to end against a real database, across multiple roles and tenants. - 100% coverage on both. Statements, branches, functions and lines, enforced as a threshold. Under 100% and the build fails. The number is realistic here precisely because most of the code is generated and its tests are generated with it — full coverage isn't a heroic effort, it's the default state of a compiled service.
- Every operation is exercised by at least one scenario. The compiler checks both directions. An endpoint that nothing tests end to end is a failed build, not a gap somebody notices six months later.
Enterprise engineering standards almost always specify a coverage gate, and it is usually the requirement teams negotiate down — 80%, then 70%, then "new code only". A generated codebase doesn't need the negotiation. The gate is set at 100% on day one and stays there as the service grows, because growth means more generated code and more generated tests in the same proportion.
Code coverage alone wouldn't catch the gap the spec-coverage gate closes. An operation can reach 100% line coverage from unit tests while never having been called through the real stack — never authenticated, never hitting the database, never validated against a live tenant boundary. Requiring a scenario per operation closes that by construction.
// what code review becomes
Review attention is a fixed budget. Most of it is normally spent badly.
A four-hundred-line pull request contains maybe forty lines that genuinely required a human decision; the rest is wiring, validation, error plumbing and test scaffolding. Reviewers spend their sharpest attention on the mechanical parts, arrive at the consequential part fatigued, and approve it. It is why large diffs get "LGTM" and small ones get argued about.
Generation splits the diff cleanly in two.
- The generated half isn't reviewed, because reviewing it is meaningless. It's a deterministic function of the spec, already checked by validation, the type system and both suites. That review time isn't spent better — it isn't spent at all.
- The other half gets the full budget. The spec diff, and a handful of small agent-written functions.
- Reviewing a spec is design review. Is this the right operation? Is it scoped to the right tenant? Should this caller have this permission? None of those are answerable by reading an implementation. And because it happens before the implementation exists, being wrong costs a document edit instead of a rewrite.
- It widens who can review. A spec diff is prose and structured English. The operations lead who actually knows how the domain is supposed to behave can read it and object — before the behaviour has been built.
// the same design makes it cheaper
Cost scales with the novel part of a service, not with its size.
- Most of the code costs no tokens. The generated 60% is emitted by a compiler in milliseconds. An agent never writes it, never reasons about it, never re-reads it for context.
- Regeneration is free. Change the spec, recompile, and the whole generated surface updates — tests included. An agent-writes-everything workflow re-pays the full bill on every refactor, and pays it again for the tests.
- Context stays small. Each task is one function with a fixed signature, so quality doesn't degrade as the codebase outgrows the context window.
- Cheaper models become viable. "Design a multi-tenant service" needs frontier reasoning. "Write this function with this signature" doesn't — small fast models handle it, and the gates catch it if they don't.
Frontier capability gets spent on the spec instead, which is where those models are genuinely strongest: reasoning about how a domain decomposes, catching the error case nobody mentioned, turning a conversation with a domain expert into something precise. Expensive reasoning goes where judgment is required and volume is low; cheap execution goes where volume is high and the answer is already determined.
// what comes with it
A verified API description — and a typed client for free.
Every service publishes its own OpenAPI description. That by itself is unremarkable. What matters is that this one is verified three independent ways: it validates against the OpenAPI meta-schema before publishing, so a malformed description is a startup failure; its schemas are the same objects the service validates requests and responses against at runtime, so there is no second copy to fall out of date; and the spec-coverage gate means no operation reaches it without an end-to-end test proving it behaves as described.
Which means the frontend gets its client for free. Standard off-the-shelf tooling turns a trustworthy description into TypeScript types for every request, response and model, TanStack Query hooks per operation, and a typed client that fails at compile time when the backend contract changes. No one writes an SDK and no one maintains one — a field renamed in the spec surfaces as a red squiggle in the web app before it ever surfaces as a bug.
- MCP, automatically. The published description is loaded by our MCP server and exposed as tools, so every operation is callable by an MCP client without extra work.
- Deployment. CloudFormation generated from environment config, deployed via the AWS SDK. No Serverless Framework.
- Storage is pluggable. DynamoDB and PostgreSQL adapters implement the same document interface; the spec and generated code are identical either way.
// where it applies
And where it doesn't.
- Good fit. New API services. Consolidating logic scattered across ad-hoc endpoints. Systems where multi-tenant isolation and permission correctness matter. Integration surfaces that must be exercised repeatedly against real dependencies.
- Rewrites, with a caveat. Applying it to a legacy migration means specifying the target behaviour, generating the replacement, and using scenario tests as the parity harness. That's a rewrite behind a spec, not an in-place code transformation — effective, but a different shape of work than a codemod.
- Not a fit. CMS work, one-off site builds, backlog clearing. Those are ordinary engineering, and we do them as ordinary engineering. The platform doesn't build your frontend either — it supplies the typed client the frontend consumes, which is a smaller and more honest claim.
// a sensible way to start
One service, end to end, in a short fixed window.
We write the spec with your team
One or two working sessions.
Your team reviews the spec
This is the decision point: a document, not a pull request.
We generate, implement and ship it
Behind your existing gates.
Your engineers own it afterwards
The spec is the handover artefact.
You end up with a working service and a concrete answer on whether spec-first suits how your team wants to work — before committing to retool around it.