Skip to content

fix(rest): parse api.projectResolution at the construction seam, and retire the green case that defended its omit (#12450) - #12765

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-12450-project-resolution-omit
Aug 27, 2026
Merged

fix(rest): parse api.projectResolution at the construction seam, and retire the green case that defended its omit (#12450)#12765
os-litant merged 1 commit into
mainfrom
claude/issue-12450-project-resolution-omit

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

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 by buildDeclaredApiConfigSchema(), which
until now read RestApiConfigSchema.omit({ requireAuth: true, projectResolution: true }).
projectResolution is out of that omit now; requireAuth stays (its warn-and-ignore
posture is #3963's decision to revisit, and 96 in-repo fixtures still pass the key).

Because RestApiConfigSchema is a non-strict object, omitting the key did not merely
skip 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 time
and 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:

 × [#12450] refuses the retired `projectResolution: "none"` — the parse finally reaches this key
AssertionError: the retired value must be refused, and the refusal must name the key:
expected '' to contain 'api.projectResolution'
Test Files 1 failed (1)
Tests 1 failed | 16 passed (17)

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.version
rationale, 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 KEEPS case is retired, not reworded

The card's own subject, and the reason a title fix would not have been enough:

a test that cannot fail when its premise dies is not protected by the suite — it is
hidden by it. The passing status is what stops anyone looking.

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 the
producer, so when #11999 (PR #12444) migrated @objectstack/runtime off that value, its
premise 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/rest gets the refusal (section A) plus a bound (section C: the three
    spellings 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/runtime gets the half packages/rest cannot see. "No boot path emits the
    retired value" is a claim about the producer, and @objectstack/runtime depends on
    @objectstack/rest — the coupling cannot be written in the other direction without a
    cycle. standalone-stack.test.ts now drives the REAL emitted api block through a REAL
    RestServer construction. That is the case that goes red if the platform ever emits
    an undeclared strategy again; the neighbouring projectResolution: 'none' is shipped by @objectstack/runtime and forwarded by os serve, but RestApiConfigSchema declares only required|optional|auto — accepted only because the schema was never executed #11999 case only proves the value is
    declared, which is a schema pin, not an execution pin.

The file header in rest-config-parse-not-cast.test.ts says all of this in place, including
the 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.ts
is 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 projectResolution value in the tree, tallied from a window scan around all 112
occurrences of the key across 36 files (excluding node_modules and dist):

valueverdict
'auto'declared — still accepted
'required'declared — still accepted
'optional'declared — still accepted (one site)
'none'retired — now refused

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: the
    declared literal type, the emission, and prose recording what it used to be. It emits
    'auto'.
  • standalone-stack.test.ts and packages/cli/src/utils/merge-boot-config.test.ts
    assertions that the retired value is refused, not emissions.
  • changesets and code comments — prose.

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 run153 files / 2540 tests passed
  • pnpm --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 under
    tsconfig.test.json, and --listFiles confirms 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 file
    this PR touches (control: the pattern lists all 9 debt files).
  • pnpm --filter @objectstack/runtime typecheck — clean, but it does not cover the case
    I added there
    . packages/runtime/tsconfig.json excludes test files, so
    --listFiles finds standalone-stack.test.ts0 times while its production sibling
    standalone-stack.ts is present (control, 1 hit). Reported as not-measured rather than
    as a pass. This is a standing ledgered condition, not something this diff introduced:
    check:type-check-coverage reports 18 packages still hiding their tests from tsc, 983
    files, 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 the
change set from git itself rather than from a hand-written list). 25 green. The two that
were not:

  • node scripts/check-adr-0087-registration.mjswas 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: no
    authorable 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 enableProjectScoping switch, so the producer's own
    migration 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.mjsexit 3, which is NOT MEASURED, not red. Its
    own 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 at a525312e7.

Lint — a declared narrowing. The repo-wide pnpm lint was not run here; the affected
files were. Three readings so the narrowing is a measurement rather than a gap:

  1. Population, read from ESLint's own config resolution over 7099 tracked files (not from
    a guess about what counts): 5314 files.

  2. 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, matching
    the root script: 3 files, 0 errors, 0 warnings.

  3. Invariance for untouched files: this repo runs one eslint.config.mjs which never
    enables 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/spec root
    import, reports 1 no-restricted-imports error. 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

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 2 documentable anchor(s).

3 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via RestServer (symbol))
  • content/docs/releases/v12.mdx(via RestServer (symbol))
  • content/docs/releases/v16.mdx(via RestServer (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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 — 13 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 5700d83b3c8b1c9396de96f87611c6f128f26528packageMentionDocs.

Which tree this was computed on

This run read content/docs from 30770f1a5e0c40cd6e5684792ae0454048bf0bba — the merge of head a525312e7b0d6c11a03193238dc5a341f41fa0f4 into base 5700d83b3c8b1c9396de96f87611c6f128f26528, 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 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

⚠️ 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 5700d83b3c8b1c9396de96f87611c6f128f26528 → 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 27, 2026
@os-litant
os-litant marked this pull request as ready for review August 27, 2026 17:16
@os-litant
os-litant added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 8965398Aug 27, 2026
34 checks passed
@os-litant
os-litant deleted the claude/issue-12450-project-resolution-omit branch August 27, 2026 18:14
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