Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): ship a declared api.projectResolution from the standalone boot path - #12444
Conversation
…lone boot `createStandaloneStack()` returned `projectResolution: 'none'`, a value `RestApiConfigSchema` does not declare, and `os serve` forwarded it unchanged. Nothing caught it because `RestServer` cast its config instead of parsing it. Routing behaviour is unchanged and measured: every reader that acts on the key is gated on `enableProjectScoping` first. The discovery advertisement is NOT — `RestServer` copies the value into `discovery.scoping.resolution` unconditionally, where `DiscoverySchema` declares the same three-member enum, so every `os serve` boot advertised a payload the platform's own schema rejects. Adds the pin that never existed: the CLI's real boot block and the block the factory actually returns are both parsed against the declared schema, each asserting the refusal of `'none'` alongside the acceptance of `'auto'`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 37 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 da58ae89fc32ea5a756715bfabbbe9274cb1b0cd && git checkout da58ae89fc32ea5a756715bfabbbe9274cb1b0cd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8515954fb6dd4102e120b59ce6410f94600d5710 163cb345408ed3fec897b745977e7238f8edc52d && git checkout -B drift-repro 8515954fb6dd4102e120b59ce6410f94600d5710 && git merge --no-ff 163cb345408ed3fec897b745977e7238f8edc52d
node scripts/docs-audit/affected-docs.mjs --json 8515954fb6dd4102e120b59ce6410f94600d5710
|
Uh oh!
There was an error while loading. Please reload this page.
…2450) Removes `projectResolution` from the `.omit()` in `buildDeclaredApiConfigSchema`, so the declared enum is finally executed at that seam. `RestApiConfigSchema` is a non-strict object, so omitting the key did not merely skip a rule: the undeclared strategy arrived as an unknown key, was silently stripped, and took `'auto'`'s branch by fallthrough. Measured red-before / green-after at the real construction. Retires the green `KEEPS` case that defended the omit rather than rewording it. Its premise died when #11999 (PR #12444) migrated the runtime onto the declared `'auto'`, and it could not go red — it called the constructor with a hand-written literal and never read the producer. A reworded version would have kept exactly that property. The refusal and its bound are pinned in packages/rest. The producer coupling that CAN fail on a regression lives at the producer, in packages/runtime/src/standalone-stack.test.ts, which drives the real emitted `api` block through a real RestServer construction: `@objectstack/runtime` depends on `@objectstack/rest`, so the import cannot be written in the other direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
Fixes#11999
Route 2 of the card's two — migrate the runtime to a declared value — taken because the ruling's premise was tested first and held where it mattered. The measurement also found a second declared contract the card did not measure, which makes route 2 strictly stronger than the card frames it.
The premise, measured
I enumerated every reader of
api.projectResolutionin the repo and checked each one's gate:enableProjectScoping?falserest-server.tsregisterRoutesif (enableProjectScoping)elsearm, value never readdirect-mount-composition.ts[versionedBase], value never readdispatcher-plugin.ts(bridge routes)enableProjectScoping && … === 'required'dispatcher-plugin.ts(mountAiRoute)dispatcher-plugin.tslogger.inforest-server.tsdiscovery handlerdiscovery.scoping.resolutionFor routing the premise HOLDS — all four readers that act on the key are gated, so
'none'and'auto'really do produce identical route tables on a standalone host. That is what licenses migrating the value instead of teaching the enum a fourth member.For the advertisement it does not, and that is the user-visible half the card missed. The discovery handler's
scopingblock is attached with noenableProjectScopingguard, andDiscoverySchema(packages/spec/src/api/discovery.zod.ts) declaresscoping.resolutionas the same three-member enum. SoGET /api/v1on everyos serveboot advertised a payload the platform's own schema rejects. Measured, not read:This does not open the fork the ruling reserved.
'none'does not mean anything'auto'does not; it propagates the same unrecognised string into a second declared enum that also lacks it. Route 1 would have to widen twopackages/specenums and give'none'a branch in four readers; route 2 fixes both in one literal.packages/spec/**was not touched.What changed
packages/runtime/src/standalone-stack.ts—StandaloneStackResult['api']declares, and the factory emits,projectResolution: 'auto'. (createDefaultHostConfigdelegates here, so this is the single emit site.)packages/cli/src/utils/merge-boot-config.ts— the doc block that quotes the bootapiobject.packages/cli/src/commands/serve.tswas not touched: it is card #12151's surface, and it needed no edit —apiConfig.projectResolution ?? 'auto'forwards the now-declared value unchanged.The pin that did not exist
The card is right that the missing check is the valuable half. Both sides are now parsed against the declared schema rather than described:
standalone-stack.test.tsparses theapiblock the factory actually returns — not a restatement of it.merge-boot-config.test.tsparses the CLI's boot block, and the merged block thatservereally forwards, plus the discovery field's enum.'none'next to the acceptance of'auto', on the offending path and issue code — a pin that cannot say no is not a pin.BOOT_APIis now typedStandaloneStackResult['api']instead ofas const, so the CLI copy can no longer drift from the producer. Hand-copies are exactly how three packages disagreed here.Reverse verification
Predicted red on both legs before running; ran with a restore trap on absolute paths. Reverting the runtime literal to
'none', confirmed on disk (2 injected / 0 remaining) and confirmed to reachdist/viaablation-dist-preflight(marker present in 2 built files):Restore leg rebuilt and verified too —
git diff HEADempty, andablation-dist-preflight --absentconfirms the marker is gone from all 6 built files, so no later reading measured the mutated tree.Note for reviewers:
packages/runtime'stypecheckprogram does not includestandalone-stack.test.ts(0 hits undertsc --listFiles), so "runtime typecheck clean" says nothing about that file. It is covered by vitest and by leg B instead.packages/cli's program does include its test file (1 hit), which is what makes theBOOT_APItype pin live.Verification — all at
163cb3454Gate families re-derived in this worktree with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackover the real change set (5 paths), not from the dispatch order. All green:check:cross-package-test-inputs·check:test-source-alias·check:type-source-resolution·check:engine-double-contract·check:where-matcher·check:query-options-erasure·check:nul-bytes·check:changeset-gate-self-tests·check:objectui-changeset·check:published-files·check:page-declaration-shape·check:slot-lookup·check:type-check-coverage·check:type-check-debt·check:i18n·check:i18n-coverage·check-empty-changeset·check-changeset-no-major·check-adr-0087-registration·check-ci-filter-parity·check-comment-mask-adoption·check-plugin-teardown-shape·docs-audit/check-affected-docs·docs-audit/check-drift-comment·release-rehearsal-clone --self-testcheck:i18n,check:i18n-coverageandcheck:type-check-debtfirst exited 1 with explicit prerequisite refusals ("Nothing was checked", "cannot run") — not measurements. Re-run green afterturbo run build --filter='./packages/*' --filter='./packages/*/*'.Deliberately not done here
.omit({ projectResolution: true })inrest-server.ts:719is left in place — the card names its removal as the landing note for a different PR, andpackages/rest/src/rest-server.tsis held by PR State SaveReportInput's requirements at the reports.save door #12421.packages/rest/src/rest-config-parse-not-cast.test.ts's case titled "KEEPSprojectResolution: 'none'— the value this platform actually ships" still passes (it constructs directly), but its rationale goes stale the moment this lands: the platform stops shipping'none'. Same follow-up, outside this surface.Generated by Claude Code