Skip to content

fix(spec)!: EngineQueryOptionsSchema.fields and the RPC request's select refuse an empty projection, as their transport siblings do - #27007

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

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

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26954

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — the skills teach only a non-empty engine fields projection (skills/objectstack-query/rules/filters.md:105, fields: ['id']; skills/objectstack-query/SKILL.md:25 lists fields among the legal find option keys) and never an empty one, so no skill sentence goes false; the one transport data-read line, skills/objectui/guides/data-integration.md:82 ($select: ['name', 'email']), stays true too.

维护者速读

  • 改了什么:引擎侧四份「选哪些字段」的公开声明(EngineQueryOptionsSchema.fields、RPC 请求里已弃用的 select、旧版 DataEngineQueryOptionsSchema.select、向量检索请求的 fields)不再接受空数组和带空白名的数组,与 fix(spec)!: transport select / $select and GetDataRequestSchema.select declare the projection the doors accept #26949 收窄的三份兄弟声明共用同一条规则(同一处定义,未复制)。
  • 为什么改:兄弟声明已拒绝空投影,这四份仍写着「空也收」,按声明生成的客户端或 AI 作者会学到相反的规则。
  • 风险与回滚:仓内没有任何运行时用这四份 schema 解析请求,线上请求零变化;受影响的只有仓外直接用这些导出 schema 解析、且发了空投影的代码。进程内 engine.find(obj, { fields: [] }) 仍读全部列,不变。回滚 = revert 本 PR。
  • 先看:packages/spec/src/data/data-engine.zod.ts(共享规则上移到 EngineQueryOptionsSchema 之前,以及四个成员)、packages/spec/src/data/data-engine.test.ts(新 describe 块)、.changeset/26954-engine-projection-empty.md。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/data-engine.test.ts -t 'engine-side projection',预期 44 个用例全绿:空数组与空白名被拒且报错文字与兄弟声明一致,非空列表通过。
  • 需要你做的:无,席位落地。

What changes

  • packages/spec/src/data/data-engine.zod.ts:
    • The projection rule block from fix(spec)!: transport select / $select and GetDataRequestSchema.select declare the projection the doors accept #26949 (PROJECTION_NAMES_NO_FIELD, TransportProjectionStringSchema, transportProjectionArray, PROJECTION_ACCEPT_SET and its docblock) moves from just above the page-window block to just above EngineQueryOptionsSchema. It is not copied. It has to move: RpcQueryOptionsSchema is a non-lazy EngineQueryOptionsSchema.extend(...), so importing the module runs the lazy factory before any later const is initialised (a TDZ ReferenceError). The docblock gains one paragraph that says so and names the four new readers.
    • EngineQueryOptionsSchema.fields: z.array(FieldNodeSchema) becomes transportProjectionArray(FieldNodeSchema), the same construction as QueryTransportParamsSchema.$select.
    • RpcQueryOptionsSchema.select (@deprecated): z.array(z.string()) becomes transportProjectionArray(z.string()), the same construction as QueryTransportParamsSchema.select and the arm GetDataRequestSchema.select reads.
    • Folded in at the seat's request (same file, same family, same ruling): DataEngineQueryOptionsSchema.select (@deprecated legacy options) and DataEngineVectorFindRequestSchema.fields (the vectorFind arm of DataEngineRequestSchema). Both change from z.array(z.string()) to transportProjectionArray(z.string()).
  • packages/spec/src/data/data-engine.test.ts: one new describe block (44 cases), described under Tests.
  • .changeset/26954-engine-projection-empty.md: @objectstack/spec patch, with both declaration lines, one ADR-0087 marker and a BREAKING banner with Before / Now / Unchanged.

No .describe() changes. None of the four members had one, and check:generated reports all 14 artifacts up to date on the head, so no reference page regenerates. A sentence saying "is refused" would also be false of the in-process engine (see Acceptance notes).

The ADR-0049 note on DataEngineRequestSchema ("every arm below now has zero readers in this repo … an ADR-0049 enforce-or-remove candidate") is unchanged. I re-measured it on this head: no code outside packages/spec parses DataEngineRequestSchema, DataEngineFindRequestSchema, DataEngineFindOneRequestSchema or DataEngineContractSchema (git grep, non-test: the hits outside data-engine.zod.ts are the generated reference page's import line, the export ledgers and CHANGELOG prose). So every sentence in it is still true. The note moved down by 33 lines with the block.

Why

After #26817 and #26909, every published transport declaration of a read's projection refuses an empty one. These four engine-side declarations still admitted [] and blank names, so a client generated from their JSON Schema, or an AI author reading them, learned the opposite rule. Seat ruling on #26954: align, do not retire.

Readers and producers (measured on origin/main 3217fc5)

  • Runtime parsers: none, for any of the four members. EngineQueryOptionsSchema is read at runtime only for its key list (runtime/src/action-execution.ts:2047, .shape keys) and by objectql's engine-unknown-option.test.ts (keys). Key sets do not change. RpcQueryOptionsSchema is module-private and reached only through the three request schemas above, which have no parser outside spec. DataEngineQueryOptionsSchema and DataEngineVectorFindRequestSchema have no reference outside packages/spec except the generated reference page's import line and one prose sentence in content/docs/kernel/contracts/data-engine.mdx:637, which names the legacy schema as deprecated and is still true.
  • In-process engine: ObjectQL.find / findOne never parse their options with this schema. engine.ts:13563 projects only when ast.fields.length > 0, and :13573 falls back to * on an empty projection. So engine.find(obj, { fields: [] }) reads every column, as before.
  • Types: z.input / z.infer stay string[] for all four members, so no TypeScript caller changes. pnpm --filter @objectstack/spec typecheck passes on the head.
  • Client SDK / codegen / OpenAPI: packages/client imports none of these schemas. JSON Schema gains minItems: 1 and a per-item \S pattern. Both are a min_length and a string_format, not custom, so there is no dropped-refinements row. api-surface, declaration-map, export-origins and authorable-surface are key- or export-level and unchanged (check:generated: 14/14 up to date). gen:openapi is ungated.
  • In-repo callers sending []: none. git grep over packages, ui, examples, apps and skills (non-test) for fields: [] / select: []: every hit is UI, flow, analytics or result metadata, not an engine query.
  • Stored or authored data: none of the four members is authorable metadata. No metadata type embeds EngineQueryOptionsSchema, DataEngineQueryOptionsSchema or the request schemas (git grep over packages/spec/src). The authored projection is QuerySchema.fields (views, reports), which this PR does not touch. So Metadata-protocol: none, and ADR-0087 is not-required (no-migration-prescription).

Tests

packages/spec/src/data/data-engine.test.ts, describe('the engine-side projection declarations admit exactly what their transport siblings admit'):

  • EngineQueryOptionsSchema.fields refuses [], ['name', ''] and ['name', ' ']. The issue path is ['fields'] or ['fields', 1]. The message equals the one QueryTransportParamsSchema.$select gives for the same value, and that sibling's path is asserted too. It keeps ['name'] and ['name', 'email'] unchanged.
  • For each of DataEngineFindRequestSchema, DataEngineFindOneRequestSchema and the DataEngineRequestSchema union:
    • query.select refuses the same three values at ['query', 'select', …], with the bare select sibling's message;
    • query.fields refuses them at ['query', 'fields', …], with $select's message;
    • query.select: ['name'] / ['name', 'email'] parses and folds onto fields.
  • DataEngineQueryOptionsSchema.select refuses the same three values at ['select', …], with the bare select sibling's message, and keeps a non-empty list unchanged.
  • DataEngineVectorFindRequestSchema and the DataEngineRequestSchema union (method: 'vectorFind'): fields refuses the three values at ['fields', …], with the bare select sibling's message, and keeps a non-empty list unchanged.

Ablation, after committing the fix, through scripts/ablation-replace.mjs (anchor hit, blob moved, restored to the HEAD blob with git diff HEAD empty):

  • fields back to z.array(FieldNodeSchema): 12 failed / 17 passed in the block.
  • select back to z.array(z.string()): 9 failed / 20 passed.
  • On ba848653 (the 44-case block): DataEngineQueryOptionsSchema.select reverted gives 3 failed / 41 passed; DataEngineVectorFindRequestSchema.fields reverted gives 6 failed / 38 passed. Both restored to the HEAD blob, with git diff HEAD empty.

Runs on head ba848653:

  • @objectstack/spec full suite under the lock: 819 files, 23498 tests passed (1 todo). Then typecheck (tsc, scripts and test typecheck): VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec build && check:generated: VERDICT command-exit 0, and all 14 generated artifacts are up to date, so nothing was regenerated.
  • Full dogfood, owed by the (narrowing) arm: built the @objectstack/dogfood^... closure, then ran pnpm --filter @objectstack/dogfood test under the lock with no shard and no path. Result: 344 files passed, 3 skipped (347); 2784 tests passed, 20 skipped (2804); VERDICT command-exit 0.
  • dispatch-gates --commands: 80 derived families. All 80 exit 0. Three of them (check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure) first exited 3 because their prerequisite was not met. They were run again after the dogfood closure build and exited 0. --ran reconciliation: 80 run, 0 NOT-MEASURED.
  • The first head, 17ce2dfb, had the same readings: 23483 spec tests, 80/80 gates, and the same dogfood counts.

Acceptance notes

Human Minimum

Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran. pnpm lint is CI's repo-wide run.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed (check:generated: none were stale).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzfjH9js8WRa8Xi2dmx

…t declarations

EngineQueryOptionsSchema.fields and RpcQueryOptionsSchema.select take the
projection's array arm (at least one name, none blank) from the declaration
their transport siblings already use. The shared arm moves above
EngineQueryOptionsSchema because RpcQueryOptionsSchema extends it at module
load.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfjH9js8WRa8Xi2dmx
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/data-engine.mdx (via DataEngineQueryOptionsSchema (symbol, a top-level const), EngineQueryOptionsSchema (symbol, a top-level const))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 e6c440900072a2d2423474962db9c7db6011b845 → packageMentionDocs.

Which tree this was computed on

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

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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e6c440900072a2d2423474962db9c7db6011b845 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…lds with the projection rule

DataEngineQueryOptionsSchema.select and DataEngineVectorFindRequestSchema.fields
take the same array arm (at least one name, none blank) as their engine-side
and transport siblings.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfjH9js8WRa8Xi2dmx
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ba84865301c609c8167b187aebdf0109f34dbd37
Local-runs: none
Scope: packages/spec/src/data/data-engine.zod.ts, packages/spec/src/data/data-engine.test.ts, .changeset/26954-engine-projection-empty.md + packages/spec/src/shared/lazy-schema.ts (lazySchema, the Proxy that resolves on first property access), packages/spec/src/data/query.zod.ts (FieldNodeSchema :368, QuerySchema.fields :519), packages/spec/src/api/protocol.zod.ts (:2386 HttpFindQueryParamsSchema.select, :2435 GetDataRequestSchema.select), packages/runtime/src/action-execution.ts (:2039-2049 findEnvelopeKeys), packages/objectql/src/engine-unknown-option.test.ts (:233-254), packages/objectql/src/engine.ts (:684 ENGINE_FIND_OPTION_KEYS, :13572 the projection fallback), packages/spec/src/data/hook-api.test.ts (:204-209) and hook-api.ts (:143 HookQuery), packages/spec/src/type-alias-convention.pin.test.ts (:810), packages/spec/{api-surface,declaration-map,export-origins,json-schema.manifest}/data.json, packages/spec/unemitted-schemas.baseline.json (:48), packages/spec/dropped-refinements.baseline.json + packages/spec/scripts/lib/refinement-projection.ts (:50), packages/spec/scripts/build-openapi.ts (:21), packages/spec/scripts/build-docs.ts, content/docs/references/data/data-engine.mdx (:386-396, :487, :870-884, :1111-1131, :1297-1330), content/docs/kernel/contracts/data-engine.mdx (:102, :637), skills/objectstack-query/SKILL.md (:25), skills/objectstack-query/rules/filters.md (:105), skills/objectui/guides/data-integration.md (:82), scripts/check-adr-0087-registration.mjs (:140-160, :553-560, :2029-2031), scripts/pm/clause2-line.mjs (:111-121), .claude/skills/pm-dispatch/SKILL.md (:313), docs/adr/0049-no-unenforced-security-properties.md, .github/workflows/pr-automation.yml (:259 Check Changeset), card #26954, issue #27008, PR #26949 and its review 6455655029

Inputs read: card #26954 body and all four comments (triage 6456603692, claim 6456968829 with the seat ruling "align, do not retire", os-dev-reports 6457855300 and 6458595611); predecessor card #26909, PR #26949 (merged 81b4599585) and its contract review 6455655029 (whose ③ NEW finding is this card's source); PR #27007 body, file list (3 paths, +163/−35), its one comment 6457861954 (docs-drift bot, advisory), the net diff against main at the head (application/vnd.github.diff), and the head's own copies of the three files; readers measured by read-only git grep on the local checkout's origin/main acc9c57d0a (the four files this diff claims about are byte-identical between the PR base 3271a083 and that ref). Check-runs on ba84865301 read at 2026-10-28T02:04Z (51 runs: 38 success, 12 skipped, 1 in_progress — the second Check Changeset, re-triggered by the 01:59Z body edit), at 02:07Z and finally at 02:09Z (51 runs: 40 completed/success, 11 completed/skipped, 0 in progress, 0 failure). /pulls/27007 head.sha re-read at 02:07Z and 02:09Z: ba84865301, unchanged from the brief.

① Derived judgments

  1. One source, moved not copied, and the move is forced. The head holds exactly one const transportProjectionArray and one const TransportProjectionStringSchema (data-engine.zod.ts :106-111); the block removed at the old site (:910-936 on main) is byte-identical to the block added at :78-111 except for the one added paragraph and "members below" rewritten to "QueryTransportParamsSchema members", which is true of the new position. The TDZ claim is true in the code: lazySchema (lazy-schema.ts) returns a Proxy whose first property access runs the factory, and RpcQueryOptionsSchema = EngineQueryOptionsSchema.extend({...}) (:810) is a bare module-level statement, so importing the module runs EngineQueryOptionsSchema's factory at load, which now calls transportProjectionArray (:140). Left at :910, that const would still be in its temporal dead zone at :810 — a ReferenceError at import. Under OS_EAGER_SCHEMAS=1 every factory runs at its own line, so the position above :135 is required on that path too. All four members (:140, :218, :813, :1525) call the one helper; fields passes FieldNodeSchema, the other three z.string(), as $select and select do (:1067, :1088).
  2. Behaviour-neutral for fix(spec)!: transport select / $select and GetDataRequestSchema.select declare the projection the doors accept #26949's members. QueryTransportParamsSchema (:1050) is lazy, so the helper's position relative to it does not change what the factory builds: $select and select are the same union of the same string arm and the same z.array(member.regex(/\S/)).min(1) construction, same regexes, same two messages (Name at least one field, A field name cannot be blank), same .describe() texts built from the same PROJECTION_ACCEPT_SET. GetDataRequestSchema.select (protocol.zod.ts :2435) and HttpFindQueryParamsSchema.select (:2386) still read QueryTransportParamsSchema.shape.select.unwrap().unwrap().options[1|0] — untouched by this diff. Each transportProjectionArray(FieldNodeSchema) call clones FieldNodeSchema through .regex(), so fields and $select share no mutable node and FieldNodeSchema's own retired-object-form error map (query.zod.ts :368) rides along unchanged. The reference page's QueryTransportParams rows (data-engine.mdx :1310, :1317) are unchanged on the head, as check:docs inside the green TypeScript Type Check job confirms.
  3. All the published transport and engine-side declarations of the read projection now agree. QueryTransportParamsSchema.select / $select, GetDataRequestSchema.select, HttpFindQueryParamsSchema.select, EngineQueryOptionsSchema.fields, RpcQueryOptionsSchema.select (so DataEngineFindRequestSchema.query, DataEngineFindOneRequestSchema.query, two arms of DataEngineRequestSchema), DataEngineQueryOptionsSchema.select and DataEngineVectorFindRequestSchema.fields all refuse [] and a blank member with the same two messages. QuerySchema.fields (query.zod.ts :519, so also expand[rel].fields) stays z.array(FieldNodeSchema): that is the authored QueryAST (views, reports), ruled outside this family in review 6455655029 ① 3 and door-refused at runtime; not a member of this card.
  4. FieldNodeSchema admits no object node. It is z.string({ error }) whose map only rewrites the invalid_type message for the removed { field, fields, alias } form ([P3] data: FieldNode's nested-select object form is declared but nothing produces or consumes it — enforce or remove #4196, ADR-0049); an object entry is refused before the blank-member regex is reached, with the retirement prescription, exactly as on $select. The blank-member rule therefore applies to strings only, which is the whole domain.
  5. Readers, measured on origin/main acc9c57d0a. Runtime parsers of any of the four members: none. EngineQueryOptionsSchema is read outside spec only for its key list — runtime action-execution.ts :2047 (Object.keys(...shape), resolved lazily) and objectql engine-unknown-option.test.ts :242/:253-254 (Object.keys(schema.shape) minus tombstones against ENGINE_FIND_OPTION_KEYS, engine.ts :684); spec's own hook-api.test.ts :204-209 pins the same key set for HookQuery (hook-api.ts :143, an Omit over EngineQueryOptions, so string[] in and out). No key is added or removed, so all three pins hold — Test Core 8/8 green. RpcQueryOptionsSchema is module-private and reached only through the three request schemas; DataEngineRequestSchema, DataEngineFindRequestSchema, DataEngineFindOneRequestSchema, DataEngineVectorFindRequestSchema, DataEngineQueryOptionsSchema and DataEngineContractSchema have no reference outside packages/spec except the generated reference page's import line (references/data/data-engine.mdx :25), kernel/contracts/data-engine.mdx :102 (a TS interface listing, fields?: FieldNode[], still true) and :637 (names the legacy schema deprecated, still true), and CHANGELOG prose. DataEngineContractSchema embeds EngineQueryOptionsSchema in find / findOne z.function inputs (:577-585) and is on unemitted-schemas.baseline.json :48; nothing parses it. packages/client imports none of these schemas; the one client hit for select: [] (index.ts :427) 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's own-side fix of a GET door call. In-repo callers sending fields: [] / select: [] to an engine query: none — every non-test hit under packages, ui, examples, apps is a result or metadata shape (cli orphan rows, security field readers, analytics result envelopes, screen-node metadata, UI state). Nothing on this tree can newly fail to parse.
  6. Authored metadata: none of the four is one. git grep over packages/spec/src finds the four names in no .zod.ts outside data-engine.zod.ts except as prose (action-params.zod.ts :373, query.zod.ts :215/:399); no metadata type, saved query, dataset, report or AI tool argument schema embeds EngineQueryOptionsSchema, DataEngineQueryOptionsSchema or the request schemas. The authored projection is QuerySchema.fields, untouched. Metadata-protocol: none is right and the ADR-0087 disposition not-required (no-migration-prescription) is honest: no stored row can newly fail, nothing for a conversion layer to rewrite, no tombstone owed (no key moves). The category is one of the gate's six (check-adr-0087-registration.mjs :553-560); exactly one marker, in the changeset; Lint · CI, release & engine gates green on the head.
  7. Generated surfaces. The JSON Schema for the four members gains minItems: 1 and a per-item non-blank pattern — a min_length and a string_format check, not custom (refinement-projection.ts :50), so no dropped-refinements row is owed and the ledger is unchanged. json-schema.manifest/data.json is a name list (data/EngineQueryOptions :68, data/DataEngineQueryOptions :44, data/DataEngineVectorFindRequest :49, data/DataEngineFindRequest :41), and api-surface, declaration-map, export-origins, authorable-surface are export- or key-level: none moves. The reference page renders .describe() only (the EngineQueryOptions.fields and DataEngineVectorFindRequest.fields rows carry an empty Description on main, :1121 and :880) and none of the four members had or gains one, so no page regenerates; the dev's "No .describe() changes" is true, and the changed text on the members is TSDoc, which the generator does not read. check:generated 14/14 up to date is therefore the expected reading, and the TypeScript Type Check job that runs it is green. gen:openapi writes the gitignored json-schema/openapi.json at build (build-openapi.ts :21), ungated as AGENTS.md records. check:doc-authoring (in the green Lint & Repo Gates) does not flag the (#26954) TSDoc tags; the file already carries (#8057) the same way at :272.
  8. The ADR-0049 note (:1551-1558) is unchanged and its measurable sentences hold at the head. "every arm below now has zero readers in this repo": true — no runtime parser outside spec's own tests (judgment 5). "there is no Virtual Data Engine implementation, only this schema describing one": true — git grep for a virtual data engine / Virtual ObjectQL implementation outside spec finds nothing. "an ADR-0049 enforce-or-remove candidate, deliberately left standing": true — the seat ruled align, not retire (claim 6456968829), so the candidacy stands. "it is tracked separately": see ③.
  9. Declarations. PR body and changeset both carry line-initial Clause-②: no (narrowing) (the — prose suffix is the spelling clause2-line.mjs :115-121 reads the arm from) and Metadata-protocol: none; the PR body carries the Skills-docs: none — REASON 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) — the value none is right (judgment 11), the reason's wording is not (③). The changeset's **BREAKING** banner names the four members, says what breaks (external parsers and generated clients sending an empty projection), carries Before / Now / Unchanged and the remedy, and does not claim a runtime refusal; the PR title carries the !. The Unchanged bullet and the PR body's Acceptance notes both state that engine.find(object, { fields: [] }) still reads every column; the TSDoc "at least one, none blank" describes the declaration's accept set and nothing in the diff says the engine refuses it — consistent with objectql: the in-process engine reads fields: [] as every column while every published projection declaration refuses an empty projection (found on #26954) #27008, which is open and names exactly that gap.
  10. Tests are honest and the ablation arithmetic matches the block. Every refusal case asserts success === false, the issue path (['fields'] / ['fields', 1], ['query', 'select', …], ['query', 'fields', …], ['select', …], ['fields', …] on vectorFind) and that the message equals the transport sibling's for the same value, with sibling() first asserting the sibling itself refuses (so toBe never compares two undefineds); the first block also pins the sibling's own path. A zod 4 union that leaves one non-aborted branch surfaces that branch's issues directly, which is why issues[0] on $select / select is the array arm's too_small / invalid_format and not an invalid_union — and Test Core green on the head is the proof. The KEPT cases pin the fold (select → fields) and non-empty lists unchanged. Reported ablations: fields reverted → 12 red = 3 values × (EngineQueryOptions + 3 request schemas' query.fields); RPC select reverted → 9 = 3 × 3; legacy select → 3; vectorFind fields → 6 = 3 × 2. Each is exactly the case count that exercises the reverted arm, so the reported reds do exercise the new arms.
  11. Skills. git grep over skills/**: no sentence teaches an empty projection; the engine fields examples are named lists (objectstack-query/rules/filters.md :105 fields: ['id'], SKILL.md :25 lists fields among engine.find options, evals :37) and objectui/guides/data-integration.md :82 is $select: ['name', 'email']. Nothing taught there becomes false; Skills-docs: none is the right value.
  12. Scope. Three paths, all on the claim's file surface (data-engine.zod.ts, its test, one @objectstack/spec changeset); no generated artifact needed regenerating; no governed surface; 198 changed lines. The branch name claude/project-thread-pz6ul8 is the platform-assigned thread branch, declared in the claim.

② 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), the same level #26949 took for the same kind of narrowing. No other package publishes anything from this diff. Check Changeset green twice on the head (01:08Z and, after the body edit, 02:01Z).

③ Boundary flags

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

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:数据引擎一侧还有四个投影声明允许空投影,现在与 fix(spec)!: transport select / $select and GetDataRequestSchema.select declare the projection the doors accept #26949 的同一条规则对齐:EngineQueryOptionsSchema.fields、RpcQueryOptionsSchema.select、DataEngineQueryOptionsSchema.select、DataEngineVectorFindRequestSchema.fields。空数组和空白成员都会被拒,拒绝文案与兄弟声明逐字相同。规则只有一处来源,是挪过来的,不是复制。
  • 为什么改:其余已发布的投影声明都拒空投影,只有这四处还放行;读这些 schema 生成客户端或写代码的 AI 会学到相反的规则。
  • 风险与回滚:仓内没有任何运行时用这四份 schema 解析请求,线上请求零变化。会受影响的只有仓外直接用这些导出 schema 解析、且发了空投影的代码。进程内 engine.find(obj, { fields: [] }) 仍读全部列,已另立卡 objectql: the in-process engine reads fields: [] as every column while every published projection declaration refuses an empty projection (found on #26954) #27008。回滚 = revert 本 PR。
  • 先看:packages/spec/src/data/data-engine.zod.ts 的 transportProjectionArray(约 :106),以及它被挪到 EngineQueryOptionsSchema 之上的原因:非 lazy 的 .extend 在模块加载时就会执行。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/data-engine.test.ts,预期全绿;投影相关 44 项,覆盖四个成员的空数组、空白成员和路径。
  • 需要你做的:无,席位落地。

席位意见:接受。契约复核 PASS(6458779713)。复核提出的两个提示已处理:PR 描述里 Skills-docs: none 的理由原先写错了,已改写(技能只教非空的 fields);数据块注释说「另行跟踪」却没有对应的卡,已立卡 #27028。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-28T02:16Z

Head: ba84865301c609c8167b187aebdf0109f34dbd37 · card #26954 · claim 6456968829 (seat ruling: align, do not retire)

  1. 形态: a draft PR to main. Its only closing keyword is Fixes #26954. It touches 3 paths, +163 / −35: the declarations, their tests and one @objectstack/spec changeset, all inside the claim surface. check-governed-merges --pr 27007 reports NOT governed, 198 lines, so the ordinary queue landing applies. The assignee is os-support-ai.

  2. 前提实测: the premise holds. Before this PR, the four members admitted [] while fix(spec)!: transport select / $select and GetDataRequestSchema.select declare the projection the doors accept #26949's transport siblings refused it. The review read the head:

  3. 门禁 check-runs (on this head): 55 runs, 42 success and 13 skipped (path filters and opt-ins), none red or pending, read at 2026-10-28T02:16Z. Contract review PASS on this head (6458779713). The dev's runs on this head:

    • spec: 819 files / 23498 tests, plus typecheck;
    • check:generated: 14/14;
    • dispatch gates: 80 derived, 80 run, 80 exit 0, 0 NOT-MEASURED;
    • full dogfood (the narrowing arm): 344 files / 2784 tests green.

    The ablations went red for every new arm: 12 / 9 / 3 / 6 failures, equal to each arm's case count.

  4. 接受的偏差: (a) Clause-②: no (narrowing) and Metadata-protocol: none, because these are transport and engine declarations, nothing authorable. ADR-0087: not-required. @objectstack/spec takes a patch. (b) The two siblings DataEngineQueryOptionsSchema.select and DataEngineVectorFindRequestSchema.fields were not named on the card. The seat folded them into this PR, because they are the same family in the same file under the same ruling. (c) The four members carry no .describe(), so their reference rows stay silent on the accept set. That is an observation, not a false sentence.

  5. 范围外去向:

  6. 落地条款: the prose faces were checked against the diff on this head:

    • The PR body was applied by the seat. Round 2 landed at 2026-10-28T02:00Z, and the Skills-docs: reason was rewritten at 2026-10-28T02:14Z: the skills teach only a non-empty engine fields projection (skills/objectstack-query/rules/filters.md:105, SKILL.md:25), so no skill sentence goes false. Both were read back byte-identical.
    • The changeset and the tests were also checked.

    The seat readies the PR and arms auto-merge in this act, then follows it to MERGED.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40621685239 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (8/8) — 失败步骤: Run this shard's tests

    @objectstack/rest:test:  FAIL   local  src/data-nested-object-door.test.ts > [#20802] the nested-relation form at the public door — sqlite > the cap: a condition matching more related records than the
      ↳ 失败原因: @objectstack/rest:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 15 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40623580358 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (8/8) — 失败步骤: Run this shard's tests

    @objectstack/rest:test:  FAIL   local  src/data-nested-object-door.test.ts > [#20802] the nested-relation form at the public door — sqlite > the cap: a condition matching more related records than the
      ↳ 失败原因: @objectstack/rest:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 17 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40625584826 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (8/8) — 失败步骤: Run this shard's tests

    @objectstack/rest:test:  FAIL   local  src/data-nested-object-door.test.ts > [#20802] the nested-relation form at the public door — sqlite > the cap: a condition matching more related records than the
      ↳ 失败原因: @objectstack/rest:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 2 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 22 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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