Repository navigation
fix(spec)!: the published searchFields declarations refuse a value naming no field, as the Data API doors do - #27725
Conversation
…ming no field QueryTransportParamsSchema.$searchFields, QuerySchema.searchFields and FullTextSearchSchema.fields take the accept set the protocol door enforces: a blank or comma-only string and an empty list are refused, with one shared sentence in each description. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufziW1GcMSZeg21L26Xk
… the body schema The rest door pin moves its POST rows to the schema door's VALIDATION_FAILED and gains a parity test holding the body schema and the protocol door to one refused set; references regenerated; changeset with its ADR-0087 marker. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufziW1GcMSZeg21L26Xk
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 167 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3f47074e4bf9b5724db9c431f73d4cb73276fc20 && git checkout 3f47074e4bf9b5724db9c431f73d4cb73276fc20
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 04b14025d55b938b9b160c194b86ae9841ae7070 4ccc5167c27f11216fd562102a6c444ae2d5c36c && git checkout -B drift-repro 04b14025d55b938b9b160c194b86ae9841ae7070 && git merge --no-ff 4ccc5167c27f11216fd562102a6c444ae2d5c36c
node scripts/docs-audit/affected-docs.mjs --json 04b14025d55b938b9b160c194b86ae9841ae7070
|
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 4ccc516 · 2026-10-29T21:18Z Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: ① Derived judgmentsFace: Premise, measured on the live tree rather than the local ref: the door refusal this narrowing joins (PR #27670, merge commit Accept-set changes the diff implies, each judged:
② Semver level
③ Boundary flags
Deviations in the os-dev-report, each answered:
Flags the dev raised in the body, answered:
④ Design judgment
Narrowing floor ( Must-fix: none. Should-fix: S1 (carried to the engine-stage card, not a change to this head). Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP 维护者速读 (席位终稿)
席位意见:接受。分诊契约复核 PASS(6508798346,就在当前 head 上),没有 must-fix;唯一的 should-fix S1 落在引擎阶段卡上,不改本 PR,席位已把它记到 #27683 卡上供那张卡的立卡方带走。 席位核对 · ACCEPT ·
|
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
Requested by Yibo Wang · project thread
Fixes #27683
Clause-②: no (narrowing) — the published declarations of the search-field override (
QueryTransportParamsSchema.$searchFields,QuerySchema.searchFields,FullTextSearchSchema.fields, reached throughQueryWithTransportSchemaandFindDataRequestSchema.query) refuse a value naming no field, which they admitted before. The doors already refused it with a 400 (#27484 / PR #27670); nothing is widened and no key is added, removed or renamed.Metadata-protocol: none
Skills-docs: none — every place under
skills/**that teaches the narrowed members still holds (git grep -n "searchFields\|search\.fields" -- skillsat this head):skills/objectstack-query/SKILL.md§ Full-Text Search teaches a non-emptysearchFieldsand "Omit it to search the object's declaredsearchableFields", its key tables (lines 25-26) list the engine option, which this PR does not narrow, and its eval andskills/objectstack-data/SKILL.md:125emit only named overrides. No skill emits or recommends an empty override.Metadata-protocol: nonewas measured, not assumed: no metadata type embedsQuerySchema,FullTextSearchSchemaor the transport bag (git grepof every*.zod.tsunderpackages/spec/src/{ui,automation,ai,system,kernel,security}for the three schema names and forsearchFields: only prose comments inui/component.zod.ts). The readers are request bodies, the driver and API contracts, and codegen.PR #27670 (#27484) merged at
86f2afebbefore this branch started, so there is no landing order to name; its predicate was measured onmain.维护者速读
searchFields声明(QueryTransportParamsSchema.$searchFields、QuerySchema.searchFields、FullTextSearchSchema.fields)不再接受“一个字段都没写”的值(空串、只有逗号和空格、空数组、对象形search.fields: []),三处描述带同一句接受集说明;POST /data/:object/query因此在请求体 schema 处拒收,回 400VALIDATION_FAILED(原为门内的VALIDATION_ERROR),与select: []一致。$searchFieldsis read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484 起已拒收这些值,但发布的 Zod 与 JSON Schema 仍声明接受,按 schema 生成的客户端和 AI 会写出被门拒收的请求;select的同类声明早已收紧(spec: QueryTransportParamsSchema.select / $select and GetDataRequestSchema.select admit an empty projection that the findData and getData doors refuse (sibling of #26817) #26909)。POST /query的错误码从VALIDATION_ERROR变VALIDATION_FAILED,仍是 400、仍不读库。引擎侧声明不动(下一阶段卡)。回滚即还原本 PR。packages/spec/src/data/search-fields-names-a-field.ts(接受集与两条臂),packages/rest/src/data-empty-search-fields-door.test.ts(POST 行迁到 schema 拒收表、末尾 parity 用例),packages/spec/src/data/query.zod.ts(两处成员)。pnpm --filter @objectstack/spec exec vitest run src/data/search-fields-names-a-field.test.ts,应见 39 个用例全绿。What changes
Measured first, on
origin/mainbfdaba43(#27670 merged)Each declaration parsed with the four spellings of "names no field", next to what the protocol door (
assertSearchFieldsAreSearchable, the gatefindDatacalls for the standalone slot and for the object-formsearch.fields) answers for the same value. The door column is the registry-less host, so it is the shape rule alone; a registered object answers the same for these values, because the empty check runs before the field-map lookup.$searchFieldsQuerySchema.searchFieldsFullTextSearchSchema.fieldsFindDataRequestSchemaquery.$searchFields/query.searchFields/query.search.fieldsEngineQueryOptionsSchema/EngineAggregateOptionsSchema/EngineCountOptionsSchema.searchFieldssearch.fields)''VALIDATION_ERROR/ 400VALIDATION_ERROR' , 'VALIDATION_ERROR/ 400VALIDATION_ERROR[]VALIDATION_ERROR/ 400VALIDATION_ERRORsearch: { fields: [] }FullTextSearchSchema.fields)FullTextSearchSchema)VALIDATION_ERROR[''](control)'name'/['name'](control)The door's predicate, read at
packages/metadata-protocol/src/protocol-query-validation.tsonmain: a string issplit(','), trimmed and emptied of blank members; an array is kept as sent; the override is refused as naming no field iff that leaves zero names. That is the set used here, exactly.After, at this PR's head
$searchFieldsQuerySchema.searchFieldsFullTextSearchSchema.fieldsFindDataRequestSchema(all three slots)'',' ',',',' , 'invalid_format, "Name at least one field, or leave the key out to search the default search fields")query.$searchFieldsVALIDATION_ERROR)[]too_small, same sentence)query.searchFields/query.$searchFields/query.search.fieldssearchFieldsunchanged (still admits[]);search.fields: []refused through the sharedFullTextSearchSchema[''],'name',' name , email ',['name']The declarations (
packages/spec/src/data/)search-fields-names-a-field.ts, outside thedatabarrel (theprojection-names-a-field.tspattern): the refusal sentenceSEARCH_FIELDS_NAMES_NO_FIELD, the shared accept-set sentenceSEARCH_FIELDS_ACCEPT_SET("at least one field name; a value naming no field (blank, only commas and spaces, or an empty list) is refused"), the string arm (regex(/[^\s,]/): holds iff some comma segment has a non-whitespace character,\sbeing the settrim()strips) and the array arm (min(1)). Its own module becausequery.zod.tscannot importdata-engine.zod.ts. Both are a pattern and a minimum rather than refinements, so the published JSON Schema states them (pattern,minItems), anddropped-refinements.baseline.jsondoes not move.QueryTransportParamsSchema.$searchFields(data-engine.zod.ts):z.union([searchFieldsString(), searchFieldsArray()]), still.nullable().optional(), so an explicitnullis still a withdrawal.QuerySchema.searchFieldsandFullTextSearchSchema.fields(query.zod.ts):searchFieldsArray().optional()..describe()texts ends with the shared accept-set sentence; the references (content/docs/references/{api/contract,api/protocol,data/data-engine,data/query}.mdx) were regenerated withcheck:generated --fix.['']) is deliberately outside the set. Unlike the projection, whose door refuses a blank member through the same "names no field" envelope, the search-field door keeps it as a name and then refuses it as an unknown field when the object is known (INVALID_FIELD). Refusing it at the schema would make the schema refuse a value the door's "names no field" predicate admits, so the two would no longer refuse the same set.The stage boundary (what the engine card owns)
Measured item 2 of the order: neither
EngineQueryOptionsSchema.searchFields(data-engine.zod.ts:189) nor the aggregate (:513) and count (~:558) siblings is a declaration the door parses.POST /data/:object/queryparsesFindDataRequestSchema(whosequeryisQueryWithTransportSchema, built fromQuerySchemaandQueryTransportParamsSchema) atpackages/rest/src/crud-routes.tsand in the dispatcher (packages/runtime/src/domains/data.ts);GET /data/:objectand export reachfindDatawith no schema at all. The three engine-side declarations are the in-process engine's options (IDataEngine.find/aggregate/count), so they stay, and so does the engine's tolerance of an empty override: the next stage, in the #27008 shape.One coupling the engine card should know about:
EngineQueryOptionsSchema.search(and its aggregate and count siblings) declare their structured form as the sharedFullTextSearchSchema, sosearch: { query, fields: [] }is now refused by those engine-side declarations too. The engine does not parsesearch(fillQueryAstDefaultsinpackages/objectql/src/engine.tssays so and why), so no runtime answer moves. LikewiseEngineQueryOptionsSchema.expandvalues areQuerySchema, so anexpandentry'ssearchFields: []is refused by the declaration; the engine does not parsesearchFieldson an expand entry either. Thestage boundarydescribe in the new spec test pins the threesearchFieldsmembers still admitting[], so the engine card flips it on purpose.POST /data/:object/query(packages/rest/src/data-empty-search-fields-door.test.ts)Cleared by the seat's claim amendment on #27683 (the file was outside the dispatched surface). The two POST rows #27670 pinned at the door's
400 VALIDATION_ERRORnow answer at the body schema,400 VALIDATION_FAILED, withfields[0].fieldnaming the key:query.searchFields,query.$searchFieldsorquery.search.fields. That is how the route already answersselect: [](#26909). The newREFUSED_POST_AT_SCHEMAtable coverssearchFields: [],$searchFields: "",$searchFields: " , ",$searchFields: []andsearch: { query, fields: [] }, each with "no read of the object". The GET list and export rows are unchanged: those doors have no body schema and still answer the door'sVALIDATION_ERROR.Tests
packages/spec/src/data/search-fields-names-a-field.test.ts(39 cases): per declaration, each empty spelling refused with the sentence at the member's path ($searchFields: four strings and[];QuerySchema.searchFieldsandFullTextSearchSchema.fields:[]), and controls kept unchanged (one name, a spaced comma list, a list, a blank member); the same refusals throughQueryWithTransportSchemaandFindDataRequestSchema.queryfor$searchFields,searchFields,search.fieldsand$search.fields; an explicitnullstill a withdrawal; a kept comma list lowered ontosearchFields; each.describe()carrying the shared sentence; and the stage-boundary pin above.packages/rest/src/data-empty-search-fields-door.test.ts: the POST rows move toREFUSED_POST_AT_SCHEMAas above, and a new parity case holds the schema door (FindDataRequestSchema, which the POST route parses) and the protocol door (protocol.findData, which the GET and export routes reach with no schema) to one refused set, probe for probe, over a real SQLite object: 8 strings and 4 lists on$searchFields, the 4 lists onsearchFieldsand onsearch.fields(20 probes). The schema refuses a probe iff the door answersVALIDATION_ERROR"names no field"; the verdict counts are pinned (8 refused, 9 served, 3INVALID_FIELDfor the blank member), so neither side can pass vacuously.Ablation
At head
7b3667ba(fix and tests committed), throughscripts/ablation-replace.mjs(trap restore), under the lock, againstsrc(the spec suite imports the schemas by relative path, so no build or dist step is involved):z.array(z.string()).min(1, SEARCH_FIELDS_NAMES_NO_FIELD)→z.array(z.string()).min(0, 'ABLATED-27683'). Landed: anchor 1 → 0, blob4cb5ab59→288d09ac.search-fields-names-a-field.test.ts: 7 failed / 32 passed (every[]refusal on all three declarations and all four body spellings). Restored: blob4cb5ab59= HEAD,git diff HEADempty.regex(/[^\s,]/, …)→regex(/ABLATED-27683|/, …)(matches everything). Landed: anchor 1 → 0, blob4cb5ab59→e13506da. 4 failed / 35 passed: the four$searchFieldsstring refusals onQueryTransportParamsSchema. The same strings throughQueryWithTransportSchemastayed refused, because the fold lowers''/' , 'tosearchFields: []and the array arm onQuerySchema.searchFieldscatches it: the two arms back each other on the transport-aware slot. Restored: blob = HEAD,git diff HEADempty.z.array(z.string())) that already occurred inside the anchor, so the tool refused it (replacement count did not rise) and restored before running anything; it was redone with the marker above.dist, so each leg would need a rebuild). What stands in for it: the first run of the new POST case on the narrowed spec was red on one row for the right reason (the structured form answeredquery.search.fields, not thequery.searchfirst written), so the case reads the live schema answer; and the parity case compares each of its 20 probes against the door's own answer with pinned verdict counts.Acceptance notes
searchFieldsslot and in the structuredsearch.fields("the engine accepts the comma-string and array forms from either slot"), whileQuerySchema.searchFieldsandFullTextSearchSchema.fieldsdeclare arrays only, soPOST {"searchFields":"label"}is refused at the body schema as it was onmain(measured:query.searchFieldsinvalid_type). Recorded for the engine card's reading; not a finding filed from here.#27484changeset's "Not yet narrowed" bullet becomes false in the same release;check-empty-changesetrefuses an edit to a changeset the merge base carries, so this PR's changeset carries a "Supersedes, in the same release" paragraph instead.$searchFieldsonly when it holds a field (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's measurement), andgit grepforsearchFields: []/fields: []besidesearchin*.test.tsfinds only the rest door file (updated) and objectql'squery-expression-conformance.test.ts, which callsprotocol.findDatadirectly and so is unaffected.Verification
All at head
4ccc5167(this PR's head: the two commits plus a merge oforigin/maina75ddba1throughscripts/pm/os-regen-merge.sh, which kept the branch's bytes of every generated reference sincemainhad not moved them), underscripts/pm/os-verify-lock.shon the shared box.Measurement script (before and after)
tsxprobe (not committed) parsed every declaration in the tables above with'',' , ',[],[''],'name',['name'], and calledassertSearchFieldsAreSearchable(registry-less host) for the standalone slot and forsearch.fields. Onbfdaba43it produced the "Measured first" table; on this head the diff against that output is exactly the rows marked refused in the "After" table, and the engine-sidesearchFieldsrows and the door rows are byte-identical.Suites: spec, rest and the full dogfood battery, all exit 0
pnpm build --concurrency=2(full turbo build, 114 tasks):Tasks: 114 successful, 114 total, lock VERDICT command-exit 0 (17m43s).pnpm --filter @objectstack/dogfood test: exit 0,Test Files 352 passed | 3 skipped (355),Tests 2844 passed | 20 skipped (2864). No failures.pnpm --filter @objectstack/spec test: exit 0,Test Files 754 passed (754),Tests 22430 passed | 1 todo (22431).pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/rest test: exit 0,Test Files 340 passed (340),Tests 6671 passed | 326 skipped (6997).pnpm --filter @objectstack/rest typecheck: exit 0.pnpm --filter @objectstack/spec check:generated(after--fix): all 14 artifacts current;--fixregenerated onlycontent/docs/references/**(4 files).pnpm check:adr-0087-registration: exit 0,1 declared-breaking changeset(s), each carrying an ADR-0087 disposition(not-required (no-migration-prescription)).spec testdoes not run thetest:repoproject; CI'sTest Coredoes.Derived gates: 104 derived, 104 run, all exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat4ccc5167: 104 families (the same list as at7b3667ba, before the merge).git fetch origin 621a4876(the teardown-shape self-test's pinned fixture): 104 exit 0, none exit 3.dispatch-gates --ran:104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.origin/mainmoved 21 commits after the merge; none touches a file in this diff (git diff --name-only HEAD...origin/mainagainst the ten paths), so no second merge was made, and the dogfood reading above is on the PR's final head.Generated by Claude Code