Skip to content

fix(objectql,metadata-protocol)!: an in-process search-field override that names no searchable field is refused, never widened to a default set (#28120) - #28175

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-tiru7x
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-tiru7x

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#3 · os-elon-musk · session_016WV4cyi29iG8J3UUXr68AZ

Fixes #28120 (family card; folds #28121, closed as folded into it)

Clause-②: no (narrowing) — two in-process tolerances the Data API doors already refuse are closed: the stored-metadata reader seam stops narrowing a search-field override that names no field and hands it to the engine's refusal, and the engine refuses an override naming none of the fields its search scans. No export, answer member, status or error code is added; the refusals are the doors' VALIDATION_ERROR / 400 and INVALID_FIELD / 400.
Metadata-protocol: none

维护者速读

  • 改了什么:进程内调用方(host handler、hook、flow)的 searchFields 覆盖,凡是 REST 门会拒的两种值,进程内现在也拒:(a) 经存储元数据读缝的空列表,不再被改写成默认集合,交给引擎按 400 VALIDATION_ERROR 拒;(b) 全部字段名都不可搜(拼错、改名、内部字段)的覆盖,引擎按 400 INVALID_FIELD 拒,不再退回整个默认集合去搜。
  • 为什么改:fix(objectql)!: the in-process engine refuses a search-field override that names no field, as the doors and the shared declarations do #28108 合同复审的 S1、S3 两条。调用方写覆盖是为了收窄,静默放宽成默认集合是错的。
  • 风险与回滚:只拒原本被放宽的值;部分字段名可识别的覆盖、未给覆盖、合法覆盖都照旧。回滚即 revert。
  • 先看:packages/objectql/src/engine.ts 的 refuseSearchOverrideNamingNoSearchableField,packages/metadata-protocol/src/protocol.ts 的 narrowStoredMetadataSearch。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/engine-empty-search-fields-refusal.test.ts,预期 47 passed。
  • 需要你做的:无;席位安排契约复审后落地。

What changes

(a) The stored-metadata reader seam (@objectstack/metadata-protocol, narrowStoredMetadataSearch). An explicit search-field list that names no field (an empty array, a blank or comma-only string, an array with no string entry) is now passed through untouched, so the engine's own refusal answers it. Before, it was read as "names none", narrowed, and the narrowed default set was written over the caller's list, so searchFields: [] from a host handler on sys_metadata / sys_metadata_history was served. The door never reaches this with such a list: its ingress gate (assertSearchFieldsAreSearchable) refuses it first, unchanged. The docblock now says what is true: an empty override is refused by the engine, not read as absent. refuseOrNarrowStoredMetadataEvaluate in packages/runtime calls this function and needed no change. A non-string, non-array value is still narrowed, as before.

(b) The engine (@objectstack/objectql, expandSearchOnAst). A searchFields or object-form search.fields override that names none of the fields the expansion scans is refused 400 INVALID_FIELD (param, field / fields naming what the caller wrote, object) before any driver read, on find, findOne, count and aggregate. "Scans" is judged on the set the expansion resolves over (resolveSearchFieldResolution over the fields left after the internal: true and credential withholds), so this refuses exactly the overrides resolveSearchFields used to drop and widen back to the whole set, including an override naming only unknown fields on the stored-metadata family, which is now refused rather than resolved to the default set. Kept as it was:

@objectstack/spec, resolveSearchFields. Docblock only: it records that the engine now refuses the fallback's input before this runs. The function itself is unchanged (it is spec's public helper and stays non-throwing).

Docs. content/docs/protocol/objectql/query-syntax.mdx: the sentence saying direct engine callers keep the tolerant intersection now says one naming no searchable field is refused.

Why

Both halves come from the contract review of PR #28108 (record 6523659371, S1 and S3) and share one defect: an in-process override that the doors refuse was widened to a default set instead of refused. Triage 6524748197 graded it one family card, Clause-②: no (narrowing).

Verification

Premises measured first on main 277275aa (fix files reset to main and rebuilt), in-process, through the seam over a real ObjectQL with the real SysMetadataObject and a driver double recording each read: (a) searchFields: [], ' , ' and search.fields: [] through the seam were served (narrowed set handed to the driver); (b) an all-unknown override widened to the default set on an ordinary object and on the family. REST callers: unchanged, refused by assertSearchFieldsAreSearchable (the door-parity cases below).

  • packages/runtime/src/stored-metadata-reader-seam-search-override.test.ts (new, 15, seam over a real engine, find / findOne / count): the empty array, comma-only string and empty object-form list are refused 400 VALIDATION_ERROR before any driver read; an all-unknown override is refused 400 INVALID_FIELD before any driver read; controls: a named override scans only what it names, an absent override is still narrowed off the body and hash columns.
  • packages/objectql/src/engine-empty-search-fields-refusal.test.ts (+32, all four verbs): all-unknown array, comma string, object form and a list with no string entry are refused 400 INVALID_FIELD before any driver read; the refusal names the field; the REST door answers the same status and code; controls: partly-known override narrows to its known names, valid and absent overrides answer as before, an object with no searchable field keeps the metadata-protocol / objectql: a search term on an object with no searchable field is dropped silently, so the read answers every row (an empty auto-default search set is not refused) #24284 refusal.
  • Two existing engine tests pinned the old fallback and now pin the refusal: engine-search-empty-set.test.ts (an override intersected to nothing) and internal-field-positions.test.ts (an override naming only an internal: true column; a partly-known one is still intersected away, never scanning it).
  • Ablations (fix files reset to main and rebuilt): seam fix out, engine fix in ⇒ 6 seam cases red (the empty-list cases), engine pins green; engine fix out, seam fix in ⇒ 14 engine cases red and 3 seam cases red (the all-unknown cases); both in ⇒ all green. Each half's pin bites on its own.
  • Package suites at head: objectql, metadata-protocol, runtime green; pnpm --filter <pkg> typecheck green for objectql, metadata-protocol, runtime, spec. Dogfood battery, every dispatch-gates --commands family and pnpm lint: results posted on the card with the os-dev report.

Skills-docs: git grep -n -i searchFields -- skills content/docs read for wording on the engine's tolerance: content/docs/protocol/objectql/query-syntax.mdx said direct engine callers keep the tolerant intersection, updated here. skills/objectstack-query/SKILL.md describes the REST refusal only, still true; content/docs/api/data-api.mdx describes the door's history, still true. Generated content/docs/references/** untouched.

Out of the order's file surface, left as is: the assertSearchFieldsAreSearchable docblock in protocol.ts (~L12546) explains the door's gate by the engine's old fallback; it stays true of resolveSearchFields itself and is outside narrowStoredMetadataSearch.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran; the lint reading is posted on the card with the gate battery.)
  • 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: its generated artifacts are regenerated and committed. (Docblock only; the spec artifact gates' reading is posted on the card with the gate battery.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the lane's maintainer summary).

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cyi29iG8J3UUXr68AZ

… that names no searchable field is refused, never widened to a default set

The stored-metadata reader seam (`narrowStoredMetadataSearch`) read an
explicit search-field list naming no field as "names none" and wrote the
narrowed default set over it, so `searchFields: []` from a host handler was
served where the door and the engine refuse it. It now passes such a list to
the engine, whose own refusal answers (`400 VALIDATION_ERROR`).

The engine refuses a `searchFields` / `search.fields` override that names
none of the fields its search scans with `400 INVALID_FIELD`, the door's
code, before any driver read, where `resolveSearchFields` dropped the names
and fell back to the whole default set. An override naming some searchable
field keeps its other names intersected away, and an object with no
searchable field keeps its own refusal.

Fixes #28120

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyi29iG8J3UUXr68AZ
…-unknown search-override fallback

`engine-search-empty-set` and `internal-field-positions` asserted that an
override naming only fields outside the allowed set fell back to that set.
They now assert the refusal, and the second keeps a partly-known override's
intersection as its control. The query-syntax page says what the engine now
does with such an override.

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

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head dedf2e8e860266741549a657a25518ea8c836b8d · 2026-10-30T15:23Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: no (narrowing), and a packages/spec/src/** path).

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/spec, touching 6 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/data/search-fields.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

18 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 a3d24c991f2d793a1600e7f25fb0d5f577ed2fff.

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/data/search-fields.ts) — pages documenting those are invisible to this run
  • 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 — 171 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 a3d24c991f2d793a1600e7f25fb0d5f577ed2fff → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b3095eb9f9ca96f9c4b48bcd5946cd562bf79c6f — the merge of head 872e1533357df9da75636f82d0487376ea537a0b into base a3d24c991f2d793a1600e7f25fb0d5f577ed2fff, 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 b3095eb9f9ca96f9c4b48bcd5946cd562bf79c6f && git checkout b3095eb9f9ca96f9c4b48bcd5946cd562bf79c6f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a3d24c991f2d793a1600e7f25fb0d5f577ed2fff 872e1533357df9da75636f82d0487376ea537a0b && git checkout -B drift-repro a3d24c991f2d793a1600e7f25fb0d5f577ed2fff && git merge --no-ff 872e1533357df9da75636f82d0487376ea537a0b

node scripts/docs-audit/affected-docs.mjs --json a3d24c991f2d793a1600e7f25fb0d5f577ed2fff

⚠️ 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 a3d24c991f2d793a1600e7f25fb0d5f577ed2fff → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…naming no searchable field

The full-text search callout said direct `engine.find()` callers were
unaffected by the override refusals; they now get both.

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

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head 5bb014d9df623ce23a969a5b94d9cd060e59742e · 2026-10-30T15:25Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: no (narrowing), and a packages/spec/src/** path).

…o-search case it leaves unjudged

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

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head 872e1533357df9da75636f82d0487376ea537a0b · 2026-10-30T15:32Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: no (narrowing), and a packages/spec/src/** path).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 872e1533357df9da75636f82d0487376ea537a0b
Local-runs: none
Scope: packages/metadata-protocol/src/protocol.ts (narrowStoredMetadataSearch 5160-5265), packages/objectql/src/engine.ts (refuseSearchOverrideNamingNoSearchableField 772-817, expandSearchOnAst 13476-13570), packages/spec/src/data/search-fields.ts (resolveSearchFields docblock 252-266), packages/objectql/src/engine-empty-search-fields-refusal.test.ts, packages/objectql/src/engine-search-empty-set.test.ts, packages/objectql/src/internal-field-positions.test.ts, packages/runtime/src/stored-metadata-reader-seam-search-override.test.ts + packages/runtime/src/stored-metadata-reader-seam.ts (refuseOrNarrowStoredMetadataEvaluate 151-154, unchanged), packages/metadata-protocol/src/protocol-query-validation.ts (assertSearchFieldsAreSearchable 839-990), packages/metadata-protocol/src/protocol.ts (findData gate order 13180-13207), packages/spec/src/data/search-fields.ts (resolveSearchFieldResolution, autoDefaultFields), packages/objectql/src/search-filter.ts (expandSearchToFilter, withholdUnqueryable 225-262), packages/metadata-protocol/src/protocol-record-reads.ts (the global-search sweep 490-620), packages/rest/src/data-action-routes.ts 1290-1300, packages/types/src/data-error-classification.ts (mapDataError), scripts/check-adr-0087-registration.mjs (the no-migration-prescription grammar); .changeset/28120-in-process-search-override-no-field.md, content/docs/protocol/objectql/query-syntax.mdx 1186-1200 and content/docs/data-modeling/queries.mdx 538-552 read as prose faces to judge the sentences, not reviewed.

Read on this head: card #28120 (body, triage 6524748197), card #28121 (closed as folded), PR #28108 (body) and its record 6523659371 (S1, S3), PR #28175 (body, 10-file list, net diff origin/main a3d24c99...872e1533, its comments: the claim lines and the docs-drift advisory), the head's check-runs, the dispatch order. The head moved twice during the review: dedf2e8e..5bb014d9 is content/docs/data-modeling/queries.mdx only (+3/−1, prose; judged under ① 10), and 5bb014d9..872e1533 is .changeset/28120-in-process-search-override-no-field.md only (+2/−2, prose; judged under ② and ③). The code and test tree is the one reviewed at dedf2e8e. Not read: the seat's own conclusions.

Check-runs at my reading of 5bb014d9 (the last code-identical head): complete and success: Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Dogfood Regression Gate, Check Documentation Links, Check PR Size, Flag docs affected, Auto Label, filter. The build, type-check, lint and test lanes were still in_progress; this record says nothing about their outcome, nor about 872e1533's own runs.

① Derived judgments

Accept-set changes the diff implies, each read in the code:

  1. (a) The seam no longer narrows an override that names no field — right. narrowStoredMetadataSearch (protocol.ts 5234-5235) returns undefined when explicit is a string or an array and its derived names is empty: [], '', ' , ', [42], and object-form search.fields: [] (the objectFormFields arm feeds the same explicit). The seam (refuseOrNarrowStoredMetadataEvaluate 151-154, untouched) then leaves query as sent, and the engine's objectql: the engine follows searchFields into its declared accept set (engine stage of #27683, the #27008 shape) #27732 refusal answers [] / comma-only / fields: [] with 400 VALIDATION_ERROR on the caller's slot, before resolveSearchFields and before any driver call. [42] (no string entry) passes the seam the same way and is refused by the new (b) rule, INVALID_FIELD, with no field member, which is the door's answer class for a non-name entry. Pinned by the new runtime test through the seam over a real engine on find / findOne / count, with reads asserted empty.
  2. Absent and non-empty valid overrides through the seam are unchanged — right. explicit === undefined falls past the new line to the existing search == null / narrowing branch; a non-empty list still reaches storedMetadataSearchRefusal first (a name the family's search rule refuses is refused there as before; an unknown name passes to the engine). A non-string, non-array explicit is still narrowed, as the PR body says; the door never sends one (its gate refuses the shape). The narrows-to-nothing branch (5250-5254) is untouched and still throws the family's own refusal; the rewritten docblock bullet for it is true. The two seam controls pin "named override scans only what it names" and "absent override is narrowed as before".
  3. (b) What the engine now refuses — right, and exactly the fallback's input. refuseSearchOverrideNamingNoSearchableField is judged on resolveSearchFieldResolution(expandOptions).allowed, the same resolution expandSearchToFilter → resolveSearchFields runs over the same scanFields, so an override whose every name is outside that set (unknown, unsearchable type, virtual, withheld, a stale declared entry) is refused; one naming at least one allowed field returns and is intersected as before. Refused: all-unknown, all-unsearchable, no-string-entry lists, on both slots (param chosen by the engine's own searchFields ?? search.fields precedence). Not refused, and stated in the PR body, the changeset, both docs pages and the engine comment: a mixed known+unknown override (control pinned per verb: narrows to its known names); an object whose allowed is empty (left to the metadata-protocol / objectql: a search term on an object with no searchable field is dropped silently, so the read answers every row (an empty auto-default search set is not refused) #24284 refusal, which names that cause; control pinned); absent, null, a non-string, non-array value; and an override sent with no search key at all (nothing is scanned; see ③).
  4. Shape parity with the door — right. status: 400, httpStatus: 400, code: 'INVALID_FIELD', param, object, field / fields naming what the caller wrote: the members assertSearchFieldsAreSearchable sets on its refusals of the same value (protocol-query-validation.ts 913-925, 979-990). mapDataError serves both callers alike; pinned against findData on the same engine. The one difference is the message text, which the door derives per offender class and the engine does not; code and status are the contract, the message is not.
  5. Before any driver read, on every verb — right. The call sits inside expandSearchOnAst before resolveSearchQueryableFields (a security-service ask, not a driver read) and before expandSearchToFilter; find, findOne reach it directly and count / aggregate through expandSearchOnOptions (unchanged). Pinned on all four verbs with reads empty.
  6. The security narrowing is untouched — right. allowed is resolved before queryableFields; an override naming only a field the caller may not query passes (b), is kept by withholdUnqueryable (a named field is not withheld, search-filter.ts 234) and reaches the predicate guard's 403 as before. No new 400 shadows a 403.
  7. The two existing engine tests — legitimate. engine-search-empty-set.test.ts:179-186 and internal-field-positions.test.ts:144-156 each pinned the all-unknown fallback itself (a filtered answer over the default set), which is the defect this card closes; the flipped pins assert the refusal with status, code, param (and field), and internal-field-positions adds the partly-known control that keeps its original invariant. Coverage is widened, not weakened; the metadata-protocol / objectql: a search term on an object with no searchable field is dropped silently, so the read answers every row (an empty auto-default search set is not refused) #24284 and security(data): the data door's filter and group-by positions do not honour a field's internal: true the way its row read does — detail withheld pending maintainer #22646 pins beside them stand.
  8. packages/spec/src/data/search-fields.ts — docblock only, confirmed (+9/−5, all inside the /** … */ above resolveSearchFields; the function body, its signature and the api-surface entry are unchanged). The docblock is true: the only in-repo call of resolveSearchFields is search-filter.ts:258 inside expandSearchToFilter, which the engine reaches only after (b) ran (or with allowed empty, where the fallback returns [] and metadata-protocol / objectql: a search term on an object with no searchable field is dropped silently, so the read answers every row (an empty auto-default search set is not refused) #24284 answers); narrowStoredMetadataSearch, assertSearchFieldsAreSearchable and the sweep call resolveSearchFieldResolution, not the fallback. "Reached only by a caller that resolves a search set without the engine" names the published export's remaining audience correctly, and the fallback itself is unchanged for them.
  9. Public surface — no export, type, option key, status, error code or answer member is added or removed. resolveSearchFieldResolution is newly imported into engine.ts from @objectstack/spec/data (already exported, already consumed by metadata-protocol). engine.ts: +60/−6, the 6 deletions are comment lines rewritten in place; the new function is inserted after refuseSearchOverrideNamingNoField, the one call after expandOptions; nothing moved or reordered. The protocol.ts hunk is inside narrowStoredMetadataSearch and its docblock only (5189-5235), away from feat(objectql,cli,metadata-protocol): backfill the currency code of rows stored before an amount switched to record mode, and acknowledge the switch with a 409 at the save #28128's regions.
  10. Docs — both sentences true against the code. query-syntax.mdx 1192-1196: tolerant intersection kept for a partly-known override, none-known refused 400 INVALID_FIELD; queries.mdx 547-550 (the 5bb014d9 delta): direct engine.find() callers get 400 VALIDATION_ERROR for an override naming no field and 400 INVALID_FIELD for one naming no searchable field, partly-known still intersected. The line it replaced ("direct callers are unaffected") was false after this diff, so that head was a correction, not drift.
  11. In-repo in-process producer census, read in the tree — nothing breaks. protocol-record-reads.ts:551-558 passes searchFields only as a non-empty subset of the object's own resolved allowed set (same resolver), so it never names zero allowed fields; data-action-routes.ts:1296-1300 feeds findData, where the door gate refuses first, unchanged; findData's own call of narrowStoredMetadataSearch runs after assertSearchFieldsAreSearchable (protocol.ts 13189-13207), so the door never reaches the seam function with an empty list. No flow, hook, service, ui-server or example file passes a literal searchFields to the engine (git grep over packages/apps/examples, non-test).

② Semver level

.changeset/28120-in-process-search-override-no-field.md at 872e1533: '@objectstack/objectql': patch, '@objectstack/metadata-protocol': patch, '@objectstack/spec': patch, fix(objectql,metadata-protocol)!: title, a BREAKING banner, Clause-②: no (narrowing) — … and Metadata-protocol: none at line start, one ADR-0087 marker, (#28120) in the title and "Folds #28121" in the body. Matches the diff: two released packages change behaviour (the engine's refusal; narrowStoredMetadataSearch's answer for a no-field list, undefined instead of a narrowed array, an exported function whose signature is unchanged), the third is a docblock in a published source file. AGENTS.md § Post-Task Checklist 3: a non-protocol narrowing ships patch with its banner and disposition. Clause-②: no (narrowing) is correct: nothing is widened, no new export, answer member, status name or error code (both codes are the doors', pre-existing). Metadata-protocol: none is right: no authorable key, value or type changes. ADR-0087 not-required (no-migration-prescription): the body carries no FROM→TO arrow, table or migration heading; "The remedy." is one sentence naming a call-site fix, the same shape record 6523659371 accepted on #28108. The 5bb014d9..872e1533 delta rewrites the Before and Unchanged bullets only: the Before bullet now states the old behaviour as a rule (an override naming no searchable field was resolved to the default set), and the Unchanged bullet adds that an override naming no searchable field sent without a search key is still not judged in process; both true against engine.ts 13480-13485 (the check runs whenever a search key is present, a blank search: '' included, and never when it is absent). Check Changeset was success on the code-identical head.

③ Boundary flags

PR author objectstack-fleet[bot]: no login note owed. open_questions: none listed in the PR body. Dev deviations, each answered:

  • refuseOrNarrowStoredMetadataEvaluate in packages/runtime needed no change — right, read in the tree: it only writes searchFields when the helper returns an array, so the pass-through needs no runtime edit; the runtime path in the order's surface is covered by the new runtime test.
  • assertSearchFieldsAreSearchable's docblock (~12546) left as is — accepted: it explains the door gate by the engine's old fallback, which is still true of resolveSearchFields itself; outside the order's function surface. Cosmetic residual, no card needed.
  • Ablation and gate battery figures — not re-run here (read-only); the two existing tests' flips are the narrowing's own consequence, judged in ① 7.

Rule-level check of the PR body, the changeset and the test names: each states only what is now refused and what is unchanged; none gives a probe or a reach path. The earlier should-fix S1 on this record (the changeset's Before bullet) is resolved at 872e1533: the bullet is now rule-level. The earlier should-fix S2 (an all-unknown override sent with no search key is not judged in process, where the door judges it with or without a term) is resolved by statement in the changeset's Unchanged bullet; it is not a widening, since nothing is scanned without a search, and it is the pre-existing shape of expandSearchOnAst's guard, so a card is not owed. No flag outstanding at the lane boundary.

④ Design judgment

The narrowing, read against the published spec on main: QuerySchema.searchFields / FullTextSearchSchema.fields declare a non-empty list that "can only narrow" (query.zod.ts), the doors refuse an empty or all-unknown override (assertSearchFieldsAreSearchable, #27484 / #4254), and the engine's docs said an override "never widens". The capability taken away, "name no scannable field and be served over the default set", is declared nowhere: it survived only in resolveSearchFields' tolerant fallback and in the seam's reading of an empty list as absent. This PR makes the declared refusal real at the two in-process points where it was not, with the doors' own codes, and keeps the one tolerance the docs do declare (a partly-known override is intersected). Judging the refusal on the expansion's own resolved set means the rule and the fallback it replaces cannot drift: (b) refuses exactly the inputs the fallback would have widened, and nothing else. On the stored-metadata family, an override the doors refuse is now refused in process too, before any read.

Tried to break it: both slots with conflicting values (searchFields: ['name'] + search.fields: ['nmae']): precedence picks searchFields, served; the converse refused on searchFields — the engine's own precedence, the door's too. A declared searchableFields whose only entries are of a type the engine withholds, on an object the sweep narrows by queryable fields: the sweep could then hand the engine an override naming only withheld fields and be refused where it was widened before — a misdeclaration the linter already refuses, not a valid deployment; noted, no finding. Registry-less host (no schema.fields): (b) does not run, the door's if (!gate) return posture. allowed empty with an all-unknown override: #24284 answers, naming that cause (control pinned).

Findings:

Must-fix: none.

Should-fix: none outstanding. S1 (changeset Before bullet at rule level) and S2 (the no-search case stated) from the 5bb014d9 reading are both resolved at 872e1533, read in the diff.

Implemented-by: claude/project-thread-tiru7x
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #28175 · head 872e1533357df9da75636f82d0487376ea537a0b · 2026-10-30T16:22Z

维护者速读

  • 改了什么: 进程内(宿主处理器、钩子、流程直接调引擎)的搜索字段覆盖 searchFields,凡是 REST 入口会拒绝的,现在进程内也拒绝:空覆盖经元数据读取通道直达引擎的 400 VALIDATION_ERROR;一个可搜索字段都没点到的覆盖以 400 INVALID_FIELD 拒绝,不再悄悄退回整套默认搜索字段(runtime: the stored-metadata reader seam still reads an in-process searchFields: [] as "names none" and serves the narrowed default set #28120,并入 engine: resolveSearchFields falls back to the whole default set when every requested search field is unknown, where the doors refuse INVALID_FIELD #28121)。
  • 为什么改: 两处进程内的宽容让「只能收窄」的覆盖反而被放宽,与入口和文档不一致。
  • 风险与回滚: 收窄(changeset 带 BREAKING 和 !)。部分字段有效的覆盖、缺省覆盖、没有搜索词的请求行为不变(已写明)。仓内没有进程内调用方依赖旧行为(复核已普查)。改动在 engine.ts 内只加不挪。回滚即还原 objectql、metadata-protocol 两包源文件。
  • 先看: packages/objectql/src/engine.ts 的 refuseSearchOverrideNamingNoSearchableField,和 protocol.ts 的 narrowStoredMetadataSearch。
  • 两分钟验证: 跑 engine-empty-search-fields-refusal.test.ts 与 stored-metadata-reader-seam-search-override.test.ts。
  • 需要你做的: 无(Tier S;契约复核 PASS 6526660897,按本期落地授权由席位落地)。

席位核对

  • Shape: draft → main, Fixes #28120 (folds engine: resolveSearchFields falls back to the whole default set when every requested search field is unknown, where the doors refuse INVALID_FIELD #28121). 10 files (+347/−27): engine.ts, protocol.ts (narrowStoredMetadataSearch + docblock), packages/spec/src/data/search-fields.ts (docblock only), two new tests, two existing objectql tests flipped from pinning the old fallback to pinning the refusal, two docs pages, one changeset. check-governed-merges --pr 28175: NOT governed.
  • Premise: re-measured on main @ 277275aa by the thread, in-process: both halves held.
  • File surface: within the claim as amended (6525076823).
  • Hot file: packages/objectql/src/engine.ts is on the lane hot-file queue ([PM seat] domain:server — 🟢 os-tesla · 由 os-tesla 账户的项目负责 #6367). No other open PR touches it at this landing, and the move-only series [maintainer] refactor(objectql): split src/engine.ts (23,903 lines, +7,501 in 30 days) along its ObjectQL seams, one move-only slice per PR #25522 has no cut in flight; this diff adds (+60/−6) and moves nothing.
  • Declarations: Clause-②: no (narrowing), Metadata-protocol: none, fix(objectql,metadata-protocol)!:, BREAKING banner, adr-0087: not-required (no-migration-prescription); three packages patch. PR body and changeset agree.
  • Contract review: CONTRACT_REVIEW_TIER, PASS at this head, 6526660897. Earlier heads dedf2e8e → 5bb014d9 (docs) → 872e1533 (changeset wording to rule level, the no-term case stated) were judged as deltas in the same record.
  • Local (card thread): dispatch-gates --ran 108/108, 0 NOT-MEASURED; pnpm lint 0; objectql 494 files / 9646 tests; dogfood battery 2883 passed / 0 failed / 20 skipped. Report 6527693868.
  • Gate check-runs on this head: 50 total, 41 success, 9 skipped by the path filter, 0 failed. mergeable_state: clean.
  • Landing: Tier S, under the maintainer's landing grant for this term (2026-10-29 22:05Z, seat thread). Ready + auto-merge through the relay.

ACCEPT

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 16:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 16:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 6fd87b7 Oct 30, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-tiru7x branch October 30, 2026 16:47
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 protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime: the stored-metadata reader seam still reads an in-process searchFields: [] as "names none" and serves the narrowed default set

2 participants