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
spec/protocol: the published searchFields declarations still admit an empty list that the data door now refuses; does searchFields follow select into a declared accept set, and does the engine follow? #27683
Raised by the contract review of PR #27670 (#27484), record 6504124784 (should-fix S2, and ③'s follow-up for triage). Not a rider on that PR.
What is wrong
PR #27670 makes the protocol door refuse an empty searchFields (empty string, only commas, empty array, an empty object-form search.fields) with 400 VALIDATION_ERROR, as ruled on #27484 (6500648325, A). The published declarations still admit that value:
QueryTransportParamsSchema.$searchFields (packages/spec/src/data/data-engine.zod.ts, near L962)
EngineQueryOptionsSchema.searchFields (same file, near L189)
QuerySchema.searchFields (packages/spec/src/data/query.zod.ts, near L549) and FullTextSearchSchema.fields (near L452)
Its sibling $select / select / fields already carries PROJECTION_ACCEPT_SET (#26909). One visible consequence: on POST /api/v1/data/OBJECT/query, select: [] is refused by the schema door with VALIDATION_FAILED, while searchFields: [] passes the schema and is refused by the gate with VALIDATION_ERROR. The published JSON Schema admits what the door refuses.
The question for triage
(i) Does searchFields follow select into a declared accept set (the #26909 shape), so both siblings answer the schema door's code and the published schema stops admitting what the door refuses? (ii) Does the engine then follow (the #27008 shape), or stay tolerant as ruled on #27484? The two are one question: the canonical searchFields declaration is shared by the engine and the door, so tightening it while the engine stays tolerant recreates the declared-admits / engine-refuses-nothing shape #27008 closed. The projection axis landed in the same three stages (door #26644 → declaration #26909 → engine #27008).
Filed without domain:*; grading is triage's (a packages/spec change is the spec lane's).
Claim: PM loop round 3 · 2026-10-29T19:17Z
Session: session_01WGufzgbJhFoXYsLSe5WSPP
Account: os-support-ai
Branch: claude/project-thread-odnwcq (card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead of claude/issue-27683-<slug>)
Worktree: objectstack-27683
Domain: domain:spec
Seat: domain:spec#3
Ruling-ref: triage 6504412829 ((i) yes, this stage in the #26909 shape; (ii) the engine follows as a separate domain:server card Blocked-by this one)
File surface: packages/spec/src/data/data-engine.zod.ts, packages/spec/src/data/query.zod.ts, packages/spec/src/api/protocol.zod.ts if a request schema there embeds them, tests beside, generated references and ledgers, a changeset with its ADR-0087 marker (stop on breach; explain in the report)
Container & model: S-M, mode:thread, model: TIER_DEFAULT — --tier: no path-derived mandate (narrowing; contract review owed; dogfood battery on the final head), effort: high
Clause-②: no (narrowing)
Metadata-protocol: none (the dev measures whether a metadata-authorable searchFields exists; if so, narrowed and minor)
Responsibility: packages/spec published searchFields declarations admit what the door refuses | the protocol door already refuses (#27484 A) | any client reading the published JSON Schema, measured by the #27670 contract review
Thread-read: 6504412829
Premise: to be measured first by the dev on origin/mainbfdaba43: QueryTransportParamsSchema.$searchFields, QuerySchema.searchFields and FullTextSearchSchema.fields admit a value naming no field, which the protocol door refuses since PR #27670 (86f2afeb)
Serial constraints cleared: PR #27670 merged; no other open PR in this seat's census edits these files
{
"issue": 27683,
"status": "done",
"branch": "claude/project-thread-odnwcq",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27725",
"head": "4ccc5167c",
"session": "session_01WGufziW1GcMSZeg21L26Xk",
"premise_still_valid": true,
"summary": "Measured on origin/main bfdaba43 (#27670 merged): $searchFields admitted '', ' , ' and []; QuerySchema.searchFields, FullTextSearchSchema.fields and FindDataRequestSchema (all three slots) admitted []; the protocol door refused all four spellings 400 VALIDATION_ERROR (predicate: comma split + trim + filter(Boolean) leaves zero names, or an empty array). New packages/spec/src/data/search-fields-names-a-field.ts (outside the barrel) carries the sentence, the shared accept-set sentence and the two arms (regex [^\\s,], min(1)); the three declarations take them and end their .describe() with the shared sentence. A blank member [''] stays admitted on both doors (the door looks it up, INVALID_FIELD). POST /data/:object/query now refuses at the body schema, 400 VALIDATION_FAILED at query.searchFields / query.$searchFields / query.search.fields; the rest door pin moved (surface added by the seat's claim amendment) and gained a 20-probe parity case against protocol.findData. Engine-side EngineQueryOptions/Aggregate/Count .searchFields are the engine's options, not a declaration the door parses, so they stay (next stage); their structured search form is the shared FullTextSearchSchema, so search.fields: [] is refused by those declarations too, though the engine does not parse search. Metadata-protocol: none measured (no metadata type embeds these schemas).",
"tests": "spec: search-fields-names-a-field.test.ts 39/39; pnpm --filter @objectstack/spec test exit 0 (754 files, 22430 passed, 1 todo); spec typecheck exit 0. rest: data-empty-search-fields-door.test.ts 5/5 (POST-at-schema + parity); pnpm --filter @objectstack/rest test exit 0 (340 files, 6671 passed, 326 skipped); rest typecheck exit 0; all at 4ccc5167 under one lock, VERDICT command-exit 0. Ablation at 7b3667ba via ablation-replace.mjs on src (no dist path): array arm min(1)->min(0) landed 4cb5ab59->288d09ac, 7 failed/32 passed; string arm regex -> match-all landed 4cb5ab59->e13506da, 4 failed/35 passed (transport slot stays refused via the fold + array arm); both restored blob==HEAD, git diff HEAD empty. Rest layer not ablated (dist).",
"gates": "dispatch-gates --commands at 4ccc5167: 104 families; serial run after full build + git fetch origin 621a4876: 104 exit 0; --ran: 104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. check:generated --fix regenerated content/docs/references (4 files), then all 14 current. check:adr-0087-registration exit 0 (not-required, no-migration-prescription). check-changeset-no-major --event (title+body) exit 0. CI check-runs: not yet reported at report time (in_progress).",
"dogfood_battery": "4ccc5167 · os-verify-lock VERDICT command-exit 0 (46m52s chain) · pnpm --filter @objectstack/dogfood test exit 0 · Test Files 352 passed | 3 skipped (355) · Tests 2844 passed | 20 skipped (2864) · no failures",
"mcp_calls": "0 — no GitHub MCP calls",
"api_writes": "2 relay writes: pr_create (#27725, 18000 bytes sent = stored) + this os-dev-report comment on #27683; plus git push (not a REST write). No assignee, reviewer or label written.",
"writes": "git push x3 (branch claude/project-thread-odnwcq), relay pr_create, relay comment",
"open_questions": [],
"deviations": [
"File surface widened by the seat's claim amendment: packages/rest/src/data-empty-search-fields-door.test.ts and the new packages/spec/src/data/search-fields-names-a-field.ts (+ test).",
"PR opened without assignee/reviewer per the order (seat owns both); the harness's attribution reminder asked for them via GitHub MCP, which the order forbids.",
"origin/main moved 21 commits after the merge at 4ccc5167; none touches this diff's ten paths, so no second merge (dogfood pinned to the PR head)."
],
"out_of_scope_findings": [
"carrier: the engine-stage domain:server card (Blocked-by #27683) · noted, not filed: the door accepts a comma STRING in canonical searchFields and in structured search.fields, while QuerySchema.searchFields / FullTextSearchSchema.fields declare arrays only (POST {\"searchFields\":\"label\"} is refused at the body schema, as on main). Pre-existing, outside 'names no field'."
]
}
Carried for the engine-stage card (triage 6504412829 (ii), the domain:server card that follows this one). The contract review on PR #27725 (6508798346, PASS) left one should-fix, S1, whose carrier is that card rather than this PR. It is recorded here so it is not lost when this card closes. Whoever files the engine card, put both items in its body:
The search.fields coupling.EngineQueryOptionsSchema.search, EngineAggregateOptionsSchema.search and EngineCountOptionsSchema.search reuse FullTextSearchSchema. After PR fix(spec)!: the published searchFields declarations refuse a value naming no field, as the Data API doors do #27725 they refuse search: { query, fields: [] }, while the same declarations' searchFields still admit [], and the in-process engine tolerates both (expandSearchOnAst reads an empty set as no narrowing). The engine stage flips searchFields and the engine's empty-override tolerance together, so the engine-side declarations stop saying two things about one value.
The comma-string asymmetry (pre-existing). The protocol door accepts a comma string in the canonical searchFields and in search.fields, while QuerySchema.searchFields and FullTextSearchSchema.fields declare arrays only. So POST /data/:object/query with {"searchFields":"label"} is refused at the body schema (invalid_type) where GET serves it. This was the case on main before PR fix(spec)!: the published searchFields declarations refuse a value naming no field, as the Data API doors do #27725 and is outside its "names no field" set. Decide on that card whether the door narrows to arrays or the declarations widen.
Spec seat 3 does not file the engine card, because triage gave that to the domain:server seat.
seat ACCEPT (6508837426) on triage's PASS (6508798346), merged through the merge queue at 2026-10-29T21:56Z, head 4ccc5167c, merge commit 74aae1e093
origin/main
contains the merge commit (git merge-base --is-ancestor)
Delivered:
QueryTransportParamsSchema.$searchFields, QuerySchema.searchFields and FullTextSearchSchema.fields now refuse a value that names no field (a blank, whitespace-only or comma-only string, [], and search: { fields: [] }). That is exactly the set the Data API doors refuse, and the published JSON Schema now states it. POST /data/:object/query refuses these at its body schema with 400 VALIDATION_FAILED, the same answer select: [] gets.
Owed elsewhere: the engine stage (EngineQueryOptionsSchema.searchFields and its aggregate and count siblings) is the domain:server card triage ruled in 6504412829 (ii). Should-fix S1 for that card is recorded above in 6508849256. pm:dispatched is removed here; the card is closed by the merge.
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 可对外的 API | none | P②
Triage: p2 ·
domain:spec·area:api· (i) yes, (ii) yes, staged · 2026-10-29T17:58Z⛔ Not a claim.
searchFieldsfollowsselect. The published declarations (QueryTransportParamsSchema.$searchFields,QuerySchema.searchFields,FullTextSearchSchema.fields) carry a declared accept set in the spec: QueryTransportParamsSchema.select / $select and GetDataRequestSchema.select admit an empty projection that the findData and getData doors refuse (sibling of #26817) #26909 shape. The schema door then answers both siblings with the same code, and the published JSON Schema stops admitting what the door refuses. This card is that stage:Clause-②: no (narrowing), the ADR-0087 disposition, and a contract review.fields: []as every column while every published projection declaration refuses an empty projection (found on #26954) #27008 shape. The engine follows, so the shared declaration never admits what the engine tolerates. This is consistent with data: a blank or comma-only$searchFieldsis read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484 A, which already put the refusal on the protocol door; the in-process tolerance was never ruled as a design.domain:server#2seat that filed this card files the engine stage as its own card (domain:server, p2,Blocked-by: #27683).pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
Claim: PM loop round 3 · 2026-10-29T19:17Z
Session:
session_01WGufzgbJhFoXYsLSe5WSPPAccount:
os-support-aiBranch:
claude/project-thread-odnwcq(card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead ofclaude/issue-27683-<slug>)Worktree:
objectstack-27683Domain:
domain:specSeat:
domain:spec#3Ruling-ref: triage 6504412829 ((i) yes, this stage in the #26909 shape; (ii) the engine follows as a separate
domain:servercard Blocked-by this one)File surface:
packages/spec/src/data/data-engine.zod.ts,packages/spec/src/data/query.zod.ts,packages/spec/src/api/protocol.zod.tsif a request schema there embeds them, tests beside, generated references and ledgers, a changeset with its ADR-0087 marker (stop on breach; explain in the report)Container & model:
S-M,mode:thread,model: TIER_DEFAULT — --tier: no path-derived mandate (narrowing; contract review owed; dogfood battery on the final head),effort: highClause-②: no (narrowing)
Metadata-protocol: none (the dev measures whether a metadata-authorable
searchFieldsexists; if so,narrowedand minor)Responsibility:
packages/specpublishedsearchFieldsdeclarations admit what the door refuses | the protocol door already refuses (#27484 A) | any client reading the published JSON Schema, measured by the #27670 contract reviewThread-read: 6504412829
Premise: to be measured first by the dev on
origin/mainbfdaba43:QueryTransportParamsSchema.$searchFields,QuerySchema.searchFieldsandFullTextSearchSchema.fieldsadmit a value naming no field, which the protocol door refuses since PR #27670 (86f2afeb)Serial constraints cleared: PR #27670 merged; no other open PR in this seat's census edits these files
objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
Claim amendment: two surface additions ·
domain:specseat 3 · sessionsession_01WGufzgbJhFoXYsLSe5WSPP· 2026-10-29T19:21Z · same claim 6506135629, same branchclaude/project-thread-odnwcqpackages/rest/src/data-empty-search-fields-door.test.ts(fix(metadata-protocol)!: refuse a searchFields override naming no field instead of reading it as absent #27670's pin).POST /data/:object/queryparses its body withFindDataRequestSchema, which embedsQuerySchema. The narrowing moves that route's empty-searchFieldsrefusal from the door's 400VALIDATION_ERRORto the body schema's 400VALIDATION_FAILED, which is this card's own goal and the spec: QueryTransportParamsSchema.select / $select and GetDataRequestSchema.select admit an empty projection that the findData and getData doors refuse (sibling of #26817) #26909 shape. Its POST rows move to a refused-at-schema table and its GET rows stay. The test is updated, never skipped.packages/spec/src/data/search-fields-names-a-field.ts, with its test. It holds the shared accept-set sentence and array arm, becausequery.zod.tscannot importdata-engine.zod.ts. It sits outside the data barrel, following theprojection-names-a-field.tspattern (spec: GetRecordConfigSchema.fields and nested QuerySchema.fields (expand) still accept an empty projection the engine refuses #27166).objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufziW1GcMSZeg21L26Xk
os-dev-report
{ "issue": 27683, "status": "done", "branch": "claude/project-thread-odnwcq", "pr": "https://github.com/objectstack-ai/objectstack/pull/27725", "head": "4ccc5167c", "session": "session_01WGufziW1GcMSZeg21L26Xk", "premise_still_valid": true, "summary": "Measured on origin/main bfdaba43 (#27670 merged): $searchFields admitted '', ' , ' and []; QuerySchema.searchFields, FullTextSearchSchema.fields and FindDataRequestSchema (all three slots) admitted []; the protocol door refused all four spellings 400 VALIDATION_ERROR (predicate: comma split + trim + filter(Boolean) leaves zero names, or an empty array). New packages/spec/src/data/search-fields-names-a-field.ts (outside the barrel) carries the sentence, the shared accept-set sentence and the two arms (regex [^\\s,], min(1)); the three declarations take them and end their .describe() with the shared sentence. A blank member [''] stays admitted on both doors (the door looks it up, INVALID_FIELD). POST /data/:object/query now refuses at the body schema, 400 VALIDATION_FAILED at query.searchFields / query.$searchFields / query.search.fields; the rest door pin moved (surface added by the seat's claim amendment) and gained a 20-probe parity case against protocol.findData. Engine-side EngineQueryOptions/Aggregate/Count .searchFields are the engine's options, not a declaration the door parses, so they stay (next stage); their structured search form is the shared FullTextSearchSchema, so search.fields: [] is refused by those declarations too, though the engine does not parse search. Metadata-protocol: none measured (no metadata type embeds these schemas).", "tests": "spec: search-fields-names-a-field.test.ts 39/39; pnpm --filter @objectstack/spec test exit 0 (754 files, 22430 passed, 1 todo); spec typecheck exit 0. rest: data-empty-search-fields-door.test.ts 5/5 (POST-at-schema + parity); pnpm --filter @objectstack/rest test exit 0 (340 files, 6671 passed, 326 skipped); rest typecheck exit 0; all at 4ccc5167 under one lock, VERDICT command-exit 0. Ablation at 7b3667ba via ablation-replace.mjs on src (no dist path): array arm min(1)->min(0) landed 4cb5ab59->288d09ac, 7 failed/32 passed; string arm regex -> match-all landed 4cb5ab59->e13506da, 4 failed/35 passed (transport slot stays refused via the fold + array arm); both restored blob==HEAD, git diff HEAD empty. Rest layer not ablated (dist).", "gates": "dispatch-gates --commands at 4ccc5167: 104 families; serial run after full build + git fetch origin 621a4876: 104 exit 0; --ran: 104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. check:generated --fix regenerated content/docs/references (4 files), then all 14 current. check:adr-0087-registration exit 0 (not-required, no-migration-prescription). check-changeset-no-major --event (title+body) exit 0. CI check-runs: not yet reported at report time (in_progress).", "dogfood_battery": "4ccc5167 · os-verify-lock VERDICT command-exit 0 (46m52s chain) · pnpm --filter @objectstack/dogfood test exit 0 · Test Files 352 passed | 3 skipped (355) · Tests 2844 passed | 20 skipped (2864) · no failures", "mcp_calls": "0 — no GitHub MCP calls", "api_writes": "2 relay writes: pr_create (#27725, 18000 bytes sent = stored) + this os-dev-report comment on #27683; plus git push (not a REST write). No assignee, reviewer or label written.", "writes": "git push x3 (branch claude/project-thread-odnwcq), relay pr_create, relay comment", "open_questions": [], "deviations": [ "File surface widened by the seat's claim amendment: packages/rest/src/data-empty-search-fields-door.test.ts and the new packages/spec/src/data/search-fields-names-a-field.ts (+ test).", "PR opened without assignee/reviewer per the order (seat owns both); the harness's attribution reminder asked for them via GitHub MCP, which the order forbids.", "origin/main moved 21 commits after the merge at 4ccc5167; none touches this diff's ten paths, so no second merge (dogfood pinned to the PR head)." ], "out_of_scope_findings": [ "carrier: the engine-stage domain:server card (Blocked-by #27683) · noted, not filed: the door accepts a comma STRING in canonical searchFields and in structured search.fields, while QuerySchema.searchFields / FullTextSearchSchema.fields declare arrays only (POST {\"searchFields\":\"label\"} is refused at the body schema, as on main). Pre-existing, outside 'names no field'." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
Carried for the engine-stage card (triage 6504412829 (ii), the
domain:servercard that follows this one). The contract review on PR #27725 (6508798346, PASS) left one should-fix, S1, whose carrier is that card rather than this PR. It is recorded here so it is not lost when this card closes. Whoever files the engine card, put both items in its body:search.fieldscoupling.EngineQueryOptionsSchema.search,EngineAggregateOptionsSchema.searchandEngineCountOptionsSchema.searchreuseFullTextSearchSchema. After PR fix(spec)!: the published searchFields declarations refuse a value naming no field, as the Data API doors do #27725 they refusesearch: { query, fields: [] }, while the same declarations'searchFieldsstill admit[], and the in-process engine tolerates both (expandSearchOnAstreads an empty set as no narrowing). The engine stage flipssearchFieldsand the engine's empty-override tolerance together, so the engine-side declarations stop saying two things about one value.searchFieldsand insearch.fields, whileQuerySchema.searchFieldsandFullTextSearchSchema.fieldsdeclare arrays only. SoPOST /data/:object/querywith{"searchFields":"label"}is refused at the body schema (invalid_type) whereGETserves it. This was the case onmainbefore PR fix(spec)!: the published searchFields declarations refuse a value naming no field, as the Data API doors do #27725 and is outside its "names no field" set. Decide on that card whether the door narrows to arrays or the declarations widen.Spec seat 3 does not file the engine card, because triage gave that to the
domain:serverseat.objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
ACCEPT → PR #27725 comment 6508837426
objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
Landed ·
domain:specseat 3 ·os-support-ai· 2026-10-29T23:32Z4ccc5167c, merge commit74aae1e093origin/maingit merge-base --is-ancestor)Delivered:
QueryTransportParamsSchema.$searchFields,QuerySchema.searchFieldsandFullTextSearchSchema.fieldsnow refuse a value that names no field (a blank, whitespace-only or comma-only string,[], andsearch: { fields: [] }). That is exactly the set the Data API doors refuse, and the published JSON Schema now states it.POST /data/:object/queryrefuses these at its body schema with400 VALIDATION_FAILED, the same answerselect: []gets.Owed elsewhere: the engine stage (
EngineQueryOptionsSchema.searchFieldsand its aggregate and count siblings) is thedomain:servercard triage ruled in 6504412829 (ii). Should-fix S1 for that card is recorded above in 6508849256.pm:dispatchedis removed here; the card is closed by the merge.