Skip to content

refactor(rest): compile the non-door getMetaItems request literals against the declared contract (#9805) - #9845

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-9805-getmetaitems-cast-retirement
Aug 19, 2026
Merged

refactor(rest): compile the non-door getMetaItems request literals against the declared contract (#9805)#9845
huangyiirene merged 1 commit into
mainfrom
claude/issue-9805-getmetaitems-cast-retirement

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#9805

Behaviour-preserving type-tightening in packages/rest/src/rest-server.ts: the non-door getMetaItems call sites now compile their request literal against the declared spec contract instead of smuggling it past the compiler with as any.

The value is not tidiness. An un-typed request literal is exactly the blindness class that lets a future key drift silently — restoring compiler coverage over a request that is fully typeable today is the invariant being restored.

The residue, re-derived on the merged ref (not read off the card)

#9741 (PR #9804, merged 2a29caa532) landed after the card was written, so the card's list is a clue. Re-derived at origin/main152bff8fcd. Population: 25 getMetaItems mentions in the file, placed exhaustively:

bucketcountdisposition
comment / prose mentions10not code
typeof … === 'function' feature-detect reads3untouched — see "what survives"
call expressions12
— the #9741 list door (:4022)1⛔ out of scope by ruling — untouched
— already request-typed (:3438, :3479)2no residue — measured below
tightened here9this PR

The three other #9741 doors (getMetaItem / Cached / Layered) are different verbs and sit outside this population entirely. They are untouched.

The nine sites

Each request literal became a named const typed TransportScopedMetaRequest< GetMetaItemsRequest >#9741's envelope, which declares the transport-level environmentId on top of the spec shape — or plain GetMetaItemsRequest at the one site that threads no environmentId (the same split #9741 used for its uncached door).

sitehelperrequest keys
:1332loadObjectItems (API-exposure gate)type, environmentId?
:1715fetchAudienceBooks — also p: anyRestProtocoltype, environmentId?
:4557book-tree book listingtype, packageId?, environmentId?
:4586book-tree doc listingtype, packageId?, environmentId?
:5138doc corpus behind the audience resolvertype, environmentId?
:8031resolveFormBySlug view listingtype, environmentId?
:8095public-form object schematype, environmentId?
:8411public-lookup reference resolutiontype, environmentId?
:8614dataset listingtype, previewDrafts

Every member is declared: type / packageId / organizationId / previewDrafts on GetMetaItemsRequest (the last one is #9741's), plus the envelope's environmentId. packages/spec needed no change — no new or widened declared key was required at any site.

Two card-named sites delivered no diff, and the measurement is why

:3438 (discovery {object} expansion) and :3479 (the api listing) are protocol?.getMetaItems?.({ … }) where protocol is typed RestProtocol | undefined. Their request literals are already compiler-checked — the card read the visible as any there, but that cast is on the result, not the request. There is no request blindness left to retire, and the only remaining any at those sites is the response-shape tolerance described below. Delivering a diff there would remove a visible any while the shape stayed untypeable — the case the dispatch brief said to name rather than do.

Conversely six sites the card did not name (:4557, :4586, :5138, :8031, :8095, :8411) carried the same defect and are fixed here.

What deliberately survives, and why each would be a behaviour change

Documented on the TransportScopedMetaRequest alias so the next reader does not "finish the job":

  • The optional call (getMetaItems?.(…)) and the typeof … === 'function' guards.getMetaItems is a required MetadataProtocol member, so the card is right that these are not feature detection in the type sense — but metaTypeIsLive's own comment records the measured reason they exist: a host may occupy the protocol slot with an object that does not implement the whole surface. Retiring one converts a tolerated absence into a TypeError at runtime.
  • The result handling. The verb is declared to return { type, items } while these sites also tolerate the bare-array shape older hosts and stubs return (metaItemsArray). The response stays runtime-shaped on purpose; only the request is typeable today, and only the request is typed.
  • Genuinely feature-detected server-only verbs (getMetaDiagnostics, listDrafts, migrateStoredMetadata, …) — untouched. Runtime casts are the documented convention there, and tightening one would turn optional capability detection into a hard dependency, silently at compile time.

Zero runtime change. Each edit hoists a literal into a const and drops a type-level cast; outgoing payloads are byte-identical (same keys, same conditional spreads), and as any erases at emit.

Verification — all at head a98b6834e3

  • pnpm --filter @objectstack/rest typecheck (tsc --noEmit) — clean, real exit 0.
  • pnpm --filter @objectstack/rest test (vitest run) — 129 files / 2105 tests passed, real exit 0 read unpiped (a redirect, not a pipe, so no masked teardown code).
  • Reverse verification, direction predicted then observed: red. Injecting bogusKey: 1 into the loadObjectItems literal → TS2353, and the printed type resolves the declared shape out of the rebuilt .d.ts: TransportScopedMetaRequest< { type: string; packageId?: string | undefined; organizationId?: string | undefined; previewDrafts?: boolean | undefined; } >previewDrafts present proves it is reading [finding] Meta-read request schemas still omit previewDrafts / state / environmentId, so the REST call-site casts survive the organizationId catch-up #9741's landed declaration, not a stale build. Injecting previewDrafts: 'yes' into the dataset literal → TS2322: Type 'string' is not assignable to type 'boolean | undefined'. Restored from the commit (never from origin/main), re-typechecked green.
  • Dependency closure built first in the fresh worktree: pnpm --filter '@objectstack/rest^...' build — green.
  • Gates re-derived from the actual diff (node scripts/pm/dispatch-gates.mjs, no hand-fed paths) at the final head: all 11 path-matched families green — check:authz-resolver, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:dispatcher-error-vocabulary, check:objectui-changeset, check:route-envelope, check-adr-0087-registration, check-changeset-no-major, check-cross-package-test-inputs, check-empty-changeset, check-affected-docs. Plus the three the derivation is known not to name: pnpm lint (eslint . --no-inline-config) green, check:slot-lookup green, check:nul-bytes green — and check:meta-type-normalized, which scans this file, green.
  • check:route-envelope is a ratchet on this file and its count is unmoved: no envelope call site is added or removed.

Changeset

@objectstack/restpatch, stated rather than assumed. skip-changeset would be wrong here: that label is for a PR that publishes nothing (tests / workflow / .claude/ only), and this one edits production source in a published package, so the package does republish — even though there is no behaviour, no wire and no public-type change. TransportScopedMetaRequest is a module-local type and fetchAudienceBooks is private, so no exported surface moves.


Generated by Claude Code

…ainst the declared contract (#9805)
Nine getMetaItems call sites in packages/rest/src/rest-server.ts outside the
four meta-read doors passed their request through `as any` (or through a
`p: any` parameter), so the compiler checked nothing about them. Every member
they thread has been expressible in declared types since #9741 landed
`previewDrafts` on GetMetaItemsRequest and the TransportScopedMetaRequest
envelope for the transport-level environmentId.
Each literal is now a named const typed TransportScopedMetaRequest of
GetMetaItemsRequest (or plain GetMetaItemsRequest where the site threads no
environmentId), the same shape #9741 gave the doors.
No behaviour change: the outgoing payloads are byte-identical (same keys, same
conditional spreads). The optional-call spelling, the typeof-function guards
and the runtime-shaped result handling all deliberately survive — retiring any
of them would change behaviour rather than typing — and the envelope alias now
documents why.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeN7F6jQFpcqW2BN56RdPa
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 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): node scripts/docs-audit/affected-docs.mjs --json e77a9b45e4ea3a0a9960446f52f0e6689ce3a9e4packageMentionDocs.

Which tree this was computed on

This run read content/docs from 176e45388c9d17cfe5f8f4a74413154422dca440 — the merge of head a98b6834e33335243131ff7c908a09ba7487e89f into base e77a9b45e4ea3a0a9960446f52f0e6689ce3a9e4, 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 176e45388c9d17cfe5f8f4a74413154422dca440 && git checkout 176e45388c9d17cfe5f8f4a74413154422dca440
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e77a9b45e4ea3a0a9960446f52f0e6689ce3a9e4 a98b6834e33335243131ff7c908a09ba7487e89f && git checkout -B drift-repro e77a9b45e4ea3a0a9960446f52f0e6689ce3a9e4 && git merge --no-ff a98b6834e33335243131ff7c908a09ba7487e89f
node scripts/docs-audit/affected-docs.mjs --json e77a9b45e4ea3a0a9960446f52f0e6689ce3a9e4

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] Non-door getMetaItems helper call sites in rest-server.ts still cast, though the declared surface + transport envelope now cover them

1 participant

@huangyiirene