Uh oh!
There was an error while loading. Please reload this page.
fix(rest): parse api.projectResolution at the construction seam, and retire the green case that defended its omit (#12450) - #12765
Conversation
…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
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 3 release-owned page(s) name something this change touched. These are read-only:
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 30770f1a5e0c40cd6e5684792ae0454048bf0bba && git checkout 30770f1a5e0c40cd6e5684792ae0454048bf0bba
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5700d83b3c8b1c9396de96f87611c6f128f26528 a525312e7b0d6c11a03193238dc5a341f41fa0f4 && git checkout -B drift-repro 5700d83b3c8b1c9396de96f87611c6f128f26528 && git merge --no-ff a525312e7b0d6c11a03193238dc5a341f41fa0f4
node scripts/docs-audit/affected-docs.mjs --json 5700d83b3c8b1c9396de96f87611c6f128f26528
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12450
Both halves of the card in one diff, because either alone strands the other: removing the
omit without rewriting the test leaves a green case defending an exemption that is gone,
and rewriting the test without removing the omit leaves the omit with nothing explaining
it.
(a) The key is finally parsed at that seam
RestServer's declared-config parse is built bybuildDeclaredApiConfigSchema(), whichuntil now read
RestApiConfigSchema.omit({ requireAuth: true, projectResolution: true }).projectResolutionis out of that omit now;requireAuthstays (its warn-and-ignoreposture is #3963's decision to revisit, and 96 in-repo fixtures still pass the key).
Because
RestApiConfigSchemais a non-strict object, omitting the key did not merelyskip a rule — it made an undeclared strategy an unknown key, silently stripped, which
then took the
'auto'branch by fallthrough. The declaration was present the whole timeand never executed. That is the same defect #11637 is named after, and why "the schema
declares it" was not evidence here.
Red before fix, green after — measured, not asserted. With the new case in place and
the source untouched:
The empty string is the finding: nothing threw. The seam accepted the value. After the
omit removal the same case passes, and the whole package is green (below).
That empty-string failure is also the positive control doing its job. The case's second
assertion is a negative one (a refusal about this key must not print the
api.versionrationale, which the original ablation measured it doing). A negative assertion over an
empty message passes vacuously, so the case proves the refusal is present before it
measures its shape.
(b) The
KEEPScase is retired, not rewordedThe card's own subject, and the reason a title fix would not have been enough:
The old case asserted that this seam KEEPS the retired value, "the value this platform
actually ships". It called
construct()with a hand-written literal and never read theproducer, so when #11999 (PR #12444) migrated
@objectstack/runtimeoff that value, itspremise died and it stayed green. Rewording it into "asserts the refusal" would have
produced a case with the same property: green whether or not the platform still emits the
value, merely worded correctly this time.
So the rewrite is split by what each half can actually see:
packages/restgets the refusal (section A) plus a bound (section C: the threespellings the enum declares all still construct, read back off the normalized config
rather than off a mount path). Their premise is "this seam parses this key" — put the
omit back and both go red. That premise is live and local.
packages/runtimegets the halfpackages/restcannot see. "No boot path emits theretired value" is a claim about the producer, and
@objectstack/runtimedepends on@objectstack/rest— the coupling cannot be written in the other direction without acycle.
standalone-stack.test.tsnow drives the REAL emittedapiblock through a REALRestServerconstruction. That is the case that goes red if the platform ever emitsan undeclared strategy again; the neighbouring
projectResolution: 'none'is shipped by@objectstack/runtimeand forwarded byos serve, butRestApiConfigSchemadeclares onlyrequired|optional|auto— accepted only because the schema was never executed #11999 case only proves the value isdeclared, which is a schema pin, not an execution pin.
The file header in
rest-config-parse-not-cast.test.tssays all of this in place, includingthe pointer to where the coupling lives, so the next reader is not left believing that file
covers it.
Declaring the third file
The card names two files; this diff touches three.
packages/runtime/src/standalone-stack.test.tsis a bounded in-place addition: same defect class as this card (declared-but-not-executed),
mechanically implied by the ruling that the rewritten case must be able to fail, held by no
other open PR (checked below), and inside the same gate family. It adds one case and one
top-level import — no production code in that package changes.
The census — exactly one value starts being rejected
Every
projectResolutionvalue in the tree, tallied from a window scan around all 112occurrences of the key across 36 files (excluding
node_modulesanddist):'auto''required''optional''none'No fourth value. The retired value survives in exactly three kinds of place, all of them
checked one by one rather than by grep, because grep cannot tell an emission from an
assertion that there is no longer an emission:
packages/runtime/src/standalone-stack.ts— three occurrences, all migrated: thedeclared literal type, the emission, and prose recording what it used to be. It emits
'auto'.standalone-stack.test.tsandpackages/cli/src/utils/merge-boot-config.test.ts—assertions that the retired value is refused, not emissions.
Nothing in the tree hands the retired value to a REST server any more except the test case
this PR retires.
Verification
All readings below are on
a525312e7, the final commit, with a clean worktree.Tests
pnpm --filter @objectstack/rest exec vitest run— 153 files / 2540 tests passedpnpm --filter @objectstack/runtime exec vitest run src/standalone-stack.test.ts—20 passed (19 before, plus the new coupling case)
Typecheck — and one honest NOT MEASURED
pnpm --filter @objectstack/rest typecheck— OK. It runs the test layer undertsconfig.test.json, and--listFilesconfirms the edited test file is in that program(1 hit; control:
rest-server.ts, 1 hit). The run's 20 errors are exactly the 9 files /20 errors already frozen in
test-typecheck-debt.json; zero of them name either filethis PR touches (control: the pattern lists all 9 debt files).
pnpm --filter @objectstack/runtime typecheck— clean, but it does not cover the caseI added there.
packages/runtime/tsconfig.jsonexcludes test files, so--listFilesfindsstandalone-stack.test.ts0 times while its production siblingstandalone-stack.tsis present (control, 1 hit). Reported as not-measured rather thanas a pass. This is a standing ledgered condition, not something this diff introduced:
check:type-check-coveragereports 18 packages still hiding their tests from tsc, 983files, 1306 frozen errors in TEST_DEBT. The case is executed by vitest and is green
there; it is only its types that nothing local checks.
Gates — 27 families, derived for these exact paths by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(which takes thechange set from git itself rather than from a hand-written list). 25 green. The two that
were not:
node scripts/check-adr-0087-registration.mjs— was a real red, and is now green.The changeset declares a breaking change and carried no ADR-0087 disposition. It now
carries
not-required (no-migration-prescription)with the argument written out: noauthorable surface is removed or renamed, the schema is untouched, and there is no
previously-legal spelling for a conversion entry to rewrite — the retired value was
never declared. It could not be decided mechanically either: the value read as "no
scoping at all", which is the
enableProjectScopingswitch, so the producer's ownmigration rests on an analysis that only holds with scoping OFF; a config that wrote it
with scoping ON states an intent nothing can pick for the author. Worth recording that
this gate reads changesets from git objects, not the worktree, so an uncommitted
marker looks exactly like no marker.
node scripts/pm/check-half-states.mjs— exit 3, which is NOT MEASURED, not red. Itsown output: "Nothing was swept... It is not a clean board and it is not a dirty one — it
is no reading at all." It needs a GitHub credential this container does not have. The
self-test half (
pnpm check:pm-half-states) is green.Every ratchet family was re-run on the final commit after the last edit, since a late
commit is exactly what makes a ratchet reading stale:
query-options-erasure,slot-lookup,where-matcher,engine-double-contract,published-files,cross-package-test-inputs,test-source-alias,type-check-coverage,objectui-changeset,check-adr-0087-registration— all exit 0 ata525312e7.Lint — a declared narrowing. The repo-wide
pnpm lintwas not run here; the affectedfiles were. Three readings so the narrowing is a measurement rather than a gap:
Population, read from ESLint's own config resolution over 7099 tracked files (not from
a guess about what counts): 5314 files.
The narrowed run: of this diff's 4 files, the changeset is ignored by that same config
and the other 3 are in the population. Linted with
allowInlineConfig: false, matchingthe root script: 3 files, 0 errors, 0 warnings.
Invariance for untouched files: this repo runs one
eslint.config.mjswhich neverenables type-aware linting — no
parserOptions.project, no typed rules, for any file— so nothing in this diff can move another file's verdict. That statement is the
config's own, and was itself measured there with a positive control.
The narrowed run carries a positive control too, because a zero from a lint run is worth
nothing without one: the same ESLint instance, given a planted
@objectstack/specrootimport, reports 1
no-restricted-importserror. It reports; it is not merely silent.CI runs the full farm regardless, and that is where the repo-wide lint is owed.
A false zero I hit myself, recorded because it is this card's lesson
The card's own re-check pattern returns zero (the real title has a backtick between the
words), so every zero here carries a positive control that is not a substring of the term
under test. One of mine came back 1 instead of 0: my check for "no case title still
carries the retired KEEPS phrase" matched the header docblock, where I quote that title on
purpose while explaining what was retired. Narrowing the pattern to executable case titles
gives 0, with two controls firing (1 other KEEPS title, 17 titles total). The check was
wrong, not the edit — but only running the control made that visible.
Generated by Claude Code