Documenting the rule was not enough — the failure is silent, and by the
time anyone notices, the pull request already reports ~92,000 deletions
and the real change is unreviewable.
spec:check verifies the committed spec against the same canonicalizer
spec:update writes with, so the writer and the gate cannot drift. It reads
only the file on disk — no server, no network, no bun install — so it runs
as a pre-commit hook on openapi.json and as a seconds-long CI job on every
pull request.
It names what went wrong rather than reporting a mismatch: minified,
reordered, unparseable or empty each get their own message and the command
that fixes it. --fix reformats what is committed; it deliberately does not
fetch, so it can never mask an out-of-date spec.
Problem
apps/temps-cli/src/api/is generated from a committed copy of the spec atapps/temps-cli/openapi.json. The server serves that same document minified on one line; the committed file is ~92,000 lines of formatted JSON.So
curl .../openapi.json > openapi.jsonturns 92,000 lines into 1, and the pull request reports ~92,000 deletions. That happened on #564. Pretty-printing alone does not fix it either: key order comes from serde and is not stable between builds, so an unsorted dump reorders large unrelated blocks.#564 established the canonical shape (keys sorted recursively, two-space indent, trailing newline) and documented the rule. This PR makes it enforced, because a documented rule does not help when the failure is silent and only visible as a diff nobody can read.
What this adds
scripts/openapi-canonical.ts— one definition of "canonical", shared by the writer and the checker so they cannot drift.scripts/check-openapi.ts(bun run spec:check) — verifies the committed file. Reads only what is on disk: no server, no network, nobun install.--fixreformats in place; it deliberately never fetches, so it cannot mask an out-of-date spec.openapi-canonical, scoped to^apps/temps-cli/openapi\.json$.OpenAPI Spec Formatinrust-tests.yml— runs in seconds on every PR.update-openapi.tsrewired onto the shared module.--fixvsspec:update.The messages name the failure rather than reporting a mismatch:
Verification
Each case run against the real spec:
673 pathspaths: {}--fixafter minifyinggit diff)Hook wiring confirmed with
prek run openapi-canonical: fires onopenapi.json, skipped for unrelated files, and blocks the commit when the spec is minified.bun run typecheckinapps/temps-clireports the same 4 pre-existing errors asmain— none from these files.Note
This does not check that
src/api/is regenerated from the spec — a different drift, not the one that costs 92,000 lines.