Skip to content

test: hold the driver-double WHERE matchers to a combinator conformance battery - #8581

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-8494-where-matcher-guard
Aug 14, 2026
Merged

test: hold the driver-double WHERE matchers to a combinator conformance battery#8581
os-zhuang merged 1 commit into
mainfrom
claude/issue-8494-where-matcher-guard

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Closes #8494.

What this is

#7620 corrected sixteen in-memory matches(row, where) doubles that short-circuited on $or, discarding every sibling equality key — so a real driver's conjunction became a different query while the suites stayed green. Three lanes landed the fixes (#7846, #8493, #7619). #8494 is the observation one level up: the doubles were made right, and nothing held them right.

pnpm check:where-matcher lifts every discovered matcher out of its test file and asks it four combinator questions. The criterion is one sentence:

Answer correctly, or refuse by throwing — never answer silently wrong.

Refusal counts as conforming on purpose: the defect class is silence, not incompleteness. A double that throws on an operator it does not implement makes the suite red the moment a combinator arrives. That is not this gate's invention — packages/objectql/src/engine-autonumber-*.test.ts already does it, with its own recorded reason, "silently ignoring an unknown operator would let a bad query pass as a good one." So the cheap correct answer for a double that only ever sees scalar equality is one throw, not a combinator implementation.

Why behavioural and not a lint rule

#8494 binds the guard to cover both failure shapes, and they share no source shape:

  • (a) early return — a combinator branch that returns, dropping the sibling keys the loop had not reached.
  • (b) no combinator branch at all — an Object.entries(where).every(…) matcher that reads $or as an ordinary field name, compares row.$or (undefined) against the array, and silently excludes the row.

Shape (b) is an absence, which no pattern-match over source can see, and the correct forms in this repo already vary too much to pin (a prelude of if (…) return false before the entries loop, and a combinator arm inside it, are both correct and share no shape).

Discovery is structural, then behaviourally admitted: every candidate must pass a control probe (matches on equality, and really filters) or it is dropped as out of scope. So membership is decided by behaviour, never by parameter names — the ambiguity check-engine-double-contract documents. It also stops a matcher passing vacuously by answering true to everything.

The four probes are load-bearing as pairs. orConjoined alone proves nothing — a shape-(b) matcher also answers false there, for the opposite reason, and would ride through as "conjoins correctly". Only orRecognised === true makes that false mean conjoined rather than excluded by accident.

⛔ No shared matchesWhere — ruled NO on #7620, not re-litigated. This gate never gives a matcher an implementation; it only asks each independent double a question.

Reverse verification — watched, not asserted

Reinstating the early return in plugin-sharing/src/authored-row-write-deferral.test.ts (one of the sixteen, corrected by #8493):

before with the defect reinstated
pnpm --filter @objectstack/plugin-sharing test 21 files / 569 tests pass 21 files / 569 tests pass — still green
pnpm check:where-matcher green RED, exit 1
• packages/plugins/plugin-sharing/src/authored-row-write-deferral.test.ts: NEW silently-wrong WHERE matcher (1 silent, 0 unjudged).
      line 182 `matches`: early-return (sibling keys discarded)

That table is the whole card: the suite cannot see it, and now something can. Shape (b) was demonstrated the same way — deleting the combinator branch outright reddens the gate with a distinct attribution, no combinator branch (combinator read as a field name), failing recognition rather than conjunction.

Restored both times by checking the file back out of HEAD (index and tree together); git hash-object returned 26d2c2fb34e97066ad0885f5dee03282dab3647a before and after each round, git status --porcelain came back clean, and the gate returned to green.

Five live defects this found

Building the guard surfaced five instances of shape (a) still live on main that all three correction lanes missed — the lanes grepped a named file list, which is exactly the limitation this card exists to remove:

  • packages/objectql/src/engine-author-state-query.test.ts
  • packages/objectql/src/engine-findone-contract.test.ts
  • packages/objectql/src/engine-unknown-option.test.ts
  • packages/objectql/src/search-companion-read-projection-conformance.test.ts
  • packages/plugins/plugin-security/src/store-fault-fail-closed.test.ts (both shapes at once)

Fixed here rather than baselined. A ledger that grandfathered the exact defect its gate exists to stop would be worth nothing — so shape (a) is enforced with an empty ledger.

The ledger

168 matchers discovered: 89 conforming (4 of them by refusing), 78 silently wrong, 1 unjudged, across 75 grandfathered files. Every grandfathered entry is shape (b). Shrink-only, hand-edited, reconciled in both directions, with the key set checked against the merge base so a newly-added file cannot ride in matching its own count (the SLOT_LOOKUP_UNSWEPT precedent). Deliberately no --update flag — a generator would admit a new silently-wrong double by "just run the update command".

Sweeping those 78 is filed separately as #8582, not smuggled in here.

Tests and gates

  • pnpm --filter @objectstack/objectql test — 200 files / 3550 tests pass
  • pnpm --filter @objectstack/plugin-security test — 55 files / 1079 tests pass
  • pnpm --filter @objectstack/plugin-sharing test — 21 files / 569 tests pass
  • typecheck on objectql + plugin-security — clean; eslint --no-inline-config on all changed files — clean

Green: check:where-matcher (incl. --self-test), check:nul-bytes, check:engine-double-contract, check:durability-log-level, check:query-options-erasure, check:i18n, check:type-check-coverage, check:type-check-debt, check:test-source-alias, check:type-source-resolution, check:cross-package-test-inputs, check:node-version, check:required-contexts, check:shard-attestation, check:workflow-status-functions.

Two of those first reported a prerequisite failure rather than a verdict, and are green once the prerequisite is met — worth knowing because neither message is about the diff: check:i18n needs the workspace CLI built ("Nothing was checked"), and check:type-check-debt refuses to re-measure without the full built closure.

No changeset, and skip-changeset applied: the diff is test doubles, two scripts/ gates, the workflow and the root package.json — nothing published changes, so an empty-frontmatter changeset would be rejected by check:empty-changeset and a package-bumping one would ship a release note about test internals. This also keeps the PR clear of check:objectui-pin-fresh, which fires on any .changeset/* touch and is currently red as pre-existing repo state (.objectui-sha behind objectui main, #3340) — unrelated to this diff either way.


Generated by Claude Code

…ce battery

Closes #8494.

#7620 corrected sixteen in-memory `matches(row, where)` doubles that
short-circuited on `$or`, discarding every sibling equality key and turning a
conjunction into a different query while the suites stayed green. Three lanes
landed the fixes -- and nothing held them fixed: reinstating the early return
failed nothing.

`scripts/check-where-matcher-conformance.mjs` lifts every discovered matcher
out of its test file and asks it four combinator questions. The criterion is
one sentence: answer correctly, or refuse by throwing -- never answer silently
wrong. Refusal conforms because the defect class is silence, not
incompleteness.

Behavioural rather than syntactic on purpose: the second failure shape #8494
binds this to cover is an ABSENCE -- a matcher with no combinator branch at
all, which reads `$or` as a field name and silently drops the row -- which no
pattern-match over source can see.

Building it found five live instances of the early-return shape that all three
correction lanes missed (four in objectql, one in plugin-security). Fixed here
rather than baselined: a ledger that grandfathered the exact defect its gate
exists to stop would be worth nothing. Shape (a) is therefore enforced with an
empty ledger; the 78 pre-existing combinator-blind doubles are grandfathered in
a shrink-only measured baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 13, 2026 11:40pm

Request Review

@os-zhuang os-zhuang added tests tooling skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 13, 2026 — with Claude
@github-actions github-actions Bot added size/xl and removed tests tooling skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 13, 2026
@os-zhuang os-zhuang added tooling skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 13, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 13, 2026 23:54
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

PM review (domain:engine-core seat, #6019): ACCEPT. Ready + auto-merge (squash) enabled.

⚠️ The three red Check Changeset runs are stale — read them before re-running anything. They ran at 23:41:17 against a head that had no skip-changeset label: the first label write was overwritten wholesale by the labeler bot, dropping it, and it was re-applied as the union straight after. The later runs of the same check (31754711230, 31754834441, 31754834500) all report skipped, which is the gate reacting to the label. Nothing to fix, and ⛔ nothing to re-queue.

The no-changeset route is the one this gate's own log recommends — "If it releases nothing … apply the skip-changeset label", and "a wrong skip-changeset label is caught by review; a wrong empty changeset is caught by nobody." Nothing published changes here (test doubles, two scripts/ gates, lint.yml, root package.json), so it is correct.

What I checked against the tree rather than the description:

  • check-engine-double-contract.mjs (+9/−4) is comment-only — it moves the read side out of that gate's "not covered" list and records why this one could not reuse its pattern. ⛔ No behavioural change to the existing gate.
  • The ratchet cannot be widened quietly. reconcile reconciles in both directions (count grows → error; count shrinks or the file goes clean → "ratchet DOWN, delete its entry"), an unbaselined file is a hard NEW silently-wrong error, monotonicity() diffs the baseline key set against the merge base so a new file cannot ride in, and there is deliberately no --update flag.
  • A check that could not run does not read as a check that passed — when the merge-base read fails, the success path prints NOT verified: … "no files added" is unchecked this run. Likewise discovered === 0 is a hard error ("a broken scan, not a clean repo"), not a quiet pass.
  • Reverse verification is watched, not asserted, and the two shapes fail with distinct attributions — early-return vs. no combinator branch (combinator read as a field name) — so the battery's probes are load-bearing as pairs rather than one probe answering for both.

⭐ The result worth recording beyond this card: building the guard turned up five live shape-(a) defects still on main that all three #7620 correction lanes missed, because those lanes grepped a named file list — the exact limitation this card existed to remove. Fixed here rather than baselined, so shape (a) ships with an empty ledger; the 78 combinator-blind doubles are grandfathered and filed as #8582.


Generated by Claude Code

Merged via the queue into main with commit bea4443 Aug 14, 2026
42 of 45 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8494-where-matcher-guard branch August 14, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

2 participants