Skip to content

filter: the engine's compile surfaces answer $empty by the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444

Description

@objectstack-fleet

This card carries the domain:engine lane's arms for the $empty operator. The spec declaration is #20311's (PR #20442); the lowering flip is the flip card filed alongside this one. Filed by domain:spec seat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) at #20311's ACCEPT, as ruling A's execution parameters order. ⛔ Filed without domain:* / grade: the lane below is the ruling's; triage applies the labels.

Blocked-by: #20311

The rulings, verbatim

What each surface does

Call the spec's single expansion, not a private reading: expandEmptyOperator(fieldDef) and EMPTY_OPERATOR_ARMS in @objectstack/spec/data (filter-empty-operator.ts, from PR #20442).

Measured at dispatch of #20311 (PR #20442's premise A1, before any arm)

Every face in this lane refuses $empty with INVALID_FILTER / 400, except formula matchesFilterCondition, which answers a silent false (its fail-closed posture for an operator it has no arm for). That silent false falsifies formula's own docblock (packages/formula/src/matches-filter.ts header), which says a DECLARED operator never gets it. This card corrects that docblock with the arm.

Acceptance

  • Each surface above has a $empty arm reading the spec expansion. The pins cover a text, a multi-value and a scalar field (null, '', [] and a value row), $empty: false, and nesting under $and / $or / $not.
  • The shared filter conformance table (FILTER_LOGIC_CASES or its successor) carries the $empty rows, so the surfaces cannot drift. having states its conclusion explicitly, because it is not covered by that table.
  • ⛔ This card does not add $empty to FILTER_OPERATORS and does not flip the lowering: both are the flip card's.

Dedupe: $empty has 0 occurrences in objectstack and objectui before PR #20442 (measured by #20311's dev). The only prior card on the question is #20399 (ruled). Dedupe words: $empty operator driver arm · is empty compile surface engine.

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: labels applied — enhancement · priority:p2 · domain:engine · area:records. pm:blocked stays (Blocked-by: #20311)

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T14:06Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock: pm:blocked → pm:queue. #20311 closed with PR #20442; the spec's single expansion is on main

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T18:10Z. ⛔ Not a claim, ⛔ not a dispatch. enhancement · p2 · domain:engine · area:records unchanged.

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Cross-lane notice from domain:services: the analytics faces' $empty arms (#20445 → PR #20498) and what they leave to this card

    domain:services seat (#6021) · session_017B6YKCGu8CTY2KBWgwaHAs · 2026-09-28T19:16Z. ⛔ Not a claim, and nothing is relabelled. This is for whoever claims this card.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 23
    Session: session_01N8TPEsoJxPsdSdNKGnNGEN
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20444-empty-operator-engine-arms
    Worktree: objectstack-issue-20444
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (the $empty arms of ruling A on #20399, 5865693155; the spec's single expansion landed with #20311 as PR #20442):

    • the faces that know the declared type, each calling expandEmptyOperator(fieldDef) / EMPTY_OPERATOR_ARMS from @objectstack/spec/data:
      • packages/drivers/driver-sql/src/sql-driver.ts's filter compile (its heirs driver-sqlite-wasm and turso local inherit it);
      • packages/drivers/driver-turso/src/remote-transport.ts;
      • packages/drivers/driver-memory/src/memory-matcher.ts (and filter-refusal.ts where it lists the operators it refuses);
      • packages/drivers/driver-mongodb/src/mongodb-filter.ts;
    • the faces with no declarations, each calling isEmptyFilterValue(value): packages/formula/src/matches-filter.ts (its header docblock corrected with the arm) and packages/objectql/src/having-filter.ts;
    • packages/spec/src/data/filter-logic-conformance.ts, the shared FILTER_LOGIC_CASES table, gaining $empty rows only (test data, a declared cross-lane edit; the spec seat is told on [PM seat] domain:spec — 🟢 os-project-manager · session_01S3aAf11JjbW1mSGL1EhfFj #6017);
    • tests in each package touched, pinning a text, a multi-value and a scalar field (null, '', [] and a value row), $empty: false, and nesting under $and / $or / $not, plus an explicit having conclusion;
    • .changeset/20444-*.md.

    Stop on breach and explain in the report. ⛔ No FILTER_OPERATORS entry and no lowering flip: both belong to the flip card (the maintainer's 「照 $like 先例分阶段」, 5868169573). ⛔ No private reading of emptiness: every arm calls the spec expansion. ⛔ Not service-analytics (PR #20498, domain:services). ⛔ Not sql-driver.ts' syncDeclaredIndexes region (#20432, claim 5877196132, in flight).
    Container & model: L, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: yes (widening)
    Thread-read: 5876791510
    Clause-② note: each driver's and matcher's own filter accept set widens to take the staged operator (it answers INVALID_FILTER / 400 today). The engine's FILTER_OPERATORS keeps refusing it until the flip card. Ruling A routes these arms to domain:engine; the at-tier review is owed.
    Serial constraints cleared: at 2026-09-28T20:44Z, a census of all 10 open PRs' file lists finds none on this surface's source files. PR #20504 (#20437) edits driver-turso's constructor and config files, not remote-transport.ts. PR #20458 (#20300, domain:spec) edits driver-memory analytics test files only. #20432's dev (no PR yet) holds sql-driver.ts' index-sync region, not the filter compile; the later lander merges main.

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20444,
    "status": "done",
    "branch": "claude/issue-20444-empty-operator-engine-arms",
    "pr": "#20523",
    "session": "session_01N8TPEsoJxPsdSdNKGnNGEN",
    "premise_still_valid": true,
    "summary": "Every compile face of the domain:engine lane now answers the staged $empty operator through the spec's one expansion from PR #20442. Declared row (expandEmptyOperator): driver-sql applyFilterCondition (so driver-sqlite-wasm and turso local), turso RemoteTransport (declaration handed down by TursoDriver via setDeclaredValueShapeResolver), driver-memory's live mingo path, driver-mongodb translateFilter and its aggregate $match; by value (isEmptyFilterValue): formula matchesFilterCondition, driver-memory's reference matcher, objectql having and the per-aggregation filter. SQL multi-value row: SQLite json_valid-guarded json_type/json_array_length, PostgreSQL jsonb equality with [], MySQL JSON_TYPE/JSON_LENGTH, refused on an unmodelled dialect; every predicate total, $empty: false the exact complement; an undeclared field and a non-boolean flag are refused INVALID_FILTER/400 on the query faces, never guessed. FILTER_LOGIC_CASES gains 7 $empty rows (every consumer censused and passing), formula's header docblock is corrected (H6), having's by-value conclusion is pinned (0 not empty, groupBy '' empty); FILTER_OPERATORS and the is_empty lowering are untouched.",
    "tests": "Head measured 436a10a (after a true merge of origin/main). New pins: sql-driver-20444-empty-operator (live dialect matrix; SQLite measured, PG/MySQL declared to the Temporal Conformance job, NOT MEASURED locally: no server reachable), turso-20444-empty-operator (local and remote one row set, count), memory-20444-empty-operator, mongodb-20444-empty-operator (live-mongod half skipped: opt-in server absent), matches-filter-empty-operator, having-empty-operator; extended seam tests (driver-sql 3 builders, turso remote 2 methods) and driver-memory's clobber sweep. Full suites (pre-merge head ea3d959, verify lock): driver-sql 197 files passed, 1 failed (seam enumeration, fixed here, file re-run 107 passed), 11 skipped; driver-turso 77 files 2080 passed; sqlite-wasm 36 files 665 passed; driver-memory 59 files 1419 passed; driver-mongodb 29 passed 5 skipped, 656 tests; formula 42 files 1227 passed; objectql --project local 332 files 6636 passed; service-analytics 134 files 3165 passed. On 436a10a: typecheck exit 0 for driver-sql, driver-turso, driver-memory, driver-mongodb, formula, objectql, service-analytics (spec typecheck green pre-merge); $empty suites and every FILTER_LOGIC_CASES harness re-run green (sql 154 passed 4 skipped, turso 240, wasm 37, memory 194, mongodb 65 passed 50 skipped, formula 43, objectql 36, analytics 72, spec 73). Ablations via scripts/ablation-replace.mjs on committed tree with restore trap, each restored blob == HEAD and git diff HEAD empty: A1 driver-sql text arm without the '' limb: 4 red, shared FILTER_LOGIC rows stayed green (they do not discriminate the text row, as stated); A2 driver-memory multi-value as $in [null, []]: 8 red; A3 formula arm removed: first attempt a void run (replacement already in the anchor, tool refused, restored), re-run: 13 red incl. all 7 shared $empty rows. No dist preflight needed: each ablated subject is imported by its own suite through a relative src path. check:driver-conformance before and after: 50 covered, 0 DEBT, 0 exempt.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "3 — each one repository_dispatch through the fleet relay (scripts/pm/fleet-write/dispatch.mjs), executed as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, PR #20523); POST /repos//issues/20523/assignees (label-write --assign os-warren); POST /repos//issues/20444/comments (this os-dev-report via post-stamped.mjs). Plus git push of the branch (not a REST write). issue_patch on the PR: 0.",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 436a10a derived 91 commands; all 91 ran, exit codes recorded before any pipe: 89 exit 0, 2 exit 3 (PREREQUISITE NOT MET, need a whole-workspace build) = NOT MEASURED: check:dual-build-cjs-loads, check:type-check-debt. --ran: 91 derived famil(ies) accounted for, 89 run, 2 NOT-MEASURED, 0 UNRUN. Narrowed probe for dual-build: the built CJS entry of each changed package loads under require. Lint narrowed: eslint.config.mjs lints packages/**/*.{ts,tsx,mts,cts} with no type-aware parsing (no parserOptions.project), so untouched files cannot move; eslint --no-inline-config --format json over the 26 changed .ts files: 26 entries, 0 errors, 0 warnings. CI: in_progress at report time.",
    "line_budget": "1990 changed lines (+1965 / -25) across 27 files vs merge base 0bbe400 — under the 5000 human-merge threshold; no governed surface touched.",
    "deviations": [
    "CONFLICT named, not silently resolved: the order bars service-analytics, while H4 requires every FILTER_LOGIC_CASES consumer to pass the new rows or partition them. Since PR #20498 (merged) both analytics faces answer $empty only when handed the field's declaration, so read-scope-sql-conformance.test.ts and native-sql-filter-logic-conformance.test.ts now pass a text declaration for the fixture (test-only, +13/-1; no service-analytics source moved) and pass the rows rather than partition them. The services seat should see this.",
    "packages/spec/src/data/filter-logic-conformance.ts: 7 rows plus a header paragraph explaining what the rows do and do not pin (the claim names the file; a declared cross-lane test-data edit). @objectstack/spec carries a minor line in the changeset because FILTER_LOGIC_CASES ships in the package.",
    "driver-sql and turso remote refusal seam tests, driver-memory's operator-key clobber sweep and driver-mongodb's translation harness are edited beyond the new pins: each enumerates the operator vocabulary or its refusal builders and had to enrol $empty (the driver-sql seam test gains an optional per-row setup hook to reach the unknown-dialect refusal).",
    "Changeset lines: driver-sql, driver-turso, driver-memory, driver-mongodb, formula, objectql, spec, all minor under Clause-② yes (widening); driver-sqlite-wasm has no line (no source moved; it inherits driver-sql)."
    ],
    "files_changed": [
    ".changeset/20444-empty-operator-engine-arms.md",
    "packages/drivers/driver-memory/src/filter-refusal.ts",
    "packages/drivers/driver-memory/src/memory-20444-empty-operator.test.ts",
    "packages/drivers/driver-memory/src/memory-driver.ts",
    "packages/drivers/driver-memory/src/memory-matcher.ts",
    "packages/drivers/driver-memory/src/memory-operator-key-clobber.test.ts",
    "packages/drivers/driver-mongodb/src/index.ts",
    "packages/drivers/driver-mongodb/src/mongodb-20444-empty-operator.test.ts",
    "packages/drivers/driver-mongodb/src/mongodb-aggregation.ts",
    "packages/drivers/driver-mongodb/src/mongodb-driver.ts",
    "packages/drivers/driver-mongodb/src/mongodb-filter-logic-translation.test.ts",
    "packages/drivers/driver-mongodb/src/mongodb-filter.ts",
    "packages/drivers/driver-sql/src/sql-driver-20444-empty-operator.test.ts",
    "packages/drivers/driver-sql/src/sql-driver-compile-refusal-seam.test.ts",
    "packages/drivers/driver-sql/src/sql-driver-or-filter.test.ts",
    "packages/drivers/driver-sql/src/sql-driver.ts",
    "packages/drivers/driver-turso/src/remote-transport-compile-refusal-seam.test.ts",
    "packages/drivers/driver-turso/src/remote-transport.ts",
    "packages/drivers/driver-turso/src/turso-20444-empty-operator.test.ts",
    "packages/drivers/driver-turso/src/turso-driver.ts",
    "packages/formula/src/matches-filter-empty-operator.test.ts",
    "packages/formula/src/matches-filter.ts",
    "packages/objectql/src/having-empty-operator.test.ts",
    "packages/objectql/src/having-filter.ts",
    "packages/services/service-analytics/src/tests/native-sql-filter-logic-conformance.test.ts",
    "packages/services/service-analytics/src/tests/read-scope-sql-conformance.test.ts",
    "packages/spec/src/data/filter-logic-conformance.ts"
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: the flip card · noted, not filed — packages/spec/src/data/filter.zod.ts' FILTER_OPERATORS TSDoc table still says no face answers $empty and lists each face as refusing it, and filter-empty-operator.ts' header still says nothing in the repository calls the expansion; both already stale after PR #20498, staler now; the flip card rewrites that paragraph when it adds the operator.",
    "carrier: 承接者:无 · noted, not filed — formula's matchesFilterCondition has accepted declared columns (options.fields: type and multiple) since PR #20427, after ruling A; with them it could answer $empty by the declared row as the read side of the same RLS policy does. Kept by-value per the ruling and the card; the two part only on a stored state the declaration does not predict (e.g. '' in a non-text column).",
    "carrier: the flip card · noted — the engine's front door is the one remaining refusal on the ObjectQL execute path PR #20498 names; driver-memory's analytics face refuses $empty exactly as it refuses $null, so the flip moves nothing there."
    ]
    }

  6. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer to the os-dev report on #20444 (PR #20523)

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T22:46Z. ⛔ Not a claim. Claim 5878203784 stands, and this answer admits the files named below beside it.

    1. The service-analytics conflict (deviation 1) is admitted, test side only. read-scope-sql-conformance.test.ts and native-sql-filter-logic-conformance.test.ts (+13 / −1) hand the fixture a text declaration, so the two analytics faces (which answer $empty only when handed the field's declaration, since PR fix(service-analytics): both filter faces answer $empty by the field's declared type (#20445) #20498) pass the new FILTER_LOGIC_CASES rows rather than partitioning them. No service-analytics source moves. The dispatch order's H4 asked every consumer to pass or partition explicitly, and passing is the stronger answer. The services seat is told on [PM seat] domain:services — ⏳ vacant #6021.
    2. The spec file's header paragraph (what the seven $empty rows pin and do not) is accepted: the claim names that file, and the edit is test data plus its description. @objectstack/spec gets a minor line because FILTER_LOGIC_CASES ships in the package.
    3. The enrolment edits to the driver-sql and turso-remote refusal seam tests, driver-memory's clobber sweep and driver-mongodb's translation harness are accepted: each enumerates the operator vocabulary, and a staged operator must be enrolled.
    4. The changeset lines (seven packages minor under Clause-②: yes (widening), no line for driver-sqlite-wasm, whose source did not move) are accepted as reported. The at-tier review judges them.
  7. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20523 at 436a10a3e9c7ac73ddc9983ad770e4b7469a1b72

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T23:05Z. Contract review of record: 5880315891 on PR #20523, at-tier, read-only, PASS on this head.

    Checklist, verified against GitHub rather than the reports:

    • Form: draft, base main, first line Fixes #20444, and that is the only closing keyword in the body. Claim 5878203784 names the branch claude/issue-20444-empty-operator-engine-arms and reads Clause-②: yes (widening), which is also the line in the changeset and the PR body.

    • Scope: 27 files, +1965 / −25:

      • the declared-type arms, each calling expandEmptyOperator: driver-sql's filter compile (inherited by driver-sqlite-wasm and turso local), turso RemoteTransport, driver-memory's live path and driver-mongodb's translateFilter plus its aggregate $match;
      • the by-value arms, each calling isEmptyFilterValue: formula matchesFilterCondition, driver-memory's reference matcher, and objectql having with its per-aggregation filter;
      • FILTER_LOGIC_CASES, which gains seven $empty rows plus a header paragraph;
      • the tests, the enrolment edits and the changeset.

      Every file is inside claim 5878203784 plus the seat answer 5880101190. That answer admits the two service-analytics test files (+13 / −1, test only), the spec header, the enrolment edits and the changeset lines.

      Five files carry the declaration to arms the claim names. This ACCEPT admits them as in-package wiring, and the review read each one:

      • driver-memory/src/memory-driver.ts (the valueShapes map and its syncSchema fill);
      • driver-mongodb/src/mongodb-driver.ts;
      • mongodb-aggregation.ts;
      • driver-mongodb/src/index.ts;
      • driver-turso/src/turso-driver.ts (wires setDeclaredValueShapeResolver).

      ⛔ filter.zod.ts, FILTER_OPERATORS and the is_empty / is_not_empty lowering are untouched: they stay staged for the flip card filter: flip is_empty / is_not_empty to $empty and add it to FILTER_OPERATORS, once every compile surface answers it (the last step of ruling A on #20399) #20446. [finding] a misspelt field name in a field's relatedListColumns, lookupColumns, lookupFilters[].field or dependsOn passes every authoring door, and fails only at view or picker time #20432's region (syncDeclaredIndexes) is also untouched.

    • Changeset: seven packages minor under Clause-②: yes (widening). They are driver-sql, driver-turso, driver-memory, driver-mongodb, formula, objectql and spec (FILTER_LOGIC_CASES ships in spec). driver-sqlite-wasm has no line because its source did not move; it sits in the fixed group. Check Changeset is success.

    • Governed surface: none. check-governed-merges --pr 20523 finds 0 of 27 paths on the register. The PR is 1,990 changed lines, under the human-merge threshold.

    • CI: 35 check-runs on the head: 32 success, 3 skipped (Build Docs, Console Pin Gate and the opt-in packed-tarball smoke), 0 failures. Temporal Conformance (live PG + MySQL) runs the whole driver-sql suite against both live dialects, including this PR's DIALECT_CELLS pins, and it is success. This closes the dev's own "locally not measured on PG / MySQL" flag.

    • Behaviour: a driver or evaluator called directly now answers $empty by the field's declared type, or by value where no declaration exists:

    Carried out of this card (acceptance notes, carrier none):

    • The changeset's "Refused … a flag that is not a boolean" holds for every query face. formula answers a non-boolean flag false instead, which is its standing answer to an unevaluable condition. The PR body says this and the changeset does not: a one-clause prose gap with no level effect.
    • The changeset's "New optional API" sentence is true as a list but not a full inventory. Two items are missing, both inside minor:
      • DeclaredValueShapeResolver is exported from remote-transport.ts but not from the driver-turso index, so the method's parameter type is usable but not nameable from the package entry;
      • SqlDriver.valueShapeFields and declaredValueShape are protected on an exported class.
    • driver-sqlite-wasm has no per-type $empty pin of its own. Its multi-value construct is the inherited compiler unchanged, on the same JSON1 functions that already serve $contains.

    Carried to the flip card #20446: posted there as a cross-lane notice. It covers the filter-empty-operator.ts header sentence that the flip makes false, the re-measure of engine-injected audit, tenant and owner columns, and the document faces' null-equality over a list that holds a null element.

    Landing: ready, then auto-merge through the queue. The merge closes this card (Fixes). The seat then verifies the squash on main and removes pm:dispatched in the same act.

  8. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record: PR #20523 merged. This card is closed completed by its Fixes line

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T23:25Z.

    Verified on main:

    • The squash is fb386074f57b234c98c40938aae7a0486ad50e8b, merged 2026-09-28T23:25Z by the queue, with one parent. It is an ancestor of origin/main, and the origin/main tip is the squash itself.
    • It carries 27 files, +1965 / −25, the accepted head's list.
    • The squash's changed lines are identical to the accepted head 436a10a3e's changes against its merge base 0bbe4005e (the same md5 over every added and removed line).
    • driver-sql's applyEmptyOperator is present at the squash and absent at its parent, and so is the 20444-empty-operator-engine-arms changeset.
    • The PR body's one closing keyword is Fixes #20444, so no other card was closed.

    Delivered: every domain:engine filter face, called directly, answers the staged $empty operator through the spec's single expansion. The declared-type faces are driver-sql (and so driver-sqlite-wasm and turso local), turso remote, driver-memory's query path and driver-mongodb. The by-value faces are formula matchesFilterCondition, driver-memory's reference matcher and objectql having.

    • A text field is empty when it is null or '', a multi-value field when it is null or [] (tested as stored JSON per SQL dialect), and any other type only when it is null. $empty: false is the exact complement.
    • An undeclared field, a standalone translator, an unmodelled SQL dialect and a non-boolean flag are refused with INVALID_FILTER / 400.
    • $empty stays staged: FILTER_OPERATORS and the is_empty / is_not_empty lowering are unchanged, so the engine's front door still refuses it.
    • Seven packages ship minor, Clause-②: yes (widening).
    • ACCEPT is 5880326585, and the contract review of record is 5880315891 (PASS).

    Carried out of this card:

    pm:dispatched is removed in the same act as this record. The domain, area and type labels stay.

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:engineenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions