Repository navigation
fix(metadata-protocol)!: refuse a searchFields override naming no field instead of reading it as absent - #27670
Conversation
…ld instead of reading it as absent (#27484) A blank or comma-only `$searchFields`, an empty array, or an empty object-form `search.fields` now answers 400 VALIDATION_ERROR naming the parameter, the projection's envelope (#26644). The export route keeps a blank value as the empty list so the same gate refuses it. The two pins that read the empty override as absent are flipped on purpose (triage ruling 6500648325, option A). Claude-Session: https://claude.ai/code/session_01L6bhgTgHg75Rd5XFiwbUZh Co-authored-by: Claude <noreply@anthropic.com>
…ERROR (#27484) Claude-Session: https://claude.ai/code/session_01L6bhgTgHg75Rd5XFiwbUZh Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTgHg75Rd5XFiwbUZh Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 26 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 93460db3b8e889cbfd9f862493460443a419464d && git checkout 93460db3b8e889cbfd9f862493460443a419464d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0561a988cb0b8b73becf80bbac8b3a407ed16e7e 7b14b3d3e51972083c03022a2be241857c3ad280 && git checkout -B drift-repro 0561a988cb0b8b73becf80bbac8b3a407ed16e7e && git merge --no-ff 7b14b3d3e51972083c03022a2be241857c3ad280
node scripts/docs-audit/affected-docs.mjs --json 0561a988cb0b8b73becf80bbac8b3a407ed16e7e
|
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head f0e6671 · 2026-10-29T17:27Z |
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F Contract reviewServed-tier: Reviewed-at: 2026-10-29T17:45Z Inputs: card #27484 (body and all 8 comments, among them triage 6496252317, retriage 6497944037 / 6498776875 / 6499611991, ruling 6500648325, claim 6499488942, os-dev-reports 6499580381 and 6503706236), PR #27670 body and file list (9 files, +286/−20), the net diff ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings. Must-fix: none. Should-fix: S1 — changeset Before/Now and PR body: state that an empty Implemented-by: VERDICT: PASS |
…l admitting an empty searchFields (#27484) Claude-Session: https://claude.ai/code/session_01L6bhgTgHg75Rd5XFiwbUZh Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F Contract reviewServed-tier: Reviewed-at: 2026-10-29T17:58Z Delta re-review of the head my record ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings. Must-fix: none. Should-fix: none open — S1, S2, S3 of Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F 维护者速读(席位终稿)
席位意见: ACCEPT。按裁决 A 实现,收窄已过两轮契约审查(原 head 与散文补丁 head);公开声明仍接受空值的问题与 json 导出截断流已分别立卡。 席位核对ACCEPT · head
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Requested by Sales · project thread
Fixes #27484
Clause-②: no (narrowing) — the data doors (
GET /data/:object,POST /data/:object/query,GET /data/:object/export, and every other caller offindData) refuse asearchFieldsvalue that names no field, which they answered 200 over the default search set before. Nothing is widened.Metadata-protocol: none
Skills-docs: none —
skills/objectstack-query/SKILL.mdalready teaches "Omit it to search the object's declaredsearchableFields", and no skill emits an empty override; the two docs pages that list the search refusals are updated here (content/docs/api/data-api.mdx,content/docs/data-modeling/queries.mdx).维护者速读
$searchFields却一个字段都没给(空串、只有逗号、空数组,或对象形search.fields为空)时,数据接口现在回 400VALIDATION_ERROR并点名参数,跟$select=一样;导出接口同样拒收。searchFields的 API 调用方(Console 与自带视图插件都只在至少有一个字段时才发);引擎直接调用不变。回滚 = revert 本 PR。packages/metadata-protocol/src/protocol-query-validation.ts(拒收那一段)、packages/rest/src/data-action-routes.ts(export 不再把空值当缺省)、packages/objectql/src/query-expression-conformance.test.ts(有意翻转的钉子)。pnpm --filter @objectstack/rest exec vitest run src/data-empty-search-fields-door.test.ts,预期 4 个用例全过(列表、POST、导出三个门回 400,对照组照旧)。What changes
assertSearchFieldsAreSearchable(metadata-protocol/src/protocol-query-validation.ts): an override that names no field used toreturn(read as ABSENT); it now throws400 VALIDATION_ERRORwithparamset to the caller's spelling and the messageQuery parameter 'PARAM' names no field; got VALUE. …— the envelopeprojectionFieldsValuegives$select(data list door: a blank $select answers every column and an unrecognised $count value counts — malformed values are read as the absent default #26644). It covers the standalone slot in every spelling ($searchFields,searchFields, string or array) and the object-formsearch.fields, because the gate is the one place both reach. It runs before the field-map lookup, so a registry-less host refuses too. Absent stays absent: the door calls the gate only when a value was sent. On the list and query doors the refusal holds with or without asearchterm:GET /data/:object?$searchFields=alone answered 200 before and is 400 now, as a non-empty override naming an unknown field already was; the export route alone still ignoressearchFieldssent withoutsearch.quotedQueryValue(metadata-protocol/src/protocol.ts): exported from the module (not from the package index) so the gate quotes the refused value exactly as the projection refusal does. One word in the hot file, besideprojectionFieldsValue; no other line ofprotocol.tsmoves.rest/src/data-action-routes.ts):searchFields=andsearchFields=,are kept as the empty list instead of being dropped, sofindData's gate refuses them on this route too. The route still ignoressearchFieldssent withoutsearch, as before.objectql/src/query-expression-conformance.test.ts— "an empty override is ABSENT, not malformed" becomes "an override naming no field is refused, not read as absent":'',',',' , ',[],$searchFields: '', and object-formfields: []/''each answer400 VALIDATION_ERRORnaming the slot; a search with no override still answers the default scan.objectql/src/engine-search-empty-set.test.ts— the junction leg{ search: 'Dune', searchFields: '' }now asserts the door's refusal (VALIDATION_ERROR,param: 'searchFields') and keeps its point: nolib_loanread, so no silent widening to every row. The other two legs still assert the engine's junction refusal unchanged.ObjectStackProtocolImplementation#findData, the same doorGET /data/:objecthandsreq.queryto, so refusing at that door and keeping them could not both hold (dev report 6499580381, ruling 6500648325).rest/src/data-empty-search-fields-door.test.ts: realRestServer→findData→ObjectQL→ sqlite. GET list ($searchFields=,=,,= ,, unprefixedsearchFields=), POST query (searchFields: [], object-formfields: []) and the csv export (searchFields=,=,) each answer 400VALIDATION_ERRORnaming the spelling, with zero reads of the object; controls (named override narrows, absent scans the default set, on all three doors) unchanged.content/docs/api/data-api.mdxand the search callout incontent/docs/data-modeling/queries.mdxname the new answer..changeset/27484-blank-search-fields-refused.md(patchon@objectstack/metadata-protocoland@objectstack/rest, BREAKING banner, ADR-0087not-required).Tests
Head
f0e6671a(merge oforigin/mainb761968 ontoe995818e); the full package runs and the dogfood battery ran one995818e, the same diff before the merge.Package suites and typecheck (e995818)
pnpm --filter @objectstack/metadata-protocol test:: exit 0 · Test Files 279 passed, 4 skipped (283) · Tests 21781 passed, 26 skippedpnpm --filter @objectstack/objectql test:: exit 0 · Test Files 487 passed (487) · Tests 9149 passedpnpm --filter @objectstack/rest test:: exit 0 · Test Files 336 passed (336) · Tests 6591 passed, 326 skippedpnpm --filter PKG typecheckfor each of the three :: exit 0 (×3)pnpm lint:: exit 0Re-run on the merged head f0e6671
metadata-protocol,objectql,rest;query-expression-conformance.test.ts+engine-search-empty-set.test.ts: 207/207;data-empty-search-fields-door.test.ts+export-integration.test.ts+data-empty-projection-count-door.test.ts: 35/35;pnpm --filter @objectstack/metadata-protocol test: 21781 passed, 26 skipped.Ablation (both legs red, then restored to the HEAD blob)
node scripts/ablation-replace.mjs --file packages/metadata-protocol/src/protocol-query-validation.ts --anchor ' if (names.length === 0) {' --replacement " if ((globalThis as any).ABLATE_27484 === 'refuse' && names.length === 0) {"— anchor 1 → 0, blob 1d6fa7dd → 1e8d9b9a; rebuilt;ablation-dist-preflight.mjs @objectstack/metadata-protocol ABLATE_27484hit indist/index.jsanddist/index.cjs. Red:query-expression-conformance"an override naming no field is refused" (promise resolved instead of rejecting),engine-search-empty-setjunction leg (got the engine'shas no searchablerefusal instead ofVALIDATION_ERROR), and 3 of 4 indata-empty-search-fields-door.test.ts(GET list, POST query, export each answered 200 with all 3 rows). Restored: blob after == HEAD 1d6fa7dd,git diff HEADempty; rebuilt; preflight--absentclean over 24 built files, tree clean.--file packages/rest/src/data-action-routes.ts --anchor " if (typeof q.searchFields === 'string') {"→ the oldq.searchFields.length > 0guard. Red: the export case only (searchFields=""answered 200); GET/POST/control stayed green. Restored: blob == HEAD 87b3e977,git diff HEADempty.Dogfood battery (e995818)
pnpm exec turbo run build --filter='@objectstack/dogfood^...'thenpnpm --filter @objectstack/dogfood testunderos-verify-lock.sh, no shard, no path:VERDICT command-exit 0· Test Files 351 passed, 3 skipped (354) · Tests 2822 passed, 20 skipped (2842) · no failures.Acceptance notes
claude/project-thread-jmw8xbis the card thread's harness-pinned branch, named in claim 6499488942; it carries no issue number.[before the firstfindData, so afindDatarefusal there (this one, and the existingsearchFields=nopeone) lands as a truncated json stream rather than a clean 400; csv and xlsx answer the envelope. Not changed here; filed as rest:GET /data/OBJECT/export?format=jsonwrites its opening bracket before the first read, so a refused query arrives as a truncated JSON stream instead of a 400 #27682.QueryTransportParamsSchema.$searchFields,EngineQueryOptionsSchema.searchFields,QuerySchema.searchFieldsandFullTextSearchSchema.fieldsaccept''and[]. So onPOST /data/:object/querythe siblingselect: []is the body schema's400 VALIDATION_FAILEDwhilesearchFields: []is the gate's400 VALIDATION_ERROR. Narrowing the declarations (and the engine question) is follow-up spec/protocol: the publishedsearchFieldsdeclarations still admit an empty list that the data door now refuses; doessearchFieldsfollowselectinto a declared accept set, and does the engine follow? #27683.engine.find()/engine.count()called directly still read an emptysearchFieldsas absent (the engine posture;protocol-record-reads.ts's palette sweep never sends an empty one).ui/packages/data-objectstack,plugin-listListView.tsx,plugin-kanban,plugin-ganttall send$searchFields/searchFieldsonly when the list has at least one entry.metadata-protocol/src/protocol.tsis harden(metadata): a permission set's name is unique across packages #27535;git merge-tree --write-tree HEADagainst its head 585e619 is clean. No open PR touches the other changed files.Verification
Local subset, head
f0e6671a:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 93 families; every one run with its exit code captured before any pipe, all 93exit 0(check:type-check-debtunderos-verify-lock.sh).--ranreconcile: "93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN". The same 93 were also allexit 0one995818ebefore the merge. Two families answered exit 3 on the first pass and were re-run after their prerequisite, not read as a verdict:check-plugin-teardown-shape.mjs --self-test(pinned fixture commit fetched) andcheck:dual-build-cjs-loads/spec check:skill-examples(seven dists built). CI-run families: see this PR's check-runs.Human Minimum
packages/specholds no runtime logic. (not touched)pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgTgHg75Rd5XFiwbUZh