11.5.2 The generated v1 client types + their freshness guards — one command, a committed artifact, and a CI diff that cannot be argued with
The pipeline that turns the emitted OpenAPI document into the types and validators packages/cli compiles against — implementing 11.5.1's Q1 and Q2 exactly as that ADR pins them. It ships no client behaviour: nothing calls the new module yet.
What to build
- The generation command — one script, runnable as a package script, that produces the generated module(s) into
packages/cli/src/api/(or wherever the ADR pinned) from the document, with no network and no running server. Per the ADR, the source is the in-repo emitter, not the HTTP route. - The committed artifact, checked in and marked generated in its own header — the file a reader must not hand-edit, saying which command regenerates it.
- Guard A — the artifact is FRESH. Regenerate in CI and fail on any diff. This is the whole reason a committed generated file is trustworthy rather than a snapshot of whenever someone last remembered.
- Guard B — the SERVED spec matches the emitter. Assert
app/api/openapi/v1.json's response body equalsemitOpenApiDocument()'s output, so the public URL external integrators generate from (ADR Amendment 4 Q3) and the bytes this pipeline consumes can never diverge. Cheap: the route isforce-static, takes no input, and the emitter is documented deterministic. - A round-trip smoke assertion — one representative response shape from each of the four shipped resource families (work item, project, sprint, ready row) validates against its generated validator, and a deliberately-corrupted copy fails with an error naming the offending field. This is what proves the pipeline produced something usable, rather than a file that merely exists.
Scope BOUNDARY
Ends at generated artifacts plus their guards. It does NOT touch MotirClient, any command, render.ts, or the hand-written interfaces — those are still the live types when this card lands, and deleting them is 11.5.6's job once nothing reads them. It does NOT add or change any /api/v1 endpoint or schema. It does NOT change the emitter or the spec route — if Guard B fails on landing, that is a real defect to report, not a reason to edit the emitter here.
Acceptance criteria
- One documented command regenerates every generated artifact from a clean checkout with no server running and no network access, and running it twice produces byte-identical output.
- The generated artifact is committed, carries a do-not-edit header naming its regeneration command, and is excluded from lint/format churn the way the repo's other generated files are.
- Guard A fails a CI run in which the emitter's output and the committed artifact disagree — proven by a test that drives the guard with a deliberately stale artifact, not by trusting the script.
- Guard B asserts the served
/api/openapi/v1.jsonbody equals the emitter's output. - A valid sample of each of the four shipped resource families validates; a corrupted copy of each fails with an error identifying the field, asserted per family.
- Every generated type the CLI will consume is exported from one module boundary, so a later card imports from one place rather than reaching into generated internals.
- No file in
packages/cli/srcimports the generated module yet, and the CLI's behaviour is unchanged — asserted by the existing suites passing untouched. - The per-file coverage floor (≥90%) holds on every new hand-written file; the generated artifact itself is excluded from coverage, as generated code is elsewhere in the repo.
Context refs
- 11.5.1 — the ADR this implements; Q1 (generator + validator) and Q2 (location, commit, both guards) are its instructions.
lib/api/v1/openapi/emit.ts—emitOpenApiDocument()and its determinism claim.app/api/openapi/v1.json/route.ts— Guard B's subject; note it is deliberately NOT awithV1Routehandler.lib/api/v1/workItems/schema.ts·projects/schema.ts·sprints/schema.ts·ready/schema.ts— the four families the smoke assertion samples.packages/cli/package.json+packages/cli/tsup.config.ts— where the generation script and any build ordering land.packages/cli/vitest.config.ts— the per-file coverage gate a new file joins, and where a generated-file exclusion goes.- Story: 11.5.