Skip to content

test(where-matcher): empty the combinator-blind ledger — 75 files, refuse unrecognised $-keys - #8618

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-8582-combinator-blind-matchers
Aug 14, 2026
Merged

test(where-matcher): empty the combinator-blind ledger — 75 files, refuse unrecognised $-keys#8618
os-zhuang merged 4 commits into
mainfrom
claude/issue-8582-combinator-blind-matchers

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#8582

What

Sweeps all 75 files (78 matchers + 1 unjudged) grandfathered in scripts/where-matcher-conformance.baseline.json — test-double matches(row, where) fakes with no combinator branch at all, which read $or/$and as an ordinary field name, compared row.$or (undefined) against the array, and silently dropped every row a combinator query touched.

Default remedy (per the issue): make the double refusethrow on an unrecognised $-prefixed key, the practice already pinned in packages/objectql/src/engine-autonumber-*.test.ts. Implemented the combinator instead only where a suite genuinely exercises it (see below).

pnpm check:where-matcher (final state):

✓ where-matcher conformance holds: 168 matcher(s) discovered, 168 answer the combinator battery
correctly or refuse it loudly (82 refuse).
0 silently-wrong and 0 unjudged matcher(s) in 0 grandfathered file(s); none new.

The ledger is now {} — empty, matching failure-shape-(a)'s steady state.

Full file surface (cross-domain exception, per the claim comment)

75 test files across metadata-protocol, objectql, plugin-approvals, plugin-auth, plugin-email, plugin-hono-server, plugin-reports, plugin-security, plugin-sharing, rest, service-analytics, service-automation, service-messaging, service-queue, service-settings, service-storage, trigger-scheduletest doubles only, zero production files. Disjoint from the two other in-flight production-file claims this batch (#8559packages/objectql/src/engine.ts, #8460packages/objectql/src/registry.ts) — neither touched here.

Commits, per-package batches with the ledger lowered in the same commit each time (self-proving against the gate):

  1. metadata-protocol, objectql, plugin-approvals, plugin-auth (39 files)
  2. plugin-email, plugin-hono-server, plugin-reports, plugin-security, plugin-sharing, rest (22 files)
  3. service-analytics, service-automation, service-messaging, service-queue, service-settings, service-storage, trigger-schedule (14 files)
  4. One follow-up fix (below)

Two files needed more than the mechanical guard

  • packages/plugins/plugin-sharing/src/business-unit-graph.test.ts already implements $or correctly (a real, exercised recursive conjunction). Added a refusal for any other$-key ($and included) rather than growing a second combinator implementation nothing in the suite exercises.
  • packages/services/service-analytics/src/__tests__/objectql-daterange.test.ts — the blanket refusal I first landed (batch 3) turned a passing test red: query() drives through fieldLeaves's real NULL-safe $ne expansion (非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298), which emits $and: [{ $or: [{field:null},{field:{$ne}}] }] — this double genuinely receives $or at runtime, it is not dormant here. Per the issue's own guidance ("option 1 only where a suite genuinely exercises combinators"), implemented $or the same way the file already implements $and (a .some() over the branches) instead of refusing it. Caught by running the full test suite before reporting, not by the gate (the gate only asks the four synthetic battery questions against the lifted function in isolation — it has no way to see what a real caller in this file actually hands the matcher).

The 1 unjudged matcher

packages/plugins/plugin-email/src/outbox-sweep.test.ts. Its nested-operator throw message (`fakeEngine: unsupported operator ${op}`) coincidentally repeats its enclosing factory function's exact name (function fakeEngine(seed: RowSeed[])), and the gate's identifiersIn() text-scanner reads that string-literal word as if it were a reference to the factory — pulling the whole closure (and its unsuppliable seed parameter) into the standalone extraction, which then threw ReferenceError: seed is not defined no matter how the retry/drop loop iterated.

Disposition: RESTRUCTURE, not extractor-reach — renamed the message so it no longer shadows the enclosing scope (one line, zero blast radius) rather than changing the shared identifiersIn() text-scanner every one of the 168 discovered matchers depends on. The matcher now judges CONFORMING (refuses).

Out-of-scope finding

Filed #8615: the gate's discovery heuristic requires a matcher to be a function with two or more identifier parameters, so a single-param .filter((r) => Object.entries(where).every(...)) closure that captures where/filter from an enclosing scope is structurally invisible to it — 17 such sites across 16 files carry the identical shape-(b) pattern, completely ungraded by check:where-matcher. Not fixed here (outside #8582's worklist, which is scoped to the ledger); left for triage.

Verification

  • pnpm check:where-matcher — green (0 silently wrong, 0 unjudged), see above.
  • pnpm test scoped to all 17 touched packages (metadata-protocol, objectql, plugin-approvals, plugin-auth, plugin-email, plugin-hono-server, plugin-reports, plugin-security, plugin-sharing, rest, service-analytics, service-automation, service-messaging, service-queue, service-settings, service-storage, trigger-schedule) — all green, real numbers:
    • metadata-protocol 85/85 files, 1261/1261 tests
    • objectql 202/202 files, 3559/3559 tests
    • plugin-approvals 21/21 files, 458/458 tests
    • plugin-auth 53/53 files, 1195/1195 tests
    • plugin-reports 3/3, 70/70 · plugin-hono-server 18/18, 211/211 · plugin-sharing 21/21, 569/569
    • service-queue 3/3, 53/53 · service-messaging 20/20, 219/219 · service-settings 23/23, 451/451
    • service-analytics 75/75, 1675/1675 (after the $or fix) · service-storage 24/24, 361/361
    • plugin-email 23/23, 369/369 · plugin-security 55/55, 1079/1079
    • rest 114/114, 1881/1881 · service-automation 80/80, 955/955 · trigger-schedule 4/4, 46/46
  • pnpm typecheck on every touched package that declares its own typecheck script — clean (service-analytics, service-automation, service-storage carry no typecheck script; pre-existing DEBT/EXEMPT-ledger packages, untouched by this test-only diff).
  • Derived gates (node scripts/pm/dispatch-gates.mjs <changed paths>), all green: check:cross-package-test-inputs, check:durability-log-level, check:test-source-alias, check:type-source-resolution, check:engine-split-ratio, check:query-options-erasure, check:type-check-coverage (436 frozen DEBT / 1533 frozen TEST_DEBT — unchanged from main, no regression), check:i18n (9 packages, all in sync), check:nul-bytes.

Not in scope


Generated by Claude Code

…rotocol, objectql, plugin-approvals, plugin-auth)
Part of the #8582 sweep: 39 test-double matches(row, where) fakes across
these four packages treated $or/$and as ordinary field names and silently
dropped rows on any combinator query. Each now throws on an unrecognised
$-prefixed key instead — the recorded practice already pinned in
packages/objectql/src/engine-autonumber-*.test.ts ("silently ignoring an
unknown operator would let a bad query pass as a good one").
Ledger (scripts/where-matcher-conformance.baseline.json) ratcheted down by
the same 39 entries in this commit — pnpm check:where-matcher confirms
0 problems for these files.
Refs #8582
…il, plugin-hono-server, plugin-reports, plugin-security, plugin-sharing, rest)
Part of the #8582 sweep. 22 more combinator-blind matches(row, where) fakes
now throw on an unrecognised $-prefixed key instead of treating it as a
field name and silently dropping every row a combinator query touches.
Two files needed more than the mechanical guard:
- packages/plugins/plugin-sharing/src/business-unit-graph.test.ts already
implements $or correctly (a real, exercised recursive conjunction) — added
a refusal for any OTHER $-key ($and included) rather than growing a second
combinator implementation nothing in the suite exercises.
- packages/plugins/plugin-email/src/outbox-sweep.test.ts was the ledger's
one UNJUDGED matcher. Its nested-operator throw message (`fakeEngine:
unsupported operator ${op}`) coincidentally repeats its enclosing factory
function's exact name, and the gate's identifiersIn() text-scanner reads
that string-literal word as if it were a reference to the factory —
pulling in the whole `fakeEngine(seed)` closure and its unsuppliable
`seed` parameter, so the standalone extraction always threw
`ReferenceError: seed is not defined`. Disposition: RESTRUCTURE (rename
the message so it no longer shadows the enclosing scope) rather than
extractor-reach — a one-line, zero-blast-radius fix versus changing the
shared identifiersIn() text-scanner every one of the 168 discovered
matchers depends on. The matcher now judges CONFORMING.
Ledger ratcheted down by these 22 entries in this commit; 14 remain
(services/*, triggers) for the next batch.
Refs #8582
…alytics, service-automation, service-messaging, service-queue, service-settings, service-storage, trigger-schedule)
Part of the #8582 sweep. Final 14 combinator-blind matches(row, where)
fakes now throw on an unrecognised $-prefixed key.
Two files already implement $and genuinely (a real, exercised recursive
conjunction) — service-analytics/src/__tests__/objectql-daterange.test.ts
and objectql-read-scope.test.ts. Both get a refusal for any OTHER $-key
($or included) rather than growing a second combinator implementation
nothing in their suites exercises.
Ledger (scripts/where-matcher-conformance.baseline.json) ratcheted to
empty — its intended steady state (pnpm check:where-matcher reference:
"78 (4 by refusing)" is now zero grandfathered, matching shape (a)'s empty
ledger). pnpm check:where-matcher: 168 matcher(s) discovered, 168
conforming (83 by refusing), 0 silently wrong, 0 unjudged.
Refs #8582
… matcher, not refuse it
Batch 3 (e089b9f) added a blanket $-key refusal to this file's matches()
double, on the (wrong) assumption that $or was dormant here like the rest
of the sweep. It is not: the "keeps both operands when a bare equality
meets an operator object" test drives query() through fieldLeaves' real
NULL-safe $ne expansion (#5298), which emits `$and: [{ $or: [{field:null},
{field:{$ne}}] }]` — the double genuinely receives $or at runtime, and the
new refusal turned a passing test red (`Error: test bridge: unhandled
operator $or`).
Per the issue's own guidance, option 1 (implement) applies where a suite
genuinely exercises the combinator — $and was already handled this way in
this file, $or now gets the same treatment (`.some()` over the branches),
matching the file's existing recursive style. pnpm check:where-matcher
confirms this matcher now CONFORMS by correctly answering the combinator
battery, not by refusing.
Refs #8582
@vercel

vercelBot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 14, 2026 2:33am

Request Review

@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 14, 2026 — with Claude
@github-actionsgithub-actionsBot added size/m and removed skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

@os-zhuangos-zhuang added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed tests labels Aug 14, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 14, 2026 03:05
@os-zhuang
os-zhuang added this pull request to the merge queueAug 14, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review (domain:engine-core seat, #6019): ACCEPT. Ready + auto-merge (squash). All 35 checks green or skipped, including check:where-matcher itself and all three Test Core shards.

Two things here are better than the card asked for, and both came from running the real suites rather than trusting the gate's synthetic battery.

  1. service-analytics/objectql-daterange.test.ts genuinely receives an $or — production fieldLeaves' NULL-safe $ne expansion hands it one. The gate's four-probe battery could not have told you that; only the real suite did. So it correctly got a real $or implementation instead of the default throw. ⚠️ That is the standing caveat on this whole remedy: "refuse" is right only where the combinator never arrives, and the suite, not the gate, is what decides that.
  2. The 1 unjudged matcher was a detector bug, not a double bug. Its throw-message text coincidentally repeated its enclosing factory's exact name, so the gate's text-scanning identifiersIn() pulled in an unsuppliable closure parameter. Fixed by restructuring the message — ⛔ correctly not by touching the shared extractor.

⚠️The empty ledger ({}) must not be read as "the repo is swept clean." The same sweep filed #8615: the discovery heuristic misses single-param inline filter closures (.filter((r) => Object.entries(where).every(...)) capturing where from an enclosing scope), and 17 sites across 16 files carry the identical shape-(b) pattern, invisible to this gate. So 0 silently-wrong in 0 grandfathered files is a true statement about what the gate can see, and the gate's field of view is now the open question. That is exactly the "the instances are fixed, nothing holds them fixed" shape one level up again — ⛔ triage's to grade, not this seat's, but it belongs in the record next to the empty ledger.

Cross-domain conditions met: full file surface declared, 17 packages re-run green, ledger lowered in the same commit so the PR self-proves.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: 78 test-double WHERE matchers have no combinator branch at all — they read $or as a field name and silently drop the row

2 participants

@os-zhuang@claude