Skip to content

Commit 2c91a55

Browse files
docs(adr-0061): record ruling 6573569376 letter A — field-level searchable is honoured on the auto branch (#29756)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8SYDKd2MYzvNu6rLT1Fwx94)_ Fixes #29391 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只改 `docs/adr/0061-record-search-architecture.md`(+32 行,无删改)。D2 末条「Field-level `searchable` stays pruned」原句不动,其下加一段修订注,记录 #29008 裁决记录 6573569376 的 A 案:对象未声明 `searchableFields` 时,标了 `searchable: true` 的字段进入自动默认集,`secret` / `password` / `vector` 与虚拟 `formula` 字段除外;声明了 `searchableFields` 的对象不读该开关;`$searchFields` 入口放行集同一套解析;既有的 `internal: true` 截留(#22646,ADR-0100 第三条凭据通道)照记:入口在解析前即拒,引擎在解析前把 internal 与凭据字段从字段表剔除,开关绕不过它。「Default behaviour (normative)」表下加一行指针(卡号与裁决记录带链接),表行不改。同注内复读 D5。头部加 `**Amended by**:` 条目(ADR-0005 形制),Status 行不动。 - 为什么改:PR #29385(`29c70cac`)落地后,D2 这句与代码相反;Prime Directive 13 要求把反转记在 ADR 上。注文每一句都对照 `origin/main` `c76c0e04` 的 `packages/spec/src/data/search-fields.ts` 与入口门禁写,并以 file#symbol 引用。 - 风险与回滚:纯文字,不发布、不改行为、不新作决定;回滚即 revert。 - 先看:① D2 下的修订注(逐句对照 `search-fields.ts#admittedBySearchableFlag` 与 `#SEARCH_FLAG_EXCLUDED_TYPES`);② 注末「D5 re-read」两句(硬排除 + `internal: true` 截留);③ 头部 `**Amended by**:` 行。 - 两分钟验证:`pnpm check:adr-symbol-anchors`,预期 3050 anchors 全部 resolve(本 PR 新增 6 个符号锚);或打开 `search-fields.ts` 的 `admittedBySearchableFlag`,对照注文第二、三句。 - 需要你做的:Tier H(`docs/adr/**`)——`GOVERNED_APPROVERS` 批准后由席位落地。 ## What changes One file, `docs/adr/0061-record-search-architecture.md`, +32 / -0: - **Header.** A new `**Amended by**:` line between `**Builds on**:` and `**Consumers**:`, the ADR-0005 shape: #29008, the ruling record, the landing PR and sha, one sentence on what is reversed, and a pointer to the note. The `**Status**:` line is untouched, so `docs/adr/README.md` (generated from that line) does not move. - **D2.** An indented amendment note directly under the last D2 bullet; the amended sentence itself is left as written. The note records the ruling and nothing else: the flag is honoured on the auto-default branch; the resolved set is the type-based auto-default plus every field marked `searchable: true` except `secret` / `password` / `vector` (`packages/spec/src/data/search-fields.ts#SEARCH_FLAG_EXCLUDED_TYPES`) and a virtual `formula` field; the flag is asked before the auto-default's own name / `hidden` / type exclusions (`#admittedBySearchableFlag`), so a flagged hidden or system-named field joins and an unreadable type does not; a declared `searchableFields` is the whole set and the flag is not read there (`#resolveSearchFieldResolution`); the `$searchFields` ingress allow-list is the same resolution (`packages/metadata-protocol/src/protocol-query-validation.ts#assertSearchFieldsAreSearchable`), so a flagged field is accepted as a D1 override and a flagged hard-excluded type is still refused `400 INVALID_FIELD`; a field declared `internal: true` (#22646, ADR-0100's third credential channel, an auth-subsystem one-way hash on a `text` column) is refused at that same door before the resolver runs, flag or no flag; `false` removes nothing. It then gives the reconciled reading of the "Default behaviour (normative)" field-resolution row (`auto` arm widened the same way; row left as written) and the **D5 re-read**. - **D5 re-read (inside the note).** The sentence "Secret / encrypted / PII fields are never searchable (excluded from default, rejected from override)" stays true under the flag, through two pre-existing mechanisms the flag does not reach. For the two masked-on-read credential types, the hard exclusions: `SEARCH_FLAG_EXCLUDED_TYPES` covers `packages/spec/src/data/masked-field-types.ts#MASKED_ON_READ_FIELD_TYPES` (`secret`, `password`; `encrypted` is not a `FieldType`), so such a column marked `searchable: true` is neither scanned nor accepted as an override. For the third channel, an `internal: true` column, the pre-existing withhold (#22646): the engine strips `internal: true` and credential fields from the field map before it resolves the set (`scanFields` in `packages/objectql/src/engine.ts#expandSearchOnAst`), and the door refuses an explicit override naming one before the resolver runs (`assertSearchFieldsAreSearchable`). The resolver reads no PII marker before or after the ruling, and the flag changes which fields resolve, not how a search executes, so the rest of D5 (one gated `find()` pipeline) is untouched. - **Default behaviour table.** A one-line blockquote under the table pointing at the D2 note, linking the card and the ruling record the way the header does; the row is not rewritten. ## Why Ruling record 6573569376 on #29008 (letter A, the maintainer's 「同意」 2026-11-02T09:22Z) chose "honour `field.searchable`, not retire it", and PR #29385 (merged 2026-11-02, `29c70cac`) landed it. ADR-0061 D2 still said the opposite. The card (#29391) is the governed follow-up PR #29385's contract review named; `docs/adr/**` is Tier H, so it could not ride that PR. The drafted note the card cites was not in project files, so this note is redrafted from the ruling record and the merged resolver, as the dispatch order asks. Scope: wording that records the ruling. No new decision and no superseding ADR; nothing outside the one file. ## Tests n/a. A docs-only edit under `docs/adr/**`: no package is touched, so no package test or typecheck is owed, and nothing publishes (fast lane, `skip-changeset`). Verification is the derived gate set below; every claim in the note was read against the merged code on `origin/main` `c76c0e04` (`search-fields.ts`, its test's "field.searchable: true joins the auto-default set" and "SEARCH_FLAG_EXCLUDED_TYPES" describes, `protocol-query-validation.ts` (including the #22646 `internal: true` refusal that runs ahead of `resolveSearchFieldResolution`), `packages/objectql/src/engine.ts` (`expandSearchOnAst`: the `scanFields` strip built from `collectInternalReadFields` and `collectCredentialFields`), `masked-field-types.ts`, `field.zod.ts` describe). ## Acceptance notes - **Shape choices for the approver.** The reconciliation of the "Default behaviour (normative)" row lives in the D2 note, plus a one-line pointer under the table so a reader who lands on the table is not left with the stale `auto` arm; the row is left as written. Strike the pointer if one note is preferred. The header entry is on its own `**Amended by**:` line, not on `**Status**:`, as the order asks. - **Pre-existing drift, not touched (outside this order).** The D2 "Resolution order" bullet lists the short-text set as `text` / `email` / `phone` / `url`; today `SEARCHABLE_TEXTUAL_TYPES` also has `autonumber`, `textarea`, `markdown`, and `SEARCHABLE_ENUM_TYPES` has `select`. The amendment note says "type-based auto-default" and does not enumerate, so it stays true. Read-only inference, not a card; carrier: none. - **D5's word "PII".** No field-level PII marker exists in `FieldSchema` and the resolver reads none; the note states this as a reading, not a decision. Not a card (no declared mechanism is violated: D5's parenthetical names type exclusion, which holds); carrier: none. - **Commit identity.** The one commit is authored `objectstack-fleet[bot]` with the model-free trailer pair; the harness's own attribution reminder names a model in its `Co-Authored-By` line, which the pre-push hook refuses, so the AGENTS.md form was used. Reported, not a deviation. - **Review rounds.** The seat's contract review passed (6594883886) with should-fixes S1 (name the `internal: true` withhold in the ingress sentence and the D5 re-read) and S2 (link #29008 in the table pointer), landed as c520802; the delta review added S3 (anchor the engine withhold on the method `expandSearchOnAst`, not its local const), landed as 0d487e0; the delta review on 0d487e0 passed (6595091192) with no findings left. - **Reviewers.** The dev requested none, and the relay's `pr_create` summons nobody (its governed-review follow-up runs only at `pr_ready`); the Tier H approvers are asked at the seat's hand-off. ## Verification - `node scripts/pm/dispatch-gates.mjs --commands` on this diff (merge base `c76c0e04`): 21 commands, the same list on heads `4d9ec80e` and `c5208020` (one file changed throughout). All 21 run on each, every exit 0; `--ran` reconciled on each: 21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN. On the final head `0d487e0e` (one line, an anchor re-spelled) the three ADR gates below were rerun. - Prerequisite build for `check:doc-formula-expressions`: `@objectstack/formula` + `@objectstack/lint` under the verify lock, VERDICT command-exit 0, held 90 s. - ADR gates, the ones this edit reaches: `check:adr-symbol-anchors` — 3050 anchors across 149 records resolve (six new symbol anchors from this note, all `declaration`); `check:adr-links` — 979 relative links resolve; `check:adr-anchors` — exit 0; `check:adr-index` — `docs/adr/README.md` is current (Status line untouched). - Also exit 0: `check:doc-authoring`, `check:nul-bytes`, `check:pm-governed-merges`, `check:pm-prior-rulings`, `check:closed-ceremony-text`, `check:cross-package-test-inputs`, `check:ci-filter-parity`, `check:driver-memory-census`, `check:gitlink-declared`, `check:refd-timer-probe`, `check:watch-hint-literal`, `check-closing-keyword-parity` (+ self-test), `check-comment-mask-corpus`, `plan-test-core-shards --self-test`, `check-adr-links --self-test`. - NOT MEASURED locally, CI-owned: the type-check lanes, the `Plan Test Core shards` step, the 11 whole-root families and the artifact roster; this PR's check-runs are the reading for those. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. (No names added.) - [x] 3. No formatter ran; `pnpm lint` passes. (No formatter; the repo-wide eslint run is CI's, and no lintable file is touched.) - [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. (Nothing publishes: `docs/adr/**` fast lane, `skip-changeset`; both declaration lines are above.) - [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. (The `维护者速读` block is the os-dev maintainer brief, in Chinese by that file's rule.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. <!-- signed: domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A --> --------- Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 77ce3d4 commit 2c91a55

1 file changed

Lines changed: 32 additions & 0 deletions

File tree

‎docs/adr/0061-record-search-architecture.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
**Status**: Accepted (2026-06-21; Tier 1) — implemented and conformance-closed per this ADR's own bar: server-resolved `$search` executor (`objectql/src/search-filter.ts` + engine wiring), `object.searchableFields` + `$searchFields` narrowing, select label→value mapping; **`search-conformance` ledger** (`dogfood/test/search-conformance.ledger.ts`, ADR-0060 `checkLedger`, proof-required) + **dogfood multi-field HTTP proof** (`showcase-search.dogfood.test.ts`: "retail"→Northwind via `industry`, label mapping, narrowing, terms-AND). Tier 2 (FTS / relevance / external engines) remains deferred — add its ledger row with a proof when a driver ships `fullTextSearch: true`.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: ADR-0045 (additive materialization & visibility gate — search must be visibility-aware), ADR-0049 (enforce-or-remove / no unenforced declaration), ADR-0054 (runtime proof per enforced surface), ADR-0060 (conformance ledger as a platform pattern)
6+
**Amended by**: [#29008](https://github.com/objectstack-ai/objectstack/issues/29008) (2026-11-02, ruling record [6573569376](https://github.com/objectstack-ai/objectstack/issues/29008#issuecomment-6573569376), letter A; landed by PR #29385, `29c70cac`) — D2's "Field-level `searchable` stays pruned" is reversed: `FieldSchema.searchable` is honoured on the auto-default branch, the "Default behaviour (normative)" field-resolution row's `auto` arm is read as widened by it, and D5 holds through the flag's hard exclusions; see the amendment note under D2.
67
**Consumers**: spec, objectql, driver-sql, driver-memory, driver-mongodb, rest, objectui (`fields`, `plugin-list`, `app-shell`), verify, dogfood
78
**References**: `docs/audits/2026-06-record-search-liveness.md`; objectui #1860 / #1863 / framework #2125 / #2128 (lookup picker UX that exposed the gap)
89

@@ -41,6 +42,35 @@ The default contract is `$search: string` (plus existing `$filter`/scope). **Fie
4142
- `view.searchableFields` is a **narrowing override** for a specific surface.
4243
- **Resolution order**: validated `$searchFields` → `view.searchableFields` → `object.searchableFields` → **auto-default** (the name/title field + short-text fields: `text`/`email`/`phone`/`url`; excluding long-text/json/secret/system).
4344
- Collapse the duplicate field list inside `SearchConfigSchema` (it keeps only Tier-2 knobs: analyzer/boost/provider). Field-level `searchable` stays pruned.
45+
> **Amendment note (2026-11-02, [#29008](https://github.com/objectstack-ai/objectstack/issues/29008) — ruling record
46+
> [6573569376](https://github.com/objectstack-ai/objectstack/issues/29008#issuecomment-6573569376), letter A; landed by
47+
> PR #29385, `29c70cac`).** "Field-level `searchable` stays pruned" is reversed: `FieldSchema.searchable` is honoured, not
48+
> retired, on the **auto-default** branch. With no `object.searchableFields` declared, the resolved set is the type-based
49+
> auto-default plus every field marked `searchable: true`, of any type except the hard exclusions `secret` / `password` /
50+
> `vector` (`packages/spec/src/data/search-fields.ts#SEARCH_FLAG_EXCLUDED_TYPES`) and a virtual `formula` field, which
51+
> has no stored column to scan. The flag is asked before the auto-default's own name, `hidden` and type exclusions
52+
> (`packages/spec/src/data/search-fields.ts#admittedBySearchableFlag`), so a flagged hidden field or a flagged
53+
> system-named column joins too; a field whose type is unreadable is not admitted. A declared `searchableFields` stays
54+
> the **whole** set and the flag is not read on that branch
55+
> (`packages/spec/src/data/search-fields.ts#resolveSearchFieldResolution`). The `$searchFields` ingress allow-list is
56+
> this same resolution (`packages/metadata-protocol/src/protocol-query-validation.ts#assertSearchFieldsAreSearchable`),
57+
> so naming a flagged field is accepted as a D1 override, and a flagged field of a hard-excluded type is still refused
58+
> `400 INVALID_FIELD`; a field declared `internal: true` (#22646 — ADR-0100's third credential channel, an
59+
> auth-subsystem one-way hash on a `text` column) is refused at that door before the resolver runs, flag or no flag.
60+
> The key defaults to `false`, and `false` removes nothing the type-based default already scans:
61+
> the flag widens the auto-default and never narrows it. The "Default behaviour (normative)" table's field-resolution
62+
> row is read with its `auto` arm widened the same way — `auto (name + short-text + every field marked searchable: true
63+
> outside the hard exclusions)`; the row itself is left as written. **D5 re-read:** "Secret / encrypted / PII fields are
64+
> never searchable (excluded from default, rejected from override)" stays true under the flag, and the hard exclusions
65+
> are what keep it so: `SEARCH_FLAG_EXCLUDED_TYPES` covers every masked-on-read credential type
66+
> (`packages/spec/src/data/masked-field-types.ts#MASKED_ON_READ_FIELD_TYPES` — `secret` and `password`; `encrypted` is
67+
> not a `FieldType`), so such a column marked `searchable: true` is neither scanned nor accepted as an override. The
68+
> third channel, an `internal: true` column, holds by the pre-existing withhold rather than by
69+
> `SEARCH_FLAG_EXCLUDED_TYPES`: the engine strips `internal: true` and credential fields from the field map before it
70+
> resolves the set (`scanFields` in `packages/objectql/src/engine.ts#expandSearchOnAst`, #22646), and the door refuses an explicit override
71+
> naming one before the resolver runs (above), so the flag never reaches it. The resolver reads no PII marker, before or
72+
> after this ruling, and the flag changes which fields resolve, not how a search executes: it still runs through the
73+
> gated `find()` pipeline, so the rest of D5 is untouched.
4474
4575
### D3 — Two-tier executor behind one `$search` API
4676
- **Tier 1 (this phase)**: the engine expands `$search` into a cross-field predicate pushed to the driver — `driver-sql` reuses existing `$or` + `$contains` (ILIKE, wildcard-escaped); `driver-memory` does in-memory `contains`. New driver capability `textSearch: true`. Matching is **case-insensitive; terms AND-ed, fields OR-ed** (each term must hit some field) — i.e. `FullTextSearchSchema.operator` default. Default is **contains** (not prefix). When a driver lacks `textSearch`, the **engine applies a bounded in-memory fallback** (degraded scale, logged) so search works everywhere.
@@ -72,6 +102,8 @@ All surfaces converge on `$search` (+ validated `$searchFields`); client-side fi
72102
| Security | over RLS + ADR-0045 visibility; secret/PII never searchable; override subset-validated |
73103
| Execution | Tier 1 driver `contains` + engine in-memory fallback; Tier 2 trigram/FTS/external |
74104

105+
> **Amended (2026-11-02, [#29008](https://github.com/objectstack-ai/objectstack/issues/29008) — ruling record [6573569376](https://github.com/objectstack-ai/objectstack/issues/29008#issuecomment-6573569376), letter A).** The `auto` arm of the field-resolution row above also includes every field marked `searchable: true` outside the hard exclusions `secret` / `password` / `vector`; the row is left as written and the reading is set out in the amendment note under D2.
106+
75107
### Conformance (ADR-0060 alignment)
76108
`$search` is **"landed" iff** a driver executes it **and** a dogfood proof asserts a multi-field match over the real HTTP API — e.g. searching `"retail"` returns an account matched by `industry`, not `name`. Add a `search-conformance` ledger entry; CI fails if `$search` is declared but no proof resolves (closing the declared-but-unenforced loop).
77109

0 commit comments

Comments
 (0)