Skip to content

test(where-matcher): discover single-param filter callbacks that capture their where - #8661

Merged
qq9340100 merged 2 commits into
mainfrom
claude/issue-8615-single-param-filter-discovery
Aug 14, 2026
Merged

test(where-matcher): discover single-param filter callbacks that capture their where#8661
qq9340100 merged 2 commits into
mainfrom
claude/issue-8615-single-param-filter-discovery

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes#8615

check-where-matcher-conformance could only see a matcher that took its filter as its
own second parameter. A single-parameter .filter() callback that closes over where
from an enclosing scope was invisible to discovery — the arity was never the defect, it was
only what discovery happened to key on. A gate that cannot see a class of instance cannot
grade it green or red, and "not in the ledger" read as "conforming" for every one of them.

The change — discovery arm 2

discoverInSource still looks for the filter as the function's own second parameter first.
Failing that, it now looks for an access path the body reads as a filter whose root
resolves in an enclosing scope
— a same-file declaration (the visibleDeclarations walk
already built for extraction) or a parameter bound by an enclosing function. For extraction
the captured path is re-bound as a synthetic second parameter, so the battery drives
(row, where) uniformly whatever arity the source was written at.

Why a path, not a bare identifier

The card proposed matching a captured identifier. Measured, that is not wide enough — and
the failure is not "misses some", it is mis-binding. Roughly half the corpus spells the
capture indirectly (opts.where, query?.where, q.where, options.where,
query.filter), and keying on the identifier alone reads the root (opts) as the
filter object. That produced 21 matchers judged UNJUDGED with a TypeError out of the
control probe — this gate treats "could not run" as an error, so the narrower rule would
have manufactured 21 failures while claiming to close a blind spot.

Paths reduce only through plain dotted access, with optional chaining and a ?? {} / || {}
tail unwrapped. A call or a computed key yields no path and no candidate, because the
battery could not synthesise a binding for it. Requiring the root to resolve is a real scope
walk, not a name test: a root binding nowhere same-file is an import or a global, and this
gate does not grade code it cannot see.

The width is filtered behaviourally, as designed

Arm 2 deliberately proposes more structural candidates than it seats. Measured on the corpus:

count
structural candidates proposed by arm 264
seated by the control probe62
dropped by the control probe2
pre-existing candidates lost0
pre-existing verdicts drifted0

Measured by building two harnesses from the real gate source differing in discoverInSource
only, the "before" harness verified byte-identical to HEAD by git hash-object.

Discovery: 169 to 231 matchers

Of the 62 newly seated:

  • 58 were combinator-blind (shape (b) — $or read as an ordinary field name) and are
    fixed here
  • 4 already conjoined correctly and needed nothing — arm 2 does not assume that
    "single-param callback" implies "defective"

58 fixed, 0 grandfathered — and the zero is structural

Every one of the 58 now refuses an unrecognised $-key, the recorded practice from
packages/objectql/src/engine-autonumber-*.test.ts and the shape #8618's sweep used for its
75 files.

Nothing is grandfathered, and nothing could have been.#8618 ratcheted
where-matcher-conformance.baseline.json to empty, and the gate's MONOTONIC invariant errors
on any key absent at the merge base. Verified rather than assumed — a throwaway entry was
added and the gate rejected it in both directions at once:

 • ...: baselined file is clean or gone -- ratchet DOWN: delete its entry
• ...: ADDED to the baseline (not present at 189a732). The grandfather list is
not a mute button -- it only ever shrinks.

So the fix-versus-ledger split for this card was forced to 100% fix. The ledger stays {},
and this PR carries no first-measurement entries because there are none to carry.

Liveness: the shape count is not a defect count

The card was explicit that liveness was unmeasured. It is measured now, by the mechanism
#8618's own last commit demonstrated — a refusal turns a live combinator red immediately.
All 14 affected packages run green: 695 test files, 11,257 tests, 0 failures. So all 58
are dormant — reachable, genuinely engine-shaped, judged by the gate, and not currently
handed a $or/$and by any suite. Dormant is not harmless: the day a test adds one, the
suite would have asserted on an empty result set with nothing erroring. That is exactly what
the refusal now prevents.

The residual the control probe drops, kept as a known limit

The 2 dropped candidates are inverted survivor filters inside a delete double —
(r) => !Object.entries(opts.where).every(...). They carry the same shape (b) defect (a
$or matches nothing, so the row is not deleted) but answer the control probe
backwards, so they are correctly not row-selecting predicates by this gate's definition.
Teaching the probe to recognise a negated predicate means guessing at intent — the
naming-based reasoning this gate exists to avoid. Both known instances are fixed here anyway;
the gate still cannot grade them. Pinned as a self-test fixture so the limit stays known
rather than drifting into an accidental one, and filed separately.

Self-test fixtures, mutation-verified

Six fixtures added. Every mutant was verified to actually mutate (git hash-object differs
from base, restored to base after) and to fail by assertion, not by crashing:

mutationpredictedobserved
neuter arm 2 entirely10 failexactly those 10
drop the root-resolution requirement1 fail (the unresolvable-root fixture)exactly that 1
reduce accessPath to identifier-only5 fail (path fixture 3 + negated fixture 2)exactly those 5
neuter the synthetic re-binding4 fail (all captured fixtures go UNJUDGED)exactly those 4

Predictions were written before each run.

Verification

  • check:where-matcher OK — 231 discovered, 231 conforming (140 refuse), 0 silently wrong,
    0 unjudged, 0 grandfathered files; --self-test OK
  • 14 affected packages: test green (11,257 tests), typecheck green
  • check:type-check-coverage --re-measureEXIT=0, nothing above its ceiling. The one
    surplus reported is @objectstack/lint's pre-existing -1, deliberately left alone
  • gate families re-derived from the actual changed paths with scripts/pm/dispatch-gates.mjs
    and all run green: check:nul-bytes, check:cross-package-test-inputs,
    check:durability-log-level, check:error-code-casing, check:kernel-hook-pairs,
    check:test-source-alias, check:type-source-resolution, check:query-options-erasure,
    check:i18n

Tests and one root scripts/ gate only — no package source, nothing publishable changes, so
this carries skip-changeset rather than a changeset.

⚠️check-changeset-no-major --self-test is expected red on this branch for a repo-wide
post-release reason tracked in #8654 (.changeset/pre.json absent, no major-declaring
changeset). Not this PR's defect and deliberately not worked around here.


Generated by Claude Code

@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 11:54am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

@qq9340100Claude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT on substance, held unmerged on #8654

Reviewed at 3a6b5ec8 by the domain:devx execution PM seat (#6023). ⛔ Deliberately not flipped out of draft and not armed for auto-merge.

Per-job conclusions — all 29, not an aggregate reading

One failure: ESLint. Read the job log rather than the job name: the failing step is check-changeset-no-major --self-test, the same two control assertions recorded on #8654. Execution reached the gate steps, which means ESLint itself passed. ⚠️ Worth naming, because it nearly misled this seat on a sibling PR: the changeset self-tests are wired into the workflow that reports as ESLint. A job name is not a diagnosis.

Everything else green, including the six that were still running when the dev reported and which it honestly declined to claim: TypeScript Type Check (09:57Z — 14 minutes, an order of magnitude behind its batch, which is exactly why "everything else is green" is not a verdict), Test Core 1-3 (the last at 09:58Z), Dogfood Regression Gate 1-3, plus Build Core, Temporal Conformance, Dogfood Verify CLI, Check Changeset (successskip-changeset is correct here), ADR maintainer approval, Check PR Size, Check Documentation Links, Console Pin Freshness, Part-of PR must not also close its card, No other open PR may claim the same issue.

What I verified independently of the PR body

  • File surface, counted in full, not sampled: 51 .test.ts files + scripts/check-where-matcher-conformance.mjs. Zero package source, zero .changeset/ touches, zero baseline touches. So skip-changeset is right, and the docs-drift bot's "0 changed packages" agrees.
  • "0 grandfathered" is structural, not a promise. The baseline file is absent from the diff entirely; test(where-matcher): empty the combinator-blind ledger — 75 files, refuse unrecognised $-keys #8618 had ratcheted it to {} and the MONOTONIC invariant rejects any key absent at the merge base. The dev did not assert this — it ablated it, adding a throwaway entry and showing the gate refuse in both directions at once.

Where my dispatch was wrong, recorded so it does not propagate

  • I instructed that grandfathered entries be labelled as first measurements. There was no route to grandfather anything. The dev reported the template's presumption not fitting instead of manufacturing an entry to satisfy it — the right call, and the instruction was mine to get wrong.
  • I passed through the card's proposal to match a captured identifier. Measured, that mis-binds: roughly half the corpus spells the capture indirectly (opts.where, query?.where), so the root is read as the filter — 21 matchers UNJUDGED with a TypeError, which this gate scores as failure. The narrower rule would have manufactured 21 failures while claiming to close a blind spot. Access paths with a real scope walk took UNJUDGED to 0.
  • I named one repo-wide red. There are two in the same family; this PR only meets the first because it carries no changeset.

On the numbers

The card's "17" was a count of a shape, and the dev did not report shapes as defects: 64 structural candidates proposed, 62 seated by the behavioural probe, of which 58 combinator-blind (fixed) and 4 already correct. Discovery 169 → 231, ledger stays {}. Liveness measured across 14 packages (11,257 tests, 0 failures) ⇒ all 58 dormant — reachable and gate-judged, not currently handed a $or. Dormant is not harmless, and the PR says why. The 2 candidates the probe drops are inverted survivor filters; both fixed on their merits, the limit pinned as a fixture and filed as #8662 rather than left to drift into an accidental one.

Mutation matrix: four mutants, predictions written before each run, every one verified to actually mutate (git hash-object) and to fail by assertion rather than by crashing, each observed count matching exactly.

Why it is not merged

⛔ Not armed into a required check that cannot currently pass — auto-merge on a blocked PR waits silently and surfaces no reason. #8654 is a property of main itself (proven on a pristine origin/main worktree), so no PR caused it and no PR can clear it. ⛔ Editing that gate is a maintainer floor.


Generated by Claude Code

@qq9340100
qq9340100 added this pull request to the merge queueAug 14, 2026
Merged via the queue into main with commit 92d4320Aug 14, 2026
27 checks passed
@qq9340100
qq9340100 deleted the claude/issue-8615-single-param-filter-discovery branch August 14, 2026 12:26
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 gatetests

Projects

None yet

2 participants

@qq9340100@claude