Part of #160. Depends on #161 (emitter), #162 (CLI), and #163 (extensions).
Goal
Add bin/altair openapi:roundtrip \xe2\x80\x94 a CI-friendly check that imports an OpenAPI 3.1 document via the emitter, re-emits it via spec:emit-openapi, and fails if the result differs from the input beyond documented normalization. Same idea as the existing spec:emit-sdk --check drift gate, applied to the OpenAPI direction.
Why
Without this gate, the import path silently degrades. Someone refactors the emitter, an extension stops round-tripping, the import-then-edit-then-re-emit workflow starts losing data, and no test catches it because each piece is green in isolation. The gate is what makes the import path safe to depend on.
It also pays a forward dividend: every new x-altair-* extension automatically has a meaningful integration test \xe2\x80\x94 if it doesn't round-trip, the gate fails on the fixture that uses it.
Normalization (documented allowable differences)
The gate is not naive diff. The following differences between input and re-emitted output are expected and don't fail the check:
- Key order. Output is alphabetical; input is whatever order the author chose.
- Empty optional arrays.
required: [], tags: [], parameters: [] are omitted in output, may be present in input.
- Tags. The emitter derives tags from path segments; hand-set tags in the input are preserved through
x-altair-tags but raw tags arrays are normalized (warn, don't fail).
- Whitespace + comments. Lost in YAML round-trip; OpenAPI 3.1 doesn't define comment semantics. (Warn, don't fail.)
default: null on nullable schemas. Output omits; input may include. (Strict semantic equality.)
Every normalization rule lands in docs/openapi/roundtrip.md with a code example.
A semantic difference (operation lost, schema property dropped, extension stripped) fails the check loudly with a diff localised by JSON pointer.
CLI surface
bin/altair openapi:roundtrip openapi.yaml # human report
bin/altair openapi:roundtrip openapi.yaml --check # exit 1 on drift (CI mode)
bin/altair openapi:roundtrip openapi.yaml --format=json # structured diff for agents
bin/altair openapi:roundtrip --fixtures=tests/fixtures/openapi/ # batch over a directory
Acceptance criteria
Out of scope
- Auto-fixing drift. The gate reports; the engineer fixes.
- Cross-version OpenAPI checks (3.0 \xe2\x86\x94 3.1 conversion). 3.1 only.
Notes
Mirror the receipt shape from spec:emit-sdk --check so agents that already know how to read one drift gate can read this one without new prompting. Consistency across --check commands is itself a usability feature.
Part of #160. Depends on #161 (emitter), #162 (CLI), and #163 (extensions).
Goal
Add
bin/altair openapi:roundtrip\xe2\x80\x94 a CI-friendly check that imports an OpenAPI 3.1 document via the emitter, re-emits it viaspec:emit-openapi, and fails if the result differs from the input beyond documented normalization. Same idea as the existingspec:emit-sdk --checkdrift gate, applied to the OpenAPI direction.Why
Without this gate, the import path silently degrades. Someone refactors the emitter, an extension stops round-tripping, the import-then-edit-then-re-emit workflow starts losing data, and no test catches it because each piece is green in isolation. The gate is what makes the import path safe to depend on.
It also pays a forward dividend: every new
x-altair-*extension automatically has a meaningful integration test \xe2\x80\x94 if it doesn't round-trip, the gate fails on the fixture that uses it.Normalization (documented allowable differences)
The gate is not naive
diff. The following differences between input and re-emitted output are expected and don't fail the check:required: [],tags: [],parameters: []are omitted in output, may be present in input.x-altair-tagsbut rawtagsarrays are normalized (warn, don't fail).default: nullon nullable schemas. Output omits; input may include. (Strict semantic equality.)Every normalization rule lands in
docs/openapi/roundtrip.mdwith a code example.A semantic difference (operation lost, schema property dropped, extension stripped) fails the check loudly with a diff localised by JSON pointer.
CLI surface
Acceptance criteria
tests/fixtures/openapi/(Petstore-class + each extension exercised)docs/openapi/roundtrip.mdx-altair-*extension that's silently dropped on re-emit is caught by the gateOut of scope
Notes
Mirror the receipt shape from
spec:emit-sdk --checkso agents that already know how to read one drift gate can read this one without new prompting. Consistency across--checkcommands is itself a usability feature.