Spec sync: drift & coverage
This is the feature TruSpec exists for. Your OpenAPI document is the source of truth; your collection is measured against it. Three checks — drift, coverage, and contract — keep them honest, run fully offline, and fail the build the moment your collection (or your API) rots away from your spec.
A fourth command, gen, closes the loop by generating a request stub for every operation so you start at full coverage.
The model
A request links to a spec operation with a spec block:
spec:
operation: "GET /pets/{id}" # "${METHOD} ${path}"
operationId: getPetById # preferred when presentTruSpec parses your OpenAPI 3 document into a flat list of operations (keyed METHOD path, e.g. GET /pets/{id}), then matches each request to an operation:
- If both have an
operationId, they match on it. - Otherwise the request's
operationstring is matched against the operation'sMETHOD pathkey.
Everything downstream — drift, coverage, the live probe — is computed from that matching.
Drift
truspec drift diffs your collection against the spec and exits non-zero when they've diverged. It reports four categories:
| Category | Meaning | Why it matters |
|---|---|---|
Untracked (added) | In the spec, but no request references it. | A new endpoint shipped with no test. |
Stale (removed) | Referenced by a request, but gone from the spec. | A request points at an endpoint that no longer exists. |
Changed (changed) | Matched, but the request no longer satisfies the spec. | The contract tightened — e.g. a parameter became required — and the request didn't keep up. |
Missing from live API (liveMissing) | With --live, a spec operation a running API doesn't serve. | The deployed API and the spec disagree. |
"Changed" currently fires when:
- the spec marks a query parameter as required and the request doesn't include it, or
- the spec marks the request body as required and the request has no body.
truspec drift --spec openapi.yaml ./apiSpec operations: 4 Collection operations: 3
Untracked in collection (1):
+ GET /users/{id}
Drift detected: 1 untracked, 0 stale, 0 changed.The drift check passes (exit 0) only when all four categories are empty.
Probing a live API (--live)
Add --live <baseUrl> to also check a running API for operations it doesn't serve. This catches the case where the spec and the deployment have diverged — the spec promises an endpoint that returns 404/405, or the host is unreachable.
truspec drift --spec openapi.yaml ./api --live https://api.staging.example.comFor safety against a production target, the live probe only sends GET and HEAD requests; mutating operations are skipped (and reported as skipped). Path parameters are filled with 1 to form a concrete URL. An operation is flagged "missing from live API" when the probe returns 404, 405, or the host is unreachable. Use --timeout <ms> to bound each probe.
JSON shape
--json emits a DriftReport:
{
"specOperations": 4,
"collectionOperations": 3,
"added": ["GET /users/{id}"],
"removed": [],
"changed": [],
"liveMissing": [],
"ok": false
}Coverage
truspec coverage reports what share of spec operations are exercised by a request that also has assertions — a request with no assertions doesn't count, because it asserts nothing about the contract.
truspec coverage --spec openapi.yaml ./apiCoverage: 75% (3/4 operations tested)
Uncovered (1):
✗ GET /users/{id}Gate on a minimum with --min:
truspec coverage --spec openapi.yaml ./api --min 80 # exit 1 if below 80%percent is round(covered / total * 100); an empty spec reports 100%. The command exits 0 when percent >= --min (default 0, i.e. report-only).
JSON shape
{
"total": 4,
"covered": ["GET /pets", "GET /pets/{id}", "POST /pets"],
"uncovered": ["GET /users/{id}"],
"percent": 75,
"ok": true
}Response validation (contract)
drift and coverage are static — they reason about whether a request references an operation and supplies its required inputs. They never look at what the API actually returns. truspec contract closes that gap: it runs the collection and validates each response body against the spec's response schema for the matched operation.
truspec contract --spec openapi.yaml ./api --env localContract: 2/3 tested operations conform to the spec
✓ GET /posts
✓ GET /posts/{id}
Violations (1):
✗ POST /posts → schema: 1 violation(s) — /author/id: missing required property 'id'
Untested — no request exercises these (1, see `coverage`):
– GET /users/{id}
Contract violations: 1.Because it sends requests, contract takes the same knobs as run — --env for the base URL and secrets, --timeout, --json. Point it at a mock or a real API. It exits non-zero only on a schema violation; untested and status-undocumented operations are reported but don't fail the gate (that's coverage/drift's job).
This is behavioral drift — the kind the structural drift check can't catch. A handler that returns a string id where the spec promises an integer, drops a required field, or adds an undeclared status passes every existing status/jsonpath assertion but fails contract.
Validated without a separate command
You don't need the contract command to get response validation. Two lighter paths:
truspec run --spec openapi.yaml ./api— every spec-linked request is validated against its response schema inline, as an extra assertion in the normal run output.{ type: schema }assertion — opt a single request in (or pin astatus/contentType). See File format → schema.
The validator covers the OpenAPI 3 schema subset (type, properties, required, items, enum, nullable, allOf/oneOf/anyOf, $ref, additionalProperties: false) and reports the JSON path of every violation. format is treated as an annotation (not enforced).
JSON shape
--json emits a ContractReport:
{
"specOperations": 4,
"conformed": ["GET /posts", "GET /posts/{id}"],
"violations": [
{
"op": "POST /posts",
"status": 201,
"message": "schema: 1 violation(s) — /author/id: missing required property 'id'",
"filePath": "api/create-post.tspec.yaml"
}
],
"skipped": [],
"untested": ["GET /users/{id}"],
"ok": false
}Scaffolding from a spec
truspec gen writes a request stub for every operation in your spec — each with a status: 200 assertion and a spec link already filled in. It's the fastest way to take a brand-new spec to a fully drift-tracked collection.
truspec gen --spec openapi.yaml --out ./apiEach generated file looks like:
tspec: "0.1"
name: getPetById
method: GET
url: "{{baseUrl}}/pets/{{id}}"
assertions:
- { type: status, equals: 200 }
spec:
operation: "GET /pets/{id}"
operationId: getPetById- Path parameters become template variables (
{id}→{{id}}). - The base-URL variable defaults to
baseUrl; override with--base-url-var. - File names are slugified from the
operationId(or theMETHOD pathkey). - Operations with an unsupported HTTP method are skipped and reported.
After gen, flesh out the stubs (real assertions, bodies, auth) and your drift count drops to zero. Agents can do this too — see the truspec_scaffold_from_spec MCP tool.
A complete workflow
Put the three together and your API contract becomes a build gate:
# 1. Bootstrap a collection from the spec.
truspec gen --spec openapi.yaml --out ./api
# 2. Fill in assertions / bodies, then run against a mock or a real API.
truspec mock --spec openapi.yaml &
truspec run ./api --env local
# 3. Gate CI on the contract.
truspec drift --spec openapi.yaml ./api # no new/removed/changed endpoints
truspec coverage --spec openapi.yaml ./api --min 80 # enough of the surface is tested
truspec contract --spec openapi.yaml ./api --env local # responses match the spec's schemasWhen someone adds an endpoint to the spec, drift fails until a request covers it. When someone makes a parameter required, drift flags the now-incomplete request. When the deployed API and the spec disagree, drift --live catches it. When a handler returns a shape the spec doesn't allow, contract catches it. That's the whole point: the collection — and the API behind it — can't silently fall out of sync.
See the CI/CD guide to wire these into GitHub Actions and other pipelines.
See also
- CLI reference — every flag for
drift,coverage,contract, andgen. - File format → Spec link — how requests reference operations.
- Programmatic API → spec — compute drift and coverage from TypeScript.