fix(objectql): a field-narrowed search no longer matches through the companion of a field outside the search-field set - #21930
Conversation
…ch-field set A field-narrowed search no longer matches through the companion of a field outside the search-field set. The clause joins a search only when every field the companion mirrors is inside the set resolveSearchFields computed; a search with no narrowing keeps it. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
… only what they may see Two public cases: a row-scoped object, where the member sees only their own matching row, and a term present only in a field hidden from the member, which yields no hit. Plus the objectql patch changeset. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ld-rule free Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…arch-companion-scope
…nion-scope pin check:test-source-alias asks for it: the plugin's fill path is part of the pin's subject, so the verdict is about this checkout's source. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…arch-companion-scope
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 19 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 34e8c15ce9ec0fa71b6245bbdb8b394d49d9925b && git checkout 34e8c15ce9ec0fa71b6245bbdb8b394d49d9925b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 211df5615dbe4b48aa7bb8ccb433cbfb96bf749b && git checkout -B drift-repro 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 && git merge --no-ff 211df5615dbe4b48aa7bb8ccb433cbfb96bf749b
node scripts/docs-audit/affected-docs.mjs --json 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8
|
ACCEPT (seat review) — PR #21930 at head
|
|
CI:
Generated by Claude Code |
|
CI:
Generated by Claude Code |
…arch-companion-scope
ACCEPT (seat review, head moved) — PR #21930 at head
|
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.
expandSearchToFilterinpackages/objectql/src/search-filter.ts, the engine's search expansion.searchAllis not touched.__searchcompanion clause is added only when every field the companion mirrors is insidesearchFields. That is the effective setresolveSearchFieldsalready computed, after the declared/auto-default precedence and any$searchFieldsnarrowing. There is one gate and no second eligibility rule for the companion.resolveSearchCompanionSources, the same function the registry provisions the companion from and plugin-pinyin-search fills it from. It is read over the samefieldsand the same display-field pointer the engine already passes to the expansion. No spec file changed, and no new export was added.resolveSearchCompanionSourcesreturns 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".__searchcolumn, and it keeps today's answer.Tests
Unit (
packages/objectql/src/search-companion.test.ts, new describe, 10 cases):$searchFieldsoverride (array and comma-separated), the narrowing carried on the term ({ query, fields }), a declaredsearchableFieldswithout the field, every term of a multi-term search, and an explicitnameFieldpointer, which moves what the companion mirrors.Dogfood (
packages/qa/dogfood/test/search-companion-field-scope.dogfood.test.ts, 7 cases). A real kernel boots with the realSecurityPluginandPinyinSearchPlugin(OS_SEARCH_PINYIN_ENABLEDon), over HTTP. Every row is named in CJK, so a pinyin term matches only through the companion.sharingModel: 'private'). The member's search answers only the member's own matching row, through/searchand through the data door. Control: the administrator gets both rows.readable: falseonname). The member gets no hit through/search?objects=and none through asearchFields: ['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 throughcode, and that hit carries nothing of the hidden field.@objectstack/plugin-pinyin-searchis added to the private@objectstack/dogfoodpackage's dependencies.check:test-source-aliasasked for its anchored source alias inpackages/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
e7b2a3c2e2and again at the final head40618fa8cc, with the same readings both times.node scripts/ablation-replace.mjsreplaced the gate with a constant-true guard carrying the marker__ABLATED_21880: anchor hits went 1 to 0, blob5a2afee37090toce8eb93139c8.pnpm --filter @objectstack/objectql build, thenablation-dist-preflightconfirmed the marker is in 4 built files ofpackages/objectql/dist.src/search-companion.test.ts: 5 failed / 34 passed. The 5 failures are exactly the (a) cases; (b) and (c) stayed green.5a2afee37090) and an emptygit diff HEAD. After rebuilding objectql,ablation-dist-preflight --absentreported the marker absent from all 14 built files and a clean working tree.Local verification (at HEAD
40618fa8cc, after mergingorigin/mainatfaf8dce482)pnpm --filter @objectstack/objectql exec vitest run --project local(the package'stestscript): 375 files, 7479 tests passed.search-companion-field-scopeplus the neighbouringsearch-skip-unreadable: 2 files, 11 tests passed.pnpm --filter @objectstack/objectql run typecheckexit 0, which includescheck:test-typecheckovertsconfig.test.json.pnpm --filter @objectstack/dogfood run typecheckexit 0.--listFilesOnlyshows both new test files are in their programs.node scripts/pm/dispatch-gates.mjs --commandsover this branch's change set derived 78 commands. All 78 were run at this head, and--ranreconciled them: 78 run, 0 NOT-MEASURED, 0 UNRUN, every exit 0. A fullturbo run buildcame first, socheck:dual-build-cjs-loadsmeasured instead of refusing.check-closing-target-claim,check-partof-closing-keywordandcheck-single-claim-paths.check-partof-closing-keywordwas then run withPR_BODYset to this body: exit 0. The other two are declared to CI.check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchorsandcheck:adr-anchorsall exit 0..tsfiles:eslint --no-inline-config --format jsonreports 4 files, 0 errors, 0 warnings, andeslint --print-configresolves a config for each, so none is ignored.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any untouched file.pnpm lintover the whole tree is CI's run.Acceptance notes
searchableFieldswithout 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 overexamples/at merge basedcb11c2ec9: one object declaressearchableFields(showcase_account), and it includesname; one object sets an explicitnameField(todo_task.subject), atextfield in the auto-default. So 0 example objects change. Derived display fields of typehtmlorrichtextwere NOT MEASURED (that needs a registry boot of each example).packages/qa/dogfood/test/search-conformance.ledger.tsis unchanged. Its rows describe the executor and the$searchFieldsoverride, and neither claim moved.Generated by Claude Code