Generates consumer + provider contract tests from an OpenAPI (2.x/3.x) or GraphQL SDL file, and diffs the current spec against the previous one to classify breaking changes (MAJOR/MINOR/PATCH). Emits contract.test.ts + contract-report.md. MAJOR diffs block the release gate. PIPELINE-1 step 4.
An L1 Truth-Validation skill. Its job is to turn an API specification into two things at once:
component-test-writer,
business-rule-validator, and test-data-manager./vibeflow:contract-test-writer <spec path>.release-decision-engine when a spec file changed
in the diff since the last release tag.| Input | Required | Notes |
|---|---|---|
| Spec file |
| yes |
OpenAPI 3.x (preferred), 2.x, or GraphQL SDL. Path arg or .vibeflow/artifacts/api-spec.yaml. |
| Previous spec | optional but preferred | .vibeflow/artifacts/contracts/previous/<name> — if absent, the skill skips breaking-change detection and reports "baseline established" instead. |
scenario-set.md | optional | When present, scenario ids link to the specific operations they exercise (SC → operationId). |
| Consumer registry | optional | .vibeflow/artifacts/contracts/consumers.json — a list of named downstream consumers with their expected operation set. |
| Framework config | derived | Read from repo-fingerprint.json / ci_analyze_structure — reuses the same detection path as component-test-writer (vitest |
Hard preconditions — refuse to run with a single blocks-merge finding rather than producing wrong tests:
$ref must resolve. Dangling refs are a hard
block; we will not silently emit any in their place.operationId. No operationId → no
stable test name → no traceability. Emit a blocker with remediation:
"add operationId to <method> <path>"..json or .yaml/.yml with a top-level openapi: or
swagger: key → OpenAPI (version from the key)..graphql / .graphqls or contains schema { query ... }
at top level → GraphQL SDL.See references/spec-parsers.md for per-format notes (how to resolve
$ref, how allOf/oneOf get normalized, how to walk a GraphQL type
graph without repeating visits).
Flatten the spec into a canonical list of operations:
interface CanonicalOperation {
id: string; // operationId (OpenAPI) or Query/Mutation field name
verb: string; // HTTP verb for OpenAPI; "query"/"mutation" for GraphQL
path: string; // path template or GraphQL root field name
requestSchema: JsonSchema | null;
responseSchemas: Record<string /*status code*/, JsonSchema>;
requiredHeaders: string[];
auth: "none" | "bearer" | "basic" | "custom";
tags: string[]; // for grouping
}
For GraphQL, status codes map to {"200": responseType, "4xx": errorUnion}
so the downstream test shape is identical.
For each operation, emit one describe(operation.id, () => { ... })
block with the following cases:
test-data-manager if it exists; otherwise inline a
minimal example from the spec's example field; fall back to
it.skip with a pending: "awaiting example" note).Each generated it(...) follows the Arrange-Act-Assert shape defined
in ../component-test-writer/references/test-patterns.md. The titles
prefix with the operationId and, when available, the scenario id:
it("SC-112 / getUserById: responds 200 with UserSummary", ...)
If consumers.json is present, for each consumer+operation pair emit
a JSON snapshot under
.vibeflow/artifacts/contracts/consumers/<consumerName>/<operationId>.json
capturing the minimal valid request the consumer MUST be able to send.
The snapshot includes the request body, path params, query params,
and required headers. Consumer-side tests (owned by the consumer
repo) load these snapshots and assert their own request builder can
reproduce them verbatim.
If consumers.json is absent, skip this step — do NOT invent
consumer names.
If a previous spec exists at .vibeflow/artifacts/contracts/previous/<name>:
operationId.references/breaking-change-rules.md.{ finding, why, impact, confidence, evidence }.Classification must come from the rules table — no ad-hoc severity. If a diff shape is not in the table, add it to the table first (and update the harness sentinel); do not silently downgrade to "unknown".
majorBreakingChanges = findings.filter(f.severity == "MAJOR").length
minorChanges = findings.filter(f.severity == "MINOR").length
patchChanges = findings.filter(f.severity == "PATCH").length
Verdict rules:
| Condition | Verdict |
|---|---|
majorBreakingChanges == 0 | APPROVED |
majorBreakingChanges > 0 but every MAJOR has an explicit migration note in the spec (x-vibeflow-migration) | NEEDS_REVISION |
majorBreakingChanges > 0 and at least one lacks a migration note | BLOCKED |
The gate contract is MAJOR breaking changes block the release —
no other condition can produce BLOCKED and no condition can suppress it.
contract.test.ts — next to the spec file. Uses the same
@generated-by banner + @generated-start/@generated-end
markers as component-test-writer, so regeneration preserves any
hand-written additions outside the skill-owned region..vibeflow/reports/contract-report.md — the breaking-change
report (see output contract below)..vibeflow/artifacts/contracts/previous/<name> — copy the
current spec here on successful APPROVED run, so the next
invocation has a fresh baseline. NEVER overwrite the baseline on
a BLOCKED verdict.contract-report.md# Contract Diff Report
## Summary
- Spec: <path>
- Previous baseline: <path or "none — first run">
- Verdict: [APPROVED|NEEDS_REVISION|BLOCKED]
- MAJOR: N
- MINOR: M
- PATCH: K
- Operations evaluated: X
- Operations added: A
- Operations removed: R
## MAJOR (gate-blocking)
For each:
- **finding**: <diff description>
- **why**: <which rule from breaking-change-rules.md>
- **impact**: <who breaks and how>
- **confidence**: <0..1>
- **evidence**: <old line → new line>
- **mitigation**: <concrete migration path; never "rewrite the API">
- **migration note present**: yes | no
## MINOR
<same shape>
## PATCH
<same shape>
## Added operations
- <verb> <path> (operationId: <id>)
## Removed operations
- <verb> <path> (operationId: <id>)
Every finding — MAJOR, MINOR, PATCH, added, removed — MUST carry
finding / why / impact / confidence / evidence. The classification
must cite a rule id from references/breaking-change-rules.md.
Undocumented classifications are forbidden.
release-decision-engine — reads majorBreakingChanges as a hard
blocker signaltraceability-engine — links operationId ↔ test ids ↔ scenario idstest-priority-engine — uses the list of changed operations to
rank affected tests for the next test run