Part of #160. Depends on #161 (spec emitter).
Goal
Add bin/altair openapi:import \xe2\x80\x94 a Symfony Console command that reads an OpenAPI 3.1 document, writes Altair YAML specs via the emitter (#161), and optionally chains into spec:scaffold to produce a runnable project. Emits a JSON receipt for agents and records a mutation event + journal entry so the operation is observable and reversible.
Why
The emitter (#161) is the engine. This issue is the headline: "one command, OpenAPI \xe2\x86\x92 working project." Without it, the library piece sits unused; with it, the porting-from-an-existing-API workflow becomes a single command and the tokens-to-ship benchmark gains a credible import-then-scaffold variant.
CLI surface
bin/altair openapi:import openapi.yaml # \xe2\x86\x92 ./api/, specs only
bin/altair openapi:import openapi.yaml --out=specs/
bin/altair openapi:import openapi.yaml --scaffold # chain into spec:scaffold
bin/altair openapi:import openapi.yaml --scaffold --persistence=cycle
bin/altair openapi:import openapi.yaml --scaffold --queue=redis
bin/altair openapi:import openapi.yaml --dry-run # report, write nothing
bin/altair openapi:import openapi.yaml --format=json # structured receipt
bin/altair openapi:import openapi.yaml --force # overwrite existing files
Receipt shape (--format=json)
{
\"ok\": true,
\"input\": \"openapi.yaml\",
\"specs_written\": [
\"api/posts/create.yaml\",
\"api/posts/get.yaml\",
\"api/posts/list.yaml\",
\"api/posts/update.yaml\",
\"api/posts/delete.yaml\"
],
\"scaffolded\": true,
\"scaffold_journal_id\": \"01J5...\",
\"event_id\": \"01J5...\",
\"unmapped\": [],
\"warnings\": []
}
Failures use the same envelope with ok: false, a non-empty unmapped[], and a non-zero exit code. The receipt is byte-stable for the same input \xe2\x80\x94 golden-file-safe for agent acceptance.
Behavior
- Resolves
Altair\Scaffold\Spec\Emitter\Emitter from the container
- Records an
openapi.import event via Altair\Events\Contracts\RecorderInterface (see .altair/events.jsonl)
- When
--scaffold is set: writes a Altair\Scaffold\Journal\Journal entry covering both the imported specs and the downstream scaffolded files, so journal:rewind undoes the whole import in one step
- When
--scaffold --persistence=cycle is set: infers an entity name from each operation's response schema and writes a persistence: block into the emitted YAML before scaffolding
- When
--scaffold --queue=<transport> is set: emits a queue: block for operations that carry the x-altair-queue extension (extension handling lands in the extensions issue; this command just respects the block if it's present)
- Existing files are never clobbered without
--force; conflicts are reported in the receipt
Acceptance criteria
Out of scope
Notes
Honour the project convention: the journal entry + event must be written after the operation succeeds, never speculatively. If --scaffold is requested and the downstream scaffold fails, the imported specs are rolled back (or, if --force was passed and rollback would clobber a hand-edited file, the receipt explains what wasn't undone).
Part of #160. Depends on #161 (spec emitter).
Goal
Add
bin/altair openapi:import\xe2\x80\x94 a Symfony Console command that reads an OpenAPI 3.1 document, writes Altair YAML specs via the emitter (#161), and optionally chains intospec:scaffoldto produce a runnable project. Emits a JSON receipt for agents and records a mutation event + journal entry so the operation is observable and reversible.Why
The emitter (#161) is the engine. This issue is the headline: "one command, OpenAPI \xe2\x86\x92 working project." Without it, the library piece sits unused; with it, the porting-from-an-existing-API workflow becomes a single command and the
tokens-to-shipbenchmark gains a credible import-then-scaffold variant.CLI surface
Receipt shape (
--format=json){ \"ok\": true, \"input\": \"openapi.yaml\", \"specs_written\": [ \"api/posts/create.yaml\", \"api/posts/get.yaml\", \"api/posts/list.yaml\", \"api/posts/update.yaml\", \"api/posts/delete.yaml\" ], \"scaffolded\": true, \"scaffold_journal_id\": \"01J5...\", \"event_id\": \"01J5...\", \"unmapped\": [], \"warnings\": [] }Failures use the same envelope with
ok: false, a non-emptyunmapped[], and a non-zero exit code. The receipt is byte-stable for the same input \xe2\x80\x94 golden-file-safe for agent acceptance.Behavior
Altair\Scaffold\Spec\Emitter\Emitterfrom the containeropenapi.importevent viaAltair\Events\Contracts\RecorderInterface(see.altair/events.jsonl)--scaffoldis set: writes aAltair\Scaffold\Journal\Journalentry covering both the imported specs and the downstream scaffolded files, sojournal:rewindundoes the whole import in one step--scaffold --persistence=cycleis set: infers an entity name from each operation's response schema and writes apersistence:block into the emitted YAML before scaffolding--scaffold --queue=<transport>is set: emits aqueue:block for operations that carry thex-altair-queueextension (extension handling lands in the extensions issue; this command just respects the block if it's present)--force; conflicts are reported in the receiptAcceptance criteria
openapi:import petstore.yamlproduces a directory of spec files that passspec:lintopenapi:import petstore.yaml --scaffoldproduces a project that boots and serves at least one operation end-to-end--dry-runwrites no files and reports a clean plan--format=jsonreceipt is byte-stable for the same input (golden test)--force; conflicts surface in the receiptopenapi.importevent lands in.altair/events.jsonlper run--scaffoldis set, a single journal entry covers the whole operation;journal:rewindundoes both the imported specs and the scaffolded filesopenapi:import --scaffoldagainst a Petstore fixture and asserts files, events, journal, and scaffolded artifactsdocs/openapi/import.mdand follows the per-package templatebenchmarks/exercising the import-then-scaffold pathOut of scope
x-altair-*extension forward/reverse handling beyond "respect blocks if present" \xe2\x80\x94 own issueNotes
Honour the project convention: the journal entry + event must be written after the operation succeeds, never speculatively. If
--scaffoldis requested and the downstream scaffold fails, the imported specs are rolled back (or, if--forcewas passed and rollback would clobber a hand-edited file, the receipt explains what wasn't undone).