Uh oh!
There was an error while loading. Please reload this page.
fix(rest): say what openapi.json info.version carries, and drop the fallback that contradicted it - #11641
Conversation
… fallback that contradicted it
`GET {basePath}/openapi.json` built `info.version` as
`this.config.api.version || enriched.info.version` under a comment promising
"the runtime version so consumers don't pin to the spec package's compile-time
version". Both halves were false.
The served value has always been `api.version` — the API version identifier
`normalizeConfig()` defaults to 'v1' and `getApiBasePath()` uses to build the
mount. The runtime version never reached the field.
OpenAPI 3.1 defines `info.version` as "the version of the OpenAPI document
(which is distinct from the OpenAPI Specification version or the API
implementation version)", so the runtime version is the one reading the field's
own definition excludes. This is why the field does NOT inherit the ruling that
settled `DiscoverySchema.version`, where "System Identity" means the serving
artifact. The comment is corrected to say what the value is and where the
runtime version is served instead (`/discovery`, `/health`).
The `|| enriched.info.version` fallback is removed. It was reachable rather
than dead: `normalizeConfig` defaults with `??` and `PluginRestApiSchema`
declares a bare `z.string()`, so a configured `api.version: ''` arrives falsy —
measured, that published `@objectstack/spec`'s compile-time version, the exact
value the old comment said the line existed to keep off the wire. A falsy
`api.version` now serves itself.
Four pins in the route's test twin fix the meaning: the declared identifier is
served and differs from the artifact's own version, a custom `api.version` is
tracked, an `OS_RUNTIME_VERSION` stamp does not reach the field, and an empty
`api.version` no longer falls back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 32a6f67350ba54ceb0bbea495d23b4510a27f9fd && git checkout 32a6f67350ba54ceb0bbea495d23b4510a27f9fd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5110f5235e857116e285deef7f2a2d9aebfae41 5728f071a183909e74beab36da444f29bbaaa9bb && git checkout -B drift-repro a5110f5235e857116e285deef7f2a2d9aebfae41 && git merge --no-ff 5728f071a183909e74beab36da444f29bbaaa9bb
node scripts/docs-audit/affected-docs.mjs --json a5110f5235e857116e285deef7f2a2d9aebfae41 |
… fallback
The comments shipped in the previous commit named `PluginRestApiSchema`, which
does not exist in this repo, and said it declares a bare `z.string()` that
permits an empty `api.version`. Both halves are wrong, and on a card about a
comment falsified by the line it introduces they are exactly the wrong thing to
ship.
Measured on origin/main:
- The export at packages/spec/src/api/plugin-rest-api.zod.ts:625 is
`RestApiPluginConfigSchema` (the file name transposed into the symbol). It is
referenced nowhere outside its own file, its own test and the api-surface
manifest, and it is not the type of anything on this path.
- The schema that governs this field is `RestApiConfigSchema`
(packages/spec/src/api/rest-server.zod.ts:48). `RestApiPluginConfig.api` is
typed `RestServerConfig`, `RestServerConfigSchema.api` is
`RestApiConfigSchema.optional()`, and `RestApiConfig = z.input<typeof
RestApiConfigSchema>` is what `normalizeConfig` casts to.
- That schema does NOT permit an empty version. It declares
`version: z.string().regex(/^[a-zA-Z0-9_\-\.]+$/).default('v1')`, and the `+`
quantifier refuses `''`.
So the fallback's reachability has the opposite cause from the one recorded: the
contract forbids `''`, and it arrives anyway because nothing parses the config
against the contract. Both hops in are casts (`config.api as any` in
rest-api-plugin.ts:388, `as Partial<RestApiConfig>` in rest-server.ts), the REST
plugin declares no `configSchema` for the kernel's plugin-config validator, and
the repo's only `RestApiConfigSchema.parse` call parses `{}` in a QA helper. The
regex never executes on a deployment path, so `??` is the only guard and it does
not catch the empty string.
Comments and changeset prose only — no behaviour change, and the removal of the
fallback stands unchanged on either account of why it could fire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqRos-zhuang
commented
Aug 24, 2026
Option A. You are right, my retraction was wrong, and I have re-measured your chain rather than flipping again on assertionAll three links verified at
So the field is governed by You were right not to take it. The standing instruction is to answer a disagreement with a And the correction you made is the one this card demandedOn a card whose entire subject is a comment falsified by the line beneath it, shipping a new
On the second open question — routed, not answeredThe A/B/C question about whether the serve-time override should exist at all is a public-document Reviewing the diff at Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
ACCEPT — review of the diff at |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11546
The fork this card was split out to resolve
Triage routed this as inheriting #11292's ruling "with its reason" — serve the derived
artifact version, because an API-version identifier was standing in for artifact identity.
The card's own filer disagreed in advance: OpenAPI's
info.versionmay legitimately be anAPI version. Measured, the filer is right and the inheritance does not transfer.
OpenAPI 3.1.0, Info Object, verbatim:
The runtime version is the API implementation version — the one value the field's own
normative definition excludes.
resolveDiscoveryVersion()returns theOS_RUNTIME_VERSIONbuild stamp, else the serving package's version: implementation identity, exactly. So
option (1) is ruled out by the specification text, not by preference.
The contrast with #11292 is real rather than cosmetic:
DiscoverySchema.versionsits underSystem Identity — the "what server is this" question, settled by #10993 — so serving the
artifact version there was correct.
info.versionasks a different question, and the answerdoes not carry over one artifact.
Consumer survey (the issue recorded this as not measured): no consumer reads this
document's
info.versionby value — not inobjectstack,objectui,cloud, orcloud-v1. The onev{info.version}render incloud-v1(
apps/cloud/components/api-docs/api-docs-viewer.tsx) reads cloud's owngenerateOpenAPISpec()document, not this one. So no consumer pull exists in eitherdirection, and the served value is left where it is.
What was measured on the line itself
Driving the real
GET {base}/openapi.jsonhandler onorigin/main@50fb191d, perconfiguration:
api.versioninfo.version'v1'(default)/api/v1"v1""17.2.0"/api/v1"v1""17.2.0"'v9'/api/v9"v9""17.2.0"'v1'+apiPath: '/backend/api/v9'/backend/api/v9"v1""17.2.0"'v1',OS_RUNTIME_VERSIONstamped/api/v1"v1"(stamp absent)"17.2.0"''/api/"17.2.0""17.2.0"Both halves of the old comment were false. The runtime version never reached the field, and
the last row is the sharp one: the
|| enriched.info.versionfallback was reachable, notdead, and firing it published the spec package's compile-time version — precisely the value
the comment said the line existed to keep off the wire.
It is reachable for the opposite reason to the one first recorded here — corrected in
5728f071after review, because the first account named a schema that does not exist(
PluginRestApiSchema; the real export atplugin-rest-api.zod.ts:625isRestApiPluginConfigSchema, and it governs nothing on this path). The contract does notpermit an empty version.
RestApiConfigSchema(packages/spec/src/api/rest-server.zod.ts:48)declares
and the
+quantifier refuses''. That is the governing schema:RestApiPluginConfig.apiis typedRestServerConfig,RestServerConfigSchema.apiisRestApiConfigSchema.optional(), andRestApiConfig = z.input<typeof RestApiConfigSchema>iswhat
normalizeConfigcasts to.''arrives anyway because nothing parses this config against that contract. Both hops inare casts —
config.api as any(rest-api-plugin.ts:388) andas Partial<RestApiConfig>(
rest-server.ts:2916) — the REST plugin declares noconfigSchemafor the kernel'splugin-config validator to run, and the repo's only
RestApiConfigSchema.parsecall parses{}in a QA helper (packages/core/src/qa/http-adapter.ts:43). So the regex never executes ona deployment path,
normalizeConfig's??is the only guard left, and it does not catch theempty string.
A declared constraint the server never applies is a defect in its own right — filed as #11637,
not fixed here. The fallback removal stands unchanged on either account of why it could fire.
The change
packages/rest/src/rest-server.ts— the openapiinfoconstruction only:is deliberately not the runtime version (quoting the OpenAPI definition), and where the
runtime version is served instead (
{basePath}/discovery,/health).|| enriched.info.versionremoved. A falsyapi.versionnow serves itself, so amisconfigured deployment reads as misconfigured rather than silently switching this field to
a different kind of fact.
Behaviour delta — declared honestly
Every non-empty
api.version, including the default, serves exactly what it served before.The only value that moves is the degenerate
api.version: ''case: servedinfo.versiongoes"17.2.0"→"". That configuration already mounts the entire API at/api/with a doubledslash (
/api//openapi.json), i.e. it is a broken deployment either way — filed separately as#11637, which this PR does not address and does not depend on.
Clause-②: no. No working configuration's served value changes, so this is not a contractmove;
needs:contract-reviewis not applied. The narrow empty-string delta above is flaggedhere rather than buried, so the call can be revisited if this seat reads it differently.
Pins and reverse verification
Four pins in the route's test twin (
packages/rest/src/rest-openapi-route.test.ts) fix whatthe field means. Reverse verification reverted only
rest-server.tstoorigin/main,carried
trap … EXIT INT TERM, and proved the mutation on disk by grepping both texts(pre-fix fallback 1 / post-fix line 0 / old comment 1 / new comment 0; restore leg: 0 / 1 / 0 / 1).
No rebuild between legs, and the reason is load-bearing rather than an omission: the code under
test is imported relatively (
import { RestServer } from './rest-server'), so vitesttransforms the source directly — no
dist/resolution stands between the mutation and theassertion. The artifact half (
@objectstack/spec'sjson-schema/openapi.json, read from diskby
loadOpenApiSpec()) is untouched by both legs.Predicted before running: pins 1–3 green pre-change, pin 4 red. Observed exactly that —
Tests 1 failed | 16 passed (17), the single failure beingAssertionError: expected '17.2.0' to be '', which is the fallback publishing the compile-timeversion. Pins 1–3 are characterization rather than anti-vacuity, and that is stated plainly:
a comment correction has no behavioural anti-vacuity by construction. Their job is to go red if
the field is ever re-pointed at the runtime version — the direction this card did not take.
Verification — re-run at
5728f071The correction commit is comments and changeset prose only, but the union was re-run on the
new head rather than carried over from
c4f3d2fc: suiteTest Files 141 passed (141)/Tests 2243 passed (2243), typecheck clean,check:nul-bytes,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:plugin-teardown-shape,check:cross-package-test-inputs,check-changeset-no-major,check-empty-changesetandcheck-adr-0087-registrationall EXIT=0, and thecheck:type-check-debtratchet re-measuredgreen after rebuilding the closure (
Tasks: 70 successful, 70 total):check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 242.1s, 1897 raw tsc error(s) total, none above its recorded number.pnpm --filter @objectstack/rest test→Test Files 141 passed (141),Tests 2243 passed (2243)pnpm --filter @objectstack/rest typecheck→tsc --noEmit, cleannode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(change set: the 3 files below, committed 3 / working tree 0 / untracked 0) — all 18
path-matched families plus the convention-triggered ones run green, including
check:type-check-debtafter building the closure it requires(
turbo run build --filter='./packages/*' --filter='./packages/*/*'→ 70/70 successful),since
@objectstack/restsits inTEST_DEBTat a count recorded exactly.Files
packages/rest/src/rest-server.ts— the openapiinfoconstructionpackages/rest/src/rest-openapi-route.test.ts— the four pins.changeset/openapi-info-version-is-the-api-version.mdNeighbours in this package are untouched: #11588 and #10345 / PR #11590 are in other faces of
packages/restand neither touches this line.Generated by Claude Code
Generated by Claude Code