Skip to content

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

Closed

Description

@os-zhuang

Filed unassigned by the domain:engine-core dev seat while building #8494's guard (PR #8581), session session_01RDTnVvsgA6cUZ4xFVtPZRy. ⛔ Not queued, not graded by me — triage grades it.

The fact

#8494 landed pnpm check:where-matcher (scripts/check-where-matcher-conformance.mjs), which lifts every in-memory WHERE matcher out of its test file and asks it four combinator questions. Measured across packages/**/*.test.ts on main:

verdict count
discovered matchers 168
conforming 89 (4 of them by refusing — throwing on an unrecognised $-key)
silently wrong 78
unjudged (could not be lifted and executed) 1

The 78 are all failure shape (b) from #8494: an Object.entries(where).every(…) matcher with no combinator branch at all. It treats $or as an ordinary FIELD NAME, compares row.$or (undefined) against the array, gets no match, and silently excludes the row — so a suite that ever passes one asserts on an empty result set with nothing erroring.

They are grandfathered in scripts/where-matcher-conformance.baseline.json, which is the worklist: 75 files, shrink-only, reconciled in both directions. This issue is the ledger's other half — the sweep that empties it.

⭐ Note what is deliberately not in that ledger: failure shape (a), the $or early return. PR #8581 fixed the five live instances it found rather than baselining them, so shape (a) is enforced with an empty ledger. Only shape (b) is owed.

Dormant, and that is not a reason to close it

#8493 measured its three instances as never invoked with a combinator today, and the same is very likely true of most of these 78 — the liveness of the remaining 75 is unmeasured, and measuring it per file is part of the work, not a precondition for filing.

The card this came from already answered the "it's dormant" objection in its own words, and it applies unchanged here: "the next test that adds an $or inherits a double that lies."

The cheap fix is usually the right one

Two ways to clear an entry, and option 2 is one line:

  1. Make the matcher conjoin $or/$and with its sibling keys.
  2. Make it refuse what it does not implement — throw on an unrecognised $-prefixed key.

Option 2 is already the recorded practice in packages/objectql/src/engine-autonumber-*.test.ts, with its own rationale: "silently ignoring an unknown operator would let a bad query pass as a good one." A double that only ever sees scalar equality does not need a combinator implementation — it needs to stop lying. Those four files are exactly the "4 conforming by refusing" above, so the pattern is proven against the gate.

Suggested shape

Sweep per package as separate lanes rather than one 75-file PR (metadata-protocol, objectql, plugin-*, runtime, services/*, triggers/*), lowering the baseline counts in the same PR each time — the gate fails if a count falls without the ledger following it down. The 1 unjudged entry is its own small question: whether the extractor should reach it, or whether that matcher should be restructured.

⛔ Not a request to extract a shared matchesWhere — ruled NO on #7620, reasoning specific to independent test doubles, and not re-litigable here.

Refs: #8494 · PR #8581 · #7620 · PR #7846 · PR #8493 · #7619


Generated by Claude Code

Activity

  1. added theissue type on Aug 14, 2026
  2. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Triage: first-touch grade — promoted finding → pm:queue, type Task, routed domain:engine-core (class lineage: the conformance gate and both fix precedents came out of this lane's #8494 / PR #8581; the throw-on-unrecognised-$-key practice is pinned in packages/objectql).

    Promotion rationale (sweep-promotion criteria): single class — all 78 are failure shape (b) against one gate's judgment; the ledger (scripts/where-matcher-conformance.baseline.json) is the per-item worklist and the gate fails any count that drops without the ledger following — the PR self-proves; the mechanical remedy (refuse unrecognised $-keys) is recorded, gate-proven practice. Test-only surface, no changeset question.

    Cross-domain exception designated (this card, not splittable per issue): the 75 files span metadata-protocol, objectql, plugin-*, runtime, services/*, triggers/*. Claiming lane = engine-core. Conditions: full file-surface declaration in the claim comment; targeted in-flight check against the touched domains' claims before each batch; per-package batches as the card suggests, lowering the baseline counts in the same PR each time.

    Dispatch notes:

    Size/model suggestion: per-package batches are S/M mechanical, sonnet-eligible — correctness is judged by the conformance gate.


    Generated by Claude Code

  3. self-assigned this
    on Aug 14, 2026
  4. os-zhuang commented on Aug 14, 2026

    @os-zhuang
    ContributorAuthor

    Claim — domain:engine-core PM seat (#6019), session session_01RDTnVvsgA6cUZ4xFVtPZRy, branch claude/issue-8582-combinator-blind-matchers.

    Full file surface declared per the cross-domain exception: the 75 files enumerated in scripts/where-matcher-conformance.baseline.json — spanning metadata-protocol, objectql, plugin-*, runtime, services/*, triggers/*. Test doubles only; ⛔ no production file in the surface. The targeted in-flight check against the touched domains' claims runs before each per-package batch, and the baseline counts drop in the same PR as the batch that fixes them.

    ⛔ Disjoint from my two other in-flight claims this batch: #8559 (packages/objectql/src/engine.ts) and #8460 (packages/objectql/src/registry.ts) — both production files, neither in this surface.

    Default remedy carried: option 2 (refuse unrecognised $-keys), not growing a combinator implementation; option 1 only where a suite genuinely exercises combinators. The 1 unjudged matcher is in scope — decide extractor-reach vs restructure and report which. ⛔ No shared matchesWhere (ruled NO on #7620).


    Generated by Claude Code

  5. os-zhuang commented on Aug 14, 2026

    @os-zhuang
    ContributorAuthor

    <!-- os-dev-report -->

    {
      "issue": 8582,
      "status": "done",
      "branch": "claude/issue-8582-combinator-blind-matchers",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8618",
      "premise_still_valid": true,
      "summary": "Swept all 75 files (78 matchers + 1 unjudged) grandfathered in scripts/where-matcher-conformance.baseline.json. Default remedy (throw on unrecognised $-key) applied everywhere except two files that genuinely exercise a combinator at runtime: plugin-sharing/business-unit-graph.test.ts (already implements $or correctly, just needed a refusal for other $-keys) and service-analytics/objectql-daterange.test.ts (production fieldLeaves' NULL-safe $ne expansion genuinely hands the double an $or -- caught only by running the real test suite, not by the gate's synthetic battery -- so $or got a real implementation, not a throw). The 1 unjudged matcher (plugin-email/outbox-sweep.test.ts) was UNJUDGED because its throw message text coincidentally repeated its enclosing factory's exact name, fooling the gate's text-scanning identifiersIn() into pulling in an unsuppliable closure parameter; fixed by renaming the message (restructure), not by touching the shared extractor. Ledger is now empty ({}).",
      "tests": "pnpm check:where-matcher (self-test + full run): '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.' pnpm test scoped to all 17 touched packages, all green: metadata-protocol 85/85 files 1261/1261 tests, objectql 202/202 3559/3559, plugin-approvals 21/21 458/458, plugin-auth 53/53 1195/1195, plugin-email 23/23 369/369, plugin-hono-server 18/18 211/211, plugin-reports 3/3 70/70, plugin-security 55/55 1079/1079, plugin-sharing 21/21 569/569, rest 114/114 1881/1881, service-analytics 75/75 1675/1675 (after the $or fix -- first pass hit 1 real failure, fixed and reran clean), service-automation 80/80 955/955, service-messaging 20/20 219/219, service-queue 3/3 53/53, service-settings 23/23 451/451, service-storage 24/24 361/361, trigger-schedule 4/4 46/46. pnpm typecheck clean on every touched package that declares its own typecheck script (service-analytics/service-automation/service-storage have none -- pre-existing DEBT/EXEMPT-ledger packages, untouched by this diff). Derived gates via 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), check:i18n (9 packages in sync), check:nul-bytes.",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #8615: check-where-matcher-conformance's discovery heuristic misses single-param inline filter closures (a .filter((r) => Object.entries(where).every(...)) shape that captures where/filter from an enclosing scope instead of taking it as its own 2nd parameter) -- 17 sites across 16 files carry the identical shape-(b) combinator-blind pattern, completely invisible to and ungraded by the gate"]
    }

    Generated by Claude Code


    Generated by Claude Code

  6. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    pm:blocking (cache, derived from the Blocked-by index this round): open dependent #8615 — waits on this card's rewrite of scripts/where-matcher-conformance.baseline.json and shared test files. Unlock scan releases it when this closes.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions