Skip to content

feat(spec): declare previewDrafts/state on meta-read requests; record environmentId as transport-level; retire REST door casts (#9741) - #9804

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-9741-preview-drafts-state-declared
Aug 19, 2026
Merged

feat(spec): declare previewDrafts/state on meta-read requests; record environmentId as transport-level; retire REST door casts (#9741)#9804
os-elon merged 2 commits into
mainfrom
claude/issue-9741-preview-drafts-state-declared

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#9741

Implements the maintainer ruling recorded 2026-08-18 on the card (comment 5334871606, accepting the four-prong recommendation's split): declare the two draft-visibility switches where the implementation enforces them, record the environmentId exclusion as a decision, and retire the as any casts at the REST meta-read doors.

What changed

1. Spec: draft-visibility switches declared exactly where enforced (packages/spec/src/api/protocol.zod.ts)

Measured against the inline parameter types in packages/metadata-protocol/src/protocol.ts on current origin/main (11b779e):

VerbInline type on mainDeclared now
getMetaItemspreviewDrafts?: boolean (no state)previewDrafts added
getMetaItemstate?: 'active' | 'draft', previewDrafts?: booleanboth added
getMetaItemCachedneithernothing added
getMetaItemLayeredneithernothing added

Each describe() states that the member is a draft-visibility switch (ADR-0033 draft-overlay preview / the strict draft-buffer read) and that ADR-0106 masking is unaffected — declaration ≠ authorization: the members only select which stored rows are read; draft access stays admin-gated upstream.

2. Spec: environmentId exclusion recorded as a decision

A schema-adjacent comment on GetMetaItemsRequestSchema (the first meta-read request schema a reader meets) names the ruling and the card: environmentId is the transport-level multi-kernel routing key and stays OUT of the protocol request shape by explicit decision — the REST layer resolves the target kernel from it before the protocol call, and the implementation's parameter types never read it off the request.

3. REST: casts retired at the meta-read doors (packages/rest/src/rest-server.ts)

A documented TransportScopedMetaRequest alias (R & { environmentId?: string }) declares the one transport-level member on top of the spec request type, so each door's request literal now compiles against the declared contract — an undeclared key is a compile error at the call site:

  • list door (getMetaItems): as any retired; literal typed TransportScopedMetaRequest of GetMetaItemsRequest.
  • cached single-item door (getMetaItemCached): as any retired; typed with GetMetaItemCachedRequest.
  • uncached single-item door (getMetaItem): as any retired; plain GetMetaItemRequest (this door never threads environmentId).
  • layered door (getMetaItemLayered): carried no cast, but p: any meant its literal was never checked either — the literal is now typed with GetMetaItemLayeredRequest.

No cast had to stay: every member the doors thread is either declared by the spec (type/name/packageId, organizationId per the organizationId catch-up card #9726 — not addressed here, it landed already — state, previewDrafts) or is the transport envelope's environmentId. Zero runtime change: the outgoing payloads are byte-identical (same conditional spreads).

Out of the card's scope and untouched: other getMetaItems helper call sites that feature-detect via (p as any) (e.g. loadObjectItems, fetchAudienceBooks, discovery/dataset listings) — the card names the four meta-read doors only.

4. Tests (packages/spec/src/api/protocol.test.ts, extending the #9726 pin pattern — that card is not addressed here)

  • parse-preservation of previewDrafts (both verbs) and state (both values) — success alone proves nothing on non-strict schemas;
  • continued accept without them (published-world reads stay valid, members stay optional);
  • type rejection: state: 'archived' and non-string-vocabulary values refused; non-boolean previewDrafts refused;
  • negative mirrors: getMetaItems declares no state; cached/layered declare neither switch (parse strips them);
  • environmentId NOT-declared pins, ruling cited: a carried value is stripped by parse on all four read schemas, plus a shape-level pin (Object.keys(schema.shape) contains no environmentId).

5. Regen + changeset

gen:schema / gen:openapi / gen:docs re-run (content/docs/references/api/protocol.mdx +3 lines; authorable-surface/api.json rewritten by the generator as designed). Changeset: spec minor (accept-set widening catch-up), rest patch. No behaviour change anywhere — the wire accepted and honoured these members before; only the declared surface and call-site typing changed.

Verification (all at head 25a3c13)

  • pnpm --filter @objectstack/spec test413 files / 11007 tests passed (re-run at final head); the 18 new [finding] Meta-read request schemas still omit previewDrafts / state / environmentId, so the REST call-site casts survive the organizationId catch-up #9741 pins pass targeted (vitest run src/api/protocol.test.ts -t 9741: 18 passed).
  • pnpm --filter @objectstack/spec typecheck — clean (incl. scripts + test-typecheck ledgers).
  • pnpm --filter @objectstack/rest test128 files / 2096 tests passed; pnpm --filter @objectstack/rest typecheck — clean with the casts retired (the cast-retirement's own proof).
  • Reverse verification (proves the doors read the rebuilt .d.ts, both directions): injecting state: 'archived' into the uncached door literal → TS2322 (not assignable to "draft" | "active" | undefined); injecting bogusKey: 1TS2353 unknown-property error whose printed type shows state?: "draft" | "active" and previewDrafts?: boolean resolved from dist; restored → green.
  • Gate derivation re-run on the actual diff (node scripts/pm/dispatch-gates.mjs, no hand-fed paths): all 26 path-matched + 5 convention-triggered gates run locally and green, including check:cross-package-test-inputs, spec check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs / check:authorable-surface, check:merge-driver, check:spec-parsed-alias, check:type-source-resolution, check:generated (13/13 artifacts in sync), check:docs, check:route-envelope, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, check:type-check-debt --re-measure (33 entries, none above recorded), check-changeset-no-major, check-nul-bytes, check-dev-prereqs.
  • Consumer sweep: full downstream closure build — turbo run build --filter=./packages/* --filter=./packages/*/*, 70/70 packages green (totality covers every consumer of @objectstack/spec).

Generated by Claude Code

…meta-read requests; record environmentId as transport-level (#9741)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run

Coarse fallback — 117 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 b8820208c0979e8132bfca571ac49c669f69e8adpackageMentionDocs.

Which tree this was computed on

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

⚠️ 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/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] Meta-read request schemas still omit previewDrafts / state / environmentId, so the REST call-site casts survive the organizationId catch-up

2 participants

@os-elon@claude