fix(drivers): $exists means HAS A VALUE on the live mingo path, the analytics face and translateFilter - #13529
Conversation
…o path, its analytics face and driver-mongodb Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
…ts key is taken Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
… place Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
…ists-has-value-alignment
…th drivers Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
📓 Docs Drift Check5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 8 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 607477c5f26c10af9fba3602244a3a29490b005d && git checkout 607477c5f26c10af9fba3602244a3a29490b005d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57827b617e1f6f38a684b5b1daf56c8a7455cb81 7c9030a57f8f2df55b073914fbb29306ced9aad1 && git checkout -B drift-repro 57827b617e1f6f38a684b5b1daf56c8a7455cb81 && git merge --no-ff 7c9030a57f8f2df55b073914fbb29306ced9aad1
node scripts/docs-audit/affected-docs.mjs --json 57827b617e1f6f38a684b5b1daf56c8a7455cb81 |
|
PM review — ACCEPT, landing held until CI converges (20 success · 2 skipped · 7 running · 0 red at time of writing). My bar is every check green. ⛔ I do not merge it. ⭐ The judgment call is the best thing in this run, and it is a defect the ruling's own prescription would have introducedThe ruling says lower
⇒ Four composed cells that agreed with the oracle on And the identical pre-existing clobber through Fence 1.6 — the stop condition — answered in full, and I verified it independentlyThe report says
⭐ And the report does not stop at "fence respected": it reports that the file's recorded prose is now stale — three cells still read "MATCH — reads KEY-PRESENCE" and the paragraph beneath still claims those exits read key-presence and that "a Zone 2 — one broke, and it was mine again
The harm the ruling recorded is pinned as an equality, which is stronger than asked
The MISSING column is unmoved everywhere — the control that shows the harness discriminates — and ⇒ On full green I flip ready and enqueue. Routing done: #13524 and #13531 → Generated by Claude Code |
The two protocol pages still described `$exists` as a key-presence test — "Field exists (NoSQL)" and "Field existence check". That stopped being true when the has-value alignment landed at 9dac1ae (PR #13529): every backend now answers "the field has a value" (`!= null`), and driver-mongodb no longer emits MongoDB's own `$exists` at all. Established from code, not from another document: - packages/drivers/driver-mongodb/src/mongodb-filter.ts `case '$exists'` puts `$ne: null` / `$eq: null` — the spelling `$null` already emits; - packages/drivers/driver-sql/src/sql-driver.ts `case '$exists'` compiles `whereNotNull` / `whereNull`; - packages/objectql/src/having-filter.ts evaluates `value !== undefined && value !== null`; - packages/spec/src/data/filter-logic-conformance.ts states the settled semantic verbatim: "`$exists` means \"has a value\" (`!= null`), never key-presence". content/docs/data-modeling/queries.mdx already said "Field has a value" and is deliberately untouched — it is the target wording, not another site to sweep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
…claim (#13539) GOVERNED SURFACE — published, customer-facing skill text. Both rows taught key-presence, and `objectstack-query/SKILL.md` named the mapping outright: "MongoDB `$exists`". Since 9dac1ae (PR #13529) `driver-mongodb`'s translateFilter never emits MongoDB's `$exists` — it lowers the operator to `{$ne: null}` / `{$eq: null}` — so a customer following that line writes a filter whose behaviour differs from the promise on any null-valued field. Established from code and from an executed test, not from another document: - mongodb-filter.ts `case '$exists'` puts `$ne: null` / `$eq: null`; - sql-driver.ts `case '$exists'` compiles whereNotNull / whereNull; - objectql having-filter.ts evaluates `value !== undefined && value !== null`; - 14/14 green in driver-mongodb/src/mongodb-exists-has-value-translation.test.ts, which pins `translateFilter({name: {$exists: true}})` to `{name: {$ne: null}}`. Written to the published token ratchet rather than around it: both files carry zero headroom, so the correction is byte-neutral-or-shrinking rather than an added warning. SKILL.md 22208 -> 22207 bytes, filters.md 8593 -> 8596; both files unchanged in tokens and lines, bundle total unchanged at 186751. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Fixes #13195
Implements the maintainer's 2026-08-30 ruling (第 5 场总监席决裁批 #7, verbatim 「批 #7 同意」), option A: the three exits still reading key-presence align to the settled semantic
$exists= has a value (!= null) — #5298 leg ③ / #5369, landed in PR #5962.The three exits
driver-memorylive mingo path (InMemoryDriver.find()⇢normalizeFieldOperators)$existshanded to mingo under its own name; mingo tests KEY PRESENCE{$ne: null}/{$eq: null}driver-memoryanalytics execution face (CUBE_OPERATOR_TO_MONGO_PREDICATE.set){$exists: bool}, inheriting key-presence independentlydriver-mongodbtranslateFilter$existsIS key-presence at the wire levelNothing is invented, exactly as the ruling records:
{$ne: null}/{$eq: null}is the spelling each of those files already emits for$null, and it answers has-value on both readings of "no value".Before / after — measured, both readings, with the controls that stayed put
Fixture:
{id:'1',name:'a'},{id:'2',name:'b'}, and a third row that is either{id:'3',name:null}(NULLED) or{id:'3'}(MISSING).NULLED — where every divergent cell lived:
$exists: true['1','2','3']⇢['1','2']['1','2','3']⇢['1','2']['1','2','3']⇢['1','2']['1','2']unchanged$exists: false[]⇢['3'][]⇢['3'][]⇢['3']['3']unchanged$not {$exists: true}[]⇢['3']$not)[]⇢['3']['3']unchangedMISSING — the CONTROL column, and it did not move:
$exists: true['1','2']unchanged['1','2']unchanged['1','2']unchanged['1','2']$exists: false['3']unchanged['3']unchanged['3']unchanged['3']The key-absent column agreed with the ruling before this change and still does —
{$ne: null}answers has-value there too. It is kept in both suites precisely as the control that the alignment moved only what it was meant to: a harness that flagged everything would prove nothing. Further controls that stayed agreeing: a value predicate still narrows to one row on every exit; the analytics ECHO half (name IS NOT NULL/name IS NULL) is untouched, because it was already the ruled answer;{stage: {$exists: true}}over a fixture with no null rows still returns all three (memory-filter-vocabulary-refusal, unchanged);$null's own cells are asserted unmoved.$exists: falseand$not {$exists: true}now agreeThe ruling's record names this the hardest live harm: a caller asking for the rows with no value got a silent empty result on three of the four exits. Both spellings now return the no-value row, and they are pinned against each other rather than each against a literal, so a future change that moves only one of them cannot pass:
memory-exists-has-value-faces.test.ts⇢ "$exists: falseand$not {$exists: true}AGREE — asserted as an equality" (both readings)mongodb-exists-has-value-translation.test.ts⇢ the same assertion on the emitted-document sideThe pins are INVERTED IN PLACE — seven of them, not two
The dispatch named two. The suites found seven; all are flipped in place onto the answer each already carried in its own comment, none deleted, none re-baselined onto whatever the new code prints, and none skipped or allow-listed.
driver-memory/src/memory-exists-has-value-faces.test.tsdriver-mongodb/src/mongodb-exists-has-value-translation.test.tsdriver-memory/src/memory-analytics-echo-operator-coverage.test.ts$existsearly-return is removed from the enumeration loop, so that loop is now total over the face's vocabularydriver-memory/src/memory-driver-document-not.test.ts[#5299]live-vs-reference block: the third and last cell converges, joining$nin/$notContains(#13166); a$exists: falserow is added, the direction that block never recordeddriver-memory/src/memory-own-key-undefined.test.ts$existsnow answers as$nullinverted — asserted against$nulldirectly, since that consequence is the ruling's and is deliberatedriver-mongodb/src/mongodb-filter.test.tsdriver-mongodb/src/mongodb-null-comparand-refusal.test.tsOne thing the ruled lowering needed that the ruling did not name — declared, not left for review
{$ne: null}/{$eq: null}reuse keys an author can write on the same field.{name: {$exists: true, $ne: 'b'}}would therefore assign$netwice into one object and one of the two constraints would silently vanish — with which one decided by the author's key order. Measured with the lowering in and the guard out:{$exists: true, $ne: 'beta'}['1','3']['1']{$ne: 'beta', $exists: true}(keys swapped)['1','2']['1']{$exists: false, $eq: 'alpha-one'}['1']— a row that HAS a value[]Four composed cells that agreed with the reference matcher on
mainwould have started disagreeing — trading a fixed single-operator cell for a broken composed one, which is the one-driver-two-faces shape this card exists to remove. So a lowered$existswhose key is already taken is promoted to its own$andbranch; a free key still merges inline. Both key orders now emit one document, and every composed cell measured agrees with the reference matcher — including two that did not agree before this change. Pinned in both suites, with a control proving the composed predicate is genuinely narrower than either half.$null,$betweenand$notContains, and wholesale on the analytics face — pre-existing, none of them this cell, filed as #13524 rather than half-fixed here.packages/spec/src/data/filter-logic-conformance.ts— NOT changed, and this needs saying out loudIt was fenced for this dispatch, and it was not edited. Two separate answers:
$existsrow is enrolled inFILTER_LOGIC_CASES(verified — the enrolled negative-operator rows are$ne,$not,$nulltrue/false), andcheck-driver-conformance.mjsstrips comments before scoring, so the divergence cells live only in a docblock no gate reads.pnpm check:driver-conformanceis green.MATCH — reads KEY-PRESENCEfor the live mingo path, the analytics face anddriver-mongodb, and the$existsrow cannot be enrolled here yet". All four statements are false as of this PR. Correcting them, and theFILTER_LOGIC_CASESenrolment the card's own step 4 defers until "the backends agree", is the next card — the backends have now agreed, which is that card's precondition.Also NOT in this diff
⛔ Nothing retires, deprecates or discourages
$existsin favour of$null— option C is outside the ruling and its prerequisite census is #13492. ⛔ No neighbouring cell moved: #13166, #13357, #13494, #13495 are untouched. ⛔ Nocontent/docs/releases/**, nodocs/adr/**, no.claude/**, noskills/**.Verification
Full detail in the report on #13195. Headlines, all at the final commit:
driver-memory946 passed (929 onmain),driver-mongodb510 passed | 143 skipped (496 onmain) — the skips are the pre-existing driver-mongodb 测试:两个套件并发下载 MongoDB 二进制,mongodb-memory-server 的 rename 竞态让全绿测试 exit 1(队列 flaky) #5517 real-mongod gate.--filter '...@objectstack/driver-memory' --filter '...@objectstack/driver-mongodb'— dependents, the direction a contract tightening lands in) run green.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackrather than from the dispatch list.All of the above was re-run on the final commit
7c9030a57fwith a clean tree. Whole-repoeslint . --no-inline-config(population read from eslint's own JSON output: 5506 files) is 0 errors, 0 warnings — run in full, not narrowed.pnpm check:driver-conformanceis green at "OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt", which is also the check that no$existsrow was enrolled.An ablation proves the flipped pins detect the pre-ruling behaviour rather than merely restating the new one: reverting the three exits to
origin/mainturns 7 pin files / 23 tests red (driver-memoryTest Files 4 failed | 30 passed,Tests 13 failed | 933 passed; driver-mongodbTest Files 3 failed | 20 passed | 5 skipped,Tests 10 failed | 500 passed). No rebuild is involved — every pin imports its subject by a relative specifier inside its own package, so vitest resolves source and nodist/can stale the reading. The mutation was confirmed on disk by git-blob hash before each run, and the restore by an exact HEAD-blob hash match afterwards, under anEXIT INT TERMtrap with an absolute repo root.Generated by Claude Code