Repository navigation
QA run · search (FULL area) · 92f26f75 · 2026-08-11 · 2 PASS / 2 PARTIAL / 2 FAIL #7629
Description
Full search area run of the checklist-test skill — all 6 items driven against a live showcase (opus subagent, isolated boot + file DB, admin + a purpose-provisioned member persona). Text-only per RUNNER.md.
Result: 2 PASS · 2 PARTIAL · 2 FAIL.
Environment — framework 92f26f75 (branch claude/platform-test-checklist-ocwugl) · vendored console 6314e87f · showcase app · isolated port + file DB.
✅ PASS — 2 items
rls-both-personas(4/4 + both negatives) — a real second persona (sign-up + bound contributor position) cannot see a foreign invoice by id (404) and gets zero search hits for it on both wire spellings, in the ⌘K palette and on the/searchpage, while admin gets the row. The member's own creations are findable, so the restriction is subtractive rather than a blackout. Counts do not leak: the member's totals reconcile with their own 3-row visible set (15 for admin), and page-local totals were shown to beskip + records.lengthfor admin too — carrying zero information about hidden rows.
Fixture drift: the item says 8 seeded invoices, the seed now ships 12.freshness-and-empty(6/6 + both negatives) — create / rename / delete are reflected on the first query attempt within 3–90 ms with no wait or retry (timestamped), and a rename flips both the new and the old term in the same second. A no-hit search returns the declared 200 envelope on both spellings, with designed empty states in the console and a clean browser console.
The runner answered the item's question directly: this is an honest empty, not a stale index. Structurally there is no latin index ($searchis a query-time WHERE expansion, so nothing can lag); behaviourally deletes vanish and creates appear with no wait; and the one materialized artifact — the__searchcompanion — is written inside the same save (the 201 body already carries it), with its only lagging path (hook-bypassing writes) demonstrated and demonstrated reconciling at the next boot.
🔴 FAIL — 2 items
1. cross-field-object-search — $search is case-sensitive on textual fields, contrary to three declarations
Cross-field OR scanning works (an industry-only hit and a name-only hit come back from one unrestricted query, positively pinned by narrowing each way), and multi-term AND semantics are correct ("retail contoso" → 0, not an OR union).
Clause 2 fails. $search is case-sensitive on textual fields, although the item title, the search-filter.ts docblock ("Matching: case-insensitive") and the search-conformance ledger row all declare it case-insensitive.
Reproduction rule
POST /api/v1/data/showcase_account/query {"search":"Retail","searchFields":["name"]}
→ 200, 1 record "Acme Retail"
POST /api/v1/data/showcase_account/query {"search":"retail","searchFields":["name"]}
→ 200, records [] total 0
Also stark/Stark, wonka/Wonka, NORTHWIND. Reproduced on two fresh boots.
Root cause. search-filter.ts fieldClausesForTerm emits {field:{$contains:term}} for textual types, and $contains is contractually case-sensitive (#4706 Q2=A) — the #6518 SQLite LIKE→GLOB change removed SQLite's incidental ASCII fold. The case-insensitive operator is $icontains: {name:{$icontains:'retail'}} does return "Acme Retail". Only enum/select terms survive, because optionValuesMatching lowercases both sides in JS before emitting $in — which is exactly why the select-label half still passes.
Why the existing pin stayed green: the dogfood pin (4/4) does not cover lowercase textual matching — its only case assertion is a select label. A single lowercase-name assertion would catch this.
2. field-scoped-narrowing — the __search companion column, declared "invisible to clients", is echoed in every record body
Narrowing splits exactly as declared, and a bad $searchFields is refused loudly rather than silently widened — an unknown field and a real-but-unsearchable field both 400 INVALID_FIELD with located guidance (the #4254 widening leak does not reproduce), on both the POST and GET spellings, with byte-identical messages.
Clause 2 (half B) fails.
Reproduction rule
POST /api/v1/data/showcase_contact/query {"search":"zhangwei","limit":1}
→ the returned record contains "__search":"zhangwei zw"
GET /api/v1/data/showcase_account/<cjk-id>
→ "__search":"huaningkeji hnkj"
GET /api/v1/search?q=INV-1003
→ each hit.record carries a __search key
The 201 body of a create carries it too. Still echoed after restarting with OS_SEARCH_PINYIN_ENABLED=false.
Root cause. search-companion.ts provisions the column hidden + system + readonly + searchable:false, which only keeps it out of auto-views, the $search auto-default and $searchFields overrides — no read path strips it from the default projection (hidden system columns generally come back, as organization_id does). The $searchFields half is correctly enforced (__search as an override → 400 "is hidden").
Impact assessment: disclosure risk is low — the value derives from the display field only, and secret/FLS-restricted sources are excluded. But the declared client-invisibility contract is broken and every payload carries a private index column.
🟡 PARTIAL — 2 items
console-global-search — the palette's recall is strictly narrower than the $search executor's
Passing: the palette opens via Ctrl+K, the header button and a ?palette=1 deep link (idempotent); it issues exactly one GET /api/v1/search (the searchAll path, not a per-object fanout); hits are grouped under a per-object heading (#3371 does not reproduce); Enter navigates to the record (aria-selected asserted before the keypress, so automation-input is excluded); empty and 1-char input fire zero requests and show "Recently viewed"; the RLS deny side is clean on both API and UI for a real member.
Clause 2 is partial. hnkj / huaningkeji return 0 hits from the global endpoint while POST /data/<obj>/query {search:…} returns the CJK row, and the seeded CJK account is unreachable by huaning from this endpoint.
Root cause (located). metadata-protocol searchAll builds its own filter (TEXT_TYPES + f.searchable===true, falling back to the title field) instead of calling resolveSearchFields/expandSearchToFilter, and never ORs the SEARCH_COMPANION_FIELD. Its comment claiming "case-insensitive substring matching via $contains" is also stale (same case bug as cross-field-object-search above).
Net effect for users: pinyin recall works in the list quick-search but not in the ⌘K command palette.
pinyin-flag-both-sides — substance proven, but the clause's stated oracle is unobtainable
Flag on: full-pinyin, initials and native CJK all hit, per-variant on two boots. Additive-and-scoped proven by contrast (a non-name CJK field is not pinyin-searchable; companion values confirm name-only materialization). Flag off: companion recall dies while native CJK still works via source columns, all 200 — and the runner disambiguated an apparent survivor (a term that still hit under OFF turned out to match a seeded email substring, not pinyin) by creating a clean probe row during the OFF window and proving full-pinyin is dead for it. The first-boot backfill line was captured, plus a bonus proof of the stale-row reconcile (a row written while the plugin was unloaded became pinyin-findable after a restart with the flag on).
Clause 3 is partial on its oracle only. The named log line never appears, because the gate is one layer upstream: with the flag off, the CLI does not add pinyin-search to requires at all, so the plugin is never loaded (47 vs 48 plugins; a 1.2 MB debug boot log contains zero occurrences of "pinyin"). The clause's substance is proven more strongly than the log line would prove it — the item should restate its evidence as the plugin-count/requires difference, since the stated string is unobtainable in a CLI-served deployment.
Checklist maintenance falling out of this run
cross-field-object-searchandconsole-global-searchboth hinge on the same$containscase decision — whichever way$regexon driver-sql is not a regex — it compiles to a substring LIKE, so it both over-matches and silently matches nothing #4706 Q2 is resolved, three places currently declare the opposite of the behaviour (item title,search-filter.tsdocblock,search-conformanceledger row).pinyin-flag-both-sidesclause 3'sevidencestring needs replacing (see above).rls-both-personasfixture count is stale (8 → 12 seeded invoices).
Activity
baozhoutao commentedon Aug 11, 2026
Correction — console pin in the Environment line.
This run's Environment line says vendored console
6314e87f. That is the repo-root.objectui-sha(updated 2026-08-11 06:35) — the pinmainwants. The bundle the server actually served during this run ispackages/console/dist/.objectui-sha=09987b680d53801c79f67d969b14e9bb732b8a22(built 2026-08-10 09:58). Root and dist disagree because dist has not been rebuilt against the newer pin.Nothing in this run's verdicts changes. The one console-lane finding here —
console-global-search's narrower recall — was located in framework source (metadata-protocolsearchAllbuilding its own filter instead of callingresolveSearchFields/expandSearchToFilter), not console source, and the palette behaviour was observed live against the running bundle either way.Flagging it because anyone reproducing a console-lane finding should check out
09987b680, not6314e87f. The two.objectui-shafiles disagreeing is itself worth knowing about.Generated by Claude Code
huangyiirene commentedon Aug 11, 2026
Extraction ledger — COMPLETE. Closing.
Every FAIL, PARTIAL and checklist-maintenance item extracted; all three code defects re-verified live on
origin/main, zero duplicates.cross-field-object-searchc2 (FAIL)$searchis case-sensitive on textual fields, contrary to three declarations —fieldClausesForTermemits$contains(case-sensitive per #4706 Q2=A; #6518LIKE→GLOBremoved SQLite's incidental fold);$icontainsmatchesdomain:engine-corefield-scoped-narrowingc2(B) (FAIL)__searchcompanion column, declared client-invisible, is echoed in every record body — no read path strips it from the default projectiondomain:engine-coreconsole-global-searchc2 (PARTIAL)metadata-protocolsearchAllbuilds its own filter, never ORs the companion field, stale$containscommentdomain:metadatapinyin-flag-both-sidesoracle unobtainable;rls-both-personasfixture 8→12domain:devxThe 2 PASS items produced no cards.
#7641 carries a decision, flagged not buried
$searchcase-sensitivity is a real behaviour-vs-contract break, but the direction of the fix is a maintainer call: (A) make$searchemit$icontainsso behaviour matches the three existing declarations (recommended in the card — search is near-universally expected case-insensitive, and three places already declare it so), or (B) change the declarations to say case-sensitive. Whichever way #4706 Q2 finally lands, the three declarations (item title,search-filter.tsdocblock,search-conformanceledger row) must be made consistent — that reconciliation is tracked in #7647.Cross-cutting: #7641 and #7643 share the same
$contains-case root (#6518); #7643 is cross-linked to #7641 in its body. One decision resolves both behaviours plus the three stale declarations.Provenance: maintainer standing rule that every QA problem becomes an issue; run records close at extraction, same exit as #7514/#7463.
Generated by Claude Code
docs(qa): correct four stale search-checklist texts from run objectst…
fix(objectql): strip the hidden `__search` companion from every recor…
docs(qa): correct the checklist and close the fixture gaps the 17.1.0…
fix(examples): the three shipped examples declare @objectstack/servic…