Repository navigation
feat(spec): a field marked searchable: true is searched when its object declares no searchableFields - #29385
Conversation
… 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>
…earch set 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>
…erence pages Claude-Session: https://claude.ai/code/session_01ADHkmSR2TvJTmoYyVtcQv9 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 169 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
|
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 |
|
domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut Contract reviewServed-tier: 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 Gate verdicts on the head at the time of this record: 35 check-runs concluded ① Derived judgmentsAccept-set changes the diff implies, each judged against the live tree:
② Semver levelChangeset 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 flagsDev
Dev ④ Design judgment
No must-fix found. Both should-fixes are on the Implemented-by: 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>
|
domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut 待裁:新公开名字
|
|
Brief-claimed-by: session_01QsR2jV5sBSzCYNEDgpYLJw · batch #458 · 2026-11-02T14:50Z Generated by Claude Code |
|
domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut Contract reviewServed-tier: This is a DELTA record for the push after the PASS record 6579041743 (head Implemented-by: VERDICT: PASS |
|
Ruling: batch #458 item 5 · letter A · maintainer 「同意」 2026-11-02T14:54Z director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw What the ruling binds
Workload of the letter
Execution
Generated by Claude Code |
|
domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut Head-sha: 维护者速读
席位意见:可落地。契约复审在 85ec001 判 PASS(6579041743),在现 head 的 delta 判 PASS(6580195493),CI 全绿,名字已裁。 席位核对 — ACCEPT ·
|
…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>
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的对象完全不变。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
packages/spec/src/data/search-fields.ts). The auto branch ofresolveSearchFieldResolutionnow admits a field markedsearchable: trueunless its type is one of the newSEARCH_FLAG_EXCLUDED_TYPES(secret,password,vector) or it is a virtualformulawith no stored column. The flag is asked before the auto-default's own exclusions, so a flaggedhiddenfield and a flagged field named like a system column join too, which is the ruling's "every field markedsearchable: true, of any type". The declared branch is unchanged and reads no flag.SearchFieldMetagains the optionalsearchable.searchFields/groupBy/aggregations指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254 ingress gate (assertSearchFieldsAreSearchable), the engine's$searchexpansion, the lint rule and the ui readers (the fields search offer andListView) all call this one resolver. Measured: the protocol and the ui pass the registry's field map unprojected, so nodomain:serverhalf was needed. The one projection that droppedsearchablewas the lint rule'sindexObjectSearchTargets; it now carries the flag.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, plusapi-surface/export-originsfor the new export).400 INVALID_FIELDand thesearchable-field-unsearchablelint verdict now name the missingsearchable: trueand 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. Theos explaintext for the rule says the same.content/docspages that described the auto-default as type-only now name the flag (data-api, queries, query-syntax, schema-design, and thesearchablerows in field-types and fields).searchablerow inpackages/spec/liveness/field.jsonnow points at the real reader. Its old evidence was a dashboard widget's ownsearchablekey, not this field.@objectstack/spec,@objectstack/metadata-protocoland@objectstack/lintare 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 (codeandhtmlfields 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 ofsecret/password/vectorstays out even when flagged, a virtualformulastays out, a declaredsearchableFieldsignores the flag, and absent andfalsegive one answer. It also pins that every otherFieldTypeis 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 pinsSEARCH_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 flaggedcode/htmlfields lints clean. An unflagged one is refused with the exact new hint. A flaggedvectoris refused with the hard-exclusion verdict and the old hint. A declared set ignores the flag.indexObjectSearchTargetscarries 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 realSqlDriver(SQLite). Its controls: an unflaggedcodefield is not scanned and naming it is400before any driver read, a flaggedsecretis400before any read, and an object that declaressearchableFieldskeeps the declaration as the whole set.packages/lint/src/validate-searchable-fields.test.ts, the skill-parity pin for alookupoutside the auto-default. It expected the verdict to say "of type 'lookup', which 'search' cannot scan". It now expects "of type 'lookup' and not markedsearchable: true". The old sentence is false after this change, because a marked lookup is now scanned.Acceptance notes
searchable: truewith nosearchableFields. These aresys_session.user_id,sys_activity.actor_idandenvironment_id,sys_audit_log.user_id,sys_comment.author_id,sys_notification_preference.user_idandsys_presence.user_id. They now join$searchon their stored ids, not on the user's name. This is a static census of the source, not a runtime reading.number,boolean,date,datetime,timeorjsonfield now joins the set, and the engine emits$icontainsfor it. That is NOT MEASURED on Postgres in this PR. The same shape already exists today for a declaredsearchableFieldsnaming such a column, so it is not new to this PR. It is listed as out of scope fordomain:server, with a one-line narrowing offered in the report.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.searchableFieldsas its only remedy, and the field form's help text "Include in global search results" is now accurate as written.Verification
Head
85ec00115a6b5179a99240050a1d9509638e2656, merge base2f7977cd2, a clean merge withorigin/mainat57cde8cb.Consumer suites (each through os-verify-lock, VERDICT command-exit 0)
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.mjsfound the marker in 2 built files ofpackages/spec/dist.Every control stayed green with the flag unread. Restore: blob
d45c69a6equals HEAD,git diff HEADempty. After rebuilding,--absentpreflight 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.One command first answered exit 3, a prerequisite and not a verdict:
node scripts/check-plugin-teardown-shape.mjs --self-testcould not read its positive control pinned at commit621a4876in 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/mainwas run against this body as the event and answered exit 0. It found no major, and on the level axis it readClause-②: yes (widening)andMetadata-protocol: semanticwith no moved package graded patch.Generated by Claude Code