Skip to content

fix(drivers): $exists means HAS A VALUE on the live mingo path, the analytics face and translateFilter - #13529

Merged
zhuangjianguo merged 6 commits into
mainfrom
claude/issue-13195-exists-has-value-alignment
Aug 30, 2026
Merged

fix(drivers): $exists means HAS A VALUE on the live mingo path, the analytics face and translateFilter#13529
zhuangjianguo merged 6 commits into
mainfrom
claude/issue-13195-exists-has-value-alignment

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

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

exitwasnow
driver-memory live mingo path (InMemoryDriver.find()normalizeFieldOperators)$exists handed to mingo under its own name; mingo tests KEY PRESENCElowers to {$ne: null} / {$eq: null}
driver-memory analytics execution face (CUBE_OPERATOR_TO_MONGO_PREDICATE.set)built its own {$exists: bool}, inheriting key-presence independentlysame lowering
driver-mongodbtranslateFilterpassed through; MongoDB $exists IS key-presence at the wire levelsame lowering

Nothing 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:

filterlive mingoanalytics executedmongodbreference matcher (oracle)
$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']n/a (face refuses $not)[]['3']['3'] unchanged

MISSING — the CONTROL column, and it did not move:

filterlive mingoanalytics executedmongodbreference matcher
$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: false and $not {$exists: true} now agree

The 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: false and $not {$exists: true} AGREE — asserted as an equality" (both readings)
  • mongodb-exists-has-value-translation.test.ts ⇢ the same assertion on the emitted-document side

The 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.

pinwhat moved
driver-memory/src/memory-exists-has-value-faces.test.tsthe named pin; 10 ⇢ 18 tests
driver-mongodb/src/mongodb-exists-has-value-translation.test.tsthe named pin; 8 ⇢ 14 tests
driver-memory/src/memory-analytics-echo-operator-coverage.test.tsthe executed-vs-echo inequality becomes an equality, and the $existsearly-return is removed from the enumeration loop, so that loop is now total over the face's vocabulary
driver-memory/src/memory-driver-document-not.test.tsthe [#5299] live-vs-reference block: the third and last cell converges, joining $nin / $notContains (#13166); a $exists: false row is added, the direction that block never recorded
driver-memory/src/memory-own-key-undefined.test.ts$exists now answers as $null inverted — asserted against $null directly, since that consequence is the ruling's and is deliberate
driver-mongodb/src/mongodb-filter.test.tsthe emitted document
driver-mongodb/src/mongodb-null-comparand-refusal.test.tsthe untouched-vocabulary control

One 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 $ne twice 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:

filterunguardedreference matcher
{$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 main would 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 $exists whose key is already taken is promoted to its own $and branch; 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.

⚠️ The guard is scoped to the operator this card moved, deliberately. The identical clobber is reachable today through $null, $between and $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 loud

It was fenced for this dispatch, and it was not edited. Two separate answers:

  • Nothing required it. No $exists row is enrolled in FILTER_LOGIC_CASES (verified — the enrolled negative-operator rows are $ne, $not, $null true/false), and check-driver-conformance.mjs strips comments before scoring, so the divergence cells live only in a docblock no gate reads. pnpm check:driver-conformance is green.
  • But its recorded prose is now STALE, and deliberately left so. Three cells of its reversal-measurement table still read MATCH — reads KEY-PRESENCE for the live mingo path, the analytics face and driver-mongodb, and the ⚠️ paragraph beneath still says those exits are "still reading key-presence" and that "a $exists row cannot be enrolled here yet". All four statements are false as of this PR. Correcting them, and the FILTER_LOGIC_CASES enrolment 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 $exists in 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. ⛔ No content/docs/releases/**, no docs/adr/**, no .claude/**, no skills/**.

Verification

Full detail in the report on #13195. Headlines, all at the final commit:

All of the above was re-run on the final commit 7c9030a57f with a clean tree. Whole-repo eslint . --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-conformance is green at "OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt", which is also the check that no $exists row 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/main turns 7 pin files / 23 tests red (driver-memory Test Files 4 failed | 30 passed, Tests 13 failed | 933 passed; driver-mongodb Test 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 no dist/ 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 an EXIT INT TERM trap with an absolute repo root.


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 57827b617e1f6f38a684b5b1daf56c8a7455cb81packageMentionDocs.

Which tree this was computed on

This run read content/docs from 607477c5f26c10af9fba3602244a3a29490b005d — the merge of head 7c9030a57f8f2df55b073914fbb29306ced9aad1 into base 57827b617e1f6f38a684b5b1daf56c8a7455cb81, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

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 introduced

The ruling says lower $exists to the spelling the $null arm already emits — {$ne: null} / {$eq: null}. Correct, and it is what landed. But an author can write $ne on the same field, so the naive lowering assigns $ne twice into one object and one constraint silently disappears — which one decided by the author's key order. Measured with the lowering in and no guard:

  • {name: {$exists: true, $ne: 'b'}}['1','3'], its key-swapped twin → ['1','2'], where the reference matcher says ['1'] for both;
  • {$exists: false, $eq: 'alpha-one'} returned a row that has a value.

⇒ Four composed cells that agreed with the oracle on main would have started disagreeing. Following the ruling literally would have traded a fixed single-operator cell for a broken composed one — the exact one-driver-two-faces shape this card exists to remove. The fix is scoped to the operator being moved: a lowered $exists whose key is already taken is promoted to its own $and branch, free key still merges inline.

And the identical pre-existing clobber through $null, $between, $notContains and the analytics face's wholesale member overwrite was filed rather than half-fixed#13524, with the key-order measurement on main ({$null: false, $ne: 'b'}['1','3'] vs ['1','2'] swapped, oracle ['1']). Widening here to "fix them all" would have been the tempting move and the wrong one.

Fence 1.6 — the stop condition — answered in full, and I verified it independently

The report says packages/spec/src/data/filter-logic-conformance.ts was neither changed nor needed. Checked on origin/main myself: every $exists occurrence sits at lines 147–217, all inside the file's docblock, while the FILTER_LOGIC_CASES array literal opens at line 297. No $exists row is enrolled, and check-driver-conformance.mjs strips comments before scoring — so the divergence cells live where no gate reads them. The fence never bound: there was no choice between correctness and the fence.

⚠️ Honest note on my own check: I ran a $null positive control alongside it and it did not fire (0 hits — my quoting was wrong through the shell). So I am resting this on the line-position argument, which stands by itself, and not on that zero. A control that does not fire proves nothing, and I would rather say so than let it pass as corroboration.

⭐ 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 $exists row cannot be enrolled here yet". I confirmed both on main (lines 184 and 217). All four claims are false as of this PR. Filed as #13531 with the enrolment guidance, deliberately not fixed here. That is the stop condition working as designed rather than merely obeyed.

Zone 2 — one broke, and it was mine again

The harm the ruling recorded is pinned as an equality, which is stronger than asked

$exists: false and $not {$exists: true} now agree on both readings and both drivers — and are pinned as an equality between the two spellings rather than each against a literal, so a future change that moves only one of them cannot pass by moving two expectations independently. Before this, a caller asking for no-value rows got a silent empty result on three of four exits.

The MISSING column is unmoved everywhere — the control that shows the harness discriminates — and memory-filter-vocabulary-refusal's fixture correctly did not move, because it has no null rows so the two semantics coincide there. That is the sharpest control in the set.

⇒ On full green I flip ready and enqueue. Routing done: #13524 and #13531pm:retriage; grading is triage's.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@zhuangjianguo@claude