Skip to content

check-where-matcher-conformance's discovery heuristic misses single-param inline filter closures — 16 files carry the same shape-(b) blind spot, undetected #8615

Description

@os-zhuang

Filed while working #8582 (session session_01RDTnVvsgA6cUZ4xFVtPZRy), which swept the 78 test-double matches(row, where) fakes that pnpm check:where-matcher discovered and grandfathered in scripts/where-matcher-conformance.baseline.json.

The gap

scripts/check-where-matcher-conformance.mjs's discovery only considers a candidate matcher when it is a function with two or more identifier parameters — see discoverInSource's params.length >= 2 && params[0] && params[1] gate. That is deliberate (it is how the control probe stays name-independent), but it has a structural blind spot: a single-param.filter() callback that closes over where/filter from its enclosing scope, rather than taking it as its own second parameter, is invisible to discovery — even when its body is textually identical to a matcher the gate already knows how to grade.

Measured on main (as of this filing), this exact shape —

.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v))

— or a same-file filter variant, appears 17 times across 16 files, none of them reachable by pnpm check:where-matcher and none in its ledger:

  • packages/plugins/plugin-email/src/attachment-reclaim.test.ts:58
  • packages/plugins/plugin-email/src/template-locale-resolution.test.ts:67
  • packages/plugins/plugin-security/src/bootstrap-declared-positions.test.ts:29
  • packages/plugins/plugin-security/src/bootstrap-declared-permissions.test.ts:18
  • packages/plugins/plugin-security/src/bootstrap-platform-admin.test.ts:30
  • packages/plugins/plugin-auth/src/admin-import-users.test.ts:36
  • packages/core/src/security/api-key.test.ts:20
  • packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts:76
  • packages/core/src/utils/migration-journal.test.ts:84
  • packages/rest/src/import-runner-selfref.test.ts:47
  • packages/rest/src/import-runner-idempotency.test.ts:56
  • packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts:61 and :88 (two instances)
  • packages/services/service-storage/src/file-reference-lifecycle.test.ts:237 (a second, undiscovered instance in a file 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 already fixed a different, discovered matcher in)
  • packages/runtime/src/migration-recovery-plugin.test.ts:48
  • packages/runtime/src/http-dispatcher.keys.test.ts:28
  • packages/runtime/src/http-dispatcher.test.ts:3947

Each treats where/filter as an ordinary field bag via Object.entries(...).every(([k,v]) => r[k] === v) with no $or/$and branch — the identical failure shape (b) #8494/#8582 exist to catch. Whether each is live (a suite actually hands it a $or/$and) is unmeasured — same as #8582's worklist was before it was swept — but the shape itself is not in question; it is byte-for-byte the pattern the gate already grades everywhere it CAN see it.

Why this is worth a decision, not a silent tolerance

check-where-matcher-conformance's own header states discovery is "self-verifying: membership is decided by behaviour, not by a name" — true for the matchers it finds, but the two-param requirement means a whole authoring STYLE (inline .filter() capturing an outer where) sits entirely outside its authority. A gate that cannot see a class of instance cannot grade it green OR red; "not in the ledger" reads as "conforming" to anyone skimming check:where-matcher's output, which is exactly backwards for these 17.

Options (not investigated further — this is the finding, not the fix)

  1. Extend discovery to also match a single-param .filter/.some/.every callback whose body reads Object.entries(<name>) where <name> resolves (via the same visibleDeclarations walk already built for extraction) to an enclosing parameter or destructured option — i.e., loosen the "must be its own 2nd parameter" requirement to "the where-shaped identifier must resolve to something in scope," while keeping the behavioural control probe as the real filter against false positives.
  2. Manually audit and fix the 17 sites the same way 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 did its 78 — cheaper per-instance, but does not close the gate's blind spot for the next one someone writes in this style.

Either way the two are not mutually exclusive; 2 without 1 leaves the hole open for new code.

Refs: #8582 (the sweep that surfaced this), #8494 (the gate this extends), #7620 (the original 16-file $or short-circuit finding).


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions