Core Concepts

Verification

Two verification systems — the CLI's executable acceptance criteria, and verification-core's markdown-native executable specs — and how they relate.

"Verification" shows up in two different, complementary systems in this codebase. It's worth being precise about which one a given tool is doing.

System 1: executable acceptance criteria (CLI / MCP)

Covered in full in Acceptance criteria — the short version: a story's acceptance criterion can carry verification: { method: "executable", command, expected }. intent check (or the MCP report_criteria flow) runs that command and gets a real pass/fail. This lives in packages/cli/src/lib/criteria.ts and run-criterion.ts — it drives day-to-day story completion.

System 2: verification-core — markdown-native executable specs

@intentdocs/verification-core (and its vitest adapter, @intentdocs/verification-vitest) is a different, more general tool: it makes prose in a markdown spec file directly executable, sentence by sentence, without a special templating syntax. It's aimed at keeping documentation and behavior from drifting apart, not at driving the story-completion loop.

The idea

Any sentence in a markdown doc that matches a registered step pattern becomes executable. There's no bespoke DSL to learn — you write ordinary prose, and register step patterns (using Cucumber expressions, e.g. "I add {int}") that recognize specific sentences and run code when they match.

I add 5. The total is 5.

Given step definitions for "I add {int}" (a stimulus — an action) and "the total is {int}" (a sensor — an assertion), that one line is a complete, runnable example: it performs the action, then checks the assertion.

Registering steps

const { stimulus, sensor } = steps(() => ({ count: 0 }));
 
stimulus("I add {int}", (state, n) => ({ count: state.count + n }));
sensor("the count is {int}", (state) => state.count);
  • A stimulus handler may mutate state — it returns undefined or a partial state patch.
  • A sensor handler returns a value that gets compared against whatever the matched sentence captured (an inline parameter, a following table, or a following code-block docstring).

Parsing

Markdown is tokenized into blocks (paragraphs, tables, code blocks, headings), each paragraph is split into sentences (respecting code spans, quoted strings, abbreviations, and decimals so it doesn't split mid-thought), and each sentence is matched against the step registry. A table immediately following a matching paragraph attaches to that step as its expected data (splitting into one example per row, if the table's headers echo the parent text); a code block attaches as an expected docstring; a ```error -tagged code block declares that the step is expected to fail with a matching error substring.

Running

Each parsed example runs its steps in order against fresh, deep-frozen state, comparing every sensor's actual value against its expected value (inline params via deep equality, docstrings via exact string match, tables cell-by-cell). An example's result is pass, fail (a step threw, or a comparison didn't match), tracked with duration and per-slot comparison detail for precise error reporting.

Drift detection

The problem drift detection solves: someone edits a spec line that used to be executable, it silently stops matching any step, and now it's unmaintained prose nobody notices went stale. verification-core guards against this with a baseline — a JSON file recording, per spec file, which paragraph line numbers matched at least one step (identity is by line number, not text, so a legitimate edit that still matches isn't a false positive). If a line that used to match no longer matches anything, that's a drift violation. There's no silent auto-heal — a human has to explicitly accept the new baseline (acceptDrift) once they've confirmed the change was intentional.

Vitest integration

// vitest setup file
import { defineVerificationTests } from "@intentdocs/verification-vitest";
await defineVerificationTests({ cwd: process.cwd() });

Configured via a verification.config.json:

{
  "docs": { "include": ["docs/**/*.md"], "exclude": ["**/private/*"] },
  "steps": ["tests/**/*.steps.ts"],
  "baseline": ".verification/baseline.json"
}

defineVerificationTests loads your step files (registering their handlers as a side effect), parses every matched spec, and generates real describe/test blocks — one describe per spec file, nested by heading, one test per example — plus a dedicated drift test ("no examples silently reverted to prose"). After a full run, it writes .verification/report.json and auto-advances the baseline only if every test passed and there was no drift.

How the two systems relate

They solve adjacent but different problems. The CLI's executable acceptance criteria drive story completion — "is this specific requirement done." verification-core drives documentation correctness — "does this markdown still describe real, passing behavior." An acceptance criterion's executable command is free to invoke a verification-core-powered vitest suite (e.g. vitest run docs/checkout.spec.md) as its check, which is the natural way to make the two agree instead of drifting into two different sources of truth.