Skip to content

fix(objectql)!: the in-process engine refuses an empty projection, as every published declaration does - #27161

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

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-y7p9hd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #27008

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: updated here — content/docs/data-modeling/queries.mdx (Field Selection) says an empty fields is refused on the data API and by engine.find() / findOne(), top level and expand alike; skills/** teaches fields only as a non-empty list and needs no change

维护者速读

  • 改了什么:引擎进程内读入口 ObjectQL.find / findOne 遇到空投影 fields: [](以及任意层 expand 条目里的 fields: [])时直接拒绝,答 400 VALIDATION_ERROR,与 REST 门对 select: [] 的回答相同;措辞直接取自 spec 声明 EngineQueryOptionsSchema.fields("Name at least one field"),不另写一份。
  • 为什么改:HTTP 门与所有已发布的投影声明都已拒绝空投影,唯独进程内引擎把 [] 当成"不投影",插件、钩子、流程误传空列表时会悄悄拿回全部列(含 owner、组织、审计列)。一条规则:空投影被拒,永不读作全部列。
  • 风险与回滚:低。仓内非测试调用方 0 个传空投影;不传 fields 仍读全部列,非空投影照旧。回滚即 revert。
  • 先看:packages/objectql/src/empty-projection-door.ts(拒绝本体与 expand 递归);packages/objectql/src/engine.ts 两处入口调用;packages/objectql/src/engine-empty-projection-refusal.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-empty-projection-refusal.test.ts,预期 10/10 通过;在 main 上同文件 6 条失败(find/findOne/系统上下文/expand 两层/REST 对等)。
  • 需要你做的:无,席位落地。

What changes

packages/objectql/src/empty-projection-door.ts (new) holds assertProjectionNamesAField, called at the entry of ObjectQL.find and ObjectQL.findOne, right after the option-key gate and before any driver read:

Read main This head
find(obj, { fields: [] }) every column, owner / organization / audit included, no error 400 VALIDATION_ERROR, no driver read
findOne(obj, { where, fields: [] }) every column refused the same way
find(obj, { fields: [], context: { isSystem: true } }) every column refused: the rule is about the shape, not the caller
expand: { rel: { fields: [] } }, at any depth the related record's every column refused at the entry, naming the path (expand['rel'].fields)
fields absent, or null every column unchanged
fields: ['title'] projects unchanged
  • The declarations' words, not a copy. The sentence's remedy is read off the published schema, EngineQueryOptionsSchema.shape.fields.safeParse([]), whose issue message is the spec's PROJECTION_NAMES_NO_FIELD ("Name at least one field"). The constant stays module-private in data-engine.zod.ts; no spec change.
  • The doors' answer. The error carries status: 400 and code: 'VALIDATION_ERROR', the shape projectionFieldsValue throws at the HTTP doors, so mapDataError serves both callers the same status and code. The test pins it against findData({ query: { select: [] } }) on the same engine.
  • Why expand is judged at the entry. An entry's projection is forwarded to the related object's own find, which now refuses it, but expandRelatedRecords catches a sub-read failure and keeps the bare foreign key. A refusal raised only there would reach the caller as a silently unexpanded field. Judged at the parent's entry, it reaches the caller as a refusal.
  • Family measured. count and aggregate take no projection (fields is not in ENGINE_COUNT_OPTION_KEYS / ENGINE_AGGREGATE_OPTION_KEYS; the key gate refuses it). DataEngineVectorFindRequestSchema has no engine implementation (vectorFind is declared in spec/contracts only). The scoped repository's find / findOne delegate to the engine's.
  • engine.ts gains 5 lines (an import and the two calls); the body lives in its own module, in the direction of the [maintainer] refactor(objectql): split src/engine.ts (23,903 lines, +7,501 in 30 days) along its ObjectQL seams, one move-only slice per PR #25522 split.

Why

Card #27008, triage 6458590093: "the engine refuses an empty projection with the declarations' own refusal. One rule (#26811's line: 'an empty projection is refused, never read as every column')." #26817, #26909 and #26954 (PR #27007) made every published declaration of a read's projection refuse []. The in-process engine never parses its options with those schemas, and its projection guards ran only when ast.fields.length > 0, so the empty list reached the driver and every driver served it as no projection.

Tests

New packages/objectql/src/engine-empty-projection-refusal.test.ts, 10 cases, on a real ObjectQL over a driver double that projects exactly (an empty list answers every column, as SqlDriver and InMemoryDriver do) and records each read:

  • Refusals, each asserting mapDataError's status 400 and code VALIDATION_ERROR, the declaration's words in the message, and zero driver reads: find, findOne, a system-context find, an expand entry, a second-level expand entry.
  • Parity: the REST door's findData({ select: [] }) and the engine's refusal map to the same status and code.
  • Controls: absent fields reads every column; fields: null reads every column; fields: ['title'] projects on find and findOne; an expand with a named nested projection still expands.
Red on main, green on this head, ablation

Red leg, the new file against origin/main 07d18e69 engine source (before the fix commit):

Tests  6 failed | 4 passed (10)   — every refusal case: "expected a refusal, got an answer"
probe on the same base:
  find  fields: []  → [{"id":"t1","title":"Ship","project_id":"p1","owner_id":"usr_1","organization_id":"org_1","created_at":"…"}]
                      driver received fields: []
  findOne fields: [] → the same row, every column
  expand { project_id: { fields: [] } } → project_id: {"id":"p1","name":"Apollo","owner_id":"usr_1","organization_id":"org_1"}
  REST door select: [] → 400 {"code":"VALIDATION_ERROR","error":"Query parameter 'select' names no field; got []. …"}

This head: Tests 10 passed (10).

Ablation, one leg, through scripts/ablation-replace.mjs (the test imports ./engine.js, so the mutation is on the resolved source): the top-level arm of assertProjectionNamesAField replaced by a marker; anchor 1 → 0, blob 19c6bcd1 → 45086f3d. Tests 4 failed | 6 passed (10): the find, findOne, system-context and REST-parity cases; the two expand cases stay green on their own arm. Restored: blob == HEAD (19c6bcd1), git diff HEAD empty.

Acceptance notes

  • Caller census, at this head. Non-test callers across packages, ui, examples, apps and skills that pass fields: [] to an engine read: 0. Computed projections handed to find / findOne (37 call sites by a call-text scan; blind to an options object built away from the call): every one is a non-empty literal or constant, or is guarded non-empty (summaryKeyColumns.length > 0). FLS does not rewrite ast.fields (it masks rows), and the two middlewares that touch ast.fields (approvals' reader-locale labels, audit's served fields) only add columns. One pass-through: the flow get_record node hands its authored cfg.fields to the engine unchanged (crud-nodes.ts), and GetRecordConfigSchema.fields is z.array(z.string()).optional(), which accepts []. No in-repo flow authors it. Such a node read every column before; it now fails with the engine's refusal (success: false, the refusal carried by refusalCarriedBy). That answer is loud and deliberate, not a widening; the declaration gap is reported to the seat (see the report).
  • Not changed: an all-unknown projection still reads every column in-process. engine.find(obj, { fields: ['nope'] }) drops the unknown plain column (the finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 tolerance, kept by ruling) and then falls back to * when nothing is left: measured at this head, the driver receives no fields and every column comes back. The REST door refuses the same request 400 INVALID_FIELD. This is outside the ruling here (it is not an empty projection from the caller) and touches the finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 ruling, so it goes to the seat as an open question.
  • expand's nested declaration. EngineQueryOptionsSchema.expand is z.record(QuerySchema), and QuerySchema.fields (query.zod.ts) is z.array(FieldNodeSchema).optional(), which still accepts []. The engine now refuses what that declaration admits. Reported to the seat as a spec follow-up rather than edited here (outside the claim's file surface).
  • Clause-②: no (narrowing), as claimed: a shape that read before is refused; nothing is widened. Metadata-protocol: none: no authored metadata key is added, removed or re-shaped, and no save or publish door refuses anything new.

Verification

All at head 2e447bbd unless a line says otherwise.

Local runs
  • pnpm --filter '@objectstack/objectql...' build: VERDICT command-exit 0.
  • pnpm --filter @objectstack/objectql build && typecheck && test at 26ebfb48: VERDICT command-exit 0; Test Files 477 passed (477), Tests 8898 passed (8898). The two later commits change only the new suite's driver double; that file at this head: Tests 10 passed (10).
  • pnpm build (full, at 26ebfb48; no source file changed after it): VERDICT command-exit 0, Tasks 116 successful, 116 total.
  • eslint . --no-inline-config scoped to packages/objectql: exit 0.
  • Full dogfood (pnpm --filter @objectstack/dogfood test, owed by the narrowing arm): VERDICT command-exit 0; Test Files 348 passed | 3 skipped (351); Tests 2803 passed | 20 skipped (2823).
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 93 commands for the 5 changed paths, all run at this head. --ran reconciliation: "93 derived famil(ies) accounted for — 93 run, 0 NOT-MEASURED". Every one exited 0. check-plugin-teardown-shape.mjs --self-test ran after git fetch --depth=1 of its pinned fixture commit; check:type-check-debt ran under the verify lock (VERDICT command-exit 0).

The first sweep (at 26ebfb48) had two reds, both in the new suite's driver double, fixed in a446baef and 2e447bbd: check:objectql-double-limit (the double ignored limit; it now applies it after the filter) and check:where-matcher (an unhandled $ key was read as a field name; the double now throws on it).

Hand-run beside the derived list: check-platform-checklist.mjs --changed-from origin/main, check-commit-card-trailers.mjs --range origin/main..HEAD, check-nul-bytes.mjs: exit 0 each.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (scoped to packages/objectql; the repo-wide run is CI's).
  • 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. (Not touched.)
  • 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_01FEkQ5mbUpy4ebfWUHxAv87


Generated by Claude Code

…shed declaration does

ObjectQL.find / findOne read fields: [] as every column, and an expand
entry's fields: [] the same way, while the HTTP doors and
EngineQueryOptionsSchema.fields refuse it. The entry now refuses it with
the declaration's own words and the doors' 400 VALIDATION_ERROR, before
any driver read. An absent fields still reads every column.

Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87
Co-authored-by: Claude <noreply@anthropic.com>
…ombinator it does not implement

Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/service-automation, touching 8 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/services/service-automation/src/builtin/crud-nodes.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json b8615afaba7a9e6511b601ebc9f15deededea5b0.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-automation/src/builtin/crud-nodes.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 56 of 208 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 — 22 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 b8615afaba7a9e6511b601ebc9f15deededea5b0 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 b8615afaba7a9e6511b601ebc9f15deededea5b0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ine's refusal the node now carries

The engine refuses an empty projection (#27008), so a get_record node
authored with fields: [] fails with that refusal instead of reading the
whole row. The control for an absent fields stays; the crud-nodes
comment says what the empty list now meets.

Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 21ac7e6a13b8562f78e7a88f432e22eedf709702
Local-runs: none
Scope: packages/objectql/src/empty-projection-door.ts, packages/objectql/src/engine.ts, packages/objectql/src/engine-empty-projection-refusal.test.ts, packages/services/service-automation/src/builtin/crud-nodes.ts, packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts + packages/spec/src/data/data-engine.zod.ts, packages/spec/src/data/query.zod.ts, packages/spec/src/automation/builtin-node-config.zod.ts, packages/metadata-protocol/src/protocol.ts, packages/types/src/data-error-classification.ts

Face: narrowing (Clause-②: no (narrowing) on the PR body and the changeset). Inputs: card #27008 (body, triage 6458590093, claim 6462987453, os-dev-report 6464754246, claim amendment 6465140445), PR #27161 body and file list, git diff origin/main...pr27161, and the 49 check-runs on this head: every required run success, the rest skipped by path filter; none failed. The Test Core (8/8) red the amendment named on 2e447bbd is green on this head, which carries the amendment's test fix (21ac7e6a).

① Derived judgments

  1. ObjectQL.find / findOne refuse fields: [] with status: 400, code: 'VALIDATION_ERROR', before any driver read — right. assertProjectionNamesAField is called at both entries (engine.ts ≈13523 and ≈13853) after foldEngineOptionAliases and rejectUnknownEngineOptions and before lowerWhereFilterArray and getDriver; fields and expand are both members of ENGINE_FIND_OPTION_KEYS (≈685), so the key gate never pre-empts the door. The status and code are the ones projectionFieldsValue (protocol.ts ≈4655) throws at the HTTP doors, so mapDataError answers both callers alike; the suite's REST-parity case pins it on the same engine.
  2. The refusal's remedy words are read off the published declaration, not copied — right. projectionNamesNoField() memoises EngineQueryOptionsSchema.shape.fields.safeParse([]).error.issues[0].message, which is PROJECTION_NAMES_NO_FIELD from transportProjectionArray(...).min(1, ...) (data-engine.zod.ts ≈106–140). No spec edit, no second constant: the claim's "⛔ no second copy" line holds. The prefix text (names no field in ...: got []. The query was not run: an empty projection was read as no projection, which answers every column.) mirrors the door's sentence by design, and is the engine's own, not a copy of the constant.
  3. Absent and null fields still read every column; a non-empty projection and the finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 unknown-plain tolerance are unchanged — right. The door tests Array.isArray(query.fields) && query.fields.length === 0 only; the filtered.length > 0 ? filtered : undefined fallback (≈13597 / ≈13900) is untouched. The claim's "⛔ No change to what an absent fields means" holds; three controls pin it.
  4. An expand entry's fields: [], at any depth, is refused at the parent's entry — right, and the placement is the correct one. expandRelatedRecords (≈12590 ff.) resolves the sub-read through this.find inside a try whose catch retains the bare FK id ("A refusal is caught below and the bare FK id is retained"), so a refusal raised only in the sub-read would reach the caller as a silently unexpanded field. assertExpandProjections walks expand[rel].fields and recurses into expand[rel].expand, naming the path; non-object entries are skipped (the engine's expand is z.record(z.string(), QuerySchema), ≈198, so no other shape reaches it). The two suites' expand cases pin one and two levels.
  5. A system-context read is refused too — right. The door runs before any context.isSystem branch; the rule is about the shape. Pinned.
  6. The family is closed — right. count and aggregate take no fields (not in their option-key sets; the key gate refuses it); vectorFind has no engine implementation; the scoped repository's reads delegate to these two entries. The REST doors already refused select: [] before the engine, so the two gates agree rather than compete.
  7. get_record hands an authored fields: [] to the engine unchanged and now fails with the refusal carried — right, and the claim amendment's ⛔ holds. projectionNamingId (crud-nodes.ts ≈316) is byte-identical on main and this head: [] passes through (fields.length === 0 returns fields); only the docblock changed. The node's catch (≈556–566) answers success: false with refusalCarriedBy(err), so innerRefusal.code is VALIDATION_ERROR; the rewritten integration case pins exactly that, through the real SqlDriver stack. No [] → undefined normalisation, no consumer-side tolerance (Prime Directive Add comprehensive test suite for Zod schema validation #12).
  8. The amended integration test keeps one case per shape, none deleted — right. The for loop over ['no fields', 'empty fields'] became two its: the no fields control unchanged in substance, the empty fields case re-pinned from "whole-row read" to "refusal carried". Count 2 → 2; no assertion lost without a replacement at the same layer.
  9. Caller census re-taken at this head — right. git grep 'fields: \[\]' over packages ui examples apps skills excluding tests: every hit is a result/declaration shape (analytics result rows, security field-reader answers, screen-node definitions, an orphan-sweep finding row), none an engine read option. The narrowing breaks nothing shipped in-repo.
  10. The new suite's driver double is faithful to the measured drivers — right. It projects exactly and serves [] as every column (as SqlDriver and InMemoryDriver do), applies limit, throws on an unimplemented $ combinator, and counts reads, so "zero driver reads" is measured. The two intermediate commits fixing the double answered the check:objectql-double-limit / check:where-matcher gates, both green here.

Doubtful, not wrong: the error message's param for the top level is spelled 'fields' with literal quotes while the expand path is expand['rel'].fields without them. Cosmetic; both tests match their own form; no reader parses param.

Nothing in ① is wrong.

② Semver level

  • .changeset/27008-engine-empty-projection-refused.md: '@objectstack/objectql': patch, BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: none, and exactly one ADR-0087 marker (not-required (no-migration-prescription)). This matches the diff: an accept-set narrowing at a runtime entry, no key of any schema added, removed, renamed or re-shaped, no export or type change. Per AGENTS.md § Post-Task Checklist 3, a non-protocol change, widening or breaking, ships patch with its banner and disposition — right. The remedy sentence ("Leave fields out ... or name the columns") is the migration a narrowing owes. Check Changeset is green.
  • Clause-②: no (narrowing) on the PR body and in the changeset body, both line-initial — right. Nothing in the diff widens.
  • Metadata-protocol: none — right at this head and as the amendment ruled: no save or publish door refuses anything new; GetRecordConfigSchema.fields and the nested QuerySchema.fields still accept [] (the run-time consequence for a flow author is the success: false carried refusal, judged in ① 7). The declaration gap is carried on spec: GetRecordConfigSchema.fields and nested QuerySchema.fields (expand) still accept an empty projection the engine refuses #27166 (open, pm:queue, domain:server), not here.
  • @objectstack/service-automation: no changeset. The diff there is a docblock and a test; projectionNamingId is unchanged, so the package publishes no behaviour of its own. The behaviour a flow sees arrives through the @objectstack/objectql dependency, and that changeset's "Who sees it" names flows. Acceptable; Check Changeset agrees.
  • Skills-docs: line, owed by triage for the narrowing arm: present (content/docs/data-modeling/queries.mdx, a prose surface outside the at-tier scope; its sentence agrees with ① 1 and ① 4).

③ Boundary flags

Implemented-by: claude/project-thread-y7p9hd
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-28T09:59Z · head 21ac7e6a · #27008

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:

    • A draft against main, assignee os-project-manager. Fixes #27008, Clause-②: no (narrowing), Metadata-protocol: none and the Skills-docs: line are line-initial.
    • The changeset is a patch on @objectstack/objectql, with the BREAKING banner the narrowing arm owes and the ADR-0087 not-required marker.
    • check-governed-merges --pr 27161 reads NOT governed, 342 changed lines.
    • The claim (6462987453) names this PR's branch. The diff stays inside its surface: one new module, two entry calls in engine.ts, a test file, the docs line and the changeset.
  2. Against the order:

    • ObjectQL.find and findOne refuse fields: [] at their entry, before any driver read, with 400 VALIDATION_ERROR, the code the HTTP doors throw.
      • One source of words: the remedy is read off EngineQueryOptionsSchema.shape.fields.safeParse([]), the spec's PROJECTION_NAMES_NO_FIELD. There is no second copy and no spec edit.
      • The call sits after the alias fold, so an aliased projection is judged too.
    • An expand entry's fields: [] is refused at the parent's entry at any depth, because expandRelatedRecords swallows a sub-read throw. A system-context read is refused too.
    • Absent or null fields and a non-empty projection are unchanged.
    • The two owed items for the narrowing arm are present:
      • the Skills-docs: line, with a queries.mdx sentence;
      • a re-census showing 0 non-test callers pass []. Of 37 computed projections, every one is a non-empty literal or is guarded.
    • Full dogfood ran green.
  3. Red first and controls (the dev's):

    • The new suite is 6 red of 10 on the base engine and 10 of 10 on this head.
    • The ablation of the top-level arm reds 4 cases (find, findOne, system context, REST parity), and the restore matches the HEAD blob.
    • REST parity is pinned through findData({ select: [] }) and mapDataError.
  4. Gates:

    • Locally 93 of 93 derived families exit 0, with 0 NOT-MEASURED, plus a full build of 116 tasks, the objectql suite (8,898 tests) and dogfood (2,803 tests).
    • CI on head 21ac7e6a: 37 success, 12 skipped, 0 red.
  5. Rework after the first CI round (claim amendment 6465140445):

    • On 2e447bbd, Test Core (8/8) was red in service-automation. get-record-id-projection.integration.test.ts still pinned a get_record node's fields: [] reading the whole row.
    • 21ac7e6a turns that case into a pin of the refusal the node now carries: the projection is handed over once, unchanged; the node answers success: false with innerRefusal.code VALIDATION_ERROR and the engine's sentence; and no output.
    • It restates the matching crud-nodes.ts comment, text only. The node has no [] → undefined tolerance.
  6. Contract review: PASS at CONTRACT_REVIEW_TIER on this head (narrowing face, 6465986975).

  7. Overlap: git merge-tree reports no conflict against main b322b5da.

  8. Follow-ups, filed for triage:

    Neither blocks this landing.


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

Projects

None yet

2 participants