Most software teams run a broken three-way bookkeeping system.

We write code to build what we want. We write unit tests to double-check our immediate assumptions. And somewhere in the same directory, we keep a spec.md trying to explain what the system was supposed to do in the first place.

Give it a month. The code changes under crunch time. The unit tests get massaged until they pass without anyone asking what they still prove. And that spec.md file quietly turns into shelfware—misleading every engineer who joins the team after you.

Spec-Driven Development (SDD) was supposed to solve this by making everyone agree on the contract first. But prose files in Git don't have teeth. If a document can't fail a build, it isn't an engineering contract. It's just folklore.

With background coding agents now able to parse text and write working code, we don't need to choose between human-readable design notes and strict test enforcement. We can turn that existing spec.md directly into the engine running our test suite.


1. Stop Writing Freeform Prose for Everything

The main reason documentation rots is that it's written entirely in loose English. Prose is great for explaining why a design choice was made, but automated tooling can't turn a vague paragraph into an assertion.

The fix isn't to replace markdown with an unreadable configuration format. The fix is to give spec.md two distinct jobs:

  1. The Human Context: The usual markdown sections explaining trade-offs, architecture decisions, and operational context.
  2. The Hard Invariants: Short, structured YAML blocks dropped right into the document that define the non-negotiables.
id: INV_TENANT_ISOLATION_04
invariant_name: "Tenant Boundary Enforcement on Cross-Reads"
description: "Queries fetching batch records must apply tenant_id predicate prior to query execution."
remediation_steps: "Wrap repo access with WithTenantScope(ctx) or apply @ScopedTenant filter on the Data Access Object (DAO)."

Notice the last field: remediation_steps. We aren't just recording an assertion; we're writing down the exact fix for the person who breaks it six months from now.


2. From Markdown Block to Presubmit Gate

You shouldn't ask engineers to hand-write unit tests that match markdown files. That's just more chore work, and people will skip it when deadlines loom.

Instead, let an automated workflow (or agent skill) bridge the gap:

[ Developer edits code / spec ]
       ↓
1. Invariant Extraction Skill
   • Reads spec.md changes
   • Normalizes strongly-typed invariant blocks
       ↓
2. Test Synthesis Engine
   • Maps invariants to existing test harnesses
   • Generates presubmit test suites
       ↓
3. Presubmit CI Validation
   • Runs autogen tests
   • Passes: PR clean
   • Fails: Prints human remediation steps

When someone edits the document:

  1. Extraction: An agent scans the spec.md, picks up any new or modified invariant blocks, and confirms the fields are valid.
  2. Synthesis: A generator maps that contract to the repository's test framework—writing integration fixtures in Go, JUnit, or PyTest that directly assert the condition.
  3. Execution: Those generated tests run on every pull request alongside hand-written unit tests.

3. The Catch: Who Owns the Truth?

If you automate this, you immediately hit a dangerous trap: What happens when the code changes?

If someone introduces a bug, and an automated agent helpfully updates spec.md to match the new behavior, your bug just got promoted to an official feature. That defeats the entire purpose of having a spec.

To make this work in a real engineering org, you need simple ground rules:

Code can move fast, but semantic changes require explicit domain sign-off. Over time, agents can pre-screen these changes against company-wide security or latency guidelines, but a human owner still holds the keys.


4. Better Failures: Remediation Instead of Stack Traces

Here is what most integration failures look like today:

AssertionError: Expected status 200, got 403
at batch_invoice.cc:142

The author has no idea which unwritten rule they tripped over, so they spend an hour stepping through mocks.

When a spec-driven test fails, the output reads like advice from the senior engineer who designed the system:

[FAILED] INV_TENANT_ISOLATION_04: Tenant Boundary Enforcement on Cross-Reads
---------------------------------------------------------------------------
Invariant:
Queries fetching batch records must apply tenant_id predicate prior to query execution.

Failure Point:
src/services/billing/batch_invoice.cc:142

Remediation:
Wrap repo access with WithTenantScope(ctx) or apply @ScopedTenant filter on the Data Access Object (DAO).
---------------------------------------------------------------------------

The test runner stops acting like an obscure barrier and starts behaving like an interactive linter for architectural intent.


The Bottom Line

Hand-written unit tests verify that the code does what the engineer thought it should do five minutes after writing it. Spec-driven tests verify that the code does what the system architecture actually requires.

By turning spec.md into an active input for automated test generation—and guarding updates with directory-level reviews—you stop documentation rot before it starts. The spec stays accurate not because someone remembers to update it, but because the build won't pass if it lies.