You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The #5869 list-comparand rule is enforced only at the engine seam: a scalar in/nin comparand still reaches a driver, and on mingo >= 7.2.3 it escapes as a raw TypeError #9228
Surfaced by the weekly Dependabot production group #9212 (mingo 7.2.2 -> 7.2.4), which is red on this and nothing else. The bump did not break us — it removed the third-party coercion that was hiding a coverage hole in an already declared rule.
Measurement (verified on GitHub, not recalled)
PR #9212 @ 45eaf8db3a03fe30d8798069439aad74e4c219c5, two independent jobs, same single failure:
FAIL packages/drivers/driver-memory/src/memory-filter-ast-vocabulary.test.ts
> InMemoryDriver filter vocabulary <-> VALID_AST_OPERATORS > expresses notin without dropping it
AssertionError: promise rejected "TypeError: b.filter is not a function" instead of resolving
> $in node_modules/.pnpm/mingo@7.2.4/.../operators/_predicates.js:95
> $nin node_modules/.pnpm/mingo@7.2.4/.../operators/_predicates.js:99
main CI was green in the same window (runs 31987144246 / 31989563215 / 31991738619, all success), so the red is introduced by the PR, not inherited from the base.
mingo's own changelog for 7.2.4 names the change: "make $elemMatch with empty field and $in/$nin with regex behave correctly (#613)". 7.2.2 tolerated a non-array $in/$nin operand; 7.2.4 does not.
Why this is our defect, not mingo's
The contract is already declared, and its gate already exists — it just is not on every path in.
}elseif(LIST_COMPARAND_OPERATORS.has(op)&&Array.isArray(comparand)){// Each member is a comparand in its own right (#5234). A non-array// comparand here is a SHAPE defect owned by the engine's #5869 gate.
So: parseFilterAST() lowers a scalar membership comparand without complaint, and only a query that goes through the objectql engine meets the gate. A caller that reaches a driver directly — InMemoryDriver.find(), which is what this driver's own conformance suite does and what an embedder does — meets nothing. Until now that path was carried by mingo's coercion; from mingo 7.2.3 on it is an unhandled third-party TypeError with no code and no status, reaching the caller.
That is the same class of escape #7872 closed for comparand types, one branch over: types are judged at the shared door, shape is not.
packages/drivers/driver-memory/src/memory-driver.ts:820-821 is where it lands ({ [field]: { $nin: store(value) } }, no normalization, no refusal). The test only exercises it because valueFor (memory-filter-ast-vocabulary.test.ts:80-87) hands an array to in / nin / not_in but a scalar to the equally-accepted notin and not in spellings — an accident that turned out to be the one probe covering this hole.
Do not create a second copy of the rule. Exactly one implementation of "a list operator takes a list" may exist when you are done; assertListComparandShapes either becomes the single implementation the door calls, or delegates to the one that moved.
driver-memory changes are test-only here (construct valid input for all membership spellings). No driver behaviour patch.
Premises — verify before you build, and report a fork instead of working around them
Dependency direction allows the sink. The rule has to end up somewhere parseFilterAST can reach, i.e. in packages/spec, with @objectstack/objectql delegating to it. objectql depends on spec, so the reverse import is not available. If the assertion cannot move without dragging engine-only concepts into spec, that is a fork — report it, do not invent a third home.
Nothing in-tree authors a scalar membership comparand. Tightening parseFilterAST refuses input that today survives to the engine gate anyway (which already answers 400), but stored view metadata, examples, docs fixtures and objectui view definitions must be checked, not assumed. Grep for authored in / nin / not_in / notin / not in rules with non-array values across examples/**, content/docs/**, packages/**/fixtures/** and the objectui view metadata. If real authored usage exists, stop and report the fork with the sites: normalizing a scalar to a one-element list is then a live option and that is the maintainer's call, not yours.
If either premise fails: report premise_still_valid: false with the measurement. Do not hard-do it, and do not quietly switch to the other option.
Suggested route (advisory — measurement beats it)
In filter-comparand-type.ts the LIST_COMPARAND_OPERATORS branch already knows the operator is list-shaped; the Array.isArray(comparand) guard silently falls through today. Make the else arm a loud refusal reusing the existing envelope (INVALID_FILTER, 400) and the #5869 wording, then have assertListComparandShapes delegate. Keep the message's front-loaded shape — the 500-char client bound is real (#5423).
Acceptance
parseFilterAST([['name', 'notin', 'alpha']]) (and the nin / not_in / not in spellings) answers a coded INVALID_FILTER / 400 refusal, with a test that asserts bothcode and status, not just that it throws.
pnpm --filter @objectstack/driver-memory test green, with memory-filter-ast-vocabulary.test.ts exercising every membership spelling on valid input.
Measured, not assumed: run the driver-memory suite once against mingo@7.2.4 via a temporary local override to confirm the escape is gone, then revert the override — report the reading in the PR body, commit no manifest or lockfile change.
Local gates for this surface (taken from node scripts/pm/dispatch-gates.mjs at dispatch time, re-take for your actual diff): pnpm check:test-source-alias, pnpm check:type-source-resolution, node scripts/docs-audit/check-affected-docs.mjs; and because a test file moves: pnpm check:query-options-erasure, pnpm check:type-check-coverage, pnpm check:type-check-debt (needs the workspace built first), pnpm check:engine-double-contract, pnpm check:where-matcher.
A patch changeset is required (published packages change).
Re-check commands for the premises above
git fetch origin main
git show origin/main:packages/spec/src/data/filter-comparand-type.ts | grep -n "SHAPE defect owned by"
git show origin/main:packages/objectql/src/filter-comparand-shape.ts | grep -n "export function assertListComparandShapes"
git grep -n "notin\|not_in" origin/main -- examples content packages/*/fixtures
Surfaced by the weekly Dependabot production group #9212 (
mingo7.2.2 -> 7.2.4), which is red on this and nothing else. The bump did not break us — it removed the third-party coercion that was hiding a coverage hole in an already declared rule.Measurement (verified on GitHub, not recalled)
PR #9212 @
45eaf8db3a03fe30d8798069439aad74e4c219c5, two independent jobs, same single failure:mainCI was green in the same window (runs 31987144246 / 31989563215 / 31991738619, allsuccess), so the red is introduced by the PR, not inherited from the base.mingo's own changelog for 7.2.4 names the change: "make
$elemMatchwith empty field and$in/$ninwith regex behave correctly (#613)". 7.2.2 tolerated a non-array$in/$ninoperand; 7.2.4 does not.Why this is our defect, not mingo's
The contract is already declared, and its gate already exists — it just is not on every path in.
FieldOperatorsSchema(packages/spec/src/data/filter.zod.ts) declares$in: z.array(z.any()),$nin: z.array(z.any()),$between: z.tuple([min, max]).packages/objectql/src/filter-comparand-shape.ts(数据 API:集合算子not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 / PR fix(objectql): 集合算子的标量比较值答 400 INVALID_FILTER 并点名期望形状,不再 500 (#5869) #6209) enforces it —assertListComparandShapes, called fromengine.ts, answers400 INVALID_FILTER. Its header states the rule verbatim: "parseFilterASTlowers['status', 'not_in', 'done']to{ status: { $nin: 'done' } }without complaint — so a scalar reached the driver", and it explains why the gate went to the engine seam rather than into each driver (both driver families are under the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 investment freeze).BigIntcrashes with a raw mingoTypeError, and four other types silently answer zero rows #7872) sits at exactly the seam where a shape check would belong, and deliberately routes past it —packages/spec/src/data/filter-comparand-type.ts:So:
parseFilterAST()lowers a scalar membership comparand without complaint, and only a query that goes through the objectql engine meets the gate. A caller that reaches a driver directly —InMemoryDriver.find(), which is what this driver's own conformance suite does and what an embedder does — meets nothing. Until now that path was carried by mingo's coercion; from mingo 7.2.3 on it is an unhandled third-partyTypeErrorwith nocodeand nostatus, reaching the caller.That is the same class of escape #7872 closed for comparand types, one branch over: types are judged at the shared door, shape is not.
packages/drivers/driver-memory/src/memory-driver.ts:820-821is where it lands ({ [field]: { $nin: store(value) } }, no normalization, no refusal). The test only exercises it becausevalueFor(memory-filter-ast-vocabulary.test.ts:80-87) hands an array toin/nin/not_inbut a scalar to the equally-acceptednotinandnot inspellings — an accident that turned out to be the one probe covering this hole.Ruling (not open for re-adjudication by the dev)
driver-memoryanddriver-mongodbare under the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 investment freeze, and PR feat(spec): comparand-type door — the accepted literal comparand set, enforced once at the shared compile face for all five drivers (#7872) #8234 already set the precedent for this exact question ("enforced once at the shared compile face for all five drivers").assertListComparandShapeseither becomes the single implementation the door calls, or delegates to the one that moved.driver-memorychanges are test-only here (construct valid input for all membership spellings). No driver behaviour patch.mingoversions,package.jsonorpnpm-lock.yamlin this PR. The bump belongs to Dependabot chore(deps)(deps): Bump the production-dependencies group with 19 updates #9212, which is expected to go green because of this card.Premises — verify before you build, and report a fork instead of working around them
parseFilterASTcan reach, i.e. inpackages/spec, with@objectstack/objectqldelegating to it.objectqldepends onspec, so the reverse import is not available. If the assertion cannot move without dragging engine-only concepts intospec, that is a fork — report it, do not invent a third home.parseFilterASTrefuses input that today survives to the engine gate anyway (which already answers 400), but stored view metadata, examples, docs fixtures andobjectuiview definitions must be checked, not assumed. Grep for authoredin/nin/not_in/notin/not inrules with non-array values acrossexamples/**,content/docs/**,packages/**/fixtures/**and the objectui view metadata. If real authored usage exists, stop and report the fork with the sites: normalizing a scalar to a one-element list is then a live option and that is the maintainer's call, not yours.If either premise fails: report
premise_still_valid: falsewith the measurement. Do not hard-do it, and do not quietly switch to the other option.Suggested route (advisory — measurement beats it)
In
filter-comparand-type.tstheLIST_COMPARAND_OPERATORSbranch already knows the operator is list-shaped; theArray.isArray(comparand)guard silently falls through today. Make the else arm a loud refusal reusing the existing envelope (INVALID_FILTER, 400) and the #5869 wording, then haveassertListComparandShapesdelegate. Keep the message's front-loaded shape — the 500-char client bound is real (#5423).Acceptance
parseFilterAST([['name', 'notin', 'alpha']])(and thenin/not_in/not inspellings) answers a codedINVALID_FILTER/ 400 refusal, with a test that asserts bothcodeandstatus, not just that it throws.pnpm --filter @objectstack/driver-memory testgreen, withmemory-filter-ast-vocabulary.test.tsexercising every membership spelling on valid input.engine-comparand-type-door.test.ts, the 数据 API:集合算子not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 suites) — one implementation, unchanged verdicts.mingo@7.2.4via a temporary local override to confirm the escape is gone, then revert the override — report the reading in the PR body, commit no manifest or lockfile change.node scripts/pm/dispatch-gates.mjsat dispatch time, re-take for your actual diff):pnpm check:test-source-alias,pnpm check:type-source-resolution,node scripts/docs-audit/check-affected-docs.mjs; and because a test file moves:pnpm check:query-options-erasure,pnpm check:type-check-coverage,pnpm check:type-check-debt(needs the workspace built first),pnpm check:engine-double-contract,pnpm check:where-matcher.Re-check commands for the premises above
Links
BigIntcrashes with a raw mingoTypeError, and four other types silently answer zero rows #7872, closed by PR feat(spec): comparand-type door — the accepted literal comparand set, enforced once at the shared compile face for all five drivers (#7872) #8234not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 / PR fix(objectql): 集合算子的标量比较值答 400 INVALID_FILTER 并点名期望形状,不再 500 (#5869) #6209