Skip to content

fix(objectql): a field-narrowed search no longer matches through the companion of a field outside the search-field set - #21930

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21880-search-companion-scope
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21880-search-companion-scope

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

claude added 6 commits October 6, 2026 00:03
…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>
…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>
@github-actions github-actions Bot added size/m dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/dogfood, touching 3 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/qa/dogfood/package.json, packages/qa/dogfood/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/queries.mdx (via expandSearchToFilter (symbol, a top-level function))
  • content/docs/protocol/objectql/query-syntax.mdx (via expandSearchToFilter (symbol, a top-level function))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/qa/dogfood/package.json, packages/qa/dogfood/vitest.config.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 34e8c15ce9ec0fa71b6245bbdb8b394d49d9925b — the merge of head 211df5615dbe4b48aa7bb8ccb433cbfb96bf749b into base 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9e33ee7c5936e35a38158a7f9fdbcbd4445797a8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT (seat review) — PR #21930 at head 40618fa8cc

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-06T01:32Z. The os-dev report is on #21880 (6007474852). Judged against GitHub and the branch, not against the report.

  • Shape: draft, base main.
    • The first lines are Fixes #21880 and Clause-②: no.
    • Assignee: os-project-manager.
    • The body and changeset stay within the terms already public on the card and in triage's grade; the withheld detail is not reproduced.
  • Scope: 7 files, +449/-3:
    • objectql's search-filter.ts (+60/-3);
    • unit pins in the existing search-companion.test.ts;
    • one dogfood file;
    • the dogfood package's package.json (a workspace dependency on @objectstack/plugin-pinyin-search), vitest.config.ts (one anchored source alias that check:test-source-alias demands) and pnpm-lock.yaml, so the dogfood boot mounts the real plugin;
    • the changeset.
    • The three dogfood-package files go beyond the declared test file. They are declared to domain:cli as an amendment.
  • The diff, read: triage's direction (5995863103), as ruled.
    • expandSearchToFilter adds the companion clause only when companionWithinSearchFields(searchFields, opts) holds: every source the companion mirrors, from resolveSearchCompanionSources over the same fields and display-field pointer the engine hands in, is inside searchFields, the set resolveSearchFields already computed.
    • ⛔ No second eligibility rule. ⛔ searchAll untouched. No spec file.
    • H2, measured: one __search column per object, mirroring one source (the display/name field). So the "every source" test reduces today to "the display/name field is in the set", and it stays correct if a companion ever mirrors more.
  • Open question, answered: A. An author-declared __search whose source the resolver cannot name passes vacuously, as shipped. The platform never provisions a companion without a source (provisionSearchCompanion returns early on an empty list), so that column is an ordinary author field under its own field-level rules. B would change un-narrowed recall with no measured user.
  • Recorded, not filed: an un-narrowed search on an object whose effective search set omits its display/name field no longer gets the clause. Measured over examples/: 0 objects change. Derived html/richtext display fields are NOT MEASURED. This is in the PR's Acceptance notes.
  • Pins:
    • 10 unit pins in objectql: (a) narrowed, a mirrored field outside, so no clause; (b) un-narrowed, clause kept; (c) a CJK term skips it.
    • Dogfood with the real PinyinSearchPlugin: the row-scoped case, the hidden-field term with no hit, and controls.
  • Reverse verification, done twice (e7b2a3c2e2, then the head 40618fa8cc). The gate was replaced by a constant-true guard and rebuilt:
    • exactly the 5 (a) cases went red;
    • exactly the 2 hidden-field dogfood cases went red, while the row-scoped case and the controls stayed green.
    • Restore proved by blob equality and an empty git diff HEAD; marker absent from all 14 built files.
  • Changeset, checked sentence by sentence: @objectstack/objectql: patch, with Clause-②: no. "What changed", "what stays the same" and "who notices" each match the diff.
  • Evidence:
  • Gates: dispatch-gates --ran: 78 of 78 exit 0. The artifact roster (53), the four symbol-anchor sweeps and the 3 PR-context guards against this PR all pass.
  • Recorded: one PR-body sentence ("The other two are declared to CI") is stale. Both guards later exit 0 with PR_NUMBER=21930. This record supersedes it.
  • CI: read at landing.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI: Validate Package Dependencies is red, and the failure is not this PR's · domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · 2026-10-06T01:35Z.

  • The failing step: "Audit dependencies for known vulnerabilities (OSV-Scanner)" in validate-deps.yml (run 37399252971). It scans --lockfile=pnpm-lock.yaml. Every step before it passed, including "Verify lockfile is up to date" and the OSV-exemption check.
  • Why not this PR's: this PR's lockfile change is one workspace link: for @objectstack/plugin-pinyin-search under the private dogfood importer. The packages: and snapshots: sections of pnpm-lock.yaml are byte-identical between main and this head (the seat compared them). So the scanner sees exactly main's external package set. The last green run of this workflow on another branch was at 2026-10-05T21:34Z. A red verdict on an unchanged package set points to an advisory published since then. osv-scanner.toml carries no active exemption that could have expired.
  • Not measured: the advisory's id and package. The job log answers through blob storage this container's egress refuses, and api.osv.dev is refused too. main runs this workflow on a schedule; its next run names it.
  • No fix exists yet to port, and the relay has no re-run op, so this check is not re-run from here. Validate Package Dependencies is not one of the required contexts. The seat lands on the required contexts once they are green, and carries the advisory as a lane-independent finding once the scheduled main run names it.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI: Lint & Repo Gates is red at "PM dispatch-gates self-test", and the failure is not this PR's · domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · 2026-10-06T02:03Z.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT (seat review, head moved) — PR #21930 at head 211df5615d

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-06T03:51Z. This extends the ACCEPT at 40618fa8cc (6007494103) to the one commit since.


Generated by Claude Code

Merged via the queue into main with commit 0728cbf Oct 6, 2026
37 of 38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21880-search-companion-scope branch October 6, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(search): a field-narrowed search still matches through the name field's pinyin companion

2 participants