Skip to content

fix(runtime): ship a declared api.projectResolution from the standalone boot path - #12444

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-11999-project-resolution-none
Aug 26, 2026
Merged

fix(runtime): ship a declared api.projectResolution from the standalone boot path#12444
os-litant merged 1 commit into
mainfrom
claude/issue-11999-project-resolution-none

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

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

Premise:enableProjectScoping: false already makes the resolution strategy moot on the standalone path, so 'none' and 'auto' are the same behaviour there.

I enumerated every reader of api.projectResolution in the repo and checked each one's gate:

ReaderGated on enableProjectScoping?With scoping false
rest-server.tsregisterRoutesyes — if (enableProjectScoping)takes the else arm, value never read
direct-mount-composition.tsyes — ternary lives inside the true armmounts [versionedBase], value never read
dispatcher-plugin.ts (bridge routes)yes — enableProjectScoping && … === 'required'short-circuits
dispatcher-plugin.ts (mountAiRoute)yes — same conjunctionshort-circuits
dispatcher-plugin.tslogger.infonologged verbatim (observability only)
rest-server.ts discovery handlernocopied verbatim into discovery.scoping.resolution

For 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 scoping block is attached with no enableProjectScoping guard, and DiscoverySchema (packages/spec/src/api/discovery.zod.ts) declares scoping.resolution as the same three-member enum. So GET /api/v1 on every os serve boot advertised a payload the platform's own schema rejects. Measured, not read:

RestApiConfigSchema {enableProjectScoping:false, projectResolution:'none'} REFUSED path=["projectResolution"] code=invalid_value
…'auto' ACCEPTED
DiscoverySchema.scoping.resolution 'none' REFUSED path=scoping.resolution code=invalid_value
'auto' ACCEPTED

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/spec enums 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.tsStandaloneStackResult['api'] declares, and the factory emits, projectResolution: 'auto'. (createDefaultHostConfig delegates here, so this is the single emit site.)
  • packages/cli/src/utils/merge-boot-config.ts — the doc block that quotes the boot api object.
  • Both sibling tests — the pin the card asks for, below.

packages/cli/src/commands/serve.ts was 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.ts parses the api block the factory actually returns — not a restatement of it.
  • merge-boot-config.test.ts parses the CLI's boot block, and the merged block that serve really forwards, plus the discovery field's enum.
  • Each case asserts the refusal of '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_API is now typed StandaloneStackResult['api'] instead of as 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 reach dist/ via ablation-dist-preflight (marker present in 2 built files):

LEG A packages/cli tsc --noEmit EXIT=1
merge-boot-config.test.ts(21,79): error TS2322: Type '"auto"' is not assignable to type '"none"'.
LEG B packages/runtime vitest EXIT=1 Tests 1 failed | 18 passed code: "invalid_value"

Restore leg rebuilt and verified too — git diff HEAD empty, and ablation-dist-preflight --absent confirms the marker is gone from all 6 built files, so no later reading measured the mutated tree.

Note for reviewers: packages/runtime's typecheck program does not include standalone-stack.test.ts (0 hits under tsc --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 the BOOT_API type pin live.

Verification — all at 163cb3454

pnpm --filter @objectstack/cli exec vitest run src/utils/merge-boot-config.test.ts 11 passed (11)
pnpm --filter @objectstack/runtime exec vitest run src/standalone-stack.test.ts 19 passed (19)
pnpm --filter @objectstack/cli run typecheck clean
pnpm --filter @objectstack/runtime run typecheck clean
pnpm lint (repo-wide, eslint . --no-inline-config) VERDICT command-exit 0

Gate families re-derived in this worktree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack over 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-test

check:i18n, check:i18n-coverage and check:type-check-debt first exited 1 with explicit prerequisite refusals ("Nothing was checked", "cannot run") — not measurements. Re-run green after turbo run build --filter='./packages/*' --filter='./packages/*/*'.

Deliberately not done here


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/runtime, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/cli/src/utils/merge-boot-config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via createStandaloneStack (symbol))
  • content/docs/deployment/single-project-mode.mdx(via createStandaloneStack (symbol))
  • content/docs/plugins/index.mdx(via createStandaloneStack (symbol))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/src/utils/merge-boot-config.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8515954fb6dd4102e120b59ce6410f94600d5710packageMentionDocs.

Which tree this was computed on

This run read content/docs from da58ae89fc32ea5a756715bfabbbe9274cb1b0cd — the merge of head 163cb345408ed3fec897b745977e7238f8edc52d into base 8515954fb6dd4102e120b59ce6410f94600d5710, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8515954fb6dd4102e120b59ce6410f94600d5710 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 26, 2026
@os-litant
os-litant marked this pull request as ready for review August 26, 2026 04:09
@os-litant
os-litant added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit 9981f31Aug 26, 2026
34 checks passed
@os-litant
os-litant deleted the claude/issue-11999-project-resolution-none branch August 26, 2026 04:30
os-litant pushed a commit that referenced this pull request Aug 27, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-litant@claude