Skip to content

fix(metadata-protocol)!: refuse a searchFields override naming no field instead of reading it as absent - #27670

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-jmw8xb
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-jmw8xb

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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 of findData) refuse a searchFields value 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.md already teaches "Omit it to search the object's declared searchableFields", 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 为空)时,数据接口现在回 400 VALIDATION_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 to return (read as ABSENT); it now throws 400 VALIDATION_ERROR with param set to the caller's spelling and the message Query parameter 'PARAM' names no field; got VALUE. … — the envelope projectionFieldsValue gives $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-form search.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 a search term: 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 ignores searchFields sent without search.
  • 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, beside projectionFieldsValue; no other line of protocol.ts moves.
  • Export route (rest/src/data-action-routes.ts): searchFields= and searchFields=, are kept as the empty list instead of being dropped, so findData's gate refuses them on this route too. The route still ignores searchFields sent without search, as before.
  • Two pins flipped on purpose (triage ruling 6500648325, option A):
    • 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-form fields: [] / '' each answer 400 VALIDATION_ERROR naming 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: no lib_loan read, so no silent widening to every row. The other two legs still assert the engine's junction refusal unchanged.
    • Why both flip: both pins drive ObjectStackProtocolImplementation#findData, the same door GET /data/:object hands req.query to, so refusing at that door and keeping them could not both hold (dev report 6499580381, ruling 6500648325).
  • New door pin rest/src/data-empty-search-fields-door.test.ts: real RestServer → findData → ObjectQL → sqlite. GET list ($searchFields=, =,, = , , unprefixed searchFields=), POST query (searchFields: [], object-form fields: []) and the csv export (searchFields=, =,) each answer 400 VALIDATION_ERROR naming the spelling, with zero reads of the object; controls (named override narrows, absent scans the default set, on all three doors) unchanged.
  • Docs: the refusal table in content/docs/api/data-api.mdx and the search callout in content/docs/data-modeling/queries.mdx name the new answer.
  • Changeset .changeset/27484-blank-search-fields-refused.md (patch on @objectstack/metadata-protocol and @objectstack/rest, BREAKING banner, ADR-0087 not-required).

Tests

Head f0e6671a (merge of origin/main b761968 onto e995818e); the full package runs and the dogfood battery ran on e995818e, 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 skipped
  • pnpm --filter @objectstack/objectql test :: exit 0 · Test Files 487 passed (487) · Tests 9149 passed
  • pnpm --filter @objectstack/rest test :: exit 0 · Test Files 336 passed (336) · Tests 6591 passed, 326 skipped
  • pnpm --filter PKG typecheck for each of the three :: exit 0 (×3)
  • pnpm lint :: exit 0
Re-run on the merged head f0e6671
  • rebuilt 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)
  • Gate leg: 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_27484 hit in dist/index.js and dist/index.cjs. Red: query-expression-conformance "an override naming no field is refused" (promise resolved instead of rejecting), engine-search-empty-set junction leg (got the engine's has no searchable refusal instead of VALIDATION_ERROR), and 3 of 4 in data-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 HEAD empty; rebuilt; preflight --absent clean over 24 built files, tree clean.
  • Export leg (gate intact): --file packages/rest/src/data-action-routes.ts --anchor " if (typeof q.searchFields === 'string') {" → the old q.searchFields.length > 0 guard. Red: the export case only (searchFields="" answered 200); GET/POST/control stayed green. Restored: blob == HEAD 87b3e977, git diff HEAD empty.
Dogfood battery (e995818)

pnpm exec turbo run build --filter='@objectstack/dogfood^...' then pnpm --filter @objectstack/dogfood test under os-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

Verification

Local subset, head f0e6671a: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 93 families; every one run with its exit code captured before any pipe, all 93 exit 0 (check:type-check-debt under os-verify-lock.sh). --ran reconcile: "93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN". The same 93 were also all exit 0 on e995818e before 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) and check:dual-build-cjs-loads / spec check:skill-examples (seven dists built). CI-run families: see this PR's check-runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer summary block is the dispatch contract's Chinese section).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgTgHg75Rd5XFiwbUZh

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/rest, touching 4 documentable anchor(s).

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

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

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0561a988cb0b8b73becf80bbac8b3a407ed16e7e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 93460db3b8e889cbfd9f862493460443a419464d — the merge of head 7b14b3d3e51972083c03022a2be241857c3ad280 into base 0561a988cb0b8b73becf80bbac8b3a407ed16e7e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 0561a988cb0b8b73becf80bbac8b3a407ed16e7e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head f0e6671 · 2026-10-29T17:27Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f0e6671a19bed0a008f4cafd04a033f93708c1f7
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-query-validation.ts, packages/metadata-protocol/src/protocol.ts, packages/rest/src/data-action-routes.ts, packages/objectql/src/engine-search-empty-set.test.ts, packages/objectql/src/query-expression-conformance.test.ts, packages/rest/src/data-empty-search-fields-door.test.ts + packages/metadata-protocol/src/index.ts, packages/rest/src/crud-routes.ts, packages/rest/src/rest-server.ts, packages/rest/src/data-empty-projection-count-door.test.ts, packages/objectql/src/engine.ts, packages/objectql/src/empty-projection-door.ts, packages/spec/src/data/data-engine.zod.ts, packages/spec/src/data/query.zod.ts, packages/spec/src/data/search-fields.ts, packages/spec/src/api/protocol.zod.ts, packages/spec/src/api/errors.zod.ts, packages/types/src/data-error-classification.ts, ui/packages/data-objectstack/src/index.ts, ui/packages/plugin-list/src/ListView.tsx, ui/packages/plugin-grid/src/objectGridRowLoad.ts, ui/packages/plugin-grid/src/useServerGrouping.ts, ui/packages/plugin-grid/src/useWholeSetSummary.ts, ui/packages/plugin-kanban/src/ObjectKanban.tsx, ui/packages/plugin-gantt/src/ObjectGantt.tsx, ui/packages/fields/src/widgets/useRecordQuery.ts, skills/objectstack-query/SKILL.md (prose faces judged in ② and ④: .changeset/27484-blank-search-fields-refused.md, content/docs/api/data-api.mdx, content/docs/data-modeling/queries.mdx)

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 origin/main...pr27670 (merge-base b761968, local ref equals the head), the 50 check-runs on the head: 37 success, 13 skipped (path-filtered UI and opt-in lanes), none failed or in progress. Of the ten required contexts, Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard and Test UI are success; Lint UI, Type Check UI, Build & E2E UI, Live E2E UI are skipped on a diff with no ui/ path. Those conclusions are the gate verdicts; nothing was built, run or re-run here (every file cited below was read with git show pr27670:PATH).

① Derived judgments

  1. Accept-set narrowing at ObjectStackProtocolImplementation#findData — right. assertSearchFieldsAreSearchable (protocol-query-validation.ts L839–L900) now throws 400 VALIDATION_ERROR with param = the caller's spelling when the normalised list is empty: blank or comma-only string, [], $searchFields of the same, and object-form search.fields [] / ''. The shape check precedes resolveQueryFields, so a registry-less host refuses too. The alias fold (protocol.ts L12982–L12989) copies a blank value ('' != null) and records $searchFields as the wire spelling, which is what the pins assert. Every HTTP door read here reaches that one gate: GET /data/:object hands req.query straight to findData (crud-routes.ts L157–L163); POST /data/:object/query parses the body against FindDataRequestSchema and then calls findData (L357–L377); GET /data/:object/export forwards searchFields only beside search (data-action-routes.ts L1355). The list's total count (L13570) and the aggregate branch (L13424) receive options.searchFields after the gate, so one refusal covers them.
  2. A second narrowing the PR body and changeset do not spell out — right in behaviour, under-declared. The gate is called "whether or not a search term rode along" (protocol.ts L13184–L13199), so GET /data/x?$searchFields= with no $search is now 400 where it was 200 (before: names = [], return, the engine then ignored the option). The posture is the axis's own — the existing pin "the override is validated even without a search term riding along" (query-expression-conformance.test.ts L2237–L2246) already refuses a stale non-empty override without a term — so the empty case inherits it consistently. The changeset's Before/Now describe only the "beside a search" case. Should-fix S1 (prose).
  3. Export route — right. typeof q.searchFields === 'string' now yields [] for a blank value; the length === 0 → undefined drop is gone; the array arm is unchanged; the route forwards searchFields only when search is set (L1355, and [] is truthy), so "ignored without search" still holds as the route's docblock declares (L958). DATA_EXPORT_PARAMS (rest-server.ts L675) is a recognition set, untouched; the multiplicity census deliberately leaves searchFields multi-valued (query-multiplicity.ts L52) — both unaffected.
  4. Public surface — unchanged at the package boundary; right. quotedQueryValue becomes a module export of protocol.ts; packages/metadata-protocol/src/index.ts re-exports by name (L3, L7, L17, L122, L134, L217) and carries no export *, so no new symbol leaves @objectstack/metadata-protocol. The changeset's "no export or type changes" holds at the boundary a consumer sees.
  5. Engine posture unchanged — right as ruled, an asymmetry worth naming. engine.find / count / aggregate with searchFields: [] or '' still fall back to the allowed set (spec/data/search-fields.ts resolveSearchFields L265–L274; the engine reads ast.searchFields ?? raw.fields at engine.ts L13380). Ruling 6500648325 keeps it so and the changeset's "Unchanged" bullet and queries.mdx L555–L556 say so. On the projection axis the engine was later taught to refuse fields: [] itself (objectql: the in-process engine reads fields: [] as every column while every published projection declaration refuses an empty projection (found on #26954) #27008, objectql/src/empty-projection-door.ts), so a hook, flow or plugin that builds an empty searchFields list by mistake still widens silently at the engine while an empty fields list does not. Escalated in ③, not a defect of this diff.
  6. The published declaration is not narrowed with the door — wrong as an end state, acceptable as a stage. QueryTransportParamsSchema.$searchFields is z.union([z.string(), z.array(z.string())]) (data-engine.zod.ts L962), EngineQueryOptionsSchema.searchFields is z.array(z.string()) (L189), QuerySchema.searchFields the same (query.zod.ts L549) and FullTextSearchSchema.fields likewise (L452): all admit '' and []. The sibling $select / select / fields carry PROJECTION_ACCEPT_SET (spec: QueryTransportParamsSchema.select / $select and GetDataRequestSchema.select admit an empty projection that the findData and getData doors refuse (sibling of #26817) #26909, L110–L141, L953, L974), with the spec pin "§6 the projection spellings admit exactly what the doors accept" (query-transport.test.ts L404). So on main the spec does not refuse an empty searchFields; this narrowing takes a declared capability away at the consumer rather than making the spec's own refusal real. The tree shows the consequence: on POST /data/:object/query, select: [] is the schema door's 400 VALIDATION_FAILED (data-empty-projection-count-door.test.ts L166–L175), while searchFields: [] passes FindDataRequestSchema.safeParse and is the gate's 400 VALIDATION_ERROR (new door test L369–L378) — two sibling axes, one shape class, different codes on one door. The projection axis itself landed door-first (data list door: a blank $select answers every column and an unrecognised $count value counts — malformed values are read as the absent default #26644), then the declaration (spec: QueryTransportParamsSchema.select / $select and GetDataRequestSchema.select admit an empty projection that the findData and getData doors refuse (sibling of #26817) #26909), then the engine (objectql: the in-process engine reads fields: [] as every column while every published projection declaration refuses an empty projection (found on #26954) #27008), so a door-first stage is the repo's own precedent and not a FAIL; what it owes is the declaration in the changeset and the follow-up card. Should-fix S2.
  7. Error code and envelope (brief point 3) — right. VALIDATION_ERROR is the standard vocabulary's generic 400 member (spec/src/api/errors.zod.ts L55; 400: 'VALIDATION_ERROR' L184), ADR-0112's closed set. The thrown object — status 400, code, param, message Query parameter 'PARAM' names no field; got VALUE. The query was not run: … — is the projectionFieldsValue shape line for line (protocol.ts L4669–L4684), and both are rendered by the same mapDataError (types/src/data-error-classification.ts L654), which puts code and error on the wire and param on neither. Same envelope, same words, one new sentence.
  8. In-repo producers (brief point 2) — none breaks; right. Every Console producer sends $searchFields / searchFields only when the list has at least one entry: the data-source adapter guards all three request paths (ui/packages/data-objectstack/src/index.ts L5133, L5250, L5385), and above it plugin-list/ListView.tsx L3447 / L5169 / L6536, plugin-grid/objectGridRowLoad.ts L575, useServerGrouping.ts L199 / L219, useWholeSetSummary.ts L163, plugin-kanban/ObjectKanban.tsx L700, plugin-gantt/ObjectGantt.tsx L876, fields/useRecordQuery.ts L215; ui/apps/console/src/services/approvalRequestsDataSource.ts refuses $searchFields before any request. packages/client/src, packages/client-react/src, examples/, packages/mcp/src and skills/ hold no searchFields producer at all (0 files each on pr27670); skills/objectstack-query/SKILL.md L319 teaches "Omit it". No test outside the three in-PR files pins the ABSENT posture (0 hits for an empty literal across packages/** and ui/** tests). The BREAKING banner therefore addresses external API callers only, which is what the changeset's "Who sees it" says.

② Semver level

  • Changeset .changeset/27484-blank-search-fields-refused.md: patch on @objectstack/metadata-protocol and @objectstack/rest; Clause-②: no (narrowing) and Metadata-protocol: none, identical to the PR body's two lines; adr-0087: not-required (no-migration-prescription), a category the gate lists (scripts/check-adr-0087-registration.mjs L147); Check Changeset is success on the head.
  • The level matches what the diff publishes: the packages whose runtime moves are exactly those two (objectql changes are test-only; packages/spec is untouched). AGENTS.md's rule — a non-protocol narrowing ships patch and keeps its BREAKING banner and ADR-0087 disposition — is met. Metadata-protocol: none is right: a query value, not a key a metadata author writes.
  • The BREAKING body carries Before / Now / Unchanged / Who sees it / Remedy; the migration is the remedy sentence (send a field name or omit the parameter) — the only one there is.
  • Two prose gaps against the diff: the no-search case (① 2) is absent from Before/Now, and the body says nothing about the published declaration still admitting the refused value nor about the POST door's VALIDATION_FAILED / VALIDATION_ERROR split with the sibling (① 6). Both are should-fix on the prose, not on the level.

③ Boundary flags

④ Design judgment

  • packages/metadata-protocol/src/protocol-query-validation.ts — holds. Shape first (string split / trim / filter; array with a non-string member → INVALID_FIELD; any other type → INVALID_FIELD), then the new empty-list refusal, then the field-map tiering — the [P3] data: FieldNode's nested-select object form is declared but nothing produces or consumes it — enforce or remove #4196 order the function names. Tried to break it: searchFields: null is != null-false at the call site, so absent stays absent; searchFields: [' '] keeps one blank member in the array arm and is refused downstream as an unknown field (a blank name is never one), the same split projectionFieldsValue's array arm makes — consistent, not a finding; object-form fields: '' reaches the gate through the search.fields != null call site. The message quotes the raw wire value through quotedQueryValue (bounded at 64 chars), since the gate runs ahead of the spec's parse-time lowering (data-engine.zod.ts L1117 belongs to the engine's side). The thrown runtime string carries no tracker number; the [#27484] tag is in a code comment, where the repo allows it.
  • packages/metadata-protocol/src/protocol.ts — holds. One word (export) and a docblock sentence; the call sites (L13192–L13199) do not move; the hot-file region claim is true of the diff.
  • packages/rest/src/data-action-routes.ts — holds. The route's other parameters already judge a blank value per parameter (limit= refused via emptyIsAbsent: false [finding] three REST doors read ?limit= with a bare Number(), so a non-numeric limit answers 200 with a widened or substituted window: search loses its cap, export returns one row, meta history returns the whole log #20062; fields=, search=, filter=, orderby= read as absent), so there is no single door rule this breaks; searchFields now answers as it does on the list door because the refusal is findData's, not the route's. The one asymmetry left — fields= blank is read as schema columns on export while $select= blank is refused on the list — predates this PR and is out of its scope. Declared: the changeset's Before names the export drop; the docs table names searchFields=.
  • packages/objectql/src/engine-search-empty-set.test.ts — holds. The flipped leg asserts VALIDATION_ERROR / status 400 / param 'searchFields' and keeps the read-count assertion (no lib_loan read on either refusal); the control object and the blank term are still pinned. What the file can no longer prove — that a blank override over the junction reaches the engine's own refusal — is unreachable by construction under option A.
  • packages/objectql/src/query-expression-conformance.test.ts — holds. Seven spellings each asserting status / code / param, plus the absent-override control. Note (no action owed): no case pins the quoted value (got "" vs got []); one toThrow on the message would.
  • packages/rest/src/data-empty-search-fields-door.test.ts — holds. Real RestServer over findData, ObjectQL and sqlite; the four GET spellings, two POST slots and two export spellings each assert status, code and the named spelling, with zero driver reads of the object; the controls cover all three doors in both directions (named narrows, absent scans). Same harness shape as the sibling file.
  • Prose faces. content/docs/api/data-api.mdx L157 row: matches the door. content/docs/data-modeling/queries.mdx L551–L556: matches, and its "Internal callers reaching engine.find() directly are unaffected" is true (① 5). The changeset: see ② and S1 / S2.

Findings. Must-fix: none. Should-fix: S1 — changeset Before/Now and PR body: state that an empty searchFields is refused with or without a search term on the list and query doors (export alone ignores it without search). S2 — changeset "Unchanged" or Acceptance notes: state that QueryTransportParamsSchema.$searchFields, EngineQueryOptionsSchema.searchFields and FullTextSearchSchema.fields still admit the refused value, and that on POST /data/:object/query the sibling select: [] answers VALIDATION_FAILED while searchFields: [] answers VALIDATION_ERROR; file the follow-up card named in ③ for the declaration (and the engine question). S3 — file the json-export truncated-stream defect as a card; an acceptance note is not a carrier.

Implemented-by: claude/project-thread-jmw8xb
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

…l admitting an empty searchFields (#27484)

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

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7b14b3d3e51972083c03022a2be241857c3ad280
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-query-validation.ts, packages/metadata-protocol/src/protocol.ts, packages/rest/src/data-action-routes.ts, packages/objectql/src/engine-search-empty-set.test.ts, packages/objectql/src/query-expression-conformance.test.ts, packages/rest/src/data-empty-search-fields-door.test.ts + .changeset/27484-blank-search-fields-refused.md (the delta), packages/spec/src/data/data-engine.zod.ts, packages/spec/src/data/query.zod.ts, packages/rest/src/crud-routes.ts, packages/rest/src/data-empty-projection-count-door.test.ts, cards #27682 and #27683 (the carriers the body now names)

Reviewed-at: 2026-10-29T17:58Z

Delta re-review of the head my record 6504124784 passed (f0e6671a19bed0a008f4cafd04a033f93708c1f7). git diff pr27670 pr27670b touches one file, .changeset/27484-blank-search-fields-refused.md, +2/−1; every code, test and docs path of the PR is byte-identical to the reviewed head, so sections ①, ③ and ④ of 6504124784 stand unchanged and are not restated. Inputs: that diff, the PR body on this head, the check-runs on this head, and the two cards the body cites.

① Derived judgments

  • No accept-set or public-surface change in the delta: a changeset body edit only. Judgments 1–8 of 6504124784 hold on this head as written.
  • The two new sentences are true of the tree I read: (a) "on the list and query doors this holds with or without a search term" — the gate is called whether or not a term rode along (protocol.ts L13184–L13199), the list door hands req.query straight to findData (crud-routes.ts L157–L163), the export route forwards searchFields only beside search (data-action-routes.ts L1355); (b) the four declarations named — QueryTransportParamsSchema.$searchFields (data-engine.zod.ts L962), EngineQueryOptionsSchema.searchFields (L189), QuerySchema.searchFields (query.zod.ts L549), FullTextSearchSchema.fields (L452) — do admit '' and [], and the POST /data/:object/query split (select: [] → body schema's VALIDATION_FAILED, data-empty-projection-count-door.test.ts L166–L175; searchFields: [] → the gate's VALIDATION_ERROR, data-empty-search-fields-door.test.ts L369–L378) is what the two door tests pin.

② Semver level

③ Boundary flags

④ Design judgment

  • .changeset/27484-blank-search-fields-refused.md: the only path that moved; both additions match the diff and the cited code (① above). No other in-scope file changed between the two heads, so the per-file judgments of 6504124784 ④ are the judgments of this head.

Findings. Must-fix: none. Should-fix: none open — S1, S2, S3 of 6504124784 are closed by this delta and its two cards. Note (no action owed, carried over): one conformance case could pin the quoted value in the refusal message.

Implemented-by: claude/project-thread-jmw8xb
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:查询里写了 $searchFields 却一个字段都没给(空串、只有逗号、空数组、对象形 search.fields 为空),列表、查询、导出三个数据门现在回 400 VALIDATION_ERROR 并点名参数,和 $select= 一样;列表与查询门不带 search 也拒。
  • 为什么改:以前这种值被当成「没写」,搜索悄悄扫全部默认搜索字段,调用方想收窄却拿到更宽的结果。
  • 风险与回滚:只影响发了空 searchFields 的 API 调用方(Console 与内置视图只在至少一个字段时才发,已核实);引擎直接调用不变(按裁决 A);两个旧钉子按裁决有意翻转。回滚 = revert 本 PR。
  • 先看:metadata-protocol/src/protocol-query-validation.ts 的拒收段,rest/src/data-action-routes.ts 的导出一处。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/data-empty-search-fields-door.test.ts。
  • 需要你做的:无。非受管面,契约审查 PASS,席位落地。

席位意见: ACCEPT。按裁决 A 实现,收窄已过两轮契约审查(原 head 与散文补丁 head);公开声明仍接受空值的问题与 json 导出截断流已分别立卡。

席位核对

ACCEPT · head 7b14b3d3e51972083c03022a2be241857c3ad280 · 2026-10-29T18:22Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant