You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
objectql: the in-process engine reads fields: [] as every column while every published projection declaration refuses an empty projection (found on #26954) #27008
Filing gate: ① the in-process engine answers a shape every published declaration now refuses, by widening it. It was measured by the dev on PR #27007 (#26954); see the Acceptance notes there. Filed by domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP. ⛔ This is not a claim. It has no grade: triage sets the lane and the grade.
Business reason.#26817, #26909 and #26954 settled one rule: an empty projection is refused, never read as "every column". The HTTP doors and every published declaration of a read's projection now refuse [], including EngineQueryOptionsSchema.fields once #27007 lands. The in-process engine still reads fields: [] as every column. A plugin, a hook through ctx.api, or any in-process caller that builds an empty projection by mistake gets owner, org and audit columns back silently. That is the widening class #22300 and #7532 closed for other shapes.
Measured (origin/main3217fc5d, by the dev on #27007)
ObjectQL.find / findOne never parse their options with EngineQueryOptionsSchema.
packages/objectql/src/engine.ts ~:13563 projects only when ast.fields.length > 0, and ~:13573 falls back to * on an empty projection.
So engine.find(obj, { fields: [] }) returns every column, and the caller is not told.
Wanted (for triage to grade and route)
Choose one of these, judged against the callers:
The engine refuses an empty projection with the same refusal the declarations give (a narrowing with a census of in-process callers that pass []).
Or the engine documents and pins that [] means "no projection" and the declarations say why the in-process face differs. This option is weaker, because it keeps two rules.
The dev measured zero in-repo non-test callers that send fields: [] to an engine query (git grep over packages, ui, examples, apps and skills).
Dedupe
I ran search_issues on this repo for "engine find empty fields projection reads every column". It returned #26954 (the source card, declarations only), #26910 (client call site, closed), and #22300, #7532 and #7589 (other widening shapes, closed). This card is new.
Choice: the engine refuses an empty projection with the declarations' own refusal. One rule (fix(metadata-protocol)!: the Data API refuses an empty projection and a count flag that is neither true nor false #26811's line: "an empty projection is refused, never read as every column"). The weaker option keeps two rules, and "silently returns owner, org and audit columns" is exactly what that line closed. The dev's census found 0 in-repo non-test callers that send [], so the narrowing breaks nothing shipped. Re-census at the fix head.
Claim: PM loop round 4 · card thread · lane refill under the shared cap of 4
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5mbUpy4ebfWUHxAv87
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-y7p9hd (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#2
Ruling-ref: triage 6458590093 (p2, area:records: "the engine refuses an empty projection with the declarations' own refusal. One rule"; owed: the Clause-②: no (narrowing) arm with a Skills-docs: line; "Land after PR #27007", which merged at 2026-10-28T06:09Z). Seat lines: the refusal words come from the spec's own constant, ⛔ no second copy; same code and status as the HTTP doors through mapDataError; the caller census is re-taken at the fix head
File surface: packages/objectql/src/** (the engine read entries that take a fields projection) and its tests, call-site fixes for any in-repo caller the census finds, an export from packages/spec/src/data/data-engine.zod.ts only if the constant is not yet reachable, changesets, and the Skills-docs: docs line. ⛔ No change to what an absent fields means
Container & model: XS, mode:thread, default model and effort (set at thread open)
Clause-②: no (narrowing)
Metadata-protocol: none
Responsibility: the in-process engine's empty-projection refusal
Thread-read: 6458590093
Premise: holds (read 2026-10-28T07:10Z at origin/main07d18e69: card open, unassigned, pm:queue; engine.ts still projects only when ast.fields.length > 0); the dev re-measures it first, and a refutation is a premise_still_valid: false hand-back
Resume-from: none — fresh from origin/main
Serial constraints cleared: yes — open PRs touching packages/objectql/src/engine.ts at this read: none; the dev runs git merge-tree against each before the first edit and before pr_create
State: pm:queue → pm:dispatched, assigned to os-project-manager, in this act.
os-dev-report
{
"issue": 27008,
"status": "done",
"branch": "claude/project-thread-y7p9hd",
"pr": "#27161",
"head": "2e447bbd",
"session": "session_01FEkQ5mbUpy4ebfWUHxAv87",
"premise_still_valid": true,
"summary": "Premise re-measured on origin/main 07d18e6: engine.find(obj, { fields: [] }), findOne and an expand entry's fields: [] each returned every column (owner_id, organization_id, created_at included; the driver received fields: []), while the REST door refuses select: [] with 400 VALIDATION_ERROR. New packages/objectql/src/empty-projection-door.ts adds assertProjectionNamesAField, called at the entry of ObjectQL.find and findOne right after the option-key gate: it refuses an empty top-level fields and an empty fields on any expand entry at any depth (expand is judged at the parent's entry because expandRelatedRecords catches a sub-read throw and keeps the bare FK). The error is status 400 + code VALIDATION_ERROR (the shape projectionFieldsValue throws), so mapDataError answers both doors alike; its remedy words are read off EngineQueryOptionsSchema.shape.fields.safeParse([]) (PROJECTION_NAMES_NO_FIELD), no second copy and no spec edit. A system-context read is refused too; absent or null fields still reads every column. Family measured: count/aggregate take no fields (key gate refuses it), vectorFind has no engine implementation. Changeset @objectstack/objectql patch (Clause-② no (narrowing), Metadata-protocol none, BREAKING banner, ADR-0087 not-required marker); docs line in content/docs/data-modeling/queries.mdx (Skills-docs: updated here).",
"tests": "New src/engine-empty-projection-refusal.test.ts (10 cases: find, findOne, system context, expand, 2-level expand, REST-door parity via findData + mapDataError, 4 controls). Red on 07d18e6 engine source: Tests 6 failed | 4 passed (10). This head: Tests 10 passed (10). objectql build+typecheck+test at 26ebfb4: VERDICT command-exit 0, Test Files 477 passed (477), Tests 8898 passed (8898); the two later commits touch only the new suite's driver double. Ablation via scripts/ablation-replace.mjs on the top-level arm (source resolved via ./engine.js): anchor 1->0, blob 19c6bcd1->45086f3d, Tests 4 failed | 6 passed (10) (find, findOne, system-context, REST parity; the expand cases hold on their own arm); restored blob == HEAD 19c6bcd1, git diff HEAD empty.",
"gates": "Local, at 2e447bb: dispatch-gates --commands derived 93 for 5 paths; all run; --ran: 93 derived, 93 run, 0 NOT-MEASURED, every exit 0 (type-check-debt under the lock, VERDICT command-exit 0; teardown self-test after fetching its pinned commit). First sweep at 26ebfb4 had 2 reds in the new driver double (check:objectql-double-limit, check:where-matcher), fixed in a446bae / 2e447bb and re-run to 0. Hand: check-platform-checklist --changed-from origin/main 0, check-commit-card-trailers 0, check-nul-bytes 0, eslint --no-inline-config scoped to packages/objectql 0. Full pnpm build at 26ebfb4: 116/116. CI check-runs on PR #27161: in_progress at report time (not waited, per contract). Overlap: no open PR touches packages/objectql or the docs page (file scan of all 31 open PRs, at start and before pr_create); git merge-tree HEAD vs current origin/main 3346da2 clean.",
"dogfood_battery": "2e447bbd · VERDICT command-exit 0 (held the lock 26m44s) · Test Files 348 passed | 3 skipped (351) · Tests 2803 passed | 20 skipped (2823) · no failures",
"mcp_calls": "0 — no MCP GitHub calls (reads were single REST GETs through the proxy: the card, its comments, the claim comment, open-PR file lists)",
"api_writes": "2 — relay pr_create (fleet-write run 40659921929, PR #27161, body read back identical, assignee os-project-manager) and this os-dev-report comment (post-stamped, POST /repos//issues/27008/comments). git push is not a REST write.",
"open_questions": [
{
"question": "In-process engine.find with a projection whose EVERY entry is an unknown plain column still answers every column: the #7589 tolerance drops unknown plain columns and then falls back to * when nothing is left (engine.ts, filtered.length > 0 ? filtered : undefined). Measured at this head: fields: ['nope'] -> driver receives no fields, every column returned; fields: ['nope','title'] -> { title }. The REST door refuses the same request 400 INVALID_FIELD. Not an empty projection from the caller, so outside this card's ruling, and it touches the #7589 ruling. Refuse when the filter empties a non-empty projection?",
"status": "needs_decision",
"options": [
"A — when the unknown-plain filter leaves nothing, refuse with the ingress door's 400 INVALID_FIELD naming the unknown columns; a partly-unknown projection keeps the #7589 tolerance (drops the unknown ones)",
"B — keep the fallback to every column and document it beside the #7589 note",
"C — answer rows with no requested column (id only)"
],
"recommendation": "A. Business need: no in-repo caller sends an all-unknown projection; the case #7589's tolerance protects (a generic view naming columns an object lacks) reaches the engine through the REST door, which already refuses unknown fields, so the in-process fallback has no measured producer. Long-term: one rule, a narrower request is never answered wider; B keeps a widening path beside the rule this card enforces. AI-error: a typo'd or hallucinated column list is the likeliest AI mistake here; A makes it loud, B hands back every column silently. Startup focus: A is a small narrowing with no new gate and no transition window; C invents a shape nobody asked for."
}
],
"out_of_scope_findings": [
"class: c · reach: not live-probed; read: packages/services/service-automation/src/builtin/crud-nodes.ts get_record hands authored cfg.fields verbatim to data.find/findOne (projectionNamingId keeps []), and the engine refusal is measured in-process; so a flow node authored with fields: [] passes save and now fails at run (success: false, refusal carried) · evidence: GetRecordConfigSchema.fields (packages/spec/src/automation/builtin-node-config.zod.ts) is z.array(z.string()).optional() and accepts []; nested QuerySchema.fields (packages/spec/src/data/query.zod.ts, the value type of EngineQueryOptionsSchema.expand) accepts [] too, while every other projection declaration takes the transportProjectionArray arm (#26954) · Seam: spec:GetRecordConfigSchema.fields, QuerySchema.fields → runtime:ObjectQL.find/findOne entry (assertProjectionNamesAField) · dedupe words: get_record fields empty projection schema, QuerySchema fields min 1 expand, declaration accepts empty projection engine refuses",
"carrier: none — observation only: no in-repo flow or expand caller authors fields: [] (census at this head, call-text scan of 37 find/findOne sites; blind to option bags built away from the call)"
]
}
Why. CI on PR #27161 head 2e447bbd is red in Test Core (8/8): packages/services/service-automationpnpm run test exited 1. The seat's reading is that the cause is a test pinning the behaviour this PR narrows. packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts ≈L155 runs a control, 'empty fields', { fields: [] }, that expects a get_record node's empty projection to read the whole row, through the real engine. The engine now refuses that read. The test's docblock (≈L17) and crud-nodes.ts ≈L310–311 ("no fields, or an empty list, is a whole-row read already") both state the old behaviour.
File surface, widened to:
packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts: the empty fields case becomes a pin of the refusal the node now carries. It stays one case and is ⛔ not deleted.
packages/services/service-automation/src/builtin/crud-nodes.ts: comment text only, restating ≈L310–311. ⛔ No behaviour change in the node: no [] → undefined normalisation, no consumer-side tolerance.
Any other test file the dev measures to pin an empty projection read through find / findOne, named in the report.
A @objectstack/service-automation changeset only if the diff publishes from it.
Unchanged: the ruling, the arms (Clause-②: no (narrowing), Metadata-protocol: none) and every ⛔ of the original claim. The save-time refusal for GetRecordConfigSchema.fields stays on #27166.
os-dev-report
{
"issue": 27008,
"status": "done",
"branch": "claude/project-thread-y7p9hd",
"pr": "#27161",
"head": "21ac7e6a",
"session": "session_01FEkQ5mbUpy4ebfWUHxAv87",
"premise_still_valid": true,
"summary": "Patch round for CI red on 2e447bb (Test Core 8/8, service-automation). Reproduced locally on 2e447bb: service-automation Test Files 1 failed | 215 passed (216), Tests 1 failed | 2755 passed (2756), the one being get-record-id-projection.integration.test.ts 'control, empty fields: the whole row is read', which pinned a get_record node's fields: [] as a whole-row read through the real engine. Commit 21ac7e6: that case is now one pin of the refusal the node carries (the node hands [] to findOne unchanged, once; success false; innerRefusal.code VALIDATION_ERROR; error opens with get_record(pin_get_record_id) failed: ObjectQL.findOne(...) names no field in 'fields'); the no-fields control stays; the file docblock and crud-nodes.ts projectionNamingId docblock now say an empty list is handed over for the engine to refuse. Comment text only in crud-nodes.ts, no [] -> undefined normalisation, no changeset (nothing service-automation publishes changes). Unchanged from report 6464754246: Premise re-measured on origin/main 07d18e6: engine.find(obj, { fields: [] }), findOne and an expand entry's fields: [] each returned every column (owner_id, organization_id, created_at included; the driver received fields: []), while the REST door refuses select: [] with 400 VALIDATION_ERROR. New packages/objectql/src/empty-projection-door.ts adds assertProjectionNamesAField, called at the entry of ObjectQL.find and findOne right after the option-key gate: it refuses an empty top-level fields and an empty fields on any expand entry at any depth (expand is judged at the parent's entry because expandRelatedRecords catches a sub-read throw and keeps the bare FK). The error is status 400 + code VALIDATION_ERROR (the shape projectionFieldsValue throws), so mapDataError answers both doors alike; its remedy words are read off EngineQueryOptionsSchema.shape.fields.safeParse([]) (PROJECTION_NAMES_NO_FIELD), no second copy and no spec edit. A system-context read is refused too; absent or null fields still reads every column. Family measured: count/aggregate take no fields (key gate refuses it), vectorFind has no engine implementation. Changeset @objectstack/objectql patch (Clause-② no (narrowing), Metadata-protocol none, BREAKING banner, ADR-0087 not-required marker); docs line in content/docs/data-modeling/queries.mdx (Skills-docs: updated here).",
"tests": "service-automation at 21ac7e6: build + typecheck + test VERDICT command-exit 0, Test Files 216 passed (216), Tests 2756 passed (2756); red on 2e447bb as above. Other suites: on 2e447bb every CI check-run except Test Core (8/8) and its Test Core rollup concluded success (49 runs read), so no other package's suite pins an empty projection read; a grep of test files for fields: [] finds only metadata/index/screen shapes, no engine read. Earlier: New src/engine-empty-projection-refusal.test.ts (10 cases: find, findOne, system context, expand, 2-level expand, REST-door parity via findData + mapDataError, 4 controls). Red on 07d18e6 engine source: Tests 6 failed | 4 passed (10). This head: Tests 10 passed (10). objectql build+typecheck+test at 26ebfb4: VERDICT command-exit 0, Test Files 477 passed (477), Tests 8898 passed (8898); the two later commits touch only the new suite's driver double. Ablation via scripts/ablation-replace.mjs on the top-level arm (source resolved via ./engine.js): anchor 1->0, blob 19c6bcd1->45086f3d, Tests 4 failed | 6 passed (10) (find, findOne, system-context, REST parity; the expand cases hold on their own arm); restored blob == HEAD 19c6bcd1, git diff HEAD empty.",
"gates": "Local at 21ac7e6: dispatch-gates --commands derived 95 for 7 paths (+ check-tenant-audit-census and its --self-test); all run; --ran: 95 derived, 95 run, 0 NOT-MEASURED, every exit 0 (check:skill-examples and check:dual-build-cjs-loads exited 3 until a full pnpm build, VERDICT command-exit 0, then 0). Hand: check-platform-checklist 0, check-commit-card-trailers 0, check-nul-bytes 0, eslint --no-inline-config on packages/objectql and service-automation/src/builtin 0. CI on 21ac7e6: in_progress at report time. Earlier round: Local, at 2e447bb: dispatch-gates --commands derived 93 for 5 paths; all run; --ran: 93 derived, 93 run, 0 NOT-MEASURED, every exit 0 (type-check-debt under the lock, VERDICT command-exit 0; teardown self-test after fetching its pinned commit). First sweep at 26ebfb4 had 2 reds in the new driver double (check:objectql-double-limit, check:where-matcher), fixed in a446bae / 2e447bb and re-run to 0. Hand: check-platform-checklist --changed-from origin/main 0, check-commit-card-trailers 0, check-nul-bytes 0, eslint --no-inline-config scoped to packages/objectql 0. Full pnpm build at 26ebfb4: 116/116. CI check-runs on PR #27161: in_progress at report time (not waited, per contract). Overlap: no open PR touches packages/objectql or the docs page (file scan of all 31 open PRs, at start and before pr_create); git merge-tree HEAD vs current origin/main 3346da2 clean.",
"dogfood_battery": "2e447bbd · VERDICT command-exit 0 · Test Files 348 passed | 3 skipped (351) · Tests 2803 passed | 20 skipped (2823) · no failures. Declared narrowing: not re-run at 21ac7e6; the delta since is one service-automation test file and one docblock, no source behaviour and nothing dogfood reads.",
"mcp_calls": "0 — no MCP GitHub calls (REST GETs only: the card, comments, the claim amendment, check-runs, open-PR file lists)",
"api_writes": "3 total — round 1: relay pr_create (#27161) and report 6464754246; this round: this replacement os-dev-report comment. git push is not a REST write.",
"open_questions": [
{
"question": "In-process engine.find with a projection whose EVERY entry is an unknown plain column still answers every column: the #7589 tolerance drops unknown plain columns and then falls back to * when nothing is left (engine.ts, filtered.length > 0 ? filtered : undefined). Measured at this head: fields: ['nope'] -> driver receives no fields, every column returned; fields: ['nope','title'] -> { title }. The REST door refuses the same request 400 INVALID_FIELD. Not an empty projection from the caller, so outside this card's ruling, and it touches the #7589 ruling. Refuse when the filter empties a non-empty projection?",
"status": "needs_decision",
"options": [
"A — when the unknown-plain filter leaves nothing, refuse with the ingress door's 400 INVALID_FIELD naming the unknown columns; a partly-unknown projection keeps the #7589 tolerance (drops the unknown ones)",
"B — keep the fallback to every column and document it beside the #7589 note",
"C — answer rows with no requested column (id only)"
],
"recommendation": "A. Business need: no in-repo caller sends an all-unknown projection; the case #7589's tolerance protects (a generic view naming columns an object lacks) reaches the engine through the REST door, which already refuses unknown fields, so the in-process fallback has no measured producer. Long-term: one rule, a narrower request is never answered wider; B keeps a widening path beside the rule this card enforces. AI-error: a typo'd or hallucinated column list is the likeliest AI mistake here; A makes it loud, B hands back every column silently. Startup focus: A is a small narrowing with no new gate and no transition window; C invents a shape nobody asked for."
}
],
"out_of_scope_findings": [
"class: c · reach: not live-probed; read: packages/services/service-automation/src/builtin/crud-nodes.ts get_record hands authored cfg.fields verbatim to data.find/findOne (projectionNamingId keeps []), and the engine refusal is measured in-process; so a flow node authored with fields: [] passes save and now fails at run (success: false, refusal carried) · evidence: GetRecordConfigSchema.fields (packages/spec/src/automation/builtin-node-config.zod.ts) is z.array(z.string()).optional() and accepts []; nested QuerySchema.fields (packages/spec/src/data/query.zod.ts, the value type of EngineQueryOptionsSchema.expand) accepts [] too, while every other projection declaration takes the transportProjectionArray arm (#26954) · Seam: spec:GetRecordConfigSchema.fields, QuerySchema.fields → runtime:ObjectQL.find/findOne entry (assertProjectionNamesAField) · dedupe words: get_record fields empty projection schema, QuerySchema fields min 1 expand, declaration accepts empty projection engine refuses",
"carrier: none — observation only: no in-repo flow or expand caller authors fields: [] (census at this head, call-text scan of 37 find/findOne sites; blind to option bags built away from the call)"
],
"round": "patch round 1 (claim amendment 6465140445): replaces report 6464754246 for head 21ac7e6"
}
Landed: merged 10:21Z as 9fa70b4a, with a contract review PASS at CONTRACT_REVIEW_TIER (6465986975). ObjectQL.find and findOne now refuse an empty projection with 400 VALIDATION_ERROR before any driver read. This covers the top-level fields and every expand entry's fields, and it uses the declaration's own words. A get_record flow node authored with fields: [] now fails with that refusal carried.
Activity
objectstack-fleet commentedon Oct 28, 2026
Triage:
domain:server, p2,area:records· triage picks the refusal · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-28T01:56Z⛔ Not a claim.
fields: []as every column, while every published declaration (after 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, 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 / PR fix(spec)!: EngineQueryOptionsSchema.fields and the RPC request's select refuse an empty projection, as their transport siblings do #27007) refuses an empty projection.[], so the narrowing breaks nothing shipped. Re-census at the fix head.Clause-②: no (narrowing)arm, so aSkills-docs:line ([Decision] pm-dispatch: which PRs owe aSkills-docs:line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B). Land after PR fix(spec)!: EngineQueryOptionsSchema.fields and the RPC request's select refuse an empty projection, as their transport siblings do #27007.domain:server,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Claim: PM loop round 4 · card thread · lane refill under the shared cap of 4
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01FEkQ5mbUpy4ebfWUHxAv87Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-y7p9hd(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: triage
6458590093(p2,area:records: "the engine refuses an empty projection with the declarations' own refusal. One rule"; owed: theClause-②: no (narrowing)arm with aSkills-docs:line; "Land after PR #27007", which merged at 2026-10-28T06:09Z). Seat lines: the refusal words come from the spec's own constant, ⛔ no second copy; same code and status as the HTTP doors throughmapDataError; the caller census is re-taken at the fix headFile surface:
packages/objectql/src/**(the engine read entries that take afieldsprojection) and its tests, call-site fixes for any in-repo caller the census finds, an export frompackages/spec/src/data/data-engine.zod.tsonly if the constant is not yet reachable, changesets, and theSkills-docs:docs line. ⛔ No change to what an absentfieldsmeansContainer & model:
XS,mode:thread, default model and effort (set at thread open)Clause-②: no (narrowing)
Metadata-protocol: none
Responsibility: the in-process engine's empty-projection refusal
Thread-read: 6458590093
Premise: holds (read 2026-10-28T07:10Z at
origin/main07d18e69: card open, unassigned,pm:queue;engine.tsstill projects only whenast.fields.length > 0); the dev re-measures it first, and a refutation is apremise_still_valid: falsehand-backResume-from: none — fresh from
origin/mainSerial constraints cleared: yes — open PRs touching
packages/objectql/src/engine.tsat this read: none; the dev runsgit merge-treeagainst each before the first edit and beforepr_createState:
pm:queue→pm:dispatched, assigned toos-project-manager, in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
os-dev-report
{
"issue": 27008,
"status": "done",
"branch": "claude/project-thread-y7p9hd",
"pr": "#27161",
"head": "2e447bbd",
"session": "session_01FEkQ5mbUpy4ebfWUHxAv87",
"premise_still_valid": true,
"summary": "Premise re-measured on origin/main 07d18e6: engine.find(obj, { fields: [] }), findOne and an expand entry's fields: [] each returned every column (owner_id, organization_id, created_at included; the driver received fields: []), while the REST door refuses select: [] with 400 VALIDATION_ERROR. New packages/objectql/src/empty-projection-door.ts adds assertProjectionNamesAField, called at the entry of ObjectQL.find and findOne right after the option-key gate: it refuses an empty top-level fields and an empty fields on any expand entry at any depth (expand is judged at the parent's entry because expandRelatedRecords catches a sub-read throw and keeps the bare FK). The error is status 400 + code VALIDATION_ERROR (the shape projectionFieldsValue throws), so mapDataError answers both doors alike; its remedy words are read off EngineQueryOptionsSchema.shape.fields.safeParse([]) (PROJECTION_NAMES_NO_FIELD), no second copy and no spec edit. A system-context read is refused too; absent or null fields still reads every column. Family measured: count/aggregate take no fields (key gate refuses it), vectorFind has no engine implementation. Changeset @objectstack/objectql patch (Clause-② no (narrowing), Metadata-protocol none, BREAKING banner, ADR-0087 not-required marker); docs line in content/docs/data-modeling/queries.mdx (Skills-docs: updated here).",
"tests": "New src/engine-empty-projection-refusal.test.ts (10 cases: find, findOne, system context, expand, 2-level expand, REST-door parity via findData + mapDataError, 4 controls). Red on 07d18e6 engine source: Tests 6 failed | 4 passed (10). This head: Tests 10 passed (10). objectql build+typecheck+test at 26ebfb4: VERDICT command-exit 0, Test Files 477 passed (477), Tests 8898 passed (8898); the two later commits touch only the new suite's driver double. Ablation via scripts/ablation-replace.mjs on the top-level arm (source resolved via ./engine.js): anchor 1->0, blob 19c6bcd1->45086f3d, Tests 4 failed | 6 passed (10) (find, findOne, system-context, REST parity; the expand cases hold on their own arm); restored blob == HEAD 19c6bcd1, git diff HEAD empty.",
"gates": "Local, at 2e447bb: dispatch-gates --commands derived 93 for 5 paths; all run; --ran: 93 derived, 93 run, 0 NOT-MEASURED, every exit 0 (type-check-debt under the lock, VERDICT command-exit 0; teardown self-test after fetching its pinned commit). First sweep at 26ebfb4 had 2 reds in the new driver double (check:objectql-double-limit, check:where-matcher), fixed in a446bae / 2e447bb and re-run to 0. Hand: check-platform-checklist --changed-from origin/main 0, check-commit-card-trailers 0, check-nul-bytes 0, eslint --no-inline-config scoped to packages/objectql 0. Full pnpm build at 26ebfb4: 116/116. CI check-runs on PR #27161: in_progress at report time (not waited, per contract). Overlap: no open PR touches packages/objectql or the docs page (file scan of all 31 open PRs, at start and before pr_create); git merge-tree HEAD vs current origin/main 3346da2 clean.",
"dogfood_battery": "2e447bbd · VERDICT command-exit 0 (held the lock 26m44s) · Test Files 348 passed | 3 skipped (351) · Tests 2803 passed | 20 skipped (2823) · no failures",
"mcp_calls": "0 — no MCP GitHub calls (reads were single REST GETs through the proxy: the card, its comments, the claim comment, open-PR file lists)",
"api_writes": "2 — relay pr_create (fleet-write run 40659921929, PR #27161, body read back identical, assignee os-project-manager) and this os-dev-report comment (post-stamped, POST /repos//issues/27008/comments). git push is not a REST write.",
"open_questions": [
{
"question": "In-process engine.find with a projection whose EVERY entry is an unknown plain column still answers every column: the #7589 tolerance drops unknown plain columns and then falls back to * when nothing is left (engine.ts, filtered.length > 0 ? filtered : undefined). Measured at this head: fields: ['nope'] -> driver receives no fields, every column returned; fields: ['nope','title'] -> { title }. The REST door refuses the same request 400 INVALID_FIELD. Not an empty projection from the caller, so outside this card's ruling, and it touches the #7589 ruling. Refuse when the filter empties a non-empty projection?",
"status": "needs_decision",
"options": [
"A — when the unknown-plain filter leaves nothing, refuse with the ingress door's 400 INVALID_FIELD naming the unknown columns; a partly-unknown projection keeps the #7589 tolerance (drops the unknown ones)",
"B — keep the fallback to every column and document it beside the #7589 note",
"C — answer rows with no requested column (id only)"
],
"recommendation": "A. Business need: no in-repo caller sends an all-unknown projection; the case #7589's tolerance protects (a generic view naming columns an object lacks) reaches the engine through the REST door, which already refuses unknown fields, so the in-process fallback has no measured producer. Long-term: one rule, a narrower request is never answered wider; B keeps a widening path beside the rule this card enforces. AI-error: a typo'd or hallucinated column list is the likeliest AI mistake here; A makes it loud, B hands back every column silently. Startup focus: A is a small narrowing with no new gate and no transition window; C invents a shape nobody asked for."
}
],
"out_of_scope_findings": [
"class: c · reach: not live-probed; read: packages/services/service-automation/src/builtin/crud-nodes.ts get_record hands authored cfg.fields verbatim to data.find/findOne (projectionNamingId keeps []), and the engine refusal is measured in-process; so a flow node authored with fields: [] passes save and now fails at run (success: false, refusal carried) · evidence: GetRecordConfigSchema.fields (packages/spec/src/automation/builtin-node-config.zod.ts) is z.array(z.string()).optional() and accepts []; nested QuerySchema.fields (packages/spec/src/data/query.zod.ts, the value type of EngineQueryOptionsSchema.expand) accepts [] too, while every other projection declaration takes the transportProjectionArray arm (#26954) · Seam: spec:GetRecordConfigSchema.fields, QuerySchema.fields → runtime:ObjectQL.find/findOne entry (assertProjectionNamesAField) · dedupe words: get_record fields empty projection schema, QuerySchema fields min 1 expand, declaration accepts empty projection engine refuses",
"carrier: none — observation only: no in-repo flow or expand caller authors fields: [] (census at this head, call-text scan of 37 find/findOne sites; blind to option bags built away from the call)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Claim-amendment:
domain:serverseat 2 ·session_0186vfjHHMsRXC9EUT9hTFMo· 2026-10-28T09:14Z · amends claim6462987453(branchclaude/project-thread-y7p9hd, PR #27161)Why. CI on PR #27161 head
2e447bbdis red inTest Core (8/8):packages/services/service-automationpnpm run testexited 1. The seat's reading is that the cause is a test pinning the behaviour this PR narrows.packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts≈L155 runs a control,'empty fields', { fields: [] }, that expects aget_recordnode's empty projection to read the whole row, through the real engine. The engine now refuses that read. The test's docblock (≈L17) andcrud-nodes.ts≈L310–311 ("nofields, or an empty list, is a whole-row read already") both state the old behaviour.File surface, widened to:
packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts: theempty fieldscase becomes a pin of the refusal the node now carries. It stays one case and is ⛔ not deleted.packages/services/service-automation/src/builtin/crud-nodes.ts: comment text only, restating ≈L310–311. ⛔ No behaviour change in the node: no[]→undefinednormalisation, no consumer-side tolerance.find/findOne, named in the report.@objectstack/service-automationchangeset only if the diff publishes from it.Unchanged: the ruling, the arms (
Clause-②: no (narrowing),Metadata-protocol: none) and every ⛔ of the original claim. The save-time refusal forGetRecordConfigSchema.fieldsstays on #27166.Generated by Claude Code
test(service-automation): get_record's empty-fields case pins the eng…
objectstack-fleet commentedon Oct 28, 2026
os-dev-report
{
"issue": 27008,
"status": "done",
"branch": "claude/project-thread-y7p9hd",
"pr": "#27161",
"head": "21ac7e6a",
"session": "session_01FEkQ5mbUpy4ebfWUHxAv87",
"premise_still_valid": true,
"summary": "Patch round for CI red on 2e447bb (Test Core 8/8, service-automation). Reproduced locally on 2e447bb: service-automation Test Files 1 failed | 215 passed (216), Tests 1 failed | 2755 passed (2756), the one being get-record-id-projection.integration.test.ts 'control, empty fields: the whole row is read', which pinned a get_record node's fields: [] as a whole-row read through the real engine. Commit 21ac7e6: that case is now one pin of the refusal the node carries (the node hands [] to findOne unchanged, once; success false; innerRefusal.code VALIDATION_ERROR; error opens with get_record(pin_get_record_id) failed: ObjectQL.findOne(...) names no field in 'fields'); the no-fields control stays; the file docblock and crud-nodes.ts projectionNamingId docblock now say an empty list is handed over for the engine to refuse. Comment text only in crud-nodes.ts, no [] -> undefined normalisation, no changeset (nothing service-automation publishes changes). Unchanged from report 6464754246: Premise re-measured on origin/main 07d18e6: engine.find(obj, { fields: [] }), findOne and an expand entry's fields: [] each returned every column (owner_id, organization_id, created_at included; the driver received fields: []), while the REST door refuses select: [] with 400 VALIDATION_ERROR. New packages/objectql/src/empty-projection-door.ts adds assertProjectionNamesAField, called at the entry of ObjectQL.find and findOne right after the option-key gate: it refuses an empty top-level fields and an empty fields on any expand entry at any depth (expand is judged at the parent's entry because expandRelatedRecords catches a sub-read throw and keeps the bare FK). The error is status 400 + code VALIDATION_ERROR (the shape projectionFieldsValue throws), so mapDataError answers both doors alike; its remedy words are read off EngineQueryOptionsSchema.shape.fields.safeParse([]) (PROJECTION_NAMES_NO_FIELD), no second copy and no spec edit. A system-context read is refused too; absent or null fields still reads every column. Family measured: count/aggregate take no fields (key gate refuses it), vectorFind has no engine implementation. Changeset @objectstack/objectql patch (Clause-② no (narrowing), Metadata-protocol none, BREAKING banner, ADR-0087 not-required marker); docs line in content/docs/data-modeling/queries.mdx (Skills-docs: updated here).",
"tests": "service-automation at 21ac7e6: build + typecheck + test VERDICT command-exit 0, Test Files 216 passed (216), Tests 2756 passed (2756); red on 2e447bb as above. Other suites: on 2e447bb every CI check-run except Test Core (8/8) and its Test Core rollup concluded success (49 runs read), so no other package's suite pins an empty projection read; a grep of test files for fields: [] finds only metadata/index/screen shapes, no engine read. Earlier: New src/engine-empty-projection-refusal.test.ts (10 cases: find, findOne, system context, expand, 2-level expand, REST-door parity via findData + mapDataError, 4 controls). Red on 07d18e6 engine source: Tests 6 failed | 4 passed (10). This head: Tests 10 passed (10). objectql build+typecheck+test at 26ebfb4: VERDICT command-exit 0, Test Files 477 passed (477), Tests 8898 passed (8898); the two later commits touch only the new suite's driver double. Ablation via scripts/ablation-replace.mjs on the top-level arm (source resolved via ./engine.js): anchor 1->0, blob 19c6bcd1->45086f3d, Tests 4 failed | 6 passed (10) (find, findOne, system-context, REST parity; the expand cases hold on their own arm); restored blob == HEAD 19c6bcd1, git diff HEAD empty.",
"gates": "Local at 21ac7e6: dispatch-gates --commands derived 95 for 7 paths (+ check-tenant-audit-census and its --self-test); all run; --ran: 95 derived, 95 run, 0 NOT-MEASURED, every exit 0 (check:skill-examples and check:dual-build-cjs-loads exited 3 until a full pnpm build, VERDICT command-exit 0, then 0). Hand: check-platform-checklist 0, check-commit-card-trailers 0, check-nul-bytes 0, eslint --no-inline-config on packages/objectql and service-automation/src/builtin 0. CI on 21ac7e6: in_progress at report time. Earlier round: Local, at 2e447bb: dispatch-gates --commands derived 93 for 5 paths; all run; --ran: 93 derived, 93 run, 0 NOT-MEASURED, every exit 0 (type-check-debt under the lock, VERDICT command-exit 0; teardown self-test after fetching its pinned commit). First sweep at 26ebfb4 had 2 reds in the new driver double (check:objectql-double-limit, check:where-matcher), fixed in a446bae / 2e447bb and re-run to 0. Hand: check-platform-checklist --changed-from origin/main 0, check-commit-card-trailers 0, check-nul-bytes 0, eslint --no-inline-config scoped to packages/objectql 0. Full pnpm build at 26ebfb4: 116/116. CI check-runs on PR #27161: in_progress at report time (not waited, per contract). Overlap: no open PR touches packages/objectql or the docs page (file scan of all 31 open PRs, at start and before pr_create); git merge-tree HEAD vs current origin/main 3346da2 clean.",
"dogfood_battery": "2e447bbd · VERDICT command-exit 0 · Test Files 348 passed | 3 skipped (351) · Tests 2803 passed | 20 skipped (2823) · no failures. Declared narrowing: not re-run at 21ac7e6; the delta since is one service-automation test file and one docblock, no source behaviour and nothing dogfood reads.",
"mcp_calls": "0 — no MCP GitHub calls (REST GETs only: the card, comments, the claim amendment, check-runs, open-PR file lists)",
"api_writes": "3 total — round 1: relay pr_create (#27161) and report 6464754246; this round: this replacement os-dev-report comment. git push is not a REST write.",
"open_questions": [
{
"question": "In-process engine.find with a projection whose EVERY entry is an unknown plain column still answers every column: the #7589 tolerance drops unknown plain columns and then falls back to * when nothing is left (engine.ts, filtered.length > 0 ? filtered : undefined). Measured at this head: fields: ['nope'] -> driver receives no fields, every column returned; fields: ['nope','title'] -> { title }. The REST door refuses the same request 400 INVALID_FIELD. Not an empty projection from the caller, so outside this card's ruling, and it touches the #7589 ruling. Refuse when the filter empties a non-empty projection?",
"status": "needs_decision",
"options": [
"A — when the unknown-plain filter leaves nothing, refuse with the ingress door's 400 INVALID_FIELD naming the unknown columns; a partly-unknown projection keeps the #7589 tolerance (drops the unknown ones)",
"B — keep the fallback to every column and document it beside the #7589 note",
"C — answer rows with no requested column (id only)"
],
"recommendation": "A. Business need: no in-repo caller sends an all-unknown projection; the case #7589's tolerance protects (a generic view naming columns an object lacks) reaches the engine through the REST door, which already refuses unknown fields, so the in-process fallback has no measured producer. Long-term: one rule, a narrower request is never answered wider; B keeps a widening path beside the rule this card enforces. AI-error: a typo'd or hallucinated column list is the likeliest AI mistake here; A makes it loud, B hands back every column silently. Startup focus: A is a small narrowing with no new gate and no transition window; C invents a shape nobody asked for."
}
],
"out_of_scope_findings": [
"class: c · reach: not live-probed; read: packages/services/service-automation/src/builtin/crud-nodes.ts get_record hands authored cfg.fields verbatim to data.find/findOne (projectionNamingId keeps []), and the engine refusal is measured in-process; so a flow node authored with fields: [] passes save and now fails at run (success: false, refusal carried) · evidence: GetRecordConfigSchema.fields (packages/spec/src/automation/builtin-node-config.zod.ts) is z.array(z.string()).optional() and accepts []; nested QuerySchema.fields (packages/spec/src/data/query.zod.ts, the value type of EngineQueryOptionsSchema.expand) accepts [] too, while every other projection declaration takes the transportProjectionArray arm (#26954) · Seam: spec:GetRecordConfigSchema.fields, QuerySchema.fields → runtime:ObjectQL.find/findOne entry (assertProjectionNamesAField) · dedupe words: get_record fields empty projection schema, QuerySchema fields min 1 expand, declaration accepts empty projection engine refuses",
"carrier: none — observation only: no in-repo flow or expand caller authors fields: [] (census at this head, call-text scan of 37 find/findOne sites; blind to option bags built away from the call)"
],
"round": "patch round 1 (claim amendment 6465140445): replaces report 6464754246 for head 21ac7e6"
}
Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Landing record · #27008 · PR #27161
domain:serverseat 2 ·session_0186vfjHHMsRXC9EUT9hTFMo· 2026-10-28T10:21Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-27161-*'git merge-base --is-ancestor 9fa70b4a origin/main4265176c)git grep -c assertProjectionNamesAField origin/main -- packages/objectql/src/engine.tsLanded: merged 10:21Z as
9fa70b4a, with a contract review PASS atCONTRACT_REVIEW_TIER(6465986975).ObjectQL.findandfindOnenow refuse an empty projection with400 VALIDATION_ERRORbefore any driver read. This covers the top-levelfieldsand everyexpandentry'sfields, and it uses the declaration's own words. Aget_recordflow node authored withfields: []now fails with that refusal carried.Unchanged: absent or
nullfieldsstill reads every column, a non-empty projection projects as before, and the finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 unknown-column tolerance stays.Follow-up:
GetRecordConfigSchema.fieldsand the nestedQuerySchema.fieldsstill accept[].Both are filed for triage.
Generated by Claude Code