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
searchAll (which serves GET /api/v1/search) resolved its own searchable set and compiled its own AND-of-OR, instead of going through the engine's ADR-0061 $search expansion that serves POST /api/v1/data/:object/query {search}. Two producers, and the palette's was a strict subset of the executor's.
Re-measured on current main before implementing — the card confirmed this at 76d74ecb and main has moved a long way since. The premise holds, but every line number in the card is stale: searchAll is now at protocol.ts:8520-8700, not ~7177-7411.
Mechanism — one producer, not two callers of one helper
The dispatch assumed routing searchAll through resolveSearchFields / expandSearchToFilter. Half of that is impossible and the other half turned out to be the weaker form:
expandSearchToFilter lives in @objectstack/objectql, which depends on@objectstack/metadata-protocol. Importing it would close a package cycle.
So the routing happens one level up: searchAll hands engine.find the query TEXT (search: q, a declared find option) per object and the engine expands it. That leaves one producer of search clauses rather than two agreeing by maintenance.
No per-object assumption blocked this. The concern that a cross-object sweep could not satisfy a per-object helper does not arise: the sweep was already a loop of per-object engine.find calls, so the per-object expansion fits exactly where the old per-object filter sat. Only the cross-object half stays local (which objects are swept, caps, ranking, title/snippet).
resolveSearchFieldResolution (from @objectstack/spec/data, already imported here for the #4254 ingress gate) is still consulted — but for the skip guard and the snippet source fields, not to build a filter.
What a caller observes changing
Three widenings and one narrowing, all detailed in the changeset:
Pinyin recall arrives on this endpoint.hnkj / huaningkeji now return the CJK row that POST /data/showcase_account/query {search} already returned.
Field resolution follows the object. Declared searchableFields, else the auto-default — not the field-level searchable: true flag, which $search has never read. searchableFields' own spec description already named global search as one of its three consumers; this makes that true.
Enum columns match by option label; virtual formula fields are excluded.
Narrowing, and found by the ablation: the old fallback chain ended in "the first text-typed field", which on an object whose only text column is its PK selects id — so every keystroke ran {id: {$icontains: term}} against system/junction/log tables. That is $search 自动字段集:nameField/name/title 被无条件前置,绕过 SEARCH_AUTO_EXCLUDED_FIELDS——搜索会打到主键 #4483's defect, which the executor had fixed and this path had not. Such objects are now skipped.
Unchanged: object sweep and opt-outs, caps, ordering, RLS/RBAC, response shape. The $search executor path is untouched — pinned, not merely asserted (below).
Row 3 ("case-insensitive substring matching via $contains") was already corrected on main by #7641's landing — the card's own root-cause paragraph naming it as stale is itself now stale. Row 2 ("case-insensitive LIKE") was still present and is corrected here: LIKE names no operator in this vocabulary and, since #6518's LIKE→GLOB change, not even the SQL the compilers emit.
Left alone as instructed: #7850 row 1 (search-conformance.ledger.ts:23, spec seat — #7850 stays open for it) and objectql/src/search-filter.ts:18, which is correct as a declaration under #4706 Q2 = A.
Verification
Union re-run after the final commit, at 663d95e1b.
New suite packages/objectql/src/global-search-palette-recall.test.ts (13 cases) — it lives in objectql because that is the only package where both real implementations meet. The load-bearing assertion is parity, not "the palette finds pinyin": a palette widened by narrowing the executor would satisfy the latter and is a failure. Each probe compares both paths against each other AND against a written expectation, and the per-object filter both paths hand the driver is compared tree-to-tree.
Reverse verification, direction predicted before running: revert protocol.ts, rebuild, expect 5 red / 8 green. Measured 6 red / 7 green — the extra red was the id-scan finding above, which is how that fourth axis was found. The first ablation attempt came back all-green and was invalid: this suite resolves @objectstack/metadata-protocol through its built dist/, so the ablation only bites after a rebuild. Both legs were re-run with scripts/ablation-dist-preflight.mjs proving the mutation reached the artifact (--absent on the way out, present on the way back), and the restore was byte-identical (git status clean against the commit).
Gate union from scripts/pm/dispatch-gates.mjs against the real changed files: all 13 derived families pass, plus check:nul-bytes
check:type-check-debt --re-measure (the ratchet) initially caught a +1 in my own new test — Array.prototype.at is outside this package's tsc lib. Fixed rather than raised; ledger flat at 1968, none above recorded
origin/main merged immediately before opening. Stayed clear of PR #8770's regions (canonicalizeMetaRequestType at :211, its call sites, the saveMetaItem door) — no conflict.
Out of scope, filed not fixed
#8786 — renderTitle in this same function reads only the deprecated displayNameField, never the canonical nameField, and provisionPrimary stamps only the latter. Titling, not recall; left alone deliberately.
content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.
Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files: node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7643
What was wrong
searchAll(which servesGET /api/v1/search) resolved its own searchable set and compiled its own AND-of-OR, instead of going through the engine's ADR-0061$searchexpansion that servesPOST /api/v1/data/:object/query {search}. Two producers, and the palette's was a strict subset of the executor's.Re-measured on current
mainbefore implementing — the card confirmed this at76d74ecbandmainhas moved a long way since. The premise holds, but every line number in the card is stale:searchAllis now atprotocol.ts:8520-8700, not~7177-7411.Mechanism — one producer, not two callers of one helper
The dispatch assumed routing
searchAllthroughresolveSearchFields/expandSearchToFilter. Half of that is impossible and the other half turned out to be the weaker form:expandSearchToFilterlives in@objectstack/objectql, which depends on@objectstack/metadata-protocol. Importing it would close a package cycle.searchAllhandsengine.findthe query TEXT (search: q, a declaredfindoption) per object and the engine expands it. That leaves one producer of search clauses rather than two agreeing by maintenance.No per-object assumption blocked this. The concern that a cross-object sweep could not satisfy a per-object helper does not arise: the sweep was already a loop of per-object
engine.findcalls, so the per-object expansion fits exactly where the old per-object filter sat. Only the cross-object half stays local (which objects are swept, caps, ranking, title/snippet).resolveSearchFieldResolution(from@objectstack/spec/data, already imported here for the #4254 ingress gate) is still consulted — but for the skip guard and the snippet source fields, not to build a filter.What a caller observes changing
Three widenings and one narrowing, all detailed in the changeset:
hnkj/huaningkejinow return the CJK row thatPOST /data/showcase_account/query {search}already returned.searchableFields, else the auto-default — not the field-levelsearchable: trueflag, which$searchhas never read.searchableFields' own spec description already named global search as one of its three consumers; this makes that true.formulafields are excluded.id— so every keystroke ran{id: {$icontains: term}}against system/junction/log tables. That is $search 自动字段集:nameField/name/title 被无条件前置,绕过 SEARCH_AUTO_EXCLUDED_FIELDS——搜索会打到主键 #4483's defect, which the executor had fixed and this path had not. Such objects are now skipped.Unchanged: object sweep and opt-outs, caps, ordering, RLS/RBAC, response shape. The
$searchexecutor path is untouched — pinned, not merely asserted (below).Folding in #7850 rows 2-3
Row 3 (
"case-insensitive substring matching via $contains") was already corrected onmainby #7641's landing — the card's own root-cause paragraph naming it as stale is itself now stale. Row 2 ("case-insensitive LIKE") was still present and is corrected here:LIKEnames no operator in this vocabulary and, since #6518's LIKE→GLOB change, not even the SQL the compilers emit.Left alone as instructed: #7850 row 1 (
search-conformance.ledger.ts:23, spec seat — #7850 stays open for it) andobjectql/src/search-filter.ts:18, which is correct as a declaration under #4706 Q2 = A.Verification
Union re-run after the final commit, at
663d95e1b.New suite
packages/objectql/src/global-search-palette-recall.test.ts(13 cases) — it lives inobjectqlbecause that is the only package where both real implementations meet. The load-bearing assertion is parity, not "the palette finds pinyin": a palette widened by narrowing the executor would satisfy the latter and is a failure. Each probe compares both paths against each other AND against a written expectation, and the per-object filter both paths hand the driver is compared tree-to-tree.Reverse verification, direction predicted before running: revert
protocol.ts, rebuild, expect 5 red / 8 green. Measured 6 red / 7 green — the extra red was theid-scan finding above, which is how that fourth axis was found. The first ablation attempt came back all-green and was invalid: this suite resolves@objectstack/metadata-protocolthrough its builtdist/, so the ablation only bites after a rebuild. Both legs were re-run withscripts/ablation-dist-preflight.mjsproving the mutation reached the artifact (--absenton the way out, present on the way back), and the restore was byte-identical (git statusclean against the commit).@objectstack/metadata-protocol— 90 files / 1352 tests pass@objectstack/objectql— 207 files / 3642 tests pass@objectstack/rest— 116 files / 1921 tests pass ·@objectstack/runtime— 157 files / 2391 tests pass (downstream consumers, prefix filter)scripts/pm/dispatch-gates.mjsagainst the real changed files: all 13 derived families pass, pluscheck:nul-bytescheck:type-check-debt --re-measure(the ratchet) initially caught a +1 in my own new test —Array.prototype.atis outside this package's tsclib. Fixed rather than raised; ledger flat at 1968, none above recordedorigin/mainmerged immediately before opening. Stayed clear of PR #8770's regions (canonicalizeMetaRequestTypeat:211, its call sites, thesaveMetaItemdoor) — no conflict.Out of scope, filed not fixed
#8786 —
renderTitlein this same function reads only the deprecateddisplayNameField, never the canonicalnameField, andprovisionPrimarystamps only the latter. Titling, not recall; left alone deliberately.Generated by Claude Code