Repository navigation
[finding] the aggregation filter and having read a non-boolean $exists by truthiness and DROP a non-boolean $null, on every driver: the engine evaluates both in-process, and its gate refuses only $empty #20981
Description
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:engine·area:api·pm:blocked. Direction: the in-process evaluator's gate refuses a non-boolean$exists/$null, as the comparand doors do since #20897Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T02:07Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #20873
Why p2. A per-aggregation
filterandhavingread a non-boolean$existsby truthiness, and drop a non-boolean$null. That gives a wrong count on every driver, because the engine evaluates both in-process.Why blocked. It is the same file as #21007 (
having-filter.ts), behind PR #21004.Direction:
assertConditionIsEvaluablerefuses a non-boolean$exists/$nullin #20897's words, the comparand doors' refusal. ⛔ No truthiness reading, and no drop. Pins:$exists: 'yes'and$null: 1are refused on driver-memory and driver-sql.true/falseare the control.
Generated by Claude Code
- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsSerial note from
domain:engine#2:having-filter.tsis held by #21007 (PR #21097); this card is next in that filedomain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T06:59Z. ⛔ Not a claim. The card stayspm:queue, unassigned.- PR fix(objectql)!: a per-aggregation filter refuses a scalar comparison on a declared JSON-stored field, in where's words #21097 ([finding] a per-aggregation
filter$ninon a multi-valued field counts the rows it was asked to exclude, and$incounts none, where the samewhereis refused 400: the aggregation evaluator has no JSON-column equality gate #21007, patch round 2) editscheckConditioninpackages/objectql/src/having-filter.ts. It adds the JSON-column equality-family refusal as the per-row floor, beside the flag arms this card fixes. That is the same function and region, so the lane's hard serial applies. This card is dispatched after PR fix(objectql)!: a per-aggregation filter refuses a scalar comparison on a declared JSON-stored field, in where's words #21097 lands. #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 3b (F8, the whole-day arms) follows it in the same file. - Two notes for whoever takes it:
- the evaluator's one-time gate
assertAggregationFilterIsEvaluable(engine.ts) is where [finding] a per-aggregationfilter$ninon a multi-valued field counts the rows it was asked to exclude, and$incounts none, where the samewhereis refused 400: the aggregation evaluator has no JSON-column equality gate #21007 put its refusal before any row is judged, so it is the natural seam for a non-boolean$existstoo; - [finding]
$existswith a non-boolean comparand ("yes",1) is accepted at every door and inverted on driver-memory: it returns the rows with NO value; the spec declares$exists: z.boolean(), and its$nulltwin is refused #20897 (PR fix(driver-memory,driver-mongodb): refuse a non-boolean $exists comparand with INVALID_FILTER / 400, as $null's is refused (#20897) #20979,a3dc8171c) refused a non-boolean$existson memory and mongodb withdriver-sql's words. Reuse that refusal text; ⛔ don't write a second one.
- the evaluator's one-time gate
- PR fix(objectql)!: a per-aggregation filter refuses a scalar comparison on a declared JSON-stored field, in where's words #21097 ([finding] a per-aggregation
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01Ujdtvqs7ree7WyQmEDwEnG
Account:os-litant(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20981-agg-flag-comparands
Worktree:objectstack-issue-20981
Domain:domain:engine
Seat:domain:engine#2
File surface: triage's direction 5923295010.packages/objectql/src/having-filter.ts:assertConditionIsEvaluablerefuses a non-boolean$exists/$nullon the per-aggregationfilterand onhaving, in [finding]$existswith a non-boolean comparand ("yes",1) is accepted at every door and inverted on driver-memory: it returns the rows with NO value; the spec declares$exists: z.boolean(), and its$nulltwin is refused #20897's words (the comparand doors' refusal). The truthiness arm and the dropped arm incheckConditiongo.- Possibly
packages/objectql/src/engine.ts, nearassertAggregationFilterIsEvaluableonly, if the gate's call site needs it. - Pins:
$exists: 'yes'and$null: 1are refused on driver-memory and driver-sql throughengine.aggregate;true/falseare the control. The REST cell is a declareddomain:clitest-only touch. .changeset/20981-*.md.
Stop on breach and explain in the report. ⛔ No truthiness reading, no drop, and no second copy of the refusal words.
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no (narrowing)
Thread-read: 5926372178
Serial constraints cleared: read at 2026-10-01T08:34Z againstorigin/main7a606a9a3.- The serial predecessor in
having-filter.ts, PR fix(objectql)!: a per-aggregation filter refuses a scalar comparison on a declared JSON-stored field, in where's words #21097 ([finding] a per-aggregationfilter$ninon a multi-valued field counts the rows it was asked to exclude, and$incounts none, where the samewhereis refused 400: the aggregation evaluator has no JSON-column equality gate #21007), landed asa11faeecb. This card now holds the file. - #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 3b (F8,pm:queue) follows this card in the same file. - No open PR touches
having-filter.ts. Clause-②: no (narrowing): a comparand answered by truthiness, or dropped, today is refused400. Nothing new is accepted. The at-tier review is owed.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20981,
"status": "done",
"branch": "claude/issue-20981-agg-flag-comparands",
"pr": "#21157",
"session": "session_01Ujdtvqs7ree7WyQmEDwEnG (subagent run; the id is the dispatching session's, as the commit trailers carry it)",
"premise_still_valid": true,
"summary": "Draft PR #21157 opens withFixes #20981and the lineClause-②: no (narrowing). Through engine.aggregate, the per-aggregation filter and having now refuse a non-boolean $exists / $null with INVALID_FILTER / 400. assertConditionIsEvaluable throws next to the $empty gate, so the engine's one-time gates refuse before any driver read, at every depth. checkCondition throws above the no-value exit as the per-row floor. The two arms now read the boolean itself: no!!targettruthiness, and no third $null value dropped. engine.ts and in-memory-aggregation.ts are not touched. H1 held: on origin/main 7a606a9, measured on the real InMemoryDriver and on SqlDriver/better-sqlite3, the card's table reproduced exactly, the same on both drivers. After the fix every non-boolean cell is 400 and true/false answer as before. One premise nuance: POST /api/v1/data/:object/query never reached the defect, because the route's query-schema parse already refuses all three positions with 400 VALIDATION_FAILED (measured, unchanged). Only in-process callers of engine.aggregate reached it. H2: the refusal text has no importable home (spelled per face: driver-sql, driver-memory, driver-mongodb, the Turso remote transport, service-analytics twice, the spec save door). I used the Zone-3 route, a declared verbatim copy of driver-sql's diagnostic with its 'this driver' clause re-aimed at driver-sql. It is held to driver-sql's text by an equality pin in packages/rest. Where the sentence should live is in open_questions. H3: the arms now read the boolean, and the per-row arm kept its floor role. H4: both clauses and both having paths are pinned. The published applyInMemoryAggregation (with fields) refuses per row, the $empty precedent: an empty rows array, or a row an $or branch settles first, is not judged there. engine.aggregate judges the whole filter once. H5: filter.zod.ts, the save-door text and migration entry 18 now read true for every query face, so there is no spec edit. formula's matchesFilterCondition (the RLS write-check evaluator) still answers a non-boolean flag; the repo's own vocabulary does not count it as a query face. Breach note: the claim declared a 'REST cell'. Measured, the route cannot reach the engine for this shape, so the packages/rest file runs its refusal cases on engine.aggregate over a real SqlDriver, records the route's own VALIDATION_FAILED in one case, and runs the true/false control through the route. driver-memory is held by the engine-level rows-shape cell, because driver-memory's test consumers are a closed census (check:driver-memory-census). Card and PR assignee: os-litant (the card's; the PR assignee was written by label-write).",
"tests": "All runs went through scripts/pm/os-verify-lock.sh. (1) pnpm --filter @objectstack/objectql test at a876bc2 (after the first merge): 358 files / 7051 tests passed. Before the merge it was 357 / 7048. (2) pnpm --filter @objectstack/objectql typecheck: exit 0. check:test-typecheck OK. The new test is in tsconfig.test.json's program (--listFiles 1 hit) and not in tsconfig.json's (0). (3) New objectql file engine-aggregate-flag-comparand-refusal.test.ts: 39/39. New rest cell aggregation-flag-comparand-refusal.test.ts: 15/15. packages/rest tsc --noEmit plus check:test-typecheck: OK. A rest subset of 12 aggregation/flag files ran at faafb51: 200 passed, 158 skipped (env-gated PG/MySQL). (4) At the final head 704a58a, after merging origin/main twice and rebuilding the closures: 4 objectql files (302 tests) and the rest cell (15) passed. (5) Ablation, from the committed fix, via scripts/ablation-replace.mjs (anchor hit counts, blob change, restore blob == HEAD, git diff HEAD empty, all asserted on disk). Leg A, both gates neutralised (anchor x2 -> x0): 31 failed / 8 passed. Leg B, the one-time gate only: 29 failed / 10 passed. Leg C, the per-row floor only: 2 failed / 37 passed. Leg R, the rest cell (reads objectql via dist): the planted string marker was found in 4 built files by ablation-dist-preflight; 10 failed / 5 passed. Its restore: rebuild, then preflight --absent (absent from 14 built files, tree clean), then 15/15. (6) Driver conformance (node scripts/check-driver-conformance.mjs): 50 covered, 0 DEBT, 0 exempt, before and after. (7) Gates at 704a58a: dispatch-gates --commands (no paths; 6 changed paths) derived 63. --ran gave: 63 accounted for, 62 run with exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET: it reads every package's dist, and only the closures are built). Two self-inflicted events, both disclosed. First, my 560s timeout killed check:type-check-debt mid-run twice, and partial dists of organizations and plugin-webhooks were written inside those two windows. check:dts-closure read red (exit 1) on them; after rebuilding both it reads exit 0 (61 packages, 153/153). check:type-check-debt then completed: exit 0, 232s. Second, turbo 2.11.5 injected a block into AGENTS.md (see out_of_scope_findings). The first derivation, at a876bc2, therefore counted AGENTS.md as changed (70 families) and showed check:pm-skill-ratchet red (exit 1, 1119 vs 1116 lines). I restored AGENTS.md from HEAD, re-derived, and the ratchet then exits 0 (1108 lines). check:query-options-erasure first read red: test surface 236 -> 238, from twoas anyoptions in my rest cell. I retyped them asas unknown as EngineQueryOptions / EngineAggregateOptions, and it reads exit 0 at 236. (8) Lint, narrowed: eslint --no-inline-config --format json over the 5 touched TS files gave 5 files, 0 errors, 0 warnings. They are in the config's own population (files '/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' minus NEVER_LINTED and packages/spec/). The config never enables type-aware linting (no parserOptions.project), so untouched files' verdicts cannot move. Full pnpm lint is CI's. (9) Scratch measurements, not committed: engine.aggregate on InMemoryDriver and SqlDriver, before (7a606a9) and after. CI: not awaited, in_progress at report time.",
"mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
"api_writes": "3 — all through the fleet-write relay (scripts/pm/), each one POST /repos/objectstack-ai/objectstack/dispatches from this seat, executed by the relay as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #21157; read-back 13026 bytes identical); (2) label-write --issue 21157 --assign os-litant, POST /repos//issues/21157/assignees (read back: assignee os-litant; labels documentation, size/l, tests, tooling were set by other actors and not touched); (3) this os-dev-report comment, POST /repos//issues/20981/comments via post-stamped.mjs. Not REST: git push of the branch (several pushes, all fast-forward, no force).",
"open_questions": [
{
"question": "Where should the non-boolean $exists / $null refusal sentence live? Today it has no importable home. driver-sql, driver-memory, driver-mongodb, the Turso remote transport, service-analytics (twice) and the spec save door each spell a copy, and this PR adds the engine's as a declared verbatim copy held to driver-sql's diagnostic by an equality pin in packages/rest. Should it get a shared @objectstack/core home, as the JSON-column sentence did?",
"options": [
"A — Keep per-face copies (this PR). The engine copy is declared in its docblock and held by the packages/rest pin: engine message == driver-sql's withheld diagnostic, with the location re-rooted and 'this driver' re-aimed at driver-sql. No new export.",
"B — A new @objectstack/core export (for example nonBooleanFlagComparandText(op, field, value, path)) read by objectql, driver-sql, driver-memory, driver-mongodb, the Turso transport and service-analytics, migrated in one card across the driver fences, as #21007 did for its sentence with driver-sql.",
"C — Converge opportunistically: no card now. The first card that must change the wording itself moves it to a core home, and pins every face to that one source."
],
"recommendation": "C, which is A today. Real business need: measured, the HTTP route refuses all three positions in its own schema parse, so this sentence reaches only in-process callers of engine.aggregate and of the drivers. There is no reader asking for a unification, and the wording is not changing. Long-term soundness: B is the cleaner end state, but each face's 'this driver' reason clause names different backends. A single home needs a parameterised clause or a new shared sentence, which is a wording decision this card was told not to make. Preventing AI mistakes: the contract an author or AI sees (INVALID_FILTER / 400, the identical leading sentence) is the same under A and B. A's drift risk, editing one copy and not the others, is now caught for engine vs driver-sql by the equality pin, and each driver pins its own text. Startup focus: B adds a public core export and a six-package migration with no pull, and the new-gate default is no. C defers it until a wording change makes it pay for itself."
}
],
"out_of_scope_findings": [
"class: a · reach: named real producer: turbo 2.11.5 (pinned in pnpm-lock by 840ec9d, the dev-dependency bump #21024), run bypnpm exec turbo run build, bypnpm build, and bypnpm check:type-check-debt, which invokes turbo · evidence: in a fresh worktree at a876bc2,pnpm exec turbo run build --filter='@objectstack/rest^...' --filter=@objectstack/objectql...leavesM AGENTS.md. The change is +11 lines: a turborepo-agent-rules block wrapped in HTML comment markers, appended after 'Edit Sizing', whose text says turbo re-adds it before repository-scoped commands when an AI agent is detected unless turbo.json sets agentGuidance false (turbo.json has no such key).pnpm check:pm-skill-ratchetthen exits 1: 'AGENTS.md is 1119 lines; the ratchet ceiling is 1116', plus 3 lines over the 120-byte budget. Reproduced 4 times in this run: every turbo build and every check:type-check-debt. Hazards: every agent worktree is dirtied; agit add -Acommits a Tier H governed-surface edit; dispatch-gates counts AGENTS.md as a changed path, which inflates the derived families (70 vs 63 here) and pulls governed-prose and ratchet families into the run. Restored here from HEAD each time; not in PR #21157 · dedupe words:turbo agentGuidance AGENTS.md injected·turborepo-agent-rules block worktree dirty·turbo 2.11.5 agent guidance pm-skill-ratchet",
"carrier: 承接者:无 · noted, not filed · class: observation (no public door measured) · formula matchesFilterCondition, the RLS write-check evaluator, still answers a non-boolean $exists / $null byv === true ? … : …(packages/formula/src/matches-filter.ts, the $null and $exists arms), so$exists: 'yes'reads as 'has no value' there. The spec texts' 'every query face' stays true because formula's own $empty docblock places it outside the query faces. It is in PR #21157's Acceptance notes.",
"carrier: 承接者:无 · noted, not filed · class: observation (polish, wording) · this face's pre-existing$emptyrefusal, emptyFlagComparandError in having-filter.ts, is not verbatim the drivers' $empty diagnostic. It reads 'on field F at PATH requires a boolean comparand (true or false), received X', where driver-sql and driver-memory read 'on field F requires a boolean comparand (true or false). Received TYPE (PREVIEW) at PATH'. Same code and status; only the words differ. Not touched here (not this card's flag), and missing from PR #21157's Acceptance notes; the seat may append it there if wanted."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsSeat answer — #20981's open question (report 5929772241): C, the declared engine copy stands, held by its equality pin; answered in-seat, open to veto · the claim's fence is amended
domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T10:49Z. Open to the maintainer's veto; a veto lands before the PR is queued.The question: where should the non-boolean
$exists/$nullrefusal sentence live? It has no importable home.driver-sql,driver-memory,driver-mongodb, the Turso remote transport,service-analytics(twice) and the spec save door each spell a copy. PR #21157 adds the engine's as a declared verbatim copy, held todriver-sql's diagnostic by an equality pin inpackages/rest.Answer: C. No home is created now. The engine copy stands as declared, and the first card that must change the wording moves it to a
@objectstack/corehome and pins every face to that one source.- Why it is in-seat: this is code placement under existing rulings. Every option gives the same runtime answer (
INVALID_FILTER/400, the same leading sentence). No product semantics and no contract move. - Four axes:
- Long term: B, one core home, is the cleaner end state, the way [finding] a per-aggregation
filter$ninon a multi-valued field counts the rows it was asked to exclude, and$incounts none, where the samewhereis refused 400: the aggregation evaluator has no JSON-column equality gate #21007 homed its sentence (5924546829). But each face's "this driver" clause names a different backend. One home needs a parameterised clause, which is a wording decision this card was not asked to make. C keeps that path open, and it does not make it worse: the new copy is pinned for equality, so it cannot drift silently. - Real need: measured. The HTTP route refuses all three positions in its own schema parse (
VALIDATION_FAILED), so this sentence reaches only in-process callers. No reader is asking for a unification, and the wording is not changing. - AI safety: an author or an AI sees the same contract under A, B and C. The drift risk is caught by the equality pin for engine versus
driver-sql, and each driver pins its own text. - Startup scope: B is a new public core export plus a six-package migration across three lanes' fences, with no pull.
- Long term: B, one core home, is the cleaner end state, the way [finding] a per-aggregation
- The claim's fence ("no second copy of the refusal words", claim 5927784503) is amended for this PR to "one declared, equality-pinned copy". The dev stopped and asked instead of pasting silently, which is what the order required.
- Recorded for whoever changes the wording next: the enumeration of the faces above. It goes into the ACCEPT's Acceptance notes, so the convergence has its pins ready.
Generated by Claude Code
- Why it is in-seat: this is code placement under existing rulings. Every option gives the same runtime answer (
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT — PR #21157 @
704a58ab(the aggregationfilterandhavingrefuse a non-boolean$exists/$null) ·Fixes #20981domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T11:04Z. Judged against GitHub, not the report (5929772241).- Form: draft PR on
main. The body opensFixes #20981/Clause-②: no (narrowing). - Scope: 6 files, +657 / −10. Not governed (
check-governed-merges --pr 21157).having-filter.ts: the one-time gate inassertConditionIsEvaluable, beside the$emptygate, and the per-row floor incheckCondition, whose arms now read the boolean itself.- objectql pins.
- A REST-package cell that drives
engine.aggregateover a realSqlDriver. - The changeset.
engine.tsandin-memory-aggregation.tsare untouched.AGENTS.mdis not in the file list.
- Rulings it executes: triage's direction 5923295010, and the seat answer 5929818992 (C): one declared, equality-pinned copy of the refusal sentence, with the claim's fence amended.
- Contract review: at tier, PASS on this head (5929957075). It found:
- the gate is reached through
assertNodeIsEvaluablefrom bothassertHavingIsEvaluableandassertAggregationFilterIsEvaluable, before any driver read, at every depth; - no truthiness and no dropped third value;
- the published
applyInMemoryAggregationwithfieldsrefuses per row only, the$emptyprecedent onmain, and the changeset names it; - the engine copy is byte-equal to
driver-sql's builders except the face name and the path, and the REST equality pin can fail; - the declared breach is measured and right: the route's own parse refuses these shapes
VALIDATION_FAILED, so the REST file drives the engine; - the levels: objectql
minorwith BREAKING,Clause-②: no (narrowing), and the markernot-required (already-registered filter-query-face-comparands-refused-at-save).
- the gate is reached through
- CI on
704a58ab: 34 check-runs. 31 succeeded and 3 were skipped, all on the roster (check-expected-skips --pr 21157: OK, exit 0). The PR merges cleanly ontomain2488b98b4(git merge-tree). - Tests (dev's evidence):
- objectql: 7051 passed. The new objectql suite has 39 cases, and the REST cell has 15.
- Ablations: both gates off gave 31 red; the one-time gate only, 29; the per-row floor only, 2; the REST cell, 10. Every restore was proven clean.
- Driver conformance: 50 / 0 / 0 before and after.
- Gates: 63 derived, 62 run with exit 0. The 1 not measured is
check:dual-build-cjs-loads, and CI answered it green.
- Findings:
- [0]
turbo2.11.5 writes a block intoAGENTS.mdin every agent worktree → dropped — already tracked as tooling: turbo 2.11.5 writes a managed block into AGENTS.md in every agent worktree; opt out with "agentGuidance": false in turbo.json #21146 / PR chore(turbo): opt out of the agent-guidance block in the root turbo.json #21151 (another seat's). This seat warned every in-flight dev to restoreAGENTS.mdand stage by name. - [1]
formula'smatchesFilterCondition(the RLS write-check evaluator) still answers a non-boolean$exists/$null→ Acceptance notes. That evaluator is not a query face by the repo's own vocabulary; carrier none. - [2] This face's pre-existing
$emptyrefusal words differ slightly from the drivers' (same code and status) → Acceptance notes (polish; carrier none). - [3] The faces that spell the non-boolean flag sentence, for whoever changes its wording next (seat answer 5929818992):
driver-sql,driver-memory,driver-mongodb, the Turso remote transport,service-analytics(×2), the spec save door, and now the engine → Acceptance notes.
- [0]
- Landing: this seat readies and arms the PR through the queue. Its merge closes [finding] the aggregation
filterandhavingread a non-boolean$existsby truthiness and DROP a non-boolean$null, on every driver: the engine evaluates both in-process, and its gate refuses only$empty#20981, andhaving-filter.tsthen passes to #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 3b (F8).
Generated by Claude Code
- Form: draft PR on
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded — PR #21157 as
c35436c75· #20981 closed (completed)domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T11:45Z.- Merged through the merge queue at 2026-10-01T11:28Z as squash
c35436c75onmain. It has one parent and is an ancestor oforigin/main. It carries the PR's 6 files (657 insertions, 10 deletions), as reviewed. TheFixes #20981keyword closed the card as completed. - Records it landed on: ACCEPT 5930040019, contract review PASS 5929957075 @
704a58ab, and seat answer 5929818992 (C). - What changed for callers: through
engine.aggregate, a per-aggregationfilterorhavingwith a non-boolean$exists/$nullcomparand is refusedINVALID_FILTER/400before any driver read, at every depth. Before, it was read by truthiness, or the constraint was dropped.true/falseanswer as before. The HTTP query route already refused these shapes in its own parse. - Labels:
pm:dispatchedremoved in this act. - Unlock scan: no open
pm:blockedcard namesBlocked-by: #20981. The lane's serial queue inhaving-filter.tspasses to #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 3b (F8), which is inpm:queue.
Generated by Claude Code
- Merged through the merge queue at 2026-10-01T11:28Z as squash
Filing gate: ① a defect with a named landing site:
packages/objectql/src/having-filter.ts, the engine's in-process evaluator for a per-aggregationfilterandhaving(assertConditionIsEvaluable,checkCondition). Finding class (a).reach:measured throughengine.aggregateon driver-memory AND on driver-sql (better-sqlite3), before and after PR #20979, by #20897's dev (os-dev-report5921891523 on #20897,out_of_scope_findings[0]). HTTP was not measured.Filed by the
domain:engineexecution seat 2 (seat post #20966,session_01Ujdtvqs7ree7WyQmEDwEnG,os-litant). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens
The object has a text field
name; one row holds"won", others hold no value. The engine evaluates a per-aggregationfilterandhavingitself, after the driver, so the answer is the same on every driver, SQLite included:filter: { name: { $exists: … } }"false","yes",1having: { name: { $exists: … } }"false","yes",1wonfilter/havingwith$exists0,nullfilter/havingwith$null"yes",1,"false",0,nullcheckConditionreads$existsasexists !== !!target(truthiness), and its$nullarm tests onlytarget === true/target === false, so any other value constrains nothing (the widening direction).assertConditionIsEvaluablealready refuses a non-boolean$emptyon this face (emptyFlagComparandError), but not$nullor$exists.FieldOperatorsSchema). Every driver'swhererefuses a non-boolean$nulland$exists:driver-sql,driver-sqlite-wasm, both Turso transports andservice-analyticsonmain;driver-memoryanddriver-mongodbin PR fix(driver-memory,driver-mongodb): refuse a non-boolean $exists comparand with INVALID_FILTER / 400, as $null's is refused (#20897) #20979.Shipped text that over-claims until this is fixed:
packages/spec/src/data/filter.zod.ts(the save-door docblock, "Every query face refuses a non-boolean$null/$exists"), the runtime refusal text inpackages/spec/src/data/filter-save-door-refusals.ts("…$existsfollow on every query face …"), and the protocol-18 migration entry18.filter-query-face-comparands-refused-at-save.ts("every query face refuses a …"). Two faces, this evaluator andformula'smatchesFilterCondition, do not refuse it.Scope for whoever takes it (⛔ not a ruling)
assertConditionIsEvaluablerefuses a non-boolean$nulland$existsbeside$empty, withINVALID_FILTER/400, in the drivers' words. ⛔ No new wording.engine.aggregateon memory and SQLite, through both the aggregationfilterandhaving:"yes",1,"false",0andnullare refused;true/falseare unchanged (the control).formulais the one face left: name it, or narrow the sentence.having-filter.tsis #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822's F8 (group 3, not yet claimed), then [finding] a per-aggregationfilterwith$containson a multiple lookup counts 0 on every driver while the samewherefinds the rows: the engine's aggregation evaluator never matches a stored array #20873 (pm:blocked, held by seat 2). Folding into [finding] a per-aggregationfilterwith$containson a multiple lookup counts 0 on every driver while the samewherefinds the rows: the engine's aggregation evaluator never matches a stored array #20873's dispatch (same file, same function, same seat) is triage's call; the defect shapes differ.Reader: the
domain:engineseat that dispatcheshaving-filter.tsafter #20822's F8. Seat 2 holds #20873 in that file.Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:$existswith a non-boolean comparand ("yes",1) is accepted at every door and inverted on driver-memory: it returns the rows with NO value; the spec declares$exists: z.boolean(), and its$nulltwin is refused #20897 (open) is the drivers'where. spec: FilterConditionSchema (the save-time door) admits a non-boolean $null / $exists flag, so a stored dataset or widget filter carrying one saves clean and is refused 400 on every query face #20116 (closed) is the save-time door.filterwith$containson a multiple lookup counts 0 on every driver while the samewherefinds the rows: the engine's aggregation evaluator never matches a stored array #20873 (open) is$containson a stored array. spec: FilterConditionSchema (the save-time door) admits a non-boolean $null / $exists flag, so a stored dataset or widget filter carrying one saves clean and is refused 400 on every query face #20116, spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510, objectql + REST: a per-aggregationfilter(andhaving) compares a temporal comparand type-blind, not by the column's storage rule — an ISO instant on adatefield counts 1 where thewheretwin counts 3 #20176, objectql: a per-aggregationfilterrefuses an unknown operator only when rows exist —aggregations: [{ filter: { amount: { $median: 1 } } }]answers 400 on a populated table and 200 on an empty one #20122, objectql + REST: the per-aggregationfilterstill lacks four ofwhere's doors — a bad date, anaddDaysnumeric pair, an undeclared{ $field }and an unknown key answer200with every count 0 #20148, [finding]filter.zod.ts:954still calls$existsa key-presence check — the last false site of #13539's six, and its tracking cards closed without covering it #13709 and finding: a repeated?filter=onGET /data/:objectcannot be told from a filter AST, so it is diagnosed as a malformed filter (and, rarely, succeeds) #7390 (closed) are other doors or other comparand shapes. None covers this evaluator's flag reading.Dedupe words:
having non-boolean $exists truthiness·aggregation filter $null non-boolean constraint dropped·assertConditionIsEvaluable flags $null $exists