Skip to content

fix(spec)!: transport select / $select and GetDataRequestSchema.select declare the projection the doors accept - #26949

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ulr6ww
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ulr6ww

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26909

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — skills/** teaches only a named projection (skills/objectui/guides/data-integration.md:82, $select: ['name', 'email']) and never an empty one; the generated reference pages content/docs/references/{api/protocol,data/data-engine}.mdx are regenerated in this diff.
ADR-0087: not-required (no-migration-prescription) — in the changeset.

BREAKING (accept-set narrowing of published transport declarations, patch): QueryTransportParamsSchema.select / $select (so QueryWithTransportSchema and FindDataRequestSchema.query) and GetDataRequestSchema.select refuse a projection naming no field, which they admitted. The doors already refused it with a 400.

维护者速读

What changes

  • One rule, three declarations. data-engine.zod.ts gains the projection's accept set beside QueryTransportParamsSchema: a string arm z.string().regex(/[^\s,]/) (byte-identical to the rule spec: HttpFindQueryParamsSchema declares count as z.coerce.boolean and select as any string, wider than the list door (which refuses 'false'-as-string oddities, unknown counts and a blank select) #26817 gave HttpFindQueryParamsSchema.select, which the fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901 review proved equal to the door's split / trim / drop-empty) and an array arm z.array(member.regex(/\S/)).min(1). select and $select use both arms ($select keeps FieldNodeSchema as its member, so the retired object-form message is unchanged). HttpFindQueryParamsSchema.select now reads the string arm off QueryTransportParamsSchema.shape.select and GetDataRequestSchema.select reads the array arm, as fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901 already read count: no new export, no copy. Both are patterns and a length check, not refinements, so they project into JSON Schema (pattern, minItems) and add no dropped-refinements site.
  • Array rule, measured. projectionFieldsValue (metadata-protocol/src/protocol.ts:4648) keeps every array member and refuses only []; assertProjectionFieldsExist then checks every member against the object's fields, where a blank name is never one (400 INVALID_FIELD). The declaration refuses [] and any blank member, which covers the all-blank list the card names and ['name', ''], both refused by the door today.
  • .describe() texts of the three members now state the accept set; the two reference pages are regenerated. HttpFindQueryParamsSchema.select's row is unchanged.
  • Door pins moved (claim amendment). POST /data/:object/query parses its body with FindDataRequestSchema (rest/src/crud-routes.ts:363, runtime/src/domains/data.ts:242), so an empty select / $select is now refused at the schema: 400 VALIDATION_FAILED, fields[0].field = query.select / query.$select, still before any read. This follows the spec: the published query schemas declare limit / offset / top / skip as plain numbers the Data API now refuses (declared ≠ enforced) #25269 precedent (rest/src/data-page-window-door.test.ts header: "the code is the schema door's rather than the normalizer's, because the schema now says what the normalizer enforces"), the same way that route already answers {"count":"maybe"}. The two POST rows in rest/src/data-empty-projection-count-door.test.ts and runtime/src/domains/data-empty-projection-count.test.ts move to new VALIDATION_FAILED tests, which also cover [] and ['name', '']. {"fields": []} (canonical QuerySchema.fields, not in this card) and every GET row stay VALIDATION_ERROR, unchanged.

Why

After #26901, two declarations of the one findData door disagreed on select (the GET querystring strict, the body side loose), and the by-id request admitted [] that getData refuses. A generated client, a docs reader or an AI author trusting the published schema learned the rule only from a runtime 400.

Readers measured

  • QueryWithTransportSchema / FindDataRequestSchema.query: the POST /data/:object/query body gate in rest and the runtime dispatcher. The one runtime behaviour change, above.
  • RPC alias table / foldRpcQueryOptions, QuerySchema.fields: untouched; QuerySchema does not read these members.
  • GetDataRequestSchema: no runtime parse (the by-id routes hand select to getData, which refuses through projectionFieldsValue); published declaration only.
  • HttpFindQueryParamsSchema: no runtime reader; same regex and message, so fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901's pins stay green unchanged.
  • Codegen / reference: content/docs/references/** regenerated; check:generated green on 14 artifacts (key-level ledgers unmoved).
  • Client SDK: data.find sending ?select= for select: [] is client: data.find sends ?select= for select: [] — refuse an empty projection at the call site, never drop the parameter (follow-up of #26817) #26910 (client lane), not touched.
  • Stored rows: none. No metadata schema embeds QueryWithTransportSchema, QueryTransportParamsSchema or GetDataRequestSchema (only FindDataRequestSchema.query), so Metadata-protocol: none.
  • In-repo callers sending an empty projection: none outside the door tests above (examples, ui, client grepped).

Tests

Locally, under os-verify-lock:

  • @objectstack/spec test (96f9915; the later commit touches only rest/runtime tests): Test Files 738 passed, Tests 22136 passed, 1 todo. typecheck: exit 0.
  • @objectstack/metadata-protocol test (96f9915): Test Files 273 passed, 4 skipped; Tests 21693 passed, 26 skipped. Unchanged, including protocol.empty-projection-unknown-count.test.ts (in-process findData, no schema).
  • @objectstack/rest (1821443): data-empty-projection-count-door, data-page-window-door, zod-union-fields, rest-server-canonical-query-ast: 4 files, 127 passed, 18 skipped.
  • @objectstack/runtime (1821443): data-empty-projection-count, data-write-request-gate, data-record-read-status-parity: 3 files, 96 passed.
  • New pins: query-transport.test.ts §6 ('', ' ', ' , ', ',', [], ['', ' '], ['name', ''] refused on select and `` by QueryTransportParamsSchema and `QueryWithTransportSchema`, the issue at the member's own path; string and array lists kept unchanged; `null` still a withdrawal); `protocol.test.ts` (`GetDataRequestSchema.select` refuses `[]`, `['', ' ']`, `['name', '']` and keeps lists; `FindDataRequestSchema.query` refuses the blank forms on both spellings). fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901's `HttpFindQueryParamsSchema` pins are untouched and green.
  • A first run of the moved door pins found a blank member is refused at its own index (query.select.1), not query.select; the pins name the index (1821443).
  • Full dogfood battery (96f9915, pnpm --filter @objectstack/dogfood test, no shard, lock VERDICT command-exit 0): Test Files 340 passed, 3 skipped; Tests 2754 passed, 20 skipped; no failures. 1821443 changes only two rest/runtime test files, which dogfood does not import.

Acceptance notes

Verification

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 1821443: 105 derived families, run serially with no build in flight, each exit captured before any pipe: 105 exit 0. --ran reconciliation: "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED". (The same list on 96f9915 had two built-output gates, check:skill-examples and check:dual-build-cjs-loads, exit 3 before the dogfood closure build; both exit 0 on the rerun.) pnpm --filter @objectstack/spec check:generated --fix: 14 artifacts, only content/docs/references/** regenerated. CI families: read from this PR's check-runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzgm45oXexLiY5mMigQ

…t declare the projection the doors accept

QueryTransportParamsSchema.select / $select admitted a blank or comma-only
string, [] and blank names, and GetDataRequestSchema.select admitted [],
all of which the findData / getData doors refuse. One rule in
data-engine.zod.ts now declares the door's accept set; HttpFindQueryParams
and GetDataRequest read its arms instead of restating them.

The POST /data/:object/query body gate parses FindDataRequestSchema, so an
empty projection there is now refused at the schema (400 VALIDATION_FAILED
naming query.select), as a malformed limit or count already is; the rest
and runtime door pins move with it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzgm45oXexLiY5mMigQ
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

7 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 147 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 aa00362fe5f567f15c71cec8f78c85263ba1eeb6 → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 18214431cb8e44001c0b415b2ff3afa18dcb0271
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/data/data-engine.zod.ts, packages/spec/src/api/protocol.test.ts, packages/spec/src/data/query-transport.test.ts, packages/rest/src/data-empty-projection-count-door.test.ts, packages/runtime/src/domains/data-empty-projection-count.test.ts, .changeset/26909-transport-select-empty-projection.md, content/docs/references/api/protocol.mdx, content/docs/references/data/data-engine.mdx + packages/spec/src/data/query.zod.ts (FieldNodeSchema :368, QuerySchema.fields :519), packages/spec/src/data/data-engine.zod.ts (RPC_QUERY_ALIAS_SLOTS :625, RpcQueryOptionsSchema :775, QUERY_TRANSPORT_DOLLAR_ALIASES, QueryWithTransportSchema :1379, foldQueryTransportBag / lowerFoldedTransportValues, DataEngineFindRequestSchema :1416), packages/metadata-protocol/src/protocol.ts (projectionFieldsValue :4651, countFlagValue :4678, call sites :13101 / :13108 / :13681), packages/metadata-protocol/src/protocol-query-validation.ts (assertProjectionFieldsExist :675), packages/rest/src/crud-routes.ts (:325-375, the POST body gate), packages/runtime/src/domains/data.ts (:221-275, :408-416), packages/client/src/index.ts (:7061, :7947), packages/spec/authorable-surface/{api,data}.json, packages/spec/api-surface/data.json, packages/spec/dropped-refinements.baseline.json + packages/spec/scripts/lib/refinement-projection.ts, scripts/check-adr-0087-registration.mjs, scripts/pm/clause2-line.mjs, .claude/skills/pm-dispatch/SKILL.md (:313, the Skills-docs: rule), skills/objectui/guides/data-integration.md (:82), PR #26901 diff / review 6452937369 (the predecessor)

Inputs read: card #26909 body + comments 6453677540 (triage), 6454397712 (claim), 6454444680 (claim amendment, two rulings), 6455440538 (os-dev-report); predecessor card #26817, PR #26901 (merged d887aa95c3) and its contract review 6452937369; PR #26949 body, file list (9 paths), its one comment 6455451433 (docs-drift bot), the net diff main...18214431 (byte-identical to the PR diff), the compare 96f99154...18214431; check-runs on 18214431 read at 2026-10-27T22:47Z (39 runs: 13 success, 5 skipped, 21 in_progress), at 2026-10-27T22:52Z (45 runs: 25 success, 7 skipped, 13 in_progress) and finally as stamped in ③. /pulls/26949 head.sha re-read at 2026-10-27T22:52Z: unchanged from the brief.

① Derived judgments

  1. The string arm admits exactly the door's accept set, from ONE source. TransportProjectionStringSchema = z.string().regex(/[^\s,]/, 'Name at least one field') (data-engine.zod.ts, new) is byte-identical in regex and message to the rule fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901 gave HttpFindQueryParamsSchema.select, and that member now READS it (QueryTransportParamsSchema.shape.select.unwrap().unwrap().options[0].optional().describe(...)) instead of restating it, the same construction fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901 used for count. The equivalence to the door was proved in review 6452937369 ① 2 and still holds against projectionFieldsValue :4651 on main (split(',').map(trim).filter(Boolean), refuse iff empty): /[^\s,]/ holds iff some comma segment has a non-whitespace character, and \s is exactly the set trim() strips. .optional() / .describe() wrap a clone, so the shared node is not mutated and the two declarations cannot drift. .unwrap().unwrap() on the lazy proxy is the shape fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901 landed for count (countFlagValue :4679 parses .shape.count the same way); a change to the nullable/optional wrapping order throws at first access, loudly.
  2. The array arm is the door's rule plus the ruled exception, and no wider. The door keeps every array member ([...value]) and refuses only [] (VALIDATION_ERROR); assertProjectionFieldsExist :675 then refuses a blank member as Unknown field '' (400 INVALID_FIELD), since gate.known never holds '' or ' '. The declaration z.array(member.regex(/\S/)).min(1) refuses [] (what the door refuses) and any blank member (what the door refuses one step later) — exactly the claim-amendment ruling 2; nothing the door accepts is refused (a non-string member was refused by z.array(z.string()) before and after, unchanged). $select keeps FieldNodeSchema (query.zod.ts :368, a z.string({ error }) carrying the retired object-form prescription) as its member; .regex() clones the def with its error map intact, so that message is unchanged — but it is pinned only on FieldNodeSchema directly (query.test.ts :146), not through $select; noted, not a defect. GetDataRequestSchema.select reads options[1] (the z.string()-member array arm), so the by-id door's declaration is the list door's rule as getData :13681 applies it through the same projectionFieldsValue(request.select, 'select'). Output types are unchanged (string | string[] | null and string[], as the regenerated tables show); api-surface, declaration-map, export-origins and authorable-surface are key/export-level and no key or export moves. Both checks are a string_format and a min_length, neither custom (refinement-projection.ts :50), so they project as JSON Schema pattern / minItems and add no dropped-refinements row; the four existing entries (api/FindDataRequest, api/HttpFindQueryParams, data/QueryTransportParams, data/QueryWithTransport) stay. Type Check · source gates — the lane that runs check:authorable-surface and check:docs (lint.yml :5437 / :5459) — is green on the head.
  3. Readers, measured on origin/main aa00362fe5. Runtime parsers of the whole QueryTransportParamsSchema: none — metadata-protocol imports it only to parse .shape.count (:4679); the GET list querystring rides VERBATIM to the normalizer (runtime/domains/data.ts :408-416; rest's GET list hands its query to findData). So GET list is unchanged by construction and GET by-id too (GetDataRequestSchema has no runtime reader; both by-id routes hand select to getData). The ONE runtime reader that changes is FindDataRequestSchema.query = QueryWithTransportSchema, whose .extend({...QueryTransportParamsSchema.shape}) (:1382) picks the new members up, parsed by POST /data/:object/query in rest (crud-routes.ts :357-371, 400 VALIDATION_FAILED + zodIssuesToFields) and in the runtime dispatcher (data.ts :242-243, requestSchemaRefusal). The base-object parse fails before the transform, so the issue path is [key] / [key, 1] → query.select / query.select.1, as the moved pins assert. RPC alias table (RPC_QUERY_ALIAS_SLOTS :625, select → fields) and foldRpcQueryOptions: untouched and not readers of these members — correct. QuerySchema.fields (:519): untouched; it IS authored metadata (views, reports, the recursive expand value), which is exactly why it is out of this card, and {"fields": []} stays the door's VALIDATION_ERROR, pinned in both door tests. MCP / batch: no door parses FindDataRequestSchema, GetDataRequestSchema or QueryWithTransportSchema outside the two above (git grep over packages, ui, examples, apps, non-test). Client SDK: data.find :7061 / :7947 still sends ?select= for select: [] — a GET, answered by the door's VALIDATION_ERROR exactly as before this PR; client: data.find sends ?select= for select: [] — refuse an empty projection at the call site, never drop the parameter (follow-up of #26817) #26910 (open, client lane) owns it. In-repo callers sending an empty projection: none (git grep for select/$select/fields as ''/[] over packages, ui, examples, apps, skills, content, non-test; every fields: [] hit is UI/flow metadata, not a query). Codegen / reference: both data-engine.mdx tables (QueryTransportParams :1307, QueryWithTransport :1385) and both protocol.mdx tables (FindDataRequest.query :920, GetDataRequest :1038) regenerated; HttpFindQueryParams's row unchanged, as its .describe() is. The new .describe() texts are true to the accept sets.
  4. One MORE declaration of select exists and stays loose — outside this card. RpcQueryOptionsSchema.select: z.array(z.string()).optional() (data-engine.zod.ts :778, @deprecated), reached through the published DataEngineFindRequestSchema / DataEngineFindOneRequestSchema / DataEngineRequestSchema (:1416-1425, api-surface/data.json :187/:197), admits []; EngineQueryOptionsSchema.fields: z.array(FieldNodeSchema) (:105) admits [] too. Neither has a runtime parser on this tree (git grep, non-test, outside spec: only prose). The card named three members and the claim's file surface was those members; the dev's "RPC alias table … untouched" is true of the TABLE but does not name this declaration. Same family (a published declaration wider than the findData door), not this PR's defect, and the DataEngineRequestSchema block is itself an ADR-0049 enforce-or-remove candidate per its own docblock (:1518-1523), so the right remedy may be the retirement rather than a narrowing. Escalated in ③; not a FAIL reason.
  5. The ruled code moves are true and pinned honestly. Before (main): POST {"select":""} / {"$select":","} → findData → projectionFieldsValue → 400 VALIDATION_ERROR naming select / $select (the removed REFUSED_POST rows asserted exactly this); POST {"select":["name",""]} → schema passed, projectionFieldsValue kept both, assertProjectionFieldsExist → 400 INVALID_FIELD (Unknown field ''). Now: all four bodies refused at FindDataRequestSchema, 400 VALIDATION_FAILED, fields[0].field = query.select / query.$select / query.select.1, before any read. The changeset's Before / Now / POST / Unchanged bullets state exactly this, including the INVALID_FIELD → VALIDATION_FAILED move for the blank member, and the "how that route already answers a malformed limit or count" precedent (spec: the published query schemas declare limit / offset / top / skip as plain numbers the Data API now refuses (declared ≠ enforced) #25269; {"count":"maybe"} is pinned VALIDATION_FAILED in the same rest file, unchanged). The new pins assert status (400), code (VALIDATION_FAILED), the field path, and that the driver was never asked (reads.n delta 0) — rest :173-184, runtime :129-150 — so nothing is weakened: a refusal is still asserted, more precisely located. The GET list rows (five in rest, four in runtime), the GET by-id rows (two + two) and {"fields": []} keep VALIDATION_ERROR with the sentence names no field, unchanged. The rest header table row was updated; the runtime file's header prose still says 400 VALIDATION_ERROR for the doors generally while the new block's own comment states the exception — cosmetic.
  6. Metadata-protocol: none is right; ADR-0087 not-required (no-migration-prescription) is right. No metadata type's schema embeds QueryTransportParamsSchema, QueryWithTransportSchema, FindDataRequestSchema or GetDataRequestSchema (git grep over packages/spec/src: only odata.zod.ts prose and the plugin-rest-api.zod.ts catalog string name them); the stored-row authored projection is QuerySchema.fields, untouched. The members DO sit on authorable-surface/{data,api}.json (data/QueryTransportParams:select, api/GetDataRequest:select, …) but so does api/HttpFindQueryParams:* and api/FindDataRequest:object — that ledger is every published object key, not the metadata an author stores, and review 6452937369 ruled none for HttpFindQueryParams on the same ground. So no stored row can newly fail to parse and no conversion-layer entry has anything to rewrite: not-required (no-migration-prescription) is the honest disposition. The changeset body carries no FROM → TO rewrite, no framing heading and no table; its - **Before.** / - **Now.** bullets are behaviour comparison, the vocabulary the gate deliberately does not read as a prescription (check-adr-0087-registration.mjs :905-921, P33). Lint · CI, release & engine gates (the gate's lane) is green on the head.
  7. Declarations. Clause-②: no (narrowing) line-initial in the PR body and the changeset (the — prose suffix is the spelling clause2-line.mjs reads the arm from, as in fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901); Metadata-protocol: none in both; Skills-docs: none — REASON present in the PR body, the line owed for a (narrowing) arm (pm-dispatch SKILL.md :313, [Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B), and the reason is true: skills/objectui/guides/data-integration.md:82 is $select: ['name', 'email'], and git grep over skills/** finds no empty projection taught anywhere; **BREAKING** banner in both; exactly one ADR-0087 marker, in the changeset. The PR title carries the !.
  8. Dogfood carry. 96f99154...18214431 is one commit touching only the two rest/runtime door test files (rest +3/−2, runtime +4/−3: the blank-member row's path query.select → query.select.1). Dogfood (packages/qa/dogfood) imports built packages, never a sibling's *.test.ts, so the 96f9915 battery (340 files / 2754 tests, no failures) describes the head's runtime byte-for-byte. The spec and metadata-protocol runs were on 96f9915 for the same reason; the rest/runtime files were re-run on the head. Carries. CI's Dogfood Regression Gate (N/3) shards are path-filtered off this PR (skipped), so the carry rests on the diff reading, not on a green shard.
  9. Scope. Nine paths, all on the amended claim's surface (the two schema files, their two tests, the two re-pinned door tests the amendment added, one changeset, two regenerated pages). No unrelated file; no governed surface; 206 changed lines.

② Semver level

@objectstack/spec patch — correct: the one published package the diff moves; Metadata-protocol: none takes patch and keeps the BREAKING banner and the ADR-0087 marker (AGENTS.md Post-Task Checklist 3). packages/rest and packages/runtime change test files only — nothing publishes, no changeset owed. content/docs/** is not a package. Check Changeset green.

③ Boundary flags

  • Dev acceptance note "the order asked that a runtime refusal stay VALIDATION_ERROR; it cannot for the POST body": answered by the seat's claim amendment 6454444680 ruling 1 before the pins moved; the pins follow it exactly. Accepted.
  • Claim amendment ruling 2 (array arm refuses a blank member, INVALID_FIELD → VALIDATION_FAILED): implemented as ruled, Before / Now stated in the changeset. Accepted.
  • Dev out-of-scope finding "client: data.find sends ?select= for select: [] — refuse an empty projection at the call site, never drop the parameter (follow-up of #26817) #26910 client select: []": already filed; the client's GET is answered by the unchanged door. No action here.
  • NEW (① 4): RpcQueryOptionsSchema.select (z.array(z.string()), data-engine.zod.ts :778) and EngineQueryOptionsSchema.fields (:105) still admit [], published through DataEngineFindRequestSchema / DataEngineRequestSchema with no runtime parser. Escalated to the seat to FILE a card (Prime Directive 10) — same family as spec: HttpFindQueryParamsSchema declares count as z.coerce.boolean and select as any string, wider than the list door (which refuses 'false'-as-string oddities, unknown counts and a blank select) #26817 / spec: QueryTransportParamsSchema.select / $select and GetDataRequestSchema.select admit an empty projection that the findData and getData doors refuse (sibling of #26817) #26909, with the ADR-0049 enforce-or-remove question on the DataEngineRequestSchema block decided first. Not a FAIL reason: outside the card's three members and the claimed surface.
  • Cosmetic: the runtime door test's header prose still generalises 400 VALIDATION_ERROR to every door; the new block's comment carries the exception. No action owed.
  • Branch name claude/project-thread-ulr6ww instead of claude/issue-26909-<slug>: declared in the claim as the platform-assigned thread branch. Accepted.
  • Gates — reading of record at 2026-10-27T22:58Z: 46 check-runs on 18214431, 31 completed/success, 7 completed/skipped (Lint UI, Test UI (N/16), Dogfood Verify CLI, Dogfood Regression Gate (N/3), Temporal Conformance, Packed-tarball smoke (opt-in), 18.x upgrade harness (opt-in) — path filters and opt-ins), 0 failure/cancelled/action_required, 8 in_progress: Test Core (1/8) … (7/8) and Type Check UI (Test Core (8/8) and Type Check · workspace concluded success between the 22:52Z reading and this one). An in_progress run is recorded as such, not as a pass: the required contexts Test Core and Type Check UI had NOT fully concluded at this reading, so this record does not assert them green; the queue guard and the owning seat read their conclusions before any landing. Green and load-bearing for this diff at the reading: Type Check · source gates (the six spec artifact gates, check:docs and check:authorable-surface included), Type Check · consumer gates, Type Check · workspace, Type Check · debt ledger, Test Core (8/8), Build Core, Build Docs, Check Changeset, Lint · CI, release & engine gates (the ADR-0087 marker gate), Lint · scoped · query-options erasure, Governed Surface Queue Guard, Spec property liveness, Lint & Repo Gates, PR-body guards. The dev's local reading of the same families on the head (@objectstack/spec 738 files / 22136 tests at 96f9915 + typecheck exit 0; rest 4 files 127 passed and runtime 3 files 96 passed at 1821443; 105 derived gates exit 0 at 1821443) is consistent with every concluded run and is what the pending shards re-run.

Implemented-by: claude/project-thread-ulr6ww
Reviewed-by: session_01WGufzgbJhFoXYsLSe5WSPP

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:数据读取「选哪些字段」的另外两份公开声明也收窄到接口真正接受的范围:QueryTransportParamsSchema.select / $select(POST 查询体)和 GetDataRequestSchema.select(按 id 读取)不再接受空投影(空串、只有空格或逗号、空数组、带空成员的数组)。三份声明现在共用同一条规则。
  • 为什么改:spec: HttpFindQueryParamsSchema declares count as z.coerce.boolean and select as any string, wider than the list door (which refuses 'false'-as-string oddities, unknown counts and a blank select) #26817 已把 GET 查询串的声明改对,这两份兄弟声明仍写着「空投影也收」,接口却回 400;按声明生成的客户端或 AI 作者会写出被拒的请求。
  • 风险与回滚:POST /data/:object/query 带空 select / $select 时仍回 400,但错误码从 VALIDATION_ERROR 变为 VALIDATION_FAILED(指向 query.select / query.$select);['name',''] 从 INVALID_FIELD 变为 VALIDATION_FAILED(指向 query.select.1)。GET 列表和按 id 读取不变。回滚 = revert 本 PR。
  • 先看:packages/spec/src/data/data-engine.zod.ts(TransportProjectionStringSchema 与 select 的数组分支)、packages/spec/src/api/protocol.zod.ts(GetDataRequestSchema.select)、.changeset/26909-transport-select-empty-projection.md(Before / Now)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/query-transport.test.ts src/api/protocol.test.ts,预期全绿,其中空串、' , '、[]、全空数组被拒,正常字段列表通过。
  • 需要你做的:无,席位落地。

席位意见:接受。契约复核 PASS(6455655029)。复核发现另外两份引擎侧声明还收空数组,已立卡 #26954。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-27T23:05Z

Head: 18214431cb8e44001c0b415b2ff3afa18dcb0271 · card #26909 · claim 6454397712 (amended with two rulings)

  1. 形态: a draft PR to main with Fixes #26909 as its only closing keyword. 9 paths, +189 / −17: the two declarations, their tests, the two re-pinned door tests named in the claim amendment, two regenerated reference pages and one changeset. All inside the amended claim surface. check-governed-merges --pr 26949: NOT governed, 206 lines ≤ 3000, so the ordinary queue landing applies. The PR carried no assignee; the seat sets os-support-ai in this act.
  2. 前提实测: the premise holds: after PR fix(spec)!: HttpFindQueryParamsSchema declares the count and select values the list door accepts #26901 only HttpFindQueryParamsSchema.select refused an empty projection. The review found one source now: the string rule TransportProjectionStringSchema (/[^\s,]/) lives in data-engine.zod.ts and HttpFindQueryParamsSchema.select reads it; the array arm (min(1), every member non-blank) is read by GetDataRequestSchema.select. The three declarations agree, and the checks project as JSON Schema pattern / minItems, so no dropped-refinements row (record ① 1).
  3. 门禁 check-runs (on this head): read by the seat at 2026-10-27T23:05Z: 47 runs, 40 success, 7 skipped, none red or pending; mergeable state clean. (The review's own reading at 22:57Z still had 8 runs in progress; the seat waited for them and they finished green.) The contract-tier review is PASS on this same head (record 6455655029), and the head has not moved since. Dogfood: the full battery ran green on 96f99154 (340 files / 2754 tests); the head adds only the two door test files on top, so the reading carries.
  4. 接受的偏差: (a) The ruled code moves (claim amendment): POST /data/:object/query with an empty select / $select answers 400 VALIDATION_FAILED at query.select / query.$select instead of VALIDATION_ERROR, after the spec: the published query schemas declare limit / offset / top / skip as plain numbers the Data API now refuses (declared ≠ enforced) #25269 precedent; ['name',''] answers VALIDATION_FAILED at query.select.1 instead of INVALID_FIELD. The changeset states both as Before / Now; GET list and GET by-id stay VALIDATION_ERROR, pinned unchanged. The re-pinned tests still assert status, code, path and zero driver reads. (b) Clause-②: no (narrowing), Metadata-protocol: none (no metadata type embeds these schemas; QuerySchema.fields is untouched), ADR-0087: not-required (no-migration-prescription), patch on @objectstack/spec with the BREAKING banner.
  5. 范围外去向: filed in this act: spec: RpcQueryOptionsSchema.select and EngineQueryOptionsSchema.fields still admit an empty projection — decide ADR-0049 enforce-or-remove, then align with the shared projection rule (found reviewing #26909) #26954. RpcQueryOptionsSchema.select and EngineQueryOptionsSchema.fields (data-engine.zod.ts) still admit []; they are published but have no runtime parser, and their block is an ADR-0049 enforce-or-remove candidate, which that card decides first. Skills-docs: none is upheld: the one skill line about data reads (skills/objectui/guides/data-integration.md) stays true. No in-repo caller sends an empty projection.
  6. 落地条款: the prose faces were checked against the diff: the changeset's BREAKING Before / Now and both .describe() texts, regenerated into protocol.mdx and data-engine.mdx. Ready, auto-merge and the assignee go through the relay in this act, and the seat follows the PR to MERGED.

Generated by Claude Code

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 protocol:data size/m tests tooling

Projects

None yet

2 participants