Skip to content

Commit 0d40aa1

Browse files
skills(data, query, ui): with no searchableFields declared, a field marked searchable: true joins the default search set (#29912)
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J <!-- claude-projects-attribution: {"github_login":"huangyiirene"} --> _Requested by **Irene** · [project thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67xPvrQ6rqAoXn7MahmXvdwUf)_ Fixes #29395 Clause-②: no Metadata-protocol: none Model: claude-fable-5-1 mandate waived by the maintainer (Fable quota exhausted); developed on Opus; contract review by triage. ## 维护者速读 - 改了什么:三个 skills 文件不再把"未声明 `searchableFields` 时的默认搜索集"讲成只按类型。现在写明:类型默认集 + 每个标了 `searchable: true` 的字段(`secret` / `password` / `vector` 与虚拟 `formula` 除外);`$searchFields` 白名单是同一集合。每处另加一句不变的事:`searchable: false`(默认值)不会移除类型默认集已扫描的字段;要自己选集合就声明 `searchableFields`。 - 为什么改:#29385(#29008)已按裁决 6573569376 字母 A 落地,技能文本仍是旧说法。 - 风险与回滚:只改 `skills/**` 文本,另加一个 `@objectstack/skills` patch changeset;无代码、无棘轮行变动。回滚即 revert。 - 先看:`objectstack-data/SKILL.md` Search Fields 段;`objectstack-query/SKILL.md` L319–327;`list-views.md` 允许集表格 L186–191。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期 exit 0。 - 需要你做的:Tier H,分诊契约复审后由你审批落地。 ## What changes The rule is read from the merged code, not the card: `packages/spec/src/data/search-fields.ts` (`autoDefaultFields`, `admittedBySearchableFlag`, `SEARCH_FLAG_EXCLUDED_TYPES = {secret, password, vector}`) and the `FieldSchema.searchable` describe in `packages/spec/src/data/field.zod.ts`. 1. **`skills/objectstack-data/SKILL.md` § Search Fields.** The auto-default sentence now adds "plus every field marked `searchable: true`, of any type except `secret` / `password` / `vector` or a virtual `formula`", then the unchanged-behaviour line: `searchable: false`, the default, never removes a field the type default scans; declare `searchableFields` to choose the set. 2. **`skills/objectstack-query/SKILL.md` (the "Omit it to search…" paragraph).** The parenthesised auto-default gains the same clause, and the paragraph gains the same unchanged-behaviour line. 3. **`skills/objectstack-ui/rules/list-views.md`, the allowed-set table.** The "declares nothing" row gains "+ every field marked `searchable: true`, of any type except `secret` / `password` / `vector` or a virtual `formula`", and one line under the table carries the unchanged-behaviour sentence. The "declares `searchableFields`" row now reads "any stored field type" (it said "whatever the field types are"), so the table no longer contradicts the refusal row below it, which refuses a virtual `formula`. 5. **Changeset.** `.changeset/29395-skills-search-auto-default.md` releases `@objectstack/skills` as a `patch`, with `Clause-②: no` and `Metadata-protocol: none`: a `skills/**` change publishes that package, so it never takes `skip-changeset`. 4. **The enum half of the type default** is now spelled `select` alone in the data skill and the list-views table. Both said `select` / `status`. `status` is not a `FieldType` member, and `SEARCHABLE_ENUM_TYPES` is `{select}`. No eval covers the edited lines. The only search evals are a mirror-field prompt in `objectstack-data/evals/hooks-security-seeds-datasources.json` and a `searchFields` prompt in `objectstack-query/evals/filters-pagination-search.json`. Neither asserts on the auto-default, so no eval changed. ### Token budget, paid in place (no ceiling row moved) | file | tokens before → after / ceiling | lines before → after | |:--|:--|:--| | `objectstack-data/SKILL.md` | 6123 → 6119 / 6123 | 466 → 464 | | `objectstack-query/SKILL.md` | 4166 → 4234 / 5547 | 403 → 407 | | `objectstack-ui/rules/list-views.md` | 3110 → 3126 / 3129 | 308 → 308 | What paid: - **objectstack-data** (at its ceiling, headroom 0): the duplicate `→ Moved verbatim … § Field Groups (MVP)` pointer stub and its `---` separator are removed. The `fieldGroups` row of the optional-properties table already links the same anchor. Three phrases are also tightened: the Quick Reference lead-in, "computed and stored on write" in the `nameField` row (a stored computed field already says it), and "by a path that bypasses hooks" becomes "past the hooks". The `formula` clause (b89c05d) is paid by two restatement cuts: "no traversal" leaves the `?search=apollo` line, and its home is the section's opening ("the search axis does not resolve traversal"); the `// ← the mirror` code comment goes, and its home is the block's first comment ("`project_name` is a stored, denormalized mirror of the parent's title"). - **objectstack-query**: 68 tokens fit inside the existing 1381 headroom, so nothing else was cut. - **list-views**: the refusal table's two `searchable-field-unknown` rows (mistyped column, dotted path) become one row, and so do its two `searchable-field-unsearchable` rows (column outside the set, virtual `formula`). "at all three layers" is dropped from the `searchableFields: []` row. For the `formula` clause, the refusal row drops its "(nothing stored to scan)", whose home is now the allowed-set table's "any stored field type", and that row is itself shorter than the "whatever the field types are" it replaces. ### Acceptance notes - **Contract review FAIL on c5f8165 (6606434667).** M1 (changeset) and S1 (virtual `formula` exception) are fixed on b89c05d as above; the `skip-changeset` label is removed. The new clauses lengthen their lines in place (data L118 is 100 bytes, query L321 102), with no re-wrap. - **Re-wrap.** The order said no re-wrap. The query paragraph's later sentences ("resolved server-side. It can only **narrow**…" through "…Tier-2 knobs below.") were re-flowed, because the insertion lands mid-paragraph. The data paragraph's last line ("Views may narrow it, never widen it.") also moved. The words are unchanged. No other paragraph was touched. The reviewer may rule this a breach; the fix would be a line-preserving rewrite of those two paragraphs. - The new list-views line under the table is 153 bytes, on one line so as not to wrap. - At dispatch, no open PR edited these lines. #29190 and #28903 touch `objectstack-data/SKILL.md`, at the mirror-field block (L147–169) and after L378 only. - The branch is based on c76c0e0. No commit on `main` since then touches the three files, the token ratchet or `search-fields.ts`. ## Why #29385 (#29008) landed ruling 6573569376 letter A: with no `searchableFields` declared, the effective search set is the type-based auto-default plus every field marked `searchable: true` of any type except `secret`, `password` and `vector`, and the `$searchFields` allow-list is the same set. These three skills still taught the default as type-only. An agent following them would tell an author that `searchable: true` on a `code` or `lookup` field does nothing, and would treat a `$searchFields` override naming such a field as a 400. ## Verification <details><summary>Local gates on b89c05d: 44 derived, 43 exit 0, 1 not measured (`dispatch-gates --ran`)</summary> - On head b89c05d, `node scripts/pm/dispatch-gates.mjs --commands` derived 44 commands. The changeset adds 10 families, including `check-empty-changeset`, `check-changeset-no-major` and `check-adr-0087-registration`. Each ran with a 900 s timeout, in a tree with `pnpm install --frozen-lockfile` done and `@objectstack/formula`, `@objectstack/lint` and `@objectstack/objectql` built. Exit codes were recorded before any pipe. - 43 exited 0. These include `node scripts/check-skills-token-ratchet.mjs` and its `--self-test`, `pnpm check:pm-skill-id-lint`, `node scripts/check-empty-changeset.mjs --base origin/main`, `pnpm --filter @objectstack/spec run check:skill-docs`, `pnpm check:skill-frame-sync`, `pnpm check:skill-identifier-liveness`, `pnpm check:doc-authoring` and `pnpm check:lean-entry-closure`. - `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: no `dist/` for most packages). It needs a full `pnpm build`, and this diff touches no `packages/**`. It is reported as not measured, and CI's run on this head is the measurement. - `dispatch-gates --ran` printed: `44 derived famil(ies) accounted for — 43 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. - `node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD` exited 0 over both commits. - On the first head c5f8165, 34 commands were derived: 33 exited 0, and `dual-build-cjs-loads` was not measured. CI there had 28 success, 18 skipped and no failure, until the label was removed and `Check Changeset` correctly failed on that head (M1). </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No `packages/**` change.) - [x] 2. Every name I added uses the casing for its kind. (No names added.) - [x] 3. No formatter ran; `pnpm lint` passes. (Markdown only.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (`@objectstack/skills`: patch.) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English, except the 维护者速读 block that governed surfaces require. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J8VVAjjRmCZgXRP1FNQS2n --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8aa2e91 commit 0d40aa1

4 files changed

Lines changed: 37 additions & 23 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): with no `searchableFields` declared, a field marked `searchable: true` joins the default search set
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `objectstack-data`, `objectstack-query` and the `objectstack-ui` list-views rule taught the record-search auto-default as type-only: the name field plus the text-like and `select` columns. A field marked `searchable: true` was described as doing nothing, and a `$searchFields` override naming one as refused.
11+
- **Now.** Each says what the platform resolves: with no `searchableFields` declared, the set `search` scans, and the set a `$searchFields` override may name, is the type-based default plus every field marked `searchable: true`, of any type except `secret`, `password`, `vector` or a virtual `formula`. Each also says what does not change: `searchable: false`, the default, never removes a field the type default scans, and declaring `searchableFields` chooses the set. The enum half of the type default is spelled `select` alone, as the resolver has it.
12+
- No schema, key or behaviour changes.

‎skills/objectstack-data/SKILL.md‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ metadata:
3535

3636
## Quick Reference — Detailed Rules
3737

38-
For comprehensive documentation with incorrect/correct examples:
38+
Detailed rules, with incorrect/correct examples:
3939

4040
- **[Naming Conventions](./rules/naming.md)** — snake_case rules, option values, config properties
4141
- **[Field Types](./rules/field-types.md)** — All field types and configs
@@ -85,7 +85,7 @@ the publish door rejects it before that:
8585
| `highlightFields` | derived | Ordered field keys used as a record's compact face: the columns a **related list** renders on the parent's detail page, and what a lookup picker shows. Declare it on the CHILD object (see [Relationships](./rules/relationships.md)) |
8686
| `namespace` | — | **Not a schema key** — `ObjectSchema.create()` rejects unknown keys, so authoring it is a build error. Embed the prefix directly in `name` instead (e.g. `name: 'crm_account'`) |
8787
| `datasource` | `'default'` | Target datasource ID for virtualized data |
88-
| `nameField` | derived (e.g. `'name'`/`'title'`) | **Canonical** record-title field — the stored field used as the record's display name. Use a single text/email field; a composite title to import, sort or search by is a stored computed field (a non-`formula` field with an `expression`, computed and stored on write); a text formula is display-only |
88+
| `nameField` | derived (e.g. `'name'`/`'title'`) | **Canonical** record-title field — the stored field used as the record's display name. Use a single text/email field; a composite title to import, sort or search by is a stored computed field (a non-`formula` field with an `expression`); a text formula is display-only |
8989
| `displayNameField` | — | **Deprecated** alias for `nameField` (still honored as a fallback) |
9090
| `titleFormat` | — | **Deprecated, not removed (ADR-0079)** — render-only: the server can't return or query it. Use `nameField` (it wins); a composite title follows the `nameField` row |
9191
| `enable` | — | Capability flags (trackHistory, searchable, apiEnabled, etc.) |
@@ -114,8 +114,10 @@ Toggle system behaviours per object:
114114
`searchableFields` on the object is the canonical set `$search` scans (ADR-0061).
115115
Leave it unset and search auto-defaults to the `nameField` plus the object's
116116
short-text and enum columns (`text` / `email` / `phone` / `url` / `autonumber` /
117-
`textarea` / `markdown` / `select` / `status`); declare it to pin the set
118-
explicitly. Views may narrow it, never widen it.
117+
`textarea` / `markdown` / `select`) plus every field marked `searchable: true`,
118+
of any type except `secret` / `password` / `vector` or a virtual `formula`. `searchable: false`, the
119+
default, never removes a field the type default scans; declare
120+
`searchableFields` to choose the set. Views may narrow it, never widen it.
119121

120122
### ⛔ Searching by a related record's title — mirror the value, always
121123

@@ -137,14 +139,14 @@ declare *that* field searchable. A task list searched by project name:
137139
fields: {
138140
name: { type: 'text', required: true },
139141
project_id: { type: 'lookup', reference: 'project' },
140-
project_name: { type: 'text', label: 'Project Name' }, // ← the mirror
142+
project_name: { type: 'text', label: 'Project Name' },
141143
},
142144
searchableFields: ['name', 'project_name'],
143145
}
144146
```
145147

146148
`?search=apollo` expands to `name $contains 'apollo' OR project_name $contains
147-
'apollo'` — one single-table scan, every driver, no traversal.
149+
'apollo'` — one single-table scan, every driver.
148150

149151
❌ **Never mirror onto a `formula` field.** It is *virtual* (no column to scan)
150152
and CEL reads only this record's own fields, so it cannot hold the related title;
@@ -154,15 +156,11 @@ a `formula` entry in any `searchableFields` is refused — `os validate`
154156
**Mirror maintenance is the trade-off** — a mirror is only as fresh as whatever
155157
writes it. Cover both paths: a `beforeInsert` / `beforeUpdate` hook on `task`
156158
stamps `project_name` from the incoming `project_id`'s parent; an `afterUpdate`
157-
hook on `project` re-stamps its tasks when it is renamed. Rows written by a path that bypasses hooks (bulk import, direct SQL) need a
159+
hook on `project` re-stamps its tasks when it is renamed. Rows written past the hooks (bulk import, direct SQL) need a
158160
one-off backfill. See [Lifecycle Hooks](./references/data-hooks.md).
159161

160162
---
161163

162-
→ Moved verbatim to [references/examples-objects.md](./references/examples-objects.md) § Field Groups (MVP).
163-
164-
---
165-
166164
## Conditional Field Rules
167165

168166
Put conditional UI/data-entry rules on the **field definition** when the rule

‎skills/objectstack-query/SKILL.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,15 @@ to stored values.
316316
protocol normalizes `$searchFields` onto it, and the object form
317317
`search: { query, fields }` spells the same narrowing `fields`.
318318

319-
Omit it to search the object's declared `searchableFields` (or an auto-default
320-
of name/title + short-text fields), resolved server-side. It can only **narrow**
321-
that set, never widen it: over the REST/protocol ingress a name outside it is
322-
`400 INVALID_FIELD`, not a silent fall-back to a full scan. The object form
323-
`search: { query, fields }` stays available for the Tier-2 knobs below.
319+
Omit it to search the object's declared `searchableFields` (or, with none
320+
declared, an auto-default: name/title + short-text fields, plus every field
321+
marked `searchable: true` of any type except `secret` / `password` / `vector` or a virtual `formula`),
322+
resolved server-side. It can only **narrow** that set, never widen it: over the
323+
REST/protocol ingress a name outside it is `400 INVALID_FIELD`, not a silent
324+
fall-back to a full scan. `searchable: false`, the default, never removes a
325+
field the type default already scans; declare `searchableFields` to choose the
326+
set. The object form `search: { query, fields }` stays available for the Tier-2
327+
knobs below.
324328

325329
> ⚠️ **Validates, then silently ignored — never emit these.** `fuzzy`, `boost`,
326330
> `operator`, `minScore`, `language` and `highlight` are the whole set; their

‎skills/objectstack-ui/rules/list-views.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,10 @@ export const CaseViews = defineView({
185185

186186
| The object … | The allowed set is |
187187
|:-------------|:-------------------|
188-
| declares `searchableFields` | **that list, verbatim** — whatever the field types are |
189-
| declares nothing | the auto-default: the name field + the text-like columns (`text` / `email` / `phone` / `url` / `autonumber` / `textarea` / `markdown` / `select` / `status`) |
188+
| declares `searchableFields` | **that list, verbatim** — any stored field type |
189+
| declares nothing | the auto-default: the name field + the text-like columns (`text` / `email` / `phone` / `url` / `autonumber` / `textarea` / `markdown` / `select`) + every field marked `searchable: true`, of any type except `secret` / `password` / `vector` or a virtual `formula` |
190+
191+
`searchable: false`, the default, removes nothing the type default scans; declare `searchableFields` to choose the set.
190192

191193
Modelling side: **objectstack-data → Search Fields**. Query side
192194
(`search.fields` over the API): **objectstack-query → Full-Text Search**.
@@ -201,11 +203,9 @@ the whole search box, for every user and every term: no result at all.
201203
|:---------------------------|:--------------|:--------------------------|
202204
| a subset of the allowed set | clean | scans exactly those columns |
203205
| key omitted | clean | scans the object's full allowed set |
204-
| `searchableFields: []` | clean | **identical to omitting it** — empty is *absent* at all three layers, so it scans MORE than `["subject"]` would |
205-
| a renamed / mistyped column | `searchable-field-unknown` | `400 INVALID_FIELD` |
206-
| a dotted path (`account_id.name`) | `searchable-field-unknown` | `400 INVALID_FIELD` |
207-
| a real column outside the allowed set | `searchable-field-unsearchable` | `400 INVALID_FIELD` |
208-
| a virtual `formula` column — nothing stored to scan | `searchable-field-unsearchable` | `400 INVALID_FIELD` |
206+
| `searchableFields: []` | clean | **identical to omitting it** — empty is *absent*, so it scans MORE than `["subject"]` would |
207+
| a renamed / mistyped column, or a dotted path (`account_id.name`) | `searchable-field-unknown` | `400 INVALID_FIELD` |
208+
| a real column outside the allowed set, or a virtual `formula` column | `searchable-field-unsearchable` | `400 INVALID_FIELD` |
209209

210210
Both diagnostics are **errors**, not warnings — `os validate` fails the build.
211211

0 commit comments

Comments
 (0)