Repository navigation
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
|
…cratch-dir scan can read (objectstack-ai#21935) Fixes objectstack-ai#21924 Clause-②: no ## What changes `main`'s hourly `Lint & Type Check` run is red on one `dispatch-gates` self-test case (run `37394652870` at `be97cf3c93`, `Lint & Repo Gates`, step `PM dispatch-gates self-test`): > ✗ no mkdtempSync site in this tree takes a base the scan cannot read — UNRESOLVED: packages/qa/dogfood/test/per-file-cwd.setup.ts:60 (a base this scan cannot read: inject('dogfoodCwdRoot')) PR objectstack-ai#21919 added that site for objectstack-ai#21914. The guard is right: a `mkdtempSync` base handed over through `inject()` is an expression the tree's scratch-directory scan cannot read. So the fix is at the site, and nothing in the guard or the scan changes. - **`packages/qa/dogfood/test/per-file-cwd.setup.ts`**: each file's directory is now `mkdtempSync(join(tmpdir(), perFileDirPrefix(runTag)))`. The base is `tmpdir()`, outside the tree by construction, and the scan reads it as such. - Only the run's TAG crosses `inject()` (key renamed `dogfoodCwdRoot` to `dogfoodRunTag`), and only as a name component. - A tag carrying a path separator or `..` is refused. - **`packages/qa/dogfood/test/per-file-cwd.global-setup.ts`**: - At start it clears a stale `.objectstack` (unchanged). It reserves the run's tag as `mkdtempSync(join(tmpdir(), 'os-dogfood-run-'))` (unchanged expression) and provides its basename. - At END it removes every system-temp entry named with this run's prefix (`perFileDirPrefix`, exported here and imported by the setup module, so the prefix has one spelling), then the reservation. - The removal stays run-level, so the memoized `shared-showcase` boot keeps its handles until the run ends. A concurrent run carries another tag and is never touched. - The teardown still judges nothing. - **`packages/qa/dogfood/vitest.config.ts`**: one comment line ("removes the run's per-file directories at the end"). Unchanged from objectstack-ai#21914: every file still runs in its own temporary cwd; the cwd is restored in `afterAll`; and the throwing `afterAll` guard on `packages/qa/dogfood/.objectstack/data` is untouched. Out of scope here: the rule, the case and `scripts/pm/dispatch-gates.mjs` are untouched. Nothing is skipped or recorded as an exception. ## Evidence - **Reproduced first**, on `origin/main` `faf8dce4` with no change: `pnpm check:pm-dispatch-gates` gave `✗ dispatch-gates self-test: 1 of 1976 case(s) failed.`. The one red case is the line quoted above, at `per-file-cwd.setup.ts:60`. The battery took 821.9s. - **At head `2edc5d59`**: `pnpm check:pm-dispatch-gates` gave `✓ no mkdtempSync site in this tree takes a base the scan cannot read` and `✓ dispatch-gates self-test: 1976 cases pass.` (780.2s). - **The case still fails on a planted unreadable base.** A second worktree was checked out at `2edc5d59`, and `scripts/ablation-replace.mjs` (wrap mode) replaced the site with `process.chdir(mkdtempSync(join(inject('dogfoodRunTag'), 'file-')));`. The anchor went from 1 to 0, and the blob from `dc1d3de3` to `51346b9f`. - Result: `✗ dispatch-gates self-test: 1 of 1976 case(s) failed.`, and the red case was exactly `... UNRESOLVED: packages/qa/dogfood/test/per-file-cwd.setup.ts:73 (a base this scan cannot read: inject('dogfoodRunTag'))`. - Restore was proven: blob after restore equals HEAD (`dc1d3de3`), `git diff HEAD` is empty, and `git status --porcelain` is empty. The second worktree was then removed. - **objectstack-ai#21914's behaviour, re-proven at `2edc5d59`**: - Whole dogfood suite, `pnpm --filter @objectstack/dogfood test`: `Test Files 208 passed | 1 skipped (209)`, `Tests 1606 passed | 9 skipped (1615)`. Afterwards `packages/qa/dogfood/.objectstack` does not exist. Zero `/tmp/os-dogfood-run-*` entries before the run and zero after it. - H4 ablation through `scripts/ablation-replace.mjs`: the central `process.chdir(...)` was dropped (blob `dc1d3de3` to `ad6684f2`) and two files were run, `showcase-external-autoconnect` and `showcase-search`. Result: `Test Files 2 failed (2)`. Both failed in the guard: `.../packages/qa/dogfood/.objectstack/data exists after this test file ran. Entries: showcase_external.db` (plus `-shm` / `-wal` on the shared-showcase file). - The restore was proven (blob equals HEAD, `git diff HEAD` empty). The restored leg gave `2 passed`, exit 0, and left nothing in the package dir. Zero temp entries were left after each leg, the red one included. - **Gates**: `dispatch-gates --commands` over the 3 changed paths derived 47 families. All 47 ran at `2edc5d59`, and `dispatch-gates --ran` reported `47 derived famil(ies) accounted for — 47 run, 0 NOT-MEASURED`. - `check:dual-build-cjs-loads` first exited 3 (prerequisite: `packages/apps/studio/dist` missing), then exited 0 after building it. - `pnpm lint` exits 0, and `pnpm --filter @objectstack/dogfood typecheck` is green. - `check:pm-dispatch-gates` is path-scoped, so this PR's CI may not schedule it, which is how the red reached `main`. It was run here in full at the head, as above. ## Acceptance notes - Open PR objectstack-ai#21930 also edits `packages/qa/dogfood/vitest.config.ts`, in the `isolated` project's alias block (about line 295). This PR changes one comment line near line 146. The hunks do not overlap. - How this red reached `main`: objectstack-ai#21919's own CI did not schedule `check:pm-dispatch-gates`, because that battery is path-scoped and the battery's live-tree half reads every tracked source. This is noted, not filed: the scoping is the merge-group ruling that `lint.yml` documents, and the hourly full run caught the red as designed. --- _Generated by [Claude Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_ Co-authored-by: Claude <noreply@anthropic.com>
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