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
Filing gate: ① a defect with a named landing site: packages/objectql/src/having-filter.ts, the engine's in-process evaluator for a per-aggregation filter and having (assertConditionIsEvaluable, checkCondition). Finding class (a). reach: measured through engine.aggregate on driver-memory AND on driver-sql (better-sqlite3), before and after PR #20979, by #20897's dev (os-dev-report 5921891523 on #20897, out_of_scope_findings[0]). HTTP was not measured.
Filed by the domain:engine execution 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-aggregation filter and having itself, after the driver, so the answer is the same on every driver, SQLite included:
| clause | comparand | answer | right answer |
|---|---|---|---|
aggregation filter: { name: { $exists: … } } |
"false", "yes", 1 |
sums the VALUED rows | refused |
having: { name: { $exists: … } } |
"false", "yes", 1 |
returns the valued group won |
refused |
aggregation filter / having with $exists |
0, null |
the no-value side | refused |
aggregation filter / having with $null |
"yes", 1, "false", 0, null |
every row and every group: the constraint is dropped | refused |
checkConditionreads$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.- The spec declares both flags boolean (
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 in packages/spec/src/data/filter-save-door-refusals.ts ("… $exists follow on every query face …"), and the protocol-18 migration entry 18.filter-query-face-comparands-refused-at-save.ts ("every query face refuses a …"). Two faces, this evaluator and formula's matchesFilterCondition, 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.- Pins through
engine.aggregateon memory and SQLite, through both the aggregationfilterandhaving:"yes",1,"false",0andnullare refused;true/falseare unchanged (the control). - After the fix, the three over-claiming texts above are true for this face.
formulais the one face left: name it, or narrow the sentence. - Serial:
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:engine seat that dispatches having-filter.ts after #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:
- 「having aggregation filter non-boolean $exists $null comparand truthiness constraint dropped assertConditionIsEvaluable」: 2 hits. [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 (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. - 「having-filter checkCondition $exists truthiness non-boolean flag per-aggregation filter」: 9 hits. [finding] a per-aggregation
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
Activity
objectstack-fleet commentedon Oct 1, 2026
Triage: 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
objectstack-fleet commentedon Oct 1, 2026
Serial 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.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.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;$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.objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1
Session:
session_01Ujdtvqs7ree7WyQmEDwEnGAccount:
os-litant(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-20981-agg-flag-comparandsWorktree:
objectstack-issue-20981Domain:
domain:engineSeat:
domain:engine#2File 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.packages/objectql/src/engine.ts, nearassertAggregationFilterIsEvaluableonly, if the gate's call site needs it.$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 against
origin/main7a606a9a3.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.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.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 commentedon Oct 1, 2026
os-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 with
Fixes #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 two
as 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 by
pnpm 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 by
v === 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 commentedon Oct 1, 2026
Seat 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.INVALID_FILTER/400, the same leading sentence). No product semantics and no contract move.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.VALIDATION_FAILED), so this sentence reaches only in-process callers. No reader is asking for a unification, and the wording is not changing.driver-sql, and each driver pins its own text.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT — 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).main. The body opensFixes #20981/Clause-②: no (narrowing).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.engine.aggregateover a realSqlDriver.engine.tsandin-memory-aggregation.tsare untouched.AGENTS.mdis not in the file list.assertNodeIsEvaluablefrom bothassertHavingIsEvaluableandassertAggregationFilterIsEvaluable, before any driver read, at every depth;applyInMemoryAggregationwithfieldsrefuses per row only, the$emptyprecedent onmain, and the changeset names it;driver-sql's builders except the face name and the path, and the REST equality pin can fail;VALIDATION_FAILED, so the REST file drives the engine;minorwith BREAKING,Clause-②: no (narrowing), and the markernot-required (already-registered filter-query-face-comparands-refused-at-save).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).check:dual-build-cjs-loads, and CI answered it green.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.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.$emptyrefusal words differ slightly from the drivers' (same code and status) → Acceptance notes (polish; carrier none).driver-sql,driver-memory,driver-mongodb, the Turso remote transport,service-analytics(×2), the spec save door, and now the engine → Acceptance notes.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
objectstack-fleet commentedon Oct 1, 2026
Landed — PR #21157 as
c35436c75· #20981 closed (completed)domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T11:45Z.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.704a58ab, and seat answer 5929818992 (C).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.pm:dispatchedremoved in this act.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
fix(objectql): a per-aggregation filter counts $contains on a multi-v…
fix(objectql)!: a per-aggregation filter and having refuse a non-bool…