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
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.
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 /search page, 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 be skip + records.length for 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 ($search is a query-time WHERE expansion, so nothing can lag); behaviourally deletes vanish and creates appear with no wait; and the one materialized artifact — the __search companion — 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.tsfieldClausesForTerm 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 oneGET /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-protocolsearchAll 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.
Full
searcharea run of thechecklist-testskill — 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(branchclaude/platform-test-checklist-ocwugl) · vendored console6314e87f· 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—$searchis case-sensitive on textual fields, contrary to three declarationsCross-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.
$searchis case-sensitive on textual fields, although the item title, thesearch-filter.tsdocblock ("Matching: case-insensitive") and thesearch-conformanceledger row all declare it case-insensitive.Reproduction rule
Also
stark/Stark,wonka/Wonka,NORTHWIND. Reproduced on two fresh boots.Root cause.
search-filter.tsfieldClausesForTermemits{field:{$contains:term}}for textual types, and$containsis contractually case-sensitive (#4706 Q2=A) — the #6518 SQLiteLIKE→GLOBchange removed SQLite's incidental ASCII fold. The case-insensitive operator is$icontains:{name:{$icontains:'retail'}}does return "Acme Retail". Only enum/select terms survive, becauseoptionValuesMatchinglowercases 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__searchcompanion column, declared "invisible to clients", is echoed in every record bodyNarrowing splits exactly as declared, and a bad
$searchFieldsis refused loudly rather than silently widened — an unknown field and a real-but-unsearchable field both 400INVALID_FIELDwith 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
The 201 body of a create carries it too. Still echoed after restarting with
OS_SEARCH_PINYIN_ENABLED=false.Root cause.
search-companion.tsprovisions the columnhidden+system+readonly+searchable:false, which only keeps it out of auto-views, the$searchauto-default and$searchFieldsoverrides — no read path strips it from the default projection (hidden system columns generally come back, asorganization_iddoes). The$searchFieldshalf is correctly enforced (__searchas 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$searchexecutor'sPassing: the palette opens via Ctrl+K, the header button and a
?palette=1deep link (idempotent); it issues exactly oneGET /api/v1/search(thesearchAllpath, not a per-object fanout); hits are grouped under a per-object heading (#3371 does not reproduce); Enter navigates to the record (aria-selectedasserted before the keypress, soautomation-inputis 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/huaningkejireturn 0 hits from the global endpoint whilePOST /data/<obj>/query {search:…}returns the CJK row, and the seeded CJK account is unreachable byhuaningfrom this endpoint.Root cause (located).
metadata-protocolsearchAllbuilds its own filter (TEXT_TYPES+f.searchable===true, falling back to the title field) instead of callingresolveSearchFields/expandSearchToFilter, and never ORs theSEARCH_COMPANION_FIELD. Its comment claiming "case-insensitive substring matching via$contains" is also stale (same case bug ascross-field-object-searchabove).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 unobtainableFlag 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
partialon its oracle only. The named log line never appears, because the gate is one layer upstream: with the flag off, the CLI does not addpinyin-searchtorequiresat 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/requiresdifference, 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).