Skip to content

feat(spec): GetMetaItemsRequestSchema declares names, the batch by-names read of a /meta/:type list - #29722

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-0neu2n
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-0neu2n

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29441

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — this PR declares the protocol member only, and the generated reference (content/docs/references/api/protocol.mdx) teaches it; the wire parameter ?names= is not served until the server PR on #29236, which owes the GET /meta/:type?names= teaching (content/docs/api/metadata-api.mdx, skills/objectstack-api/SKILL.md)

维护者速读

  • 改了什么:GetMetaItemsRequestSchema 新增可选的 names(条目名列表),让 /meta/:type 列表一次只取指定的几个条目;HTTP 上写作一个逗号分隔值 ?names=a,b,c。
  • 为什么改:执行 Metadata API: no batch by-names read, so the Console roster gate costs one /meta/object request per name #29236 裁决 A(6580261740,维护者「同意」),裁决要求先落 spec 这一半;服务端 PR 被本卡阻塞。调用方今天要么读整类、要么逐个按名读。
  • 风险与回滚:只加一个可选成员,原来能解析的请求照样解析;空列表在 schema 层就拒收(JSON Schema 的 minItems: 1),不会被当成「全部」。服务端未落地前,带 names 的请求仍按不带处理。回滚即还原本 PR。
  • 先看:packages/spec/src/api/protocol.zod.ts(GetMetaItemsRequestSchema 的 names 成员与上方 docblock)、packages/spec/src/api/protocol.test.ts(新 describe)、.changeset/29441-meta-items-names.md。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/protocol.test.ts -t 'batch by-names',应见 7 个用例全绿;把 .min(1, …) 改成 .min(0, …) 再跑,空列表与 JSON Schema 两个用例转红。
  • 需要你做的:无,席位落地。

What changes

packages/spec/src/api/protocol.zod.ts, GetMetaItemsRequestSchema (measured at ≈:242 on origin/main 744211fc): a new optional member between packageId and previewDrafts.

names: z.array(z.string())
  .min(1, 'Name at least one item, or leave `names` out to list every item')
  .optional()
  .describe(…)

The describe carries the rule only: only the named items of the type are answered, among those the list would answer without it; over HTTP it is one comma-separated value (?names=a,b,c); a name that matches no item is simply absent from the answer (no 404); an empty value is refused with 400 rather than read as every item, so leave the key out to list every item. No tracker number is in the describe. The schema's docblock carries the rationale: the ruling, why the member is a list while the wire is one comma string, the push-down to the store read, and why the minimum.

The member's type. The request schema is the protocol's post-parse shape, so names is a list and the REST door splits the comma string. Measured sibling: GetDataRequestSchema.select in the same file is the post-parse list of the data route's comma-separated select and is typed as a string list (transportProjectionArray(z.string())); GetMetaItemRequestSchema.name is a bare z.string(), so the members are z.string() too (no name grammar: a name outside it is a name that matches no item, which is absent, not refused).

The empty-list choice: the schema refuses []. The order asked to follow what the ?id= sibling does in spec. Measured: ?id= has no spec member at all; its empty refusal lives only at the REST door (refuseEmptyListFilter, packages/rest/src/meta-item-read-gate.ts ≈:3483), so it answers nothing about a list member. The siblings the ruling names for the spelling (fields, searchFields) do declare it: searchFieldsArray() (packages/spec/src/data/search-fields-names-a-field.ts) is z.array(z.string()).min(1, …), refusing only the empty list and judging no member, and transportProjectionArray refuses [] for fields / select. Both say why: absent means "all", so an empty list read as absent is a widening. names follows searchFieldsArray exactly: .min(1), no member check. It is a minimum, not a refinement, so the published JSON Schema states it (minItems: 1) and dropped-refinements.baseline.json does not move. The door's 400 for ?names= stays the server PR's (it is in the ruling's server workload).

Generated artifacts. pnpm --filter @objectstack/spec build then check:generated named one stale artifact, content/docs/references/**; gen:docs regenerated it (one table row in content/docs/references/api/protocol.mdx). The build's gen:schema added api/GetMetaItemsRequest:names to packages/spec/authorable-surface/api.json. api-surface/, export-origins/ and declaration-map/ did not move: no export is added, removed or renamed (GetMetaItemsRequestSchema and GetMetaItemsRequest exist already). check:generated then reported all 14 artifacts current.

The declaration lines differ from the card's Metadata-protocol: none. Measured: scripts/check-changeset-no-major.mjs refuses none with any minor entry (verdict none-minor) and refuses none whenever an authorable-surface/ shard moves (verdict none-baseline-moved). This diff moves authorable-surface/api.json, so none cannot pass at any level, and the ruling's "spec minor" needs a kind. The two PRs that last moved that shard with an api key, #29521 (45b4922d) and #29100 (f1d795b1), declared Clause-②: yes (widening) + Metadata-protocol: added + @objectstack/spec minor. This PR declares the same. Seat 2 agreed and amended the claim on #29441. The Clause-②: line keeps the card's (widening) arm.

Changeset. .changeset/29441-meta-items-names.md: @objectstack/spec minor, both declaration lines at line start. It says what the member answers, the wire spelling, the empty refusal, and that until a release carries the server change a request carrying names is answered as if it were absent.

Tests

packages/spec/src/api/protocol.test.ts gains describe('GetMetaItemsRequestSchema declares `names` — the batch by-names list read'), beside the file's other meta-read request blocks, with 7 cases:

  • accepts names and preserves the list through parse (the parsed value is asserted, since the object is non-strict and a stripped key also reads success);
  • the absent-names control: a request without it parses and parse invents no names;
  • refuses [] with one too_small issue at path ['names'];
  • judges no member: ['no_such_app'], [''] and ['crm', 'crm'] parse and keep their value;
  • refuses the wire spelling ('crm,helpdesk') and a non-string member;
  • z.toJSONSchema states names as { type: 'array', minItems: 1, items: { type: 'string' } } and not required;
  • a type-level pin: GetMetaItemsRequest['names'] is string[] | undefined (input and output alike, which also keeps Iso_api_protocol__GetMetaItemsRequestSchema in type-alias-convention.pin.test.ts true).
Runs

All under scripts/pm/os-verify-lock.sh (shared box; wall-clock figures are not idle-box figures).

  • On the final head 064d4e01d (after the origin/main merge): pnpm --filter @objectstack/spec test: Test Files 803 passed (803), Tests 23793 passed | 1 todo (23794). pnpm --filter @objectstack/spec typecheck: exit 0 (tsc --noEmit, check:scripts-typecheck, check:test-typecheck OK). pnpm --filter @objectstack/spec test:repo: Test Files 89 passed (89), Tests 1522 passed (1522).
  • On the pre-merge head c968b47f: spec test 802/802 files, 23739 tests passed; typecheck exit 0; test:repo 89/89 files, 1522 tests passed.
Ablation (one, on the tested behaviour)

Committed head c968b47f, in a separate detached worktree so the gate run was not disturbed. Spec suites import the schema by relative src path, so no build or dist preflight is involved.

  • Control leg (unmutated): vitest run src/api/protocol.test.ts -t 'batch by-names': Tests 7 passed | 230 skipped (237).
  • Mutation via node scripts/ablation-replace.mjs: anchor .min(1, 'Name at least one item, or leave `names` out to list every item') → .min(0, 'ABLATED-29441'); the tool reports anchor x1 -> x0, replace x0 -> x1, blob e8475ac80907 -> e261fbcb5c77.
  • Result: Tests 2 failed | 5 passed. Failed: "refuses an EMPTY list, at names" and "states the minimum in the published JSON Schema" (minItems: 0 received). Expected direction: red.
  • Restore: blob after restore e8475ac80907 = HEAD blob, git diff HEAD empty, marker count 0.

Acceptance notes

  • Declared before enforced, by the ruling's order: until the server PR on Metadata API: no batch by-names read, so the Console roster gate costs one /meta/object request per name #29236 lands, getMetaItems in @objectstack/metadata-protocol (inline parameter type { type; packageId?; previewDrafts? }, protocol-meta-list-reads.ts:54) does not read names, and the /meta/:type list door has no closed query set yet, so ?names= is ignored and the full list is answered. The ruling puts the push-down, the door's empty refusal and the closed set in that PR.
  • The schema judges no member. Whether the door refuses a blank segment such as ?names=a,,b is not ruled; this PR neither refuses nor promises it, and the docblock says only that a name matching no item is absent.
  • #29415 (seat 2, same file, the cascadeDeleteObjects describes ≈:646/:767/:2480/:2501) touches a different region; the later lander merges origin/main and regenerates.

Verification

Local subset, on the final head 064d4e01d (merge of origin/main 3d5933a97); the PR's check-runs carry everything CI runs.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 102 derived families (5 changed paths vs merge base 3d5933a97), the same 102 as on the pre-merge head. Run serially, each exit code captured before any pipe: 102 exit 0. dispatch-gates --ran: "102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN".
  • On the pre-merge head c968b47f the same 102 ran: 96 exit 0 first pass, 6 exit 3 (PREREQUISITE NOT MET: unbuilt @objectstack/lint / @objectstack/formula dist and the dist-reading gates), all 6 exit 0 after a full turbo run build (117/117); --ran reconciled 102/102.
  • GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event (this body): exit 0, "LEVEL AXIS: this PR declares clause-② yes (widening) and metadata-protocol added". Control leg, the same body with Metadata-protocol: none: exit 1, none-baseline-moved ("Moved: packages/spec/authorable-surface/api.json").
  • pnpm --filter @objectstack/spec check:generated after the build of the merged head: "All 14 generated artifacts are up to date".
  • node scripts/check-closing-keyword-parity.mjs --body (this body): its report mode (exit 2) names one closing declaration, Fixes #29441.
  • pnpm check:dev-prereqs (local-only family): exit 0.
  • No export was renamed or removed, so check-platform-checklist --changed-from is not owed.

Generated by Claude Code

…mes list read

An optional list of item names narrows a /meta/:type list to the named
items. An unknown name is simply absent; an empty list is refused rather
than read as every item. On the wire it is one comma-separated value
(?names=a,b,c), split by the REST door.

Claude-Session: https://claude.ai/code/session_01WGufzik1dDv4DzrV1fWBV9
Co-Authored-By: Claude <noreply@anthropic.com>
…rd for names; changeset

Claude-Session: https://claude.ai/code/session_01WGufzik1dDv4DzrV1fWBV9
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Nov 3, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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
  • the SDK route bridge reached 61 of 218 client-bound route-ledger rows — the other 157 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 157: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 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 c76c0e042ea1efc9ccb7b209a82dd8f0fc119953 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 35e7cae43d2805a73ad8ad8f61e0c249961c3136 — the merge of head bb07931cb445ee6b0d4fc308b359dd87f9766d31 into base c76c0e042ea1efc9ccb7b209a82dd8f0fc119953, 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 35e7cae43d2805a73ad8ad8f61e0c249961c3136 && git checkout 35e7cae43d2805a73ad8ad8f61e0c249961c3136
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c76c0e042ea1efc9ccb7b209a82dd8f0fc119953 bb07931cb445ee6b0d4fc308b359dd87f9766d31 && git checkout -B drift-repro c76c0e042ea1efc9ccb7b209a82dd8f0fc119953 && git merge --no-ff bb07931cb445ee6b0d4fc308b359dd87f9766d31

node scripts/docs-audit/affected-docs.mjs --json c76c0e042ea1efc9ccb7b209a82dd8f0fc119953

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 064d4e01dc713cb2df6c30f60acf97c0a1cb6866
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/spec/src/data/search-fields-names-a-field.ts, packages/spec/src/data/projection-names-a-field.ts, packages/rest/src/meta-item-read-gate.ts (META_QUERY_ARITY_DOORS.list, refuseEmptyListFilter, the ?id= / ?object= steps of createMetaListAnswer), packages/metadata-protocol/src/protocol-meta-list-reads.ts and packages/metadata-protocol/src/protocol.ts (getMetaItems, readFlattenedMetaItems), packages/runtime/src/domains/meta.ts (the dispatcher's getMetaItems call), packages/client/src/index.ts (meta.getItems) — all read on origin/main 3d5933a9, the merge base, as they stand

Inputs: card #29441 (body, 6582535222, 6592535841, 6592613238, 6594197602), ruling A on #29236 (6580261740), PR #29722 body, file list and the net diff against main at the head (5 files, +107/-0), and the head's check-runs. Not the dispatch order and not the dispatching seat's conclusions.

① Derived judgments

Every accept-set and public-surface change the diff implies, judged against ruling A:

  1. GetMetaItemsRequestSchema gains an optional member names — right. The ruling's spec workload is exactly GetMetaItemsRequestSchema.names; the member is optional, so every request that parsed on main parses unchanged (widening only). Verified on the tree: packages/spec/src/api/protocol.zod.ts ≈:264, between packageId and previewDrafts.
  2. Element shape z.string(), no name grammar, no blank-member refusal — right. The ruling says an unknown name is simply absent, no 404; a member the schema judged would turn "absent" into "refused". It follows searchFieldsArray() (z.array(z.string()).min(1, …), search-fields-names-a-field.ts), the sibling the ruling names for the spelling, rather than transportProjectionArray (which refuses a blank member with a regex): on the list door a blank name is a name that matches no item, which the ruling says is absent. GetMetaItemRequestSchema.name is a bare z.string() too.
  3. .min(1) — an empty list refused at the schema, as a minimum, not a refinement — right. The ruling: "An empty value is refused 400 like ?id=". ?id= has no spec member (its refusal is the door's refuseEmptyListFilter, meta-item-read-gate.ts ≈:3497, step 3 of createMetaListAnswer ≈:3722), so it decides nothing about a list member; the spec siblings that ARE list members (searchFieldsArray, transportProjectionArray) both refuse [] for the reason the ruling gives (absent means "every item", so an empty filter read as absent is a widening). Published as minItems: 1 in the JSON Schema, asserted by the test that calls z.toJSONSchema with io: 'input'; authorable-surface/api.json gains api/GetMetaItemsRequest:names and no other shard moves. The 400 itself stays the door's, in the server PR, as the ruling orders.
  4. Post-parse shape is a list, the wire is one comma-separated value split by the REST door — right. The ruling fixes the wire spelling to the data routes' comma convention; the list arity door (META_QUERY_ARITY_DOORS.list) refuses a repeated key, so the protocol member cannot be the wire's string and must be the split list. The test that refuses 'crm,helpdesk' at the schema pins that the comma string is the door's, not the protocol's.
  5. Push-down: the member sits on the request type MetadataProtocol.getMetaItems(request: GetMetaItemsRequest) consumes (protocol.zod.ts ≈:4455 on the head), so the server PR can carry it into readFlattenedMetaItems — right; the ruling names GetMetaItemsRequest.names as the push-down carrier.
  6. GetMetaItemsRequest['names'] is string[] | undefined, input and output alike — right; Iso_api_protocol__GetMetaItemsRequestSchema in type-alias-convention.pin.test.ts stays true, and the implementation class's inline parameter type { type; packageId?; previewDrafts? } (metadata-protocol/src/protocol.ts ≈:3977) still accepts the widened request (an extra optional member is assignable), so nothing on main is broken by the widening.
  7. No export added, removed or renamed; api-surface/, export-origins/, declaration-map/ unmoved — right, read off the diff: the schema and the type existed already.

② Semver level

.changeset/29441-meta-items-names.md: @objectstack/spec: minor, Clause-②: yes (widening), Metadata-protocol: added; the PR body carries the same two lines at line start. Matches what the diff publishes. Measured against the gate and the precedents, not the body's account:

  • The diff moves packages/spec/authorable-surface/api.json (one key, api/GetMetaItemsRequest:names). scripts/check-changeset-no-major.mjs (verdict table ≈:1876) refuses Metadata-protocol: none whenever an authorable-surface/ shard moves (none-baseline-moved) and refuses none beside any minor entry (none-minor). The ruling's "spec minor" therefore cannot coexist with the card's none; the card's and the first claim's none was gate-impossible, and the seat amended the claim (6592613238) before the PR opened.
  • added is the kind AGENTS.md assigns a newly accepted key, and added beside Clause-②: yes (widening) is the gate's consistent pairing. Precedents verified on origin/main: feat(spec): the package export manifest declares withheldMaps, classified door for the unmanaged import #29521 (45b4922d, .changeset/29373-export-withheld-maps.md) and feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) #29100 (f1d795b1, .changeset/28914-package-export-bundle.md) each moved the same api.json shard with one API key and declared @objectstack/spec: minor + Clause-②: yes (widening) + Metadata-protocol: added. This PR declares the same triple.
  • Check Changeset on the head: success. The Clause-② arm is (widening): no accept set narrows (every main request still parses), so no BREAKING banner and no ADR-0087 disposition are owed.
  • One carried note for the server seat (from the dev report's out-of-scope finding, confirmed against the gate): the ruling's server workload reads "rest / metadata-protocol / client minor" beside Metadata-protocol: none; the gate refuses that pairing (none-minor), so the server PR must declare a kind (added, by the same precedent) or ship patch entries. Escalated, not this PR's to fix.

③ Boundary flags

Dev report 6594197602 on #29441: open_questions: []; six deviations, one out_of_scope_findings, three acceptance notes in the PR body. Each answered:

  • Deviation 1, Metadata-protocol: added against the card's and the ruling's none — accepted, gate-forced and precedent-backed (② above); the seat's amended claim 6592613238 is the record of agreement.
  • Deviation 2, Clause-②: yes (widening) on body and changeset while the claim spells bare Clause-②: yes — accepted; the same semver reading, and the ruled (widening) arm is the one the PR carries.
  • Deviation 3, pr_create set the assignee os-support-ai and requested a reviewer — accepted; AGENTS.md says the PR's assignee mirrors the card's, set by the executor at pr_create; the harness line in os-dev.md yields to AGENTS.md.
  • Deviation 4, the attribution line links the card thread, not the seat thread — accepted; the harness's required block, attribution only.
  • Deviation 5, origin/main merged before opening, PR not subscribed — accepted; the merge keeps the net diff at the 5 files reviewed here; CI convergence is read below, not from a subscription.
  • Deviation 6, api-surface/ did not move — accepted and verified on the diff (no export change).
  • Out-of-scope finding (server PR's none vs minor) — escalated to domain:server#5 in ② above; not filed as a card, since it is a declaration the server PR's own gate will refuse at its first push.
  • Acceptance note "declared before enforced" — accepted with a should-fix (④ item 1): the ruling orders the spec half first and the server PR is Blocked-by this card, so the lag is ruled, but the published describe says nothing of it.
  • Acceptance note "blank segment ?names=a,,b unruled" — accepted; the schema keeps [''] (a name matching no item, absent by the ruling), and whether the door trims or refuses a blank segment is the server PR's to decide and teach; the schema's .min(1) backs either choice (a door that drops blanks and arrives at [] meets the ruled empty refusal).
  • Acceptance note on spec(migrations): register the user-field deleteBehavior enforcement (#29253) as an ADR-0087 semantic entry and stop the spec prose saying a user field never joins the delete walk #29415 (same file, different region) — accepted; ordinary concurrency, the later lander regenerates.
  • Card-level: the dispatch tier line in the claim (model: TIER_DEFAULT) governs the executor; this record is the contract-tier review the schema face owes, served here.

Gate verdicts, read off the head's check-runs at 2026-11-03T03:03Z: 33 success, 8 skipped (the path-filtered UI and opt-in jobs), 0 failure. Every gate-carrying job that had concluded is green — Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset, Build Core, Type Check · workspace, Type Check · consumer gates, Live E2E UI, Build & E2E UI, Test Core (1/8), (6/8), (8/8). Still in progress at that reading: Test Core shards 2, 3, 4, 5, 7, Test UI 15 of 16 shards and Type Check UI. ⛔ This record is posted only once those conclude success; a failure among them voids the PASS below and the record is re-rendered, not posted.

④ Design judgment

packages/spec/src/api/protocol.zod.ts only (tests, api.json, protocol.mdx and the changeset read, not reviewed).

Against ruling A: only the named items are answered ("among those the list would answer without it" keeps the per-caller gate and packageId ahead of the narrowing, where the ruling puts the step); an unknown name is absent, no 404 (no member is judged); an empty value is refused rather than read as every item (.min(1), with the door's 400 named as the wire's answer); the push-down carrier is GetMetaItemsRequest.names. All four hold on the tree. Usable by the first consumer: the server PR reads query.names once at the list door (one more name in META_QUERY_ARITY_DOORS.list's closed set), splits it, refuses '' before building the request as ?id= is refused, and passes names through getMetaItems into readFlattenedMetaItems; meta.getItems(type, { names }) serialises names.join(','), and the client's [] becomes the door's ?names=, refused with 400, the same answer the schema gives — the two layers agree. The describe is a rule only: no tracker number in the describe or in the .min message; the docblock's citations (#29236, record 6580261740) sit in TSDoc beside the file's existing ruling citations, not in a runtime string. The docblock's cross-references were read and are accurate: searchFieldsArray at ../data/search-fields-names-a-field.ts, refuseEmptyListFilter in @objectstack/rest, the list door's repeated-key arity rule.

Tried to break it:

  • names beside packageId: both narrow, order-independent, no conflict. names beside previewDrafts: the ruling places the narrowing after the gate, so a draft-overlaid item named in names is answered as the overlay answers it; nothing in the schema contradicts that.
  • ['crm', 'crm'] accepted: a duplicate name narrows to the same item once; the answer shape is unchanged. Not a defect.
  • .min(1) before .optional(): the right order; undefined stays accepted, [] refused with one too_small at ['names'].
  • A caller on today's runtime sending names: getMetaItems ignores it and the full list is answered (protocol-meta-list-reads.ts ≈:54 reads only type, packageId, previewDrafts; the dispatcher calls protocol.getMetaItems({ type })). This is the ruled interim, declared in the changeset — but not in the describe (item 1 below).

Findings:

  1. should-fix — the describe is published to content/docs/references/api/protocol.mdx as a served capability while no door reads names until the server PR on Metadata API: no batch by-names read, so the Console roster gate costs one /meta/object request per name #29236 lands. The file's own precedent for a member declared ahead of its runtime is a [PLANNED — not yet served] opener on the describe (the device-registration schemas in this file, ≈:3587 on the head; feat(spec): the package export manifest declares withheldMaps, classified door for the unmanaged import #29521 and feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) #29100, the two precedents this PR cites for its declarations, both carried it), removed by the PR that serves it. The changeset says it; the published reference row does not. Prime Directive chore: version packages #10 ("never advertise a capability the runtime doesn't deliver") is the rule; the ruling's ordered two-PR shape and the Blocked-by chain bound the window, which is why this is should-fix, not must-fix. Either prefix the describe now, or have the server PR's seat accept that its PR is the one that makes the published row true and land it before any version pass.
  2. note, no fix — the describe mentions the HTTP status (400) a spec schema cannot itself answer; precedent in the same schema family (GetMetaItemRequestSchema.member: "Refused with 400 VALIDATION_ERROR") makes this the file's convention, and the ruling's own words are "refused 400".

No must-fix.

Implemented-by: claude/project-thread-0neu2n
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bb07931cb445ee6b0d4fc308b359dd87f9766d31
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/spec/src/shared/lazy-schema.ts (the proxy the new pin reads .shape through), packages/rest/src/meta-collection-routes.ts and packages/rest/src/meta-item-read-gate.ts (the /meta/:type list door, which has no closed query set on main), packages/rest/src/query-allowlist.ts (refuseUnknownQueryParams, mounted on the item-history, crud and write doors only), packages/metadata-protocol/src/protocol-meta-list-reads.ts (getMetaItems), packages/runtime/src/domains/meta.ts (the dispatcher's list read) — read on origin/main 3d5933a9 as they stand

Delta review: two commits on top of the reviewed head 064d4e01 — 8b17a599 (src + test) and bb07931c (regenerated content/docs/references/api/protocol.mdx); git diff 064d4e01..bb07931c is 3 files, +20/−6. The changeset, authorable-surface/api.json and every judgment of the record on 064d4e01 that the delta does not touch carry over unchanged; this record re-judges only what moved and re-reads the gates on the new head.

① Derived judgments

  1. Accept set unchanged — right. The delta edits a describe string, a TSDoc paragraph and one test; names keeps z.array(z.string()).min(1).optional(), minItems: 1 and string[] | undefined. No shard other than the generated reference row moves, so no declaration line moves.
  2. The describe now opens [PLANNED — not yet served] — right, and true against the code today. No list door reads names: getMetaItems (protocol-meta-list-reads.ts:54) reads only type, packageId, previewDrafts; the dispatcher calls protocol.getMetaItems({ type }); meta.getItems in the client sets only package. The marker is the file's convention for a member declared ahead of its producer (the device-registration schemas, feat(spec): the package export manifest declares withheldMaps, classified door for the unmanaged import #29521, feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) #29100), which the record on 064d4e01 named as its should-fix.
  3. The new closing sentence "No list door reads it yet: until one does, a request carrying it is answered as if it were absent." — true against the code today. refuseUnknownQueryParams is mounted on the item-history, crud and write doors, not on the /meta/:type list (meta-collection-routes.ts refuses only an unknown list TYPE, refuseUnknownMetaListType); the ruling itself says the closed set (package, preview, object, include, id, names) is the server PR's. So ?names=a,b on main is neither read nor refused and the full list is answered: "as if it were absent" is exactly the behaviour. It is also true for a protocol-level call carrying names, which getMetaItems ignores.
  4. Published reference row regenerated — right; the protocol.mdx row is byte-equal to the describe's concatenation, and only that row moved.

② Semver level

Unchanged from the record on 064d4e01: @objectstack/spec: minor, Clause-②: yes (widening), Metadata-protocol: added, matching the diff (the api.json shard still moves by the one key and check-changeset-no-major.mjs still refuses none). A describe edit changes no accept set and no kind. Check Changeset on bb07931c is read below.

③ Boundary flags

  • The prior record's should-fix 1 is resolved: the published row now says the member is not served and what a request carrying it gets until it is. No must-fix existed.
  • The dev's account of the delta (marker added, closing sentence, docblock paragraph, one new pin, changeset untouched, gen:docs moved only protocol.mdx) matches the tree — judged on the diff, not the account.
  • New obligation on the server seat, carried, not a fix here: the docblock promises "the server PR on Metadata API: no batch by-names read, so the Console roster gate costs one /meta/object request per name #29236 drops the marker in the change that serves the member", and the new pin (protocol.test.ts ≈:1933) makes that a red test until it does. That puts one packages/spec edit (describe + pin removal + gen:docs) into the server PR's file surface, which the ruling's server workload lists as rest / runtime / metadata-protocol / client only. The server seat's claim must name it; the alternative — a spec PR of its own to drop the marker — would leave the published row stale for one lap. Escalated to domain:server#5 with the none-vs-minor declaration note already carried from the prior record.
  • The docblock paragraph carries #29236 in TSDoc, not in a runtime string; the pin itself asserts the describe carries no tracker id (/#\d{3,}/), and the describe's 400 / 404 are status codes with no #, so the pin is satisfiable and is.
  • Check-runs on the superseded head 064d4e01 were cancelled mid-run by the push (Test Core (3/8) concluded cancelled at 03:12Z); that head is no longer the PR's and its record is not posted.

Gate verdicts on bb07931c, every check-run complete: 57 success, 8 skipped (path filters and opt-in lanes), 0 failure or cancelled. The PASS below stands on a green head.

④ Design judgment

packages/spec/src/api/protocol.zod.ts only. The member still holds against ruling A exactly as judged on 064d4e01 (named items only, unknown name absent, empty refused, push-down carrier GetMetaItemsRequest.names); the delta adds no behaviour. The marker sentence and the closing sentence are both statements about today's runtime and both are true (① 2–3). The describe stays a rule plus a served-state statement, with no tracker number. Usable by the first consumer: the server PR removes a fixed prefix and a trailing sentence and deletes one it(...); nothing else in the member changes under it.

Tried to break it: the new pin reads GetMetaItemsRequestSchema.shape.names.description through lazySchema's proxy, whose get trap forwards every non-_zod property to the resolved real schema (lazy-schema.ts ≈:125), and .describe() on the .optional() wrapper sets that wrapper's description, so the read answers the describe (same shape as EvalUserSchema.shape.isPlatformAdmin.description in identity/eval-user.test.ts). Under OS_EAGER_SCHEMAS=1 the real object is returned and the read is the same. The marker regex requires one space after ], which the describe has. No finding.

Findings: none. No must-fix, no should-fix.

Implemented-by: claude/project-thread-0neu2n
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

席位核对 — ACCEPT · domain:spec#2 · head bb07931cb445ee6b0d4fc308b359dd87f9766d31

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(api): add names to GetMetaItemsRequestSchema — a batch by-names read on every /meta/:type list (#29236 ruling A)

2 participants