fix(driver-memory): a no-value row satisfies $nin / $notContains in the reference matcher (#13166) - #13356
Conversation
…he reference matcher The reference matcher `driver-sql` was aligned TO for #5146 diverged from the platform's settled INCLUDE direction in 3 of 6 measured cells. Two independent causes, one per reading of "no value": 1. `checkCondition`'s pre-switch guard allowlisted `$ne` but not `$nin` / `$notContains`, so a MISSING key short-circuited to "no match" before those arms ran. 2. The `$notContains` arm rejected a `null` on its `typeof value !== 'string'` TYPE test rather than on the predicate — the half the guard cannot reach. Both now answer one named predicate, `noValueSatisfiesNegation`. The four in-code statements that spelled the #5499 freeze as LIVE and used it to excuse the divergence are settled together with the disposition rather than re-tensed: the freeze dissolved 2026-08-11, and the "driver-mongodb does this too" clause was never true for this operator family. `$exists` is untouched — it is the neighbouring cell with a different backend list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…mory-matcher-no-value-negated-operators # Conflicts: # packages/services/service-analytics/src/read-scope-sql.ts
📓 Docs Drift Check2 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7cd1357b7ac4c10f87d36a951a5ece5fc4eae3bf && git checkout 7cd1357b7ac4c10f87d36a951a5ece5fc4eae3bf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3ec8646f1b71deaa95a6675ddbbbf6303c79ba05 5ed889a2bf3481406a3c8e2e596be6a41d495b36 && git checkout -B drift-repro 3ec8646f1b71deaa95a6675ddbbbf6303c79ba05 && git merge --no-ff 5ed889a2bf3481406a3c8e2e596be6a41d495b36
node scripts/docs-audit/affected-docs.mjs --json 3ec8646f1b71deaa95a6675ddbbbf6303c79ba05 |
PM review — accepted for landing (held in draft until CI is complete and green)Reviewed at head ScopeSeven files. Zero All three fences held, and fence 1 was cleared by measurementFence 1 was the real risk: this matcher is the reference the SQL family was aligned to in #5146, so the stop condition was whether aligning it overturns already-aligned SQL behaviour. It does not — Fence 2 (enrolment, not adding a row) was respected in the way that costs the most and is therefore worth naming: the backend is now fixed, which is the precondition — but enrolment lives in Fence 3 (
|
Docs-drift advisory — resolved by measurement. Nothing to change.The run lists nothing but flags that two changed files yielded no anchor ( Read on
The only ⇒ No hand-written page teaches the behaviour this PR corrects, and none carries the stale-freeze claim the four in-code sites did. Nothing filed, nothing edited. Recorded so the next reviewer does not have to re-derive it, and because the advisory's own "not a clean bill of health" caveat applies squarely here — the empty list was not the answer; the controlled search was. Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33297884013 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
队列弹出 —— ⛔ 不是本 PR 的失败。已定位到根因,且不重排,理由如下。失败的是什么队列构建 33297884013: 为什么不是本 PR 的本 PR 的文件面是:changeset ·
我在 #13258 落地前就把这一点写给了它的作者,指名了是哪一条断言、为什么会变红,并明确提出可以压住 #13329 让他们先落:#13258 (comment) 。他们选择继续。 ⇒ 合并队列把两者放进了同一条投机栈,所以 #13258 的红把排在它后面的本 PR 一起弹了出来。队列分诊 bot 自己也这么判:「已弹出 2 个 PR(#13258、#13356),但它们同属 1 条投机栈 ⇒ 1 次独立命中」。 ⛔ 为什么不重排修复不在我手里:它是另一个席位 PR 里的一条断言,⛔ 我不会去改别人的 PR,也没有可以「移植进本 PR」的补丁 —— 本 PR 根本不包含那个文件。 而队列分诊清单的第 3 条正对上:「等前面的 PR 落地或失败出队后再重排一次即可,⛔ 不要连续重排」。#13258 仍带着那条陈旧断言排在前面,现在重排只会再烧一轮全队列重建、然后以同样的原因再弹一次。 ⇒ 等 #13258 落地或出队,再重排一次。 我这一次重排额度未动用,留给那个时候。 本 PR 自身的状态未变
Generated by Claude Code |
Part of #13166
Implements the settled ruling (#5146 → #5298 option A, re-affirmed 2026-08-10 after the reversal was priced and withdrawn): a row with no value SATISFIES a negation-carrying operator.
driver-memory's reference matcher diverged in 3 of the card's 6 measured cells. Both independent causes are fixed, and the four in-code statements that spelled the #5499 freeze as LIVE are settled together with the disposition rather than re-tensed.The defect, reproduced first
The card's fixture, run before any source edit — 6 of 14 assertions red, and exactly the three cells the card named:
Two independent causes, one stated ruling
Both live in
packages/drivers/driver-memory/src/memory-matcher.ts:checkCondition's pre-switch guard allowlisted$nebut not$nin/$notContains, so a missing key short-circuited to "no match" before those arms ran.$notContainsarm rejected anullon itstypeof value !== 'string'type test rather than on the predicate — thename: nullhalf, which guard 1 cannot reach (it only ever seesundefined).Both now answer one named predicate,
noValueSatisfiesNegation, whose docblock carries the ruling chain. Two spellings of one ruling is how they came apart in the first place.A present, non-string value keeps the answer it had — only the no-value cells moved, and there is an assertion pinning that.
Fence 1 cleared by measurement, not by assumption
This matcher is the reference the SQL family was aligned to for #5146, so the stop condition was: does aligning it overturn already-aligned SQL-family behaviour? It does not, and no SQL-side file is touched.
driver-sqlandformulaalready answered the include direction, so this change movesdriver-memoryto them:@objectstack/driver-sql@objectstack/formula@objectstack/driver-memory@objectstack/service-analytics@objectstack/objectqlsql-driver-not-null-safe.test.tsstill asserts['1']for both filters the memory pin asserts; the two files agree again because this one moved.What does NOT change for users
InMemoryDriver.find()is unaffected.match()is not in this package's export surface, and the live mingo path users actually reach already answered the include direction — measured on the card's fixture through the public API,['2','3']for all three operators. The observable effect is that this package's two filter faces now agree where they used to disagree. The changeset is graded to exactly that.The four prose sites, settled with the disposition
Not re-tensed — #13089's triage forbade that, and rightly: a tense-only rewrite would have carried a measured-false claim forward as settled-looking prose. Each site was wrong twice: (a) the freeze dissolved 2026-08-11; (b) the "driver-mongodb does this too" clause was never true for this operator family (
translateFieldOperatorspasses$ninthrough and compiles$notContainsto{ $not: { $regex } }, both of which match a missing or null field).packages/objectql/src/having-filter.ts— the card's site 1.packages/services/service-analytics/src/read-scope-sql.ts— the card's site 2.packages/drivers/driver-memory/src/memory-matcher-not-null-safe.test.ts— the card's site 3, including the load-bearing sentence that was the stated reason the assertions had not been flipped. Two assertions inverted in place, with the old values and the reason recorded beside them.packages/drivers/driver-memory/src/memory-driver-document-not.test.ts— found during execution, not named in the card. Itsdescribealready read "disposition open (driver-memory's reference matcher still answers $notContains / $nin the pre-ruling way on a no-value row — the #5499 freeze that excused it dissolved 2026-08-11, so the divergence is now unexcused and untracked #13166/$exists still reads KEY-PRESENCE rather than has-value on driver-memory's live mingo path and driver-mongodb — the #5499 freeze that excused it dissolved, #13166 explicitly excludes it, so it is now unexcused AND untracked #13195)" and its note already named the LIVE column correct for these two operators, so it was built for exactly this edit. Its$ninand$notContainsrows now assert live and reference agreeing; its$existsrow is untouched and stays a pinned divergence.⛔ No assertion was re-baselined onto whatever the matcher began printing. Each was inverted onto a target named as correct before the fix existed — the live path's answer, and
formula's.Merge note — PR #13321 landed mid-flight and owns half of site 2
#13321 merged as
881f8d8e88while this branch was in flight and rewroteread-scope-sql.ts:179-180, one of the sites this card was assigned. Its text correctly records the thaw and that the debt is now DUE, but it kept the second error — it still reads as thoughdriver-mongodbanswers the cell differently.Resolved by extending its landed text, not reverting it: the thaw/DUE framing is kept verbatim in substance, and the #13166 disposition is added on top (mongodb was never a holdout; driver-memory was, on its reference face only, and is aligned now).
having-filter.tswas left byte-identical to main by #13321 as promised, so there was no conflict there.⛔ Also unchanged: the other freeze-tense sites #13321 owns.
Non-vacuity — ablation, direction predicted in writing first
The prediction was recorded before either leg ran. The subject resolves through a relative import, so vitest reads
src/, notdist/— evidenced by the repro failing and the fix passing with no build between them, so no rebuild leg is owed here.$exists/$ne/$null$notContainsarm reverted to the bare type test$notContainscells; "both readings agree" stays GREENLeg B is the one worth reading. With the guard fixed,
undefinedreaches the arm and fails the same type test asnull, so both columns are equally wrong and the agreement assertion cannot see the cause. That is the concrete reason the six-cell structure is load-bearing and must not be collapsed: a table asserting only "the two readings answer alike" would be blind to cause 2.Each mutation was proven on disk by content grep plus a
git hash-objectdiffering from the HEAD blob, and each restore was proven byte-identical to the HEAD blob by hash comparison rather than by an exit code, with an absolute-pathtrap ... EXIT INT TERMon both legs. Final tree hash equals the HEAD blob.Verification
All readings below are from the final commit,
5ed889a2bf.driver-memory,objectql,service-analytics— all Done, exit 0.tsc --listFilesconfirms all three edited/added test files are inside the program, so "typecheck clean" genuinely covers them rather than silently excluding*.test.ts.check-driver-conformance: OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.and
where-matcher conformance holds: 316 matcher(s) discovered … 0 silently-wrong and 0 unjudged … none new.check-test-completeness.mjs— "There is no local log to hand it, so the local reading for this gate is NOT MEASURED. ⛔ It is not a red."check-half-states.mjs— "Nothing was swept … it is no reading at all." (needs a GitHub credential; a board sweep, unrelated to this diff)check:dual-build-cjs-loads— "PREREQUISITE NOT MET … ⛔ This is NOT a pass: nothing was measured." (needs a fullpnpm build; its own self-test passed, 59 cases)check-engine-split-ratio.mjsfirst refused on the shallow clone; deepened with--shallow-sinceand it then measured and exited 0. A refusal was never counted as a pass.Declared narrowing: the repo-wide
pnpm lintand the full-workspacecheck:type-check-debtratchet were not run locally — the latter refuses without a built workspace closure, and a full build risks the container's foreground cap. The structural half,check:type-check-coverage, is green, and the added test file typechecks clean inside its package program.Lint & Repo Gatesruns gates the derivation never names (#13333), so CI owns the farm here.Deliberately out of scope
$exists— the neighbouring cell ($exists still reads KEY-PRESENCE rather than has-value on driver-memory's live mingo path and driver-mongodb — the #5499 freeze that excused it dissolved, #13166 explicitly excludes it, so it is now unexcused AND untracked #13195). Different backend list (driver-mongodbreads key-presence too). Untouched, and still pinned as a divergence in both pin files.FILTER_LOGIC_CASES— the backend is fixed, which is the precondition fence 2 names, but enrolment lives inpackages/spec/src/**, which this dispatch fenced off. That is why this PR saysPart ofand not a closing keyword. Details and the residualfilter-logic-conformance.ts:179cell are in the report on the card.packages/spec/src/**,docs/adr/**,.claude/**,skills/**orcontent/docs/releases/**changes. No test skipped, disabled or quarantined; no empty commit.Generated by Claude Code
Generated by Claude Code