You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0728cbf
Browse filesBrowse the repository at this point in the historyBrowse files
fix(objectql): a field-narrowed search no longer matches through the companion of a field outside the search-field set (#21930)
Fixes#21880
Clause-②: no
## What changed
When the optional pinyin search companion is on, a field-narrowed search
no longer matches through the companion of a field outside the
search-field set.
- **Where.** `expandSearchToFilter` in
`packages/objectql/src/search-filter.ts`, the engine's search expansion.
`searchAll` is not touched.
- **The gate.** The `__search` companion clause is added only when every
field the companion mirrors is inside `searchFields`. That is the
effective set `resolveSearchFields` already computed, after the
declared/auto-default precedence and any `$searchFields` narrowing.
There is one gate and no second eligibility rule for the companion.
- **Where the mirrored fields come from.**
`resolveSearchCompanionSources`, the same function the registry
provisions the companion from and plugin-pinyin-search fills it from. It
is read over the same `fields` and the same display-field pointer the
engine already passes to the expansion. No spec file changed, and no new
export was added.
- **What the companion is (measured).** One column per object, holding
the normalized form of ONE source field: the resolved display/name field
(`resolveSearchCompanionSources` returns that field, or `[]`). The gate
is written as "every mirrored field is in the set", never "some",
because a clause over one shared column matches through every field it
mirrors. Today that means "the display/name field is in the set".
- **What stays the same.** A search with no narrowing keeps the clause
whenever the display/name field is in the object's searchable set, so
pinyin recall there is unchanged. A CJK term still skips the clause.
With the companion off, nothing changes. An empty mirror list passes
vacuously. The registry never provisions a companion without a source,
so that case is only an author-declared `__search` column, and it keeps
today's answer.
## Tests
**Unit** (`packages/objectql/src/search-companion.test.ts`, new
describe, 10 cases):
- (a) A field-narrowed search that leaves the mirrored field out gets no
companion clause. Covered: a `$searchFields` override (array and
comma-separated), the narrowing carried on the term (`{ query, fields
}`), a declared `searchableFields` without the field, every term of a
multi-term search, and an explicit `nameField` pointer, which moves what
the companion mirrors.
- (b) A search with no narrowing keeps it. Covered: the auto-default
set, a narrowed set that still holds the mirrored field, and a request
naming no allowed field, which falls back to the full set.
- (c) A CJK term still skips it, with and without narrowing.
**Dogfood**
(`packages/qa/dogfood/test/search-companion-field-scope.dogfood.test.ts`,
7 cases). A real kernel boots with the real `SecurityPlugin` and
`PinyinSearchPlugin` (`OS_SEARCH_PINYIN_ENABLED` on), over HTTP. Every
row is named in CJK, so a pinyin term matches only through the
companion.
- **Row-scoped object** (`sharingModel: 'private'`). The member's search
answers only the member's own matching row, through `/search` and
through the data door. Control: the administrator gets both rows.
- **A term present only in a field hidden from the member** (`readable:
false` on `name`). The member gets no hit through `/search?objects=` and
none through a `searchFields: ['code']` data-door query. Controls: the
field really is hidden at the data door; the administrator hits the row
through the companion (no narrowing keeps it); the member hits the same
row through `code`, and that hit carries nothing of the hidden field.
- `@objectstack/plugin-pinyin-search` is added to the private
`@objectstack/dogfood` package's dependencies. `check:test-source-alias`
asked for its anchored source alias in
`packages/qa/dogfood/vitest.config.ts`, because the plugin's fill path
is part of the pin's subject.
## Reverse verification (one-off, nothing left in the tree)
The base clause was restored on committed HEAD, first at `e7b2a3c2e2`
and again at the final head `40618fa8cc`, with the same readings both
times. `node scripts/ablation-replace.mjs` replaced the gate with a
constant-true guard carrying the marker `__ABLATED_21880`: anchor hits
went 1 to 0, blob `5a2afee37090` to `ce8eb93139c8`.
- `pnpm --filter @objectstack/objectql build`, then
`ablation-dist-preflight` confirmed the marker is in 4 built files of
`packages/objectql/dist`.
- Unit, `src/search-companion.test.ts`: **5 failed / 34 passed**. The 5
failures are exactly the (a) cases; (b) and (c) stayed green.
- Dogfood file: **2 failed / 5 passed**. The 2 failures are exactly the
two hidden-field "no hit" cases. The row-scoped case and every control
stayed green.
- Direction observed: red, as expected.
- Restore: blob equal to HEAD (`5a2afee37090`) and an empty `git diff
HEAD`. After rebuilding objectql, `ablation-dist-preflight --absent`
reported the marker absent from all 14 built files and a clean working
tree.
## Local verification (at HEAD `40618fa8cc`, after merging `origin/main`
at `faf8dce482`)
- `pnpm --filter @objectstack/objectql exec vitest run --project local`
(the package's `test` script): **375 files, 7479 tests passed**.
- Dogfood, `search-companion-field-scope` plus the neighbouring
`search-skip-unreadable`: **2 files, 11 tests passed**.
- `pnpm --filter @objectstack/objectql run typecheck` exit 0, which
includes `check:test-typecheck` over `tsconfig.test.json`. `pnpm
--filter @objectstack/dogfood run typecheck` exit 0. `--listFilesOnly`
shows both new test files are in their programs.
- `node scripts/pm/dispatch-gates.mjs --commands` over this branch's
change set derived 78 commands. All 78 were run at this head, and
`--ran` reconciled them: **78 run, 0 NOT-MEASURED, 0 UNRUN**, every exit
0. A full `turbo run build` came first, so `check:dual-build-cjs-loads`
measured instead of refusing.
- Artifact-roster block, the 53 commands printed outside the total: 50
exit 0. Three are NOT WIRED locally because they need PR context:
`check-closing-target-claim`, `check-partof-closing-keyword` and
`check-single-claim-paths`. `check-partof-closing-keyword` was then run
with `PR_BODY` set to this body: exit 0. The other two are declared to
CI.
- Symbol-anchor sweeps: `check:adr-symbol-anchors`,
`check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors` and
`check:adr-anchors` all exit 0.
- Lint, narrowed to the 4 changed `.ts` files: `eslint
--no-inline-config --format json` reports 4 files, 0 errors, 0 warnings,
and `eslint --print-config` resolves a config for each, so none is
ignored. `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so this diff cannot change the verdict on any
untouched file. `pnpm lint` over the whole tree is CI's run.
## Acceptance notes
- **Recall change on un-narrowed searches.** One case of a search with
no narrowing loses the clause: an object whose effective set omits its
display/name field. Examples are a declared `searchableFields` without
it, or a display field of a type the auto-default does not scan (`html`,
`richtext`, which are title-eligible). That follows from the ruling: the
declared set says the field is not searched. Measured over `examples/`
at merge base `dcb11c2ec9`: one object declares `searchableFields`
(`showcase_account`), and it includes `name`; one object sets an
explicit `nameField` (`todo_task.subject`), a `text` field in the
auto-default. So 0 example objects change. Derived display fields of
type `html` or `richtext` were NOT MEASURED (that needs a registry boot
of each example).
- `packages/qa/dogfood/test/search-conformance.ledger.ts` is unchanged.
Its rows describe the executor and the `$searchFields` override, and
neither claim moved.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
A field-narrowed `$search` no longer matches through the pinyin search companion of a field outside the search-field set (#21880).
6
+
7
+
Clause-②: no
8
+
9
+
-**What changed.** When the optional pinyin search companion is on (`OS_SEARCH_PINYIN_ENABLED`), the engine's search expansion (`expandSearchToFilter`) adds the companion clause only when every field the companion mirrors is inside the effective search-field set: the set `resolveSearchFields` computes, after any `$searchFields` narrowing. The mirrored fields are read from `resolveSearchCompanionSources`, the same function the companion is provisioned and filled from.
10
+
-**What stays the same.** A search with no narrowing keeps the clause whenever the display/name field is in the object's searchable set, so pinyin recall there is unchanged. A CJK term still skips the clause. Deployments with the companion off see no change.
11
+
-**Who notices.** A search narrowed to fields that leave out the display/name field, by a `$searchFields` override, by the narrowing global search applies to the fields a caller may query, or by a declared `searchableFields` that omits it, no longer matches through that field's pinyin form.
0 commit comments