Filed unassigned by the #13553 dev. Recording only — no severity asserted, routing is triage's. It carries a contract question as well as a divergence, so it may be a decision-box card rather than a bug.
#13553 repaired the ordering arms on the VALUE axis (a no-value ROW). This is the COMPARAND axis of the same four arms — the sibling relationship #13495 and #13549 had for $between, and it was deliberately left undecided by PR #14077.
Measured — the contract door, by position
Executed on 43b799bc66, calling parseFilterAST from @objectstack/spec/data directly:
| filter |
contract door |
{n: {$in: [null]}} |
REFUSED INVALID_FILTER / 400 |
{n: {$nin: [null]}} |
REFUSED INVALID_FILTER / 400 |
{n: {$between: [null, null]}} |
REFUSED INVALID_FILTER / 400 |
{n: {$between: [null, 1]}} |
REFUSED INVALID_FILTER / 400 |
{n: {$gt: null}} |
ACCEPTED |
{n: {$gte: null}} |
ACCEPTED |
{n: {$lt: null}} |
ACCEPTED |
{n: {$lte: null}} |
ACCEPTED |
{n: {$eq: null}} |
ACCEPTED (ruled — #5332, it IS the null predicate) |
{n: {$ne: null}} |
ACCEPTED (ruled — same) |
The maintainer's ruling of 2026-08-31 (option C, #13357) refused a null member of $in / $nin and a null $between endpoint at the validation entrance. The four ordering positions were not in its scope, so they are now the only null-comparand positions that are neither RULED (as $eq / $ne are) nor REFUSED. #5332's landing had already named this position in writing as one "no ruling covers" — quoted in #13357's own thread as: "it still serves comparand positions NO RULING COVERS ($gt: null, $in: [null])." The $in: [null] half has since been ruled. The $gt: null half has not.
Measured — the two faces disagree on it
Same commit, both faces of packages/drivers/driver-memory, fixture {id:'1', n:5} {id:'2', n:0} {id:'3', n:null} {id:'4'} (key ABSENT). These cells are byte-identical before and after PR #14077 — that PR's guard is scoped to leave them exactly as it found them:
| filter |
live mingo path |
reference matcher |
|
{n: {$gt: null}} |
[] |
['1'] |
DISAGREE |
{n: {$gte: null}} |
['3'] |
['1','2','3'] |
DISAGREE |
{n: {$lt: null}} |
[] |
[] |
agree |
{n: {$lte: null}} |
['3'] |
['2','3'] |
DISAGREE |
The live path treats two absences as EQUAL — $gte/$lte admit the no-value row, $gt/$lt do not — and treats a valued row as not comparable to an absent bound. The reference matcher compares through JS coercion instead: 5 > null is 5 > 0.
A STRING fixture hides this exactly as it hid #13553: on {id:'4', v:null} among ISO date strings both faces answer ['4'] for $gte: null, because '2026-07-01' >= null is a comparison against NaN. Measured both ways.
Why it was not folded into #13553
Two independent reasons, both measured rather than reasoned:
- The obvious repair needs a rule shape that is landed nowhere. Reusing
valueWithinRange's comparability half looks right, but that function is built from >= and <= only and has no notion of strictness. Applied to the ordering arms it would answer {$gt: null} with ['3'] where the live path answers [] — a NEW divergence created by the repair. Matching the live path needs the extra rule "two absences compare EQUAL", which no ruling on file states.
- It would pick a camp the platform declined to pick. The only sibling position that ever reached the maintainer was settled by REFUSING the shape, not by choosing an answer for it — and that ruling explicitly declined cross-backend alignment work (「⛔ 不做跨后端对齐工程」) and declined repairing a path that refusal makes unreachable (「⛔ 不单独修一个到不了的路径」). Deciding this in a driver arm would be a seat making that call.
For triage — the shape of the decision
If the answer is refuse at the contract door, this is a packages/spec change in the same place and the same shape as the 2026-08-31 ruling, the divergence becomes unreachable, and no driver moves. Note it is a clause ② narrowing of the published filter accept-set, like its sibling.
If the answer is define the semantics, then the live path's reading is the obvious candidate (it is this file's standing tie-break) but it requires the strictness rule above to be stated, and the other backends' answers have not been measured here — only driver-memory's two faces were.
⚠️ Degenerate input, as its sibling was: {$gte: null} is not a filter anyone writes deliberately. The #13357 ruling's own basis was a measured zero of authored occurrences, and the same census has NOT been run for this shape. That census, with a positive control, is the first thing an executor should do.
Related
#13553 / PR #14077 (the VALUE axis of the same four arms) · #13357 (the sibling positions, ruled 2026-08-31 and refused) · #13495 (the $between comparand axis) · #5332 (the ruling that named this position as uncovered) · #5298 (the no-value row family)
Filed unassigned by the #13553 dev. Recording only — no severity asserted, routing is triage's. It carries a contract question as well as a divergence, so it may be a decision-box card rather than a bug.
#13553 repaired the ordering arms on the VALUE axis (a no-value ROW). This is the COMPARAND axis of the same four arms — the sibling relationship #13495 and #13549 had for
$between, and it was deliberately left undecided by PR #14077.Measured — the contract door, by position
Executed on
43b799bc66, callingparseFilterASTfrom@objectstack/spec/datadirectly:{n: {$in: [null]}}INVALID_FILTER/ 400{n: {$nin: [null]}}INVALID_FILTER/ 400{n: {$between: [null, null]}}INVALID_FILTER/ 400{n: {$between: [null, 1]}}INVALID_FILTER/ 400{n: {$gt: null}}{n: {$gte: null}}{n: {$lt: null}}{n: {$lte: null}}{n: {$eq: null}}{n: {$ne: null}}The maintainer's ruling of 2026-08-31 (option C, #13357) refused a null member of
$in/$ninand a null$betweenendpoint at the validation entrance. The four ordering positions were not in its scope, so they are now the only null-comparand positions that are neither RULED (as$eq/$neare) nor REFUSED. #5332's landing had already named this position in writing as one "no ruling covers" — quoted in #13357's own thread as: "it still serves comparand positions NO RULING COVERS ($gt: null,$in: [null])." The$in: [null]half has since been ruled. The$gt: nullhalf has not.Measured — the two faces disagree on it
Same commit, both faces of
packages/drivers/driver-memory, fixture{id:'1', n:5}{id:'2', n:0}{id:'3', n:null}{id:'4'}(key ABSENT). These cells are byte-identical before and after PR #14077 — that PR's guard is scoped to leave them exactly as it found them:{n: {$gt: null}}[]['1']{n: {$gte: null}}['3']['1','2','3']{n: {$lt: null}}[][]{n: {$lte: null}}['3']['2','3']The live path treats two absences as EQUAL —
$gte/$lteadmit the no-value row,$gt/$ltdo not — and treats a valued row as not comparable to an absent bound. The reference matcher compares through JS coercion instead:5 > nullis5 > 0.A STRING fixture hides this exactly as it hid #13553: on
{id:'4', v:null}among ISO date strings both faces answer['4']for$gte: null, because'2026-07-01' >= nullis a comparison againstNaN. Measured both ways.Why it was not folded into #13553
Two independent reasons, both measured rather than reasoned:
valueWithinRange's comparability half looks right, but that function is built from>=and<=only and has no notion of strictness. Applied to the ordering arms it would answer{$gt: null}with['3']where the live path answers[]— a NEW divergence created by the repair. Matching the live path needs the extra rule "two absences compare EQUAL", which no ruling on file states.For triage — the shape of the decision
If the answer is refuse at the contract door, this is a
packages/specchange in the same place and the same shape as the 2026-08-31 ruling, the divergence becomes unreachable, and no driver moves. Note it is a clause ② narrowing of the published filter accept-set, like its sibling.If the answer is define the semantics, then the live path's reading is the obvious candidate (it is this file's standing tie-break) but it requires the strictness rule above to be stated, and the other backends' answers have not been measured here — only
driver-memory's two faces were.{$gte: null}is not a filter anyone writes deliberately. The #13357 ruling's own basis was a measured zero of authored occurrences, and the same census has NOT been run for this shape. That census, with a positive control, is the first thing an executor should do.Related
#13553 / PR #14077 (the VALUE axis of the same four arms) · #13357 (the sibling positions, ruled 2026-08-31 and refused) · #13495 (the
$betweencomparand axis) · #5332 (the ruling that named this position as uncovered) · #5298 (the no-value row family)