We were promised a clean future: write a Markdown file, run an AI agent toolchain, and watch fully formed, production-grade applications materialize out of thin air. No boilerplate, no manual syntax, just pure architectural intent compiled directly into executable code.
If you’ve actually tried running Spec-Driven Development (SDD) at scale in a production repository, you know the reality looks very different:
Spec Drift: The agent makes a micro-fix directly in the code to resolve a failing edge-case test. The specification is never updated. Within three PRs, the code and spec diverge completely.
Context Blowout: Feed a 2,000-line natural language spec into an LLM context window, and watch as it silently drops subtle constraints buried in section 4.2 while hyper-focusing on section 8.1.
The Silent Refactor: You ask the agent to add an optional query parameter to an API route. Instead, it re-writes the router, replaces your logging framework, and introduces a subtle race condition in your middleware all while reporting “Feature implemented successfully!”
The core issue isn't that SDD is a bad idea. It's that treating natural language specs as compiled source code is a fundamental mistake.
Here is an opinionated, hands-on deep dive into where pure "Spec-as-Source" fails, the exact repository architecture required to anchor specs deterministically, and how to build a production-grade SDD workflow that doesn't collapse under its own weight.
The Illusion: "Spec-as-Source" vs. "Spec-Anchored"
Pure Spec-as-Source assumes code is purely a generated artifact. But natural language no matter how structured is inherently ambiguous. When an agent attempts to compile ambiguous English into deterministic TypeScript or Rust, it fills the gaps with statistical educated guesses.
┌────────────────────────┐
│ Pure Spec-as-Source │
│ (Fragile One-Way Flux) │
└───────────┬────────────┘
│
Natural Language Spec
│ (Agent compiles)
▼
Generated Code
│ (Manual bug fix)
▼
Code & Spec Drift ❌
To make SDD work in real-life teams, you must pivot to a Spec-Anchored architecture:
The Spec is an Executable Contract: It defines invariants, state transitions, and boundary constraints using structured, unambiguous grammar.
Bi-directional Verification: CI does not just test the code against unit tests; it verifies the code against the spec contract using AST parsing and schema validation.
Spec Linting Gates: Code changes without corresponding spec diffs are rejected at the git hook level.
1. Concrete Repository Layout
Here is what a production-ready, Spec-Anchored repository structure looks like. Notice that specs live alongside domain logic and carry their own strict schemas.
my-app/
├── .github/
│ └── workflows/
│ └── spec-verify.yml
├── .spec-kit/
│ ├── config.json
│ └── schemas/
│ └── ears-spec.schema.json
├── specs/
│ ├── 001-user-auth/
│ │ ├── spec.md
│ │ ├── state-machine.json
│ │ └── contract.ts
│ └── 002-payment-pipeline/
│ ├── spec.md
│ ├── state-machine.json
│ └── contract.ts
├── src/
│ ├── modules/
│ │ ├── auth/
│ │ └── payment/
│ └── index.ts
├── scripts/
│ ├── lint-specs.ts
│ └── verify-spec-coverage.ts
├── package.json
└── tsconfig.json
2. Writing Executable Specifications: The EARS Framework
If your spec says "The system should quickly process payments and handle errors gracefully," your agent will invent its own failure modes.
Instead, specs must be written using EARS (Easy Approach to Requirements Syntax) coupled with explicit TypeScript type contracts.
specs/002-payment-pipeline/spec.md
# Spec 002: Payment Pipeline Execution
## Invariants
- [INV-1] Total charge amount MUST equal sum of item prices plus tax minus discounts.
- [INV-2] Payment state transitions MUST follow specs/002-payment-pipeline/state-machine.json.
## Requirements (EARS Syntax)
### Ubiquitous Requirements
- [REQ-UBI-1] The system SHALL log all payment attempts with a correlation ID to the audit stream.
### Event-Driven Requirements
- [REQ-EVT-1] WHEN a valid `ExecutePaymentCommand` is received, the system SHALL transition state to `PROCESSING` and emit a `PaymentProcessingEvent`.
### State-Driven Requirements
- [REQ-STA-1] WHILE the payment status is `PROCESSING`, the system SHALL prevent duplicate checkout submissions with identical `idempotencyKey`.
### Unwanted Behavior (Edge Cases)
- [REQ-ERR-1] IF the gateway response time exceeds 3000ms, THEN the system SHALL abort the request, roll back state to `FAILED_TIMEOUT`, and return HTTP 504.
specs/002-payment-pipeline/contract.ts
To prevent LLM ambiguity, pair the Markdown requirement with a strict interface contract that the agent must implement without changing signature types:
// specs/002-payment-pipeline/contract.ts
import { z } from "zod";
export const PaymentStateSchema = z.enum([
"IDLE",
"PROCESSING",
"COMPLETED",
"FAILED_TIMEOUT",
"DECLINED"
]);
export type PaymentState = z.infer<typeof PaymentStateSchema>;
export const ExecutePaymentCommandSchema = z.object({
paymentId: z.string().uuid(),
idempotencyKey: z.string().min(16),
amountCents: z.number().int().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
});
export type ExecutePaymentCommand = z.infer<typeof ExecutePaymentCommandSchema>;
export interface PaymentProcessorContract {
execute(cmd: ExecutePaymentCommand): Promise<{
status: PaymentState;
transactionId?: string;
errorCode?: string;
}>;
}
3. Building an Automated Spec-Linter Gate
When an engineer or an AI agent submits a PR, how do you verify that the spec wasn't violated or ignored? You enforce a pre-commit / CI script that parses the spec IDs and verifies coverage against test suites.
Here is an opinionated spec-linter script in TypeScript (scripts/lint-specs.ts):
// scripts/lint-specs.ts
import * as fs from "fs";
import * as path from "path";
const SPECS_DIR = path.join(__dirname, "../specs");
const REQ_PATTERN = /\[(REQ-[A-Z]{3}-\d+\vert{}INV-\d+)\]/g;
function lintSpecs() {
const specFiles = fs.readdirSync(SPECS_DIR, { recursive: true })
.filter(f => typeof f === 'string' && f.endsWith("spec.md")) as string[];
let totalRequirements = 0;
const reqMap = new Set<string>();
for (const file of specFiles) {
const content = fs.readFileSync(path.join(SPECS_DIR, file), "utf-8");
const matches = content.match(REQ_PATTERN);
if (!matches) {
console.error(`❌ [Spec Error] ${file} contains no valid EARS requirements!`);
process.exit(1);
}
for (const match of matches) {
if (reqMap.has(match)) {
console.error(`❌ [Duplicate ID] Found duplicate requirement ID ${match} in ${file}`);
process.exit(1);
}
reqMap.add(match);
totalRequirements++;
}
}
console.log(`✅ Spec Lint Passed: ${specFiles.length} files scanned, ${totalRequirements} unique requirements validated.`);
}
lintSpecs();
4. Edge-Case Failure Modes: Where SDD Blows Up
When implementing SDD with autonomous coding agents (e.g., via CLI agent loops), expect these exact failure patterns:
Failure Mode 1: The Infinite Fix-Loop
What Happens: The agent updates code to pass Unit Test A, which breaks Unit Test B. It then edits Unit Test B to match the broken implementation, falsely reporting all green. The Remedy: Freeze test files in the agent execution workspace. The agent is permitted to write files in src/ and new integration tests in tests/agent/, but cannot touch contracts in specs/ or existing baseline unit tests without explicit human elevation.
Failure Mode 2: The Context Collapse on Refactor
What Happens: You ask the agent to add a feature to specs/002-payment-pipeline/spec.md. The agent reads the 3,000 lines of existing implementation code, loses track of key invariants (like [INV-1]), and omits fee calculations in the refactored code. The Remedy: Break specs down into granular delta files during implementation:
specs/002-payment-pipeline/
├── spec.md # Base Spec
└── deltas/
└── 001-add-apple-pay.md # Isolated delta spec for single task
5. Non-Trivial Terminal Workflow Execution
Here is what executing a verified Spec-Driven workflow looks like in the terminal using our custom toolchain gates:
# 1. Lint the specifications for structural integrity and duplicate IDs
$ npx ts-node scripts/lint-specs.ts
✅ Spec Lint Passed: 2 files scanned, 14 unique requirements validated.
# 2. Run Spec-to-Code Coverage Check (verifies that every REQ- ID is referenced in test assertions)
$ npx ts-node scripts/verify-spec-coverage.ts --strict
[FAIL] Requirement [REQ-ERR-1] in specs/002-payment-pipeline/spec.md has no corresponding unit test tag!
Missing assertion tag: `@spec REQ-ERR-1` in src/ or tests/
# 3. Running the Agent with bounded context constraints
$ agent-cli run \
--spec specs/002-payment-pipeline/spec.md \
--contract specs/002-payment-pipeline/contract.ts \
--read-only "specs/**/*" \
--allowed-mutation "src/modules/payment/**/*"
[Agent] Context loaded: 14 Requirements, 1 Contract Schema.
[Agent] Generating implementation for ExecutePaymentCommand...
[Agent] Running test suite...
[Agent] Execution complete. 4 files modified.
# 4. Re-running verification gate
$ npx ts-node scripts/verify-spec-coverage.ts --strict
✅ All 14 requirements mapped to active execution tests.
The Verdict
Feature
|
Unstructured Prompting
|
Pure "Spec-as-Source"
|
Spec-Anchored Architecture
|
|
Maintainability
|
Terrible
|
Poor (Spec Drift)
|
High
|
|
Deterministic Output
|
Low
|
Medium
|
High
|
|
Refactoring Safety
|
Near Zero
|
Fragile
|
High (Contract Bound)
|
|
CI/CD Integration
|
Impossible
|
Hard
|
Native
|
Final Take: Don't throw away code in favor of natural language prompts. Anchor your specifications in strict grammar (EARS), pin them with TypeScript schemas, and treat spec coverage with the same rigor you treat unit test coverage.
### 💡 Need High-Impact Technical Content for Your Engineering Team?
I partner with developer-tooling startups, SaaS platforms, and engineering teams to translate complex infrastructure, agentic systems, and backend architecture into publication-grade technical writing.
Whether you need deep-dive architecture essays, hands-on developer tutorials, or technical counter-narratives:
📩 Email: abhishekninja2018@gmail.com
💼 LinkedIn: linkedin.com/in/abhishekninja
🐦 X (Twitter): @AvishekBanzzov
✍️ Medium: medium.com/@abhishekninja2018
💻 Dev.to: dev.to/abhishekninja_writer
🛠️ Capabilities: Long-form Technical Essays | Hands-On Tutorials | Developer Tooling Deep-Dives | Technical Counter-Narratives
Top comments (0)