Skip to content

[finding] driver-memory answers the equality and ordering family per element on a multi-valued field ($eq / $in / $nin / $gt on a multiple: true lookup), where driver-sql refuses all ten with 400 INVALID_FILTER #21066

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site: packages/drivers/driver-memory (filter-refusal.ts, the shape gate in front of every memory query face). Finding class (a). reach: engine.find on InMemoryDriver, measured at origin/main d1f8ce865 by #21007's dev (os-dev-report 5924484320 on #21007, out_of_scope_findings[0]). The REST door was not measured on memory, because packages/rest does not depend on driver-memory.

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 fixture is #21004's: owners is a multiple: true lookup over six rows (d1…d6), and tags is a tags field.

where InMemoryDriver SqlDriver (SQLite and PostgreSQL 16.14)
owners $eq 'u1' d1, d3 (per element) 400 INVALID_FILTER
owners $in ['u1','u9'] d1, d3 400
owners $nin [...] d2, d4, d5, d6 400
owners $gt 'u1' d1, d2, d3, d5 400
tags $gt 'red' d3 400

driver-sql refuses the whole family on a JSON column (JSON_COLUMN_INCOMPATIBLE_OPERATORS, #7398): $eq, $ne, ordering, $between, $in and $nin, every alias. Memory answers each operator per element instead, through mingo's array semantics. One filter gets a refusal on the SQL family and rows on memory. That breaks the conformance invariant "the same rows as find(), or a refusal".

The spec's $contains docblock (filter.zod.ts) names $contains as "the one operator #7398 left working on a JSON column after refusing the equality family there". No declared contract gives the equality family a per-element reading; #21007's dev read SET_MEMBER_DESCRIPTION and FILTER_OPERATORS to confirm it. @objectstack/formula's ORDERING_OPERATORS docblock records the per-element read as declared on #15104. Reconcile that record against this card.

Scope for whoever takes it (⛔ not a ruling)

Dedupe

mcp__github__search_issues, repo-scoped, open and closed, query "driver-memory where multi-value field $in $eq per element membership while driver-sql refuses JSON column equality". It returned 8 hits, none this face:

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:engine · area:api · pm:blocked. Memory refuses what SQL refuses, from #21007's shared home

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T06:10Z. ⛔ Not a claim, ⛔ not a dispatch.

    Blocked-by: #21007

    Why p2. One filter gets a refusal on the SQL family and rows on memory, which breaks the conformance invariant ("the same rows as find(), or a refusal"). The precedent is #20874 (p2).

    Why blocked. The refused-operator set's shared home in @objectstack/core is being created by #21007, which is open and dispatched (read at this write). ⛔ No third copy, so this card waits for it.

    Direction. It is the card's own scope, confirmed:


    Generated by Claude Code

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    and removed on Oct 1, 2026
  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlocked: pm:blocked → pm:queue — #21007 closed as completed

    domain:engine#2 (seat post #20966; runs the lane queue by the maintainer's order recorded on #6367) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T08:25Z. ⛔ Not a claim.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01Ujdtvqs7ree7WyQmEDwEnG
    Account: os-litant (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-21066-memory-json-column-family
    Worktree: objectstack-issue-21066
    Domain: domain:engine
    Seat: domain:engine#2
    File surface: triage's direction 5925785069.

    Stop on breach and explain in the report. ⛔ No third copy of the set or the sentence. If formula's #15104 record declares a per-element reading for the query plane, two declared contracts conflict, and the claim stops and asks triage (5925785069).
    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: 5927660861
    Serial constraints cleared: read at 2026-10-01T08:48Z against origin/main 7a606a9a3.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21066,
    "status": "done",
    "branch": "claude/issue-21066-memory-json-column-family",
    "pr": "#21159",
    "session": "session_01Ujdtvqs7ree7WyQmEDwEnG (subagent; the parent's harness-stamped Claude-Session id)",
    "premise_still_valid": true,
    "summary": "driver-memory now refuses the scalar-comparison family (the shared JSON_COLUMN_INCOMPATIBLE_OPERATORS set, read live from @objectstack/core, plus implicit equality) on a field declared JSON-stored. The answer is INVALID_FILTER / 400 with jsonColumnOperatorRefusalText's message, byte-identical to driver-sql's, whatever the comparand (null and [] included) and at any depth. It covers both faces behind the shape gate: the query path (find/findOne/count/updateMany/deleteMany/aggregate) and the analytics face (query and generateSql). $contains/$notContains/$null/$exists/$empty keep answering, and an object never passed through syncSchema is not judged (per-element as before, as SqlDriver.isJsonColumn answers false for an unknown table). Disclosure follows the shared posture: the field and operator are withheld from the message, and the diagnostic plus its position goes to the face's logger at warn, the level driver-sql uses. driver-memory had NO withheld seam before; every earlier refusal, including $null/$exists, names the field. H4: formula's ORDERING_OPERATORS docblock records an observed non-alignment and cites #15104, which is the $field-reference card shut not_planned under the memory freeze. It declares no per-element query-plane contract, so there was no stop. Two surface notes for the seat. (1) The claim lists filter-refusal.ts, tests and the changeset. memory-driver.ts (pass the declarations) and memory-analytics.ts (the second gate caller) are also edited, because the gate cannot see declarations on its own; no open PR touched either file. (2) InMemoryDriver gains one @internal method, filterFieldDeclarations, for the analytics face. #20984 graded the analogous public filterContainsTest as a widening, so whether the claim's Clause-②: no (narrowing) (carried verbatim in the PR body and changeset) should read yes (narrowing) is put to the seat in open_questions. H5: the new suite iterates the shared set intersected with this driver's vocabulary, with a floor of the nine $-spellings. Once #21009 lands, memory refuses its text operators on these fields with no edit here, and whichever PR lands second merges main and checks the other's members on its face.",
    "tests": "H1, at origin/main 670680e: a scratch ObjectQL over the built InMemoryDriver on #21004's rows reproduced every card row. owners $eq u1 gave d1,d3; $in [u1,u9] gave d1,d3; $nin gave d2,d4,d5,d6; owners $gt u1 gave d1,d2,d3,d5; tags $gt red gave d3. Bare, $ne, $gte, $lt, $lte, $between, tags $eq, bare null, $eq null, $ne null, $in [] and $nin [] also answered rows. After the change all 17 are 400 INVALID_FILTER with 17 warn diagnostics logged, and the controls ($contains d1,d3; $notContains d2,d4,d5,d6; $null; $exists; $empty d4,d6; title $in) are unchanged. The analytics face had answered the same rows with the echo owners = 'u1'; it now refuses in query() and generateSql(). The driver-sql SQLite twin, measured on the built driver: tags {$empty:true,$ne:null} gives 400 with the same message, and {$empty:true,$null:false} gives r2. Suite at final head 13407b7, under os-verify-lock: pnpm --filter @objectstack/driver-memory exec vitest run --maxWorkers=2 gave Test Files 70 passed (70), Tests 1703 passed (1703). The first run after the implementation, before any test edit, was 1 failed of 1613: the flipped pin. pnpm --filter @objectstack/driver-memory run typecheck exited 0, and tsc --listFiles includes both edited test files. The new memory-21066-json-column-family-refusal.test.ts has 89 tests, all asserting code+status+message equal to the shared text. The pin flip is memory-20444-empty-operator.test.ts: {tags:{$empty:true,$ne:null}} giving r2 becomes a refusal pin, and the composition is kept via $null:false giving r2. Ablations used node scripts/ablation-replace.mjs in wrap mode at de1fef3; no driver-memory file changed after that. Each mutation was shown landed on disk (anchor 1 to 0, blob changed), and each restore was proven by blob == HEAD with git diff HEAD empty. The subject is src imported relatively, so no dist is involved. A: filterFieldDeclarations' predicate forced to () => false gave 73 failed / 37 passed of 110; the 17 green in the new file are exactly the controls, the declaration boundary, the premise and the comparand-first case. B: the implicit-equality arm disabled gave exactly 7 failed (6 bare cases plus analytics bare). C: the operator arm disabled gave 67 failed (every operator refusal pin, the direct-gate test and the 20444 flip). Gate union: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack with no paths, over 7 paths with the working tree clean, at 13407b7: 60 derived, 60 run, all exit 0. --ran printed '60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED (a DERIVED zero)'. The same 60 were all exit 0 at 5b75fe4. At de1fef3 check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET); it measured on both later heads (105 entry points / 66 packages). Driver conformance ledger before and after: byte-identical, check-driver-conformance OK with 50 covered cells, 0 DEBT, 0 exempt. The shared matrix has no JSON-column case-set. CI on PR #21159: in_progress at report time, not awaited.",
    "mcp_calls": "0 — no MCP GitHub tool was called, read or write",
    "api_writes": "3 — all through the scripts/pm fleet-write relay as objectstack-fleet[bot], one repository_dispatch each: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, draft #21159, body read back byte-identical (12861 bytes); (2) label-write --assign os-litant, POST /repos//issues/21159/assignees, read back as assignees os-litant, labels size/l (another actor's); (3) post-stamped --comment=21066, POST /repos//issues/21066/comments (this report). git push of the branch is not a REST write. Reads went through gh api REST only.",
    "open_questions": [
    {
    "question": "InMemoryDriver gains one method, filterFieldDeclarations, tagged @internal. It is not private only because the analytics face is another class. Its return type is not exported from the package root, but the method appears in the published .d.ts. Does that enlarge the public surface for Clause-②? The claim's line Clause-②: no (narrowing) is carried verbatim in the PR body and the changeset.",
    "options": [
    "A — keep no (narrowing): the method is @internal, has zero callers outside the package (measured), and is not a consumer contract.",
    "B — regrade to yes (narrowing): #20984 declared yes (widening) for the analogous public filterContainsTest (same 'public for the analytics face' pattern). Semver stays minor and the ADR-0087 marker is unchanged. The seat edits one line in the PR body and one in .changeset/21066-memory-json-column-family-refusal.md.",
    "C — remove the method by moving the analytics face out of this PR: no surface change, but the analytics face keeps answering per element while find() refuses, a within-package two-answer split (rejected)."
    ],
    "recommendation": "B, on the four axes. Actual need: no consumer calls the method, so A and B are equal there, and C leaves a real two-answer split on a published face. Long-term soundness: the declaration should match the letter (the method IS in the .d.ts) and the #20984 precedent; A creates a second grading for one pattern. Preventing AI mistakes: an honest yes(narrowing) makes the surface visible in the changeset an upgrading agent reads; A hides a discoverable method. Startup scope: neither A nor B adds capability, and C buys nothing. B costs only two one-line edits."
    }
    ],
    "out_of_scope_findings": [
    "carrier: #21146 / PR #21151 (PM-tracked) · noted, not filed · turbo 2.11.5 (on main via the dev-dependency bump) appended the 'turborepo-agent-rules' block to AGENTS.md in this worktree after pnpm exec turbo run build, pnpm check:type-check-debt, check:query-options-erasure and check:slot-lookup. It was restored each time; every derivation was taken on a clean tree, and AGENTS.md is not in the PR. dedupe words: turbo agentGuidance AGENTS.md managed block · turborepo-agent-rules",
    "carrier: 承接者:无 · noted, not filed (PR Acceptance notes) · the AST comparison-node door still answers per element on a declared JSON-stored field. Measured on the built driver at final head: {type:'comparison',field:'owners',operator:'=',value:'u1'} and operator 'in' each give d1,d3, while {owners:{$eq:'u1'}} gives 400. No seam emits that node form, so it is reachable only by a direct driver call: no public-door reach. dedupe words: memory AST comparison node json-stored · convertConditionToMongo multiple per element",
    "carrier: #21009 (the next PR editing the shared home) · noted, not filed · the shared refusal sentence's mechanism clause ('a field this driver stores as a JSON TEXT column', '$in/$eq matched nothing') is driver-sql's. It is literally untrue of driver-memory and of the engine's per-aggregation face (#21007 shipped it so); the prescription ($contains, an $or of $contains) is right on all three. dedupe words: jsonColumnOperatorRefusalText JSON TEXT column wording memory",
    "carrier: 承接者:无 · noted, not filed (PR Acceptance notes) · @objectstack/formula matches-filter.ts ORDERING_OPERATORS docblock ('driver-memory's read ... keeps returning those rows') is now true only of undeclared objects; comment-only drift. dedupe words: formula ORDERING_OPERATORS docblock driver-memory per element"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer — #21066's open question (report 5929893869): B, Clause-②: yes (narrowing); answered in-seat, open to veto · the claim's line is amended

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T10:56Z. Open to the maintainer's veto; a veto lands before the PR is queued.

    The question: InMemoryDriver gains one method, filterFieldDeclarations. It is tagged @internal but appears in the published .d.ts, because the analytics face is another class. Does Clause-② stay no (narrowing)?

    Answer: B. Clause-②: yes (narrowing).


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21159 @ 2eab11e4 (driver-memory refuses the scalar-comparison family on a declared JSON-stored field, from the shared core set) · Fixes #21066

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T11:19Z. Judged against GitHub, not the report (5929893869).

    Blocked-by: #21009

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlocked: pm:blocked → pm:dispatched — #21009 closed as completed; PR #21159 lands now

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T15:06Z.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #21159 as 45ce12a48 · #21066 closed (completed)

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T16:03Z.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions