Skip to content

feat(spec): a field marked searchable: true is searched when its object declares no searchableFields - #29385

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-8ymp4z
Nov 2, 2026
Merged

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

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

Ruled: 6580271401 · letter A · 2026-11-02T14:58Z

Requested by Steve · project thread

Fixes #29008

Clause-②: yes (widening)
Metadata-protocol: semantic
Skills-docs: follow-up owed — skills/objectstack-data/SKILL.md, skills/objectstack-query/SKILL.md, skills/objectstack-ui/rules/list-views.md (the content/docs pages are updated here)

维护者速读

  • 改了什么:对象没有声明 searchableFields 时,标了 searchable: true 的字段现在会被 $search 扫描,$searchFields 点名它也会被接受,不再返回 400;secret / password / vector 即使标了也永远不搜。声明了 searchableFields 的对象完全不变。
  • 为什么改:这个开关以前能写、有默认值、schema 里也返回 searchable: true,但没有任何代码读它:作者标了却搜不到,点名还被 400 拒绝。按裁决记录 6573569376 的 A 案(维护者已同意)补上读取方。
  • 风险与回滚:这是放宽。已经标了 searchable: true 的非文本字段会开始被搜;平台对象实测有 6 个对象的 7 个用户 id 类 lookup 字段因此被搜到(匹配的是存储的 id,不是关联记录的标题)。回滚即 revert 本 PR;单个对象也可以删掉标记,或声明 searchableFields 把集合钉死。
  • 先看:① packages/spec/src/data/search-fields.ts 的 admittedBySearchableFlag 与 autoDefaultFields 第一行(标记先于 hidden / 系统字段名排除判断);② packages/metadata-protocol/src/protocol-query-validation.ts 的 400 文案;③ packages/lint/src/validate-searchable-fields.ts 的 lint 文案。
  • 两分钟验证:在已构建的工作区跑 pnpm --filter @objectstack/rest exec vitest run src/data-search-field-searchable-flag-door.test.ts,预期 4/4 通过:卡片的三个请求里 QQCODE、QQHTML 都能找到那一行,$searchFields=snippet 返回 200。
  • 需要你做的:无,席位落地

What changes

  • The resolver (packages/spec/src/data/search-fields.ts). The auto branch of resolveSearchFieldResolution now admits a field marked searchable: true unless its type is one of the new SEARCH_FLAG_EXCLUDED_TYPES (secret, password, vector) or it is a virtual formula with no stored column. The flag is asked before the auto-default's own exclusions, so a flagged hidden field and a flagged field named like a system column join too, which is the ruling's "every field marked searchable: true, of any type". The declared branch is unchanged and reads no flag. SearchFieldMeta gains the optional searchable.
  • Every reader moves with it. The REST 读路径:searchFields / groupBy / aggregations 指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254 ingress gate (assertSearchFieldsAreSearchable), the engine's $search expansion, the lint rule and the ui readers (the fields search offer and ListView) all call this one resolver. Measured: the protocol and the ui pass the registry's field map unprojected, so no domain:server half was needed. The one projection that dropped searchable was the lint rule's indexObjectSearchTargets; it now carries the flag.
  • The describe on FieldSchema.searchable (packages/spec/src/data/field.zod.ts) says the ruling's sentence instead of "Is searchable", and the reference pages are regenerated (content/docs/references/data/field.mdx, data/object.mdx, system/migration.mdx, plus api-surface / export-origins for the new export).
  • Messages. For a field outside the auto-default, the ingress 400 INVALID_FIELD and the searchable-field-unsearchable lint verdict now name the missing searchable: true and offer it as the fix, where they used to say the type cannot be scanned. For the three hard-excluded types they keep the old remedy and say the flag does not admit them. The os explain text for the rule says the same.
  • Docs. content/docs pages that described the auto-default as type-only now name the flag (data-api, queries, query-syntax, schema-design, and the searchable rows in field-types and fields).
  • Liveness. The searchable row in packages/spec/liveness/field.json now points at the real reader. Its old evidence was a dashboard widget's own searchable key, not this field.
  • Changeset. @objectstack/spec, @objectstack/metadata-protocol and @objectstack/lint are minor (widening, new export), with the claim's two lines at line start.

Tests

  • packages/spec/src/data/search-fields.test.ts, new describe "field.searchable: true joins the auto-default set — never a declared one". It covers the card's reproduction (code and html fields marked searchable resolve into the set) and an override naming a flagged field narrowing to it. It also covers these CONTROLS: an unflagged field resolves as before, each of secret / password / vector stays out even when flagged, a virtual formula stays out, a declared searchableFields ignores the flag, and absent and false give one answer. It also pins that every other FieldType is admitted when flagged, that a flagged hidden field and a flagged system name join (with unflagged controls), that a field with no readable type is not admitted, and that the display field still only orders. A second describe pins SEARCH_FLAG_EXCLUDED_TYPES: exactly the three types, covering every masked-on-read type, and a subset of the auto-default's own exclusions.
  • packages/lint/src/validate-searchable-fields.test.ts, new describe for the flag. A view narrowing to flagged code / html fields lints clean. An unflagged one is refused with the exact new hint. A flagged vector is refused with the hard-exclusion verdict and the old hint. A declared set ignores the flag. indexObjectSearchTargets carries the flag.
  • packages/rest/src/data-search-field-searchable-flag-door.test.ts (new) runs the card's three requests end to end over the Data API door on a real SqlDriver (SQLite). Its controls: an unflagged code field is not scanned and naming it is 400 before any driver read, a flagged secret is 400 before any read, and an object that declares searchableFields keeps the declaration as the whole set.
  • Forced test edit (named): packages/lint/src/validate-searchable-fields.test.ts, the skill-parity pin for a lookup outside the auto-default. It expected the verdict to say "of type 'lookup', which 'search' cannot scan". It now expects "of type 'lookup' and not marked searchable: true". The old sentence is false after this change, because a marked lookup is now scanned.

Acceptance notes

  1. What already matches more, measured on the platform's own objects. Seven lookup fields on six objects are already marked searchable: true with no searchableFields. These are sys_session.user_id, sys_activity.actor_id and environment_id, sys_audit_log.user_id, sys_comment.author_id, sys_notification_preference.user_id and sys_presence.user_id. They now join $search on their stored ids, not on the user's name. This is a static census of the source, not a runtime reading.
  2. Flagged types with no text column. The ruling admits a flagged field "of any type", so a flagged number, boolean, date, datetime, time or json field now joins the set, and the engine emits $icontains for it. That is NOT MEASURED on Postgres in this PR. The same shape already exists today for a declared searchableFields naming such a column, so it is not new to this PR. It is listed as out of scope for domain:server, with a one-line narrowing offered in the report.
  3. ADR-0061 D2 still says field-level searchable "stays pruned". That sentence predates ruling 6573569376, and the code docblock says so. An amendment note is owed on a governed path (Tier H). It is not in this PR, so this PR stays ungoverned. The seat has a draft.
  4. Left unchanged on purpose: the engine's empty-search-set refusal still names searchableFields as its only remedy, and the field form's help text "Include in global search results" is now accurate as written.
  5. The ui readers agree with the server without a ui edit. They pass the full field meta to the same resolver, and their tests are green (below).

Verification

Head 85ec00115a6b5179a99240050a1d9509638e2656, merge base 2f7977cd2, a clean merge with origin/main at 57cde8cb.

Consumer suites (each through os-verify-lock, VERDICT command-exit 0)
turbo build ./packages/**          Tasks 78 successful, 78 total
@objectstack/spec test             Test Files 791 passed (791) · Tests 23409 passed | 1 todo
@objectstack/spec typecheck        exit 0 (check:test-typecheck clean)
@objectstack/lint test             Test Files 189 passed (189) · Tests 7835 passed
@objectstack/lint typecheck        exit 0
@objectstack/metadata-protocol test  Test Files 301 passed | 4 skipped (305) · Tests 22127 passed | 26 skipped
@objectstack/metadata-protocol typecheck  exit 0
@objectstack/objectql search files (search-filter, search-companion, engine-empty-search-fields-refusal, query-expression-conformance, filter-comparand-shape)
                                   Test Files 5 passed (5) · Tests 331 passed
@objectstack/rest search files (data-search*, list-search, meta-object-search, search-clone, discovery-search, list-view-grouping-query-door, rest-data*)
                                   Test Files 15 passed (15) · Tests 425 passed | 22 skipped
@objectstack/rest typecheck        exit 0
ui: fields searchOffer, RecordPickerDialog.searchlessTarget-12966, ListView.searchlessObject-12966, ListView.searchWithheldTreeChart-10326
                                   Test Files 4 passed (4) · Tests 39 passed
Ablation: the flag line removed, rebuilt, proven in dist, then restored

The line if (meta && admittedBySearchableFlag(meta)) return true; was replaced with a marker that never matches (ablation-replace.mjs --hold). After a rebuild, ablation-dist-preflight.mjs found the marker in 2 built files of packages/spec/dist.

spec  search-fields.test.ts                     Tests 5 failed | 45 passed (50)  (the reproduction, the override, every-other-type, hidden/system-name, display-field order)
lint  validate-searchable-fields.test.ts        Tests 1 failed | 72 passed (73)  (flagged code/html lints clean)
rest  data-search-field-searchable-flag-door    Tests 1 failed | 3 passed (4)    (the card's three requests)

Every control stayed green with the flag unread. Restore: blob d45c69a6 equals HEAD, git diff HEAD empty. After rebuilding, --absent preflight found the marker absent from all 588 built files and the working tree clean.

Derived gates (dispatch-gates --commands, then --ran)

At head 85ec0011, 21 changed paths against the merge base. Each command ran with stdin closed, and its exit code was recorded before any pipe.

derived 106 · run 106 · exit 0 on 106 · NOT-MEASURED 0 · UNRUN 0
✓ dispatch-gates --ran: 106 derived famil(ies) accounted for — 106 run, 0 NOT-MEASURED

One command first answered exit 3, a prerequisite and not a verdict: node scripts/check-plugin-teardown-shape.mjs --self-test could not read its positive control pinned at commit 621a4876 in this shallow clone. After fetching that one commit it answered exit 0 (48 cases pass).

Offline node scripts/check-changeset-no-major.mjs --base origin/main was run against this body as the event and answered exit 0. It found no major, and on the level axis it read Clause-②: yes (widening) and Metadata-protocol: semantic with no moved package graded patch.


Generated by Claude Code

… the object declares no searchableFields

resolveSearchFieldResolution's auto branch now admits every field whose
own `searchable` is true, of any type except secret / password / vector
(SEARCH_FLAG_EXCLUDED_TYPES) and a virtual field. A declared
searchableFields stays the whole set. The $searchFields ingress gate and
the lint rule read the same resolution; the lint field-meta projection
now carries `searchable`, and the refusal prose that called a non-text
type unscannable names the opt-in instead.

Claude-Session: https://claude.ai/code/session_01ADHkmSR2TvJTmoYyVtcQv9
Co-authored-by: Claude <noreply@anthropic.com>
…geset and liveness row

Claude-Session: https://claude.ai/code/session_01ADHkmSR2TvJTmoYyVtcQv9
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/metadata-protocol, @objectstack/spec, touching 9 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/liveness/field.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

21 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 96fb7b5d07afaf828f4c0de4ba0fe3d179984e86.

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

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/liveness/field.json) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 169 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 96fb7b5d07afaf828f4c0de4ba0fe3d179984e86 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 96fb7b5d07afaf828f4c0de4ba0fe3d179984e86

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract-review-claim: session_01ADHkmSduNGC2SZXszyrNut · head 85ec001 · 2026-11-02T13:48Z

The lane seat reviews this head itself at CONTRACT_REVIEW_TIER (triage ruling 6575141496 on #28741 returned contract review to the lane): a widening of the search accept set (field.searchable: true joins the auto-default search set and the $searchFields allow-list) and a new export SEARCH_FLAG_EXCLUDED_TYPES.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 85ec00115a6b5179a99240050a1d9509638e2656
Local-runs: none
Scope: packages/spec/src/data/field.zod.ts, packages/spec/src/data/search-fields.ts + packages/metadata-protocol/src/protocol-query-validation.ts (assertSearchFieldsAreSearchable, resolveQueryFields), packages/metadata-protocol/src/protocol-record-reads.ts (the global-search sweep's resolver call), packages/metadata-protocol/src/protocol.ts (narrowStoredMetadataSearch), packages/objectql/src/engine.ts (expandSearchOnAst, the find() door ladder), packages/objectql/src/search-filter.ts (fieldClausesForTerm), packages/objectql/src/text-operator-declared-type-door.ts, packages/lint/src/validate-searchable-fields.ts (declaredFieldTarget, checkSearchableFieldList), ui/packages/fields/src/widgets/searchOffer.ts, ui/packages/plugin-list/src/ListView.tsx, packages/spec/src/data/masked-field-types.ts, docs/adr/0061-record-search-architecture.md, docs/adr/0049-no-unenforced-security-properties.md, packages/spec/src/data/search-fields.test.ts, packages/rest/src/data-search-field-searchable-flag-door.test.ts, and the shipped object metadata under packages/platform-objects, packages/plugins, packages/services, packages/metadata-core (census of searchable: true)

Inputs read: card #29008 body and all six comments (triage 6571567772, director claim 6573084852, ruling 6573569376, exit stroke 6576961902, claim 6577104281, os-dev-report 6578774069); PR #29385 body, 21-file list and the net diff against the merge base 2f7977cd at the head; the check-runs on the head. The live tree was read with git show at the head after git fetch; nothing was built, run or re-run.

Gate verdicts on the head at the time of this record: 35 check-runs concluded success — among them Lint & Repo Gates, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · repo & tooling gates, Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates, Spec property liveness, Check Changeset, Governed Surface Queue Guard, PR-body guards, Build Core, Build Docs, Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Test Core (8/8) and seven Test UI shards; none concluded failure. 19 were still in_progress: Test Core (1–7/8), nine Test UI shards, Type Check UI, Type Check · workspace (the last carries check:docs / check:api-surface for the regenerated references and the new export). This record judges the contract and the design on the tree; it does not stand in for those conclusions — the seat lands only once every required context is success (in_progress is not a pass).

① Derived judgments

Accept-set changes the diff implies, each judged against the live tree:

  1. The auto-default search set widens (search-fields.ts autoDefaultFields): with no searchableFields, a field with searchable === true, a readable type not in SEARCH_FLAG_EXCLUDED_TYPES and not virtual joins the set, asked before the system-name / hidden / type exclusions. Right — this is ruling 6573569376 letter A verbatim ("the type-based auto-default plus every field marked searchable: true, of any type, except the hard exclusions"). The declared branch reads no flag — right, the ruling keeps a declaration "the whole search set (unchanged)".
  2. The $searchFields ingress allow-list widens identically (protocol-query-validation.ts:928 assertSearchFieldsAreSearchable → resolveSearchFieldResolution({ fields: gate.fields, … })). Read resolveQueryFields (:339): gate.fields is the registry's schema.fields unprojected, so the flag reaches the gate. Right, and the ruling's "resolveSearchFields and the REST 读路径:searchFields / groupBy / aggregations 指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254 gate stay one source" holds without a domain:server edit. The gate's internal: true refusal (first) and credential refusal (before the source branch) are unchanged and still precede the flag's effect — a flagged internal text field is 400 at the door and withheld in the engine; consistent.
  3. The engine's scan set widens identically (engine.ts:14161 expandSearchOnAst): scanFields is schema.fields minus collectInternalReadFields ∪ collectCredentialFields, handed to the same resolver, then narrowed by getQueryableFields (withholdUnqueryable). The field meta is the object's whole field definition (search-filter.ts:146 says so for multiple), so searchable is read. Right. The credential strip is what makes the secret / password hard exclusion a one-source fact rather than a door/engine disagreement — the set's docblock cites ADR-0100 for this and the engine code confirms it.
  4. The global-search sweep (protocol-record-reads.ts:495) and the stored-metadata narrowing (protocol.ts:5458) both pass the object's full field records to the resolver — right, they move with it; no second definition of the set survives (protocol.ts:15192's palette history describes the pre-⌘K global-search palette recall is strictly narrower than the $search executor (no pinyin/companion recall) #7643 reader that was removed, not a live one).
  5. The lint allowed set widens identically: declaredFieldTarget (validate-searchable-fields.ts:259) now carries searchable: f.searchable === true; it was the one projection that dropped the flag. Right. The system-declared stub {} in resolveAllowedSet cannot be flagged, so the stub still cannot leak into the auto-default — the module's own invariant holds.
  6. The ui readers (searchOffer.ts:53, ListView.tsx:1488) pass the served field meta whole — right, they agree with the server without an edit (card step 1 confirms the served schema carries searchable: true).
  7. Public surface: new export SEARCH_FLAG_EXCLUDED_TYPES (@objectstack/spec/data), new optional SearchFieldMeta.searchable; api-surface/data.json and export-origins/data.json carry the export — right (check:api-surface reads built dist, so the Type Check · workspace conclusion is the gate's word). The .describe() change regenerates content/docs/references/data/field.mdx, data/object.mdx, system/migration.mdx — right per the § Touched packages/spec table (check:docs). No authorable key is added, so check:authorable-surface has nothing to move — right.
  8. Refusal wording at the door (type 'code', not marked 'searchable: true', Mark the field 'searchable: true', or declare 'searchableFields'…; hard-excluded types keep the old remedy and say "never scans even when marked") and the lint verdict/hint mirror the resolver's precedence exactly (system name → hidden → type, with flaggable computed for every arm). Checked the combinations: flagged+hard-excluded, unflagged system name, hidden+secret, !meta — no message offers a remedy the resolver would refuse. Right. No runtime string carries a tracker number.
  9. Liveness row (liveness/field.json searchable): evidence moved from a dashboard widget's own searchable key to admittedBySearchableFlag and its three readers — right, and Spec property liveness concluded success.
  10. Shipped metadata census (read, not reviewed): across packages/platform-objects, packages/plugins, packages/services, packages/metadata-core, every searchable: true on a field outside the type default is one of seven lookups on six objects that declare no searchableFields (sys_session.user_id, sys_activity.actor_id / environment_id, sys_audit_log.user_id, sys_comment.author_id, notification_preference.user_id, sys_presence.user_id) — the PR body's census is exact. Every other hit is text / textarea / email / select (already in the default) or the object-level enable.searchable, which is a different key. So the only platform-side widening is a $icontains over seven stored user-id strings — string-capable columns, no driver hazard.

② Semver level

Changeset .changeset/29008-field-searchable-honoured.md: @objectstack/spec, @objectstack/metadata-protocol, @objectstack/lint at minor; body carries Clause-②: yes (widening) and Metadata-protocol: semantic at line start, matching the PR body's two lines. Matches what the diff publishes: the three packages whose src moves are exactly the three listed (rest and objectql move only in tests / not at all); the key's meaning changes from inert to read, which is semantic; the rule "a kind takes at least minor on a package the diff moves" gives minor on all three. Check Changeset concluded success; the dev's offline check-changeset-no-major reading (no major, level axis widening/semantic) is consistent with the rule. No BREAKING arm, so no ADR-0087 disposition marker is owed. The "What may now match more" paragraph gives the author the two remedies (remove the flag, or declare searchableFields) — the migration note a widening owes.

Clause-②: yes (widening) — the search accept set (auto-default, ingress allow-list, engine scan set, lint allowed set) widens by every flagged non-excluded field; nothing is narrowed, no caller that worked stops working.

③ Boundary flags

Dev open_questions (os-dev-report 6578774069):

  • Q1 — ADR-0061 D2 still says field-level searchable "stays pruned"; where does the amendment land? Answered A: the seat files a separate Tier H card on docs/adr/0061-record-search-architecture.md (the seat's draft spec-seat2/29008-adr-0061-note.md), and this PR stays ungoverned — B would hold a non-governed widening behind a governed approval, and the code is not "quietly doing the opposite" (Prime Directive [WIP] Add Chinese version of the documentation #13): search-fields.ts and the changeset name ruling record 6573569376 as the decision and say D2 predates it. Escalated to the maintainer / the seat's own queue: the amendment is owed, not optional — until it lands, D2 and the shipped behaviour contradict, and ADR-0061 D5 ("secret / encrypted / PII never searchable") should be re-read in the same note because the hard exclusions are what keep D5 true under the flag. File it before or at landing; the card is the seat's.
  • Q2 — a flagged hidden field or a flagged system-column name joins the set; intended? Answered A (as taken), and it holds against the tree: hidden is "Hidden from default UI" (field.zod.ts:2364), a UI fact, not a security boundary; the security boundaries on $search are internal: true (engine strip + door refusal), the credential types (engine strip, hard exclusion, door credential branch) and getQueryableFields (withholdUnqueryable), all enforced upstream of or beside the resolver and untouched here. A flagged system-name field (owner_id, id) is the author's explicit act, the per-field twin of naming it in searchableFields, which already bypasses the same three exclusions. Pinned with unflagged controls. No change asked.
  • Q3 — a flagged field whose type can never hold a string (number, boolean, date, datetime, time, json, …) joins the set and the engine emits $icontains over it; narrow the flag? Escalated — see ④ should-fix 2. Verified on the tree: in find() the [Decision] refuse a text operator ($contains family) over a field whose DECLARED type is not textual — INVALID_FILTER 400 at the engine's field-aware door (option C of #14079); the textual-type vocabulary is the question #15661 door (assertTextOperatorTargetsAreStringCapable, engine.ts:1302, run from the filter ladder at :14464-ish on the caller's own where) runs BEFORE expandSearchOnAst (:14478), so the expansion's clauses are never judged by it and reach the driver; SQLite coerces, Postgres does not (NOT MEASURED, as the dev says). The ruling's "of any type" is explicit, the shape pre-exists for a declared searchableFields naming such a column, and the shipped census (① item 10) has no such field — so this is not a FAIL, but it is the one decision the maintainer should take rather than the seat.
  • Q4 — keep the export name SEARCH_FLAG_EXCLUDED_TYPES? Answered A: keep. It sits beside SEARCH_AUTO_EXCLUDED_TYPES / SEARCH_VIRTUAL_TYPES, is pinned to exactly three members and to cover MASKED_ON_READ_FIELD_TYPES, and the two refusal faces read it rather than carrying a literal — the one-source posture the file exists for. It is an export name, not a metadata key.

Dev deviations and declarations: the lint / door / os explain message rewrites and the six content/docs pages fall under "made false must be fixed" — accepted (the old sentence "which 'search' cannot scan" is false after this change; the forced test edit is named in the PR body). The three-package minor is the rule's own answer (②). check-dev-prereqs not green locally for ui/** dist — CI's Type Check UI is the word, pending at write time. The Skills-docs: follow-up owed line (skills/objectstack-data, objectstack-query, objectstack-ui list-views teach the auto-default as type-only) names a Tier H follow-up — the seat's, not this PR's; noted so it is not lost. out_of_scope_findings class a (non-text $icontains on Postgres, domain:server) is the same item as Q3 and carries the escalation; class c (the engine's empty-set refusal names only searchableFields as its remedy) is wording, correctly left to the next objectql search PR.

④ Design judgment

packages/spec/src/data/search-fields.ts — judged against ruling 6573569376 (A), ADR-0061 D1/D2/D5, ADR-0049 (enforce-or-remove), and its first consumers (the door at protocol-query-validation.ts:928, the engine at engine.ts:14161, the lint rule, the sweep, the ui readers).

packages/spec/src/data/field.zod.ts — judged against the ruling's second clause ("the key's .describe() says this sentence instead of 'Is searchable'; the field reference pages are regenerated") and its first consumer, the generated content/docs/references/data/field.mdx.

  • The describe states the ruling's rule in author terms: when it is read (no searchableFields), what it adds (any type except the three), the storage caveat (virtual formula), and when it is not read (declared list, ADR-0061). The TSDoc above it names the ruling record and the reader. The reference pages are regenerated (three files, byte-consistent with the describe diff). Right.
  • Should-fix 1 — the describe does not say that false is the default and does NOT withdraw a field the type default already scans. An author who reads the reference page and writes searchable: false on a text column to keep it out of $search gets no refusal and no effect; the only remedies are declaring searchableFields (or hidden: true, which also hides the column). The SearchFieldMeta.searchable TSDoc says exactly this ("false and absent are one answer … it can widen the auto-default and never narrows it"), but that sentence is on the internal type, not on the author-facing key. One clause on the describe ("false, the default, does not remove a field the type default scans — declare searchableFields to choose the set") and a gen:docs regeneration closes it. Not a FAIL: the behaviour is the ruling's and is pinned; only the author's expectation is unaddressed.

No must-fix found. Both should-fixes are on the hits files; the second carries the one escalation.

Implemented-by: claude/project-thread-8ymp4z
Reviewed-by: session_01ADHkmSduNGC2SZXszyrNut

VERDICT: PASS

…ield

The `searchable` describe now says that `false`, the default, does not
remove a field the type-based default already scans, and that choosing the
set is what `searchableFields` is for. The `SearchFieldMeta.searchable`
TSDoc already said so; the author-facing key now does too. The field
reference pages are regenerated.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

待裁:新公开名字 SEARCH_FLAG_EXCLUDED_TYPES(#29008)

  • 一句话问题:这个 PR 给 @objectstack/spec/data 新加一个公开导出,作者和插件可以按名字引用它,它就是"标了 searchable: true 也不进搜索的字段类型"(secret、password、vector)。名字一旦发布就是公开契约,需要您定。
  • 背景:裁决 6573569376(A)要求排除这三种类型;契约复审在 PR 上判 PASS,这个名字是唯一待人定的项。
  • 选项:
    • A 保留 SEARCH_FLAG_EXCLUDED_TYPES:零额外工作。它与同文件已有的 SEARCH_AUTO_EXCLUDED_TYPES、SEARCH_VIRTUAL_TYPES 同一命名法,看名字就知道和 searchable 标记有关。
    • B 改名(例如 SEARCHABLE_FLAG_FORBIDDEN_TYPES):开发线程改名、重新生成 api-surface/export-origins,复审出 delta,约半小时,同一个 PR。
  • 推荐:A。理由是与同文件现有的两个名字保持一个命名族,读者不用学第二套说法。盲区:没有对外部插件做命名调研。
  • 自检:只看①选 A;②③④ 是否翻转:否。
  • 落地:在此卡标签取下前,席位不挂自动合并。

Copy link
Copy Markdown
Collaborator

Brief-claimed-by: session_01QsR2jV5sBSzCYNEDgpYLJw · batch #458 · 2026-11-02T14:50Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f60ceb0b96264ef39edc520812d0f0f3696f842a
Local-runs: none

This is a DELTA record for the push after the PASS record 6579041743 (head 85ec0011). The delta 85ec0011...f60ceb0b is one fast-forward commit across 4 files, and it is Should-fix 1 done as asked. The searchable .describe() in packages/spec/src/data/field.zod.ts gains one sentence: "false, the default, does not remove a field the type-based default already scans; to choose the set, declare searchableFields." The generated reference tables (field.mdx, object.mdx ×2, migration.mdx ×2) carry the same sentence, regenerated. No resolver, test, changeset or export moved, so every judgment in 6579041743 (①–④, semver minor ×3, Clause-②: yes (widening), Metadata-protocol: semantic) carries to this head unchanged. Should-fix 2 is filed as #29394 (domain:server). The export name is with the maintainer (needs-user-decision, comment 6579958826).

Implemented-by: claude/project-thread-8ymp4z
Reviewed-by: session_01ADHkmSduNGC2SZXszyrNut

VERDICT: PASS

Copy link
Copy Markdown
Collaborator

Ruling: batch #458 item 5 · letter A · maintainer 「同意」 2026-11-02T14:54Z

director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

What the ruling binds

  • A — SEARCH_FLAG_EXCLUDED_TYPES lands as filed on @objectstack/spec/data (search-fields.ts), the set secret / password / vector that a searchable: true flag never admits. It is the same naming family as the file's SEARCH_AUTO_EXCLUDED_FIELDS, SEARCH_AUTO_EXCLUDED_TYPES and SEARCH_VIRTUAL_TYPES; FLAG names the path it governs.

Workload of the letter

  • Zero rework on this PR (head 85ec0011, contract review PASS 6579041743, 57 green).

Execution

  1. Spec seat 2 clears the label and enqueues; the seat lands it (no maintainer click owed).
  2. The ADR-0061 D2 sentence that still says field-level searchable "stays pruned" contradicts ruling 6573569376; the seat files the Tier H ADR note card, which comes to the maintainer for the click when its PR is up.
  3. Label strokes are the lane seat's.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Head-sha: f60ceb0b96264ef39edc520812d0f0f3696f842a · read 2026-11-02T15:11Z

维护者速读

  • 改了什么:对象没有声明 searchableFields 时,标了 searchable: true 的字段现在会被 $search 扫描,$searchFields 点名它也会被接受,不再返回 400。secret、password、vector 即使标了也永远不搜。声明了 searchableFields 的对象完全不变。
  • 为什么改:这个开关以前能写、有默认值,schema 里也返回 searchable: true,但没有任何代码读它:作者标了却搜不到,点名还被 400 拒绝。这次按裁决 6573569376 的 A 案(维护者已同意)补上读取方。
  • 风险与回滚:这是放宽。已经标了 searchable: true 的非文本字段会开始被搜:平台对象里实测有 6 个对象的 7 个用户 id 类 lookup 字段因此被搜到(匹配的是存储的 id)。非文本列在 Postgres 上的 $icontains 风险已立卡 objectql: a $search expansion emits $icontains over a column the #15661 text-operator door calls never-string-capable (number, boolean, date, json), and the door never judges it; once #29008 honours searchable: true this can reach Postgres #29394。回滚就是 revert 本 PR;单个对象也可以去掉标记,或声明 searchableFields 把集合钉死。
  • 先看:packages/spec/src/data/search-fields.ts 的 admittedBySearchableFlag 与 autoDefaultFields。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/data-search-field-searchable-flag-door.test.ts,预期 4/4 通过。
  • 需要你做的:无。新导出名 SEARCH_FLAG_EXCLUDED_TYPES 已由总监裁定 A(6580271401)。席位落地。

席位意见:可落地。契约复审在 85ec001 判 PASS(6579041743),在现 head 的 delta 判 PASS(6580195493),CI 全绿,名字已裁。

席位核对 — ACCEPT · domain:spec#2 · head f60ceb0b

  1. Shape: draft, base main, Fixes #29008. Clause-②: yes (widening) and Metadata-protocol: semantic sit at line start in the body and the changeset (@objectstack/spec, @objectstack/metadata-protocol and @objectstack/lint, each minor). There are 21 files, +559/−33, inside claim 6577104281. The forced lint skill-parity pin is named in the body. No governed path (skills/** is untouched; the follow-up is skills: three files teach the record-search auto-default as type-only; after #29008 a field marked searchable: true also joins it when searchableFields is not declared #29395).
  2. Premise measured: on main the card's repro (code and html fields marked searchable: true, no searchableFields) is not found by $search, and $searchFields=snippet answers 400. On the head, data-search-field-searchable-flag-door.test.ts finds the row for both and answers 200. The dev's ablation of the flag line turns spec 5, lint 1 and rest 1 red, and the restore is byte-identical.
  3. Gates: ci-failure.mjs --sha f60ceb0b… GREEN: 65 check-runs, none failed (7 superseded rows not read).
  4. Accepted deviations: the 400, lint and os explain messages now offer the flag as the remedy. Six content/docs pages are corrected because "which search cannot scan" became false. Both fall under "made false must be fixed".
  5. Out of scope: docs(adr-0061): record that field-level searchable is honoured on the auto branch (D2 says it stays pruned; ruling 6573569376 letter A reversed it in #29008) #29391 (ADR-0061 D2 amendment, Tier H; the director's execution step 2), objectql: a $search expansion emits $icontains over a column the #15661 text-operator door calls never-string-capable (number, boolean, date, json), and the door never judges it; once #29008 honours searchable: true this can reach Postgres #29394 (the expansion's $icontains over never-string-capable columns, domain:server) and skills: three files teach the record-search auto-default as type-only; after #29008 a field marked searchable: true also joins it when searchableFields is not declared #29395 (Skills-docs: follow-up owed for the three skills files).
  6. Landing: not Tier H and under 3000 lines. The name ruling is A and the label is off, so the seat lands it with pr_ready and auto-merge. After merge, the seat writes a landing note on field.searchable: true has no reader: $search skips the field and $searchFields naming it answers 400 "is not searchable" while the served schema says searchable: true #29008 and removes pm:dispatched.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 15:12
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 29c70ca Nov 2, 2026
72 of 73 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-8ymp4z branch November 2, 2026 16:02
os-steve pushed a commit that referenced this pull request Nov 2, 2026
…true)

#29385 landed: with no searchableFields, a field marked searchable: true
joins the auto set of any type. A flagged number / date / json field is
admitted to the set and still gets no text clause; the text control is
scanned, and naming a flagged field is accepted and matches nothing. On
the engine (AST) and the SQLite Data API door.

Claude-Session: https://claude.ai/code/session_01ADHkmRnyoAJD6oToQ2QZUK
Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

2 participants