Repository navigation
[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
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:engine·area:api·pm:blocked. Memory refuses what SQL refuses, from #21007's shared homeTriage 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/coreis 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:
- memory's shape gate refuses the same family on a declared JSON-stored field, with the same code, reading the shared set;
$containsstays as the control;- memory tests that assert per-element rows for these operators flip to refusal pins.
- The formula record ([finding] driver-memory's own reference matcher has no
$fieldarm — a cross-field comparand (bare or withaddDays) reaching it is presumably compared as a literal object rather than resolved or refused (grep reading, to be measured) #15104): the claim reads it. If it declares a per-element reading for the query plane, two declared contracts conflict, and the claim stops and asks triage. A formula-plane record alone does not change this card.
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 actionsUnlocked:
pm:blocked→pm:queue— #21007 closed as completeddomain: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.- The condition: triage's transition 5925785069 blocked this card on [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 (Blocked-by: #21007). [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 is closed as completed: PR fix(objectql)!: a per-aggregation filter refuses a scalar comparison on a declared JSON-stored field, in where's words #21097 merged asa11faeecb(landed record 5927642384). - The double check: the newest transition is 5925785069, and no PR has merged on this card since. Re-derived: no new blocker is named on the thread.
- What the dispatch reads first: the shared home now exists on
mainaspackages/core/src/utils/json-column-operator-refusal.ts(the refused setJSON_COLUMN_INCOMPATIBLE_OPERATORSand the sentence builder). The dispatch re-verifies the premise againstmainas it stands then.
Generated by Claude Code
- The condition: triage's transition 5925785069 blocked this card on [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-21066-memory-json-column-family
Worktree:objectstack-issue-21066
Domain:domain:engine
Seat:domain:engine#2
File surface: triage's direction 5925785069.packages/drivers/driver-memory/src/filter-refusal.ts: memory's shape gate refuses the same family on a declared JSON-stored field (STRUCTURED_JSON_TYPESorisMultiValueField), with the same code. It reads the shared set and sentence from@objectstack/core(json-column-operator-refusal.ts, homed by PR fix(objectql)!: a per-aggregation filter refuses a scalar comparison on a declared JSON-stored field, in where's words #21097).driver-memorytests: per-element pins for these operators flip to refusal pins, ⛔ not deleted. A$containscontrol stays..changeset/21066-*.md.
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 againstorigin/main7a606a9a3.- The predecessor, 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), landed asa11faeecband created the shared home. - [finding]
$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009, claimed in the previous act, widens the same shared set to the text operators. Whichever lands second mergesmainand pins the other's members on its face. - No open PR touches
driver-memory'sfilter-refusal.ts. Clause-②: no (narrowing): rows answered per element today are 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": 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'sClause-②: no (narrowing)(carried verbatim in the PR body and changeset) should readyes (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=2gave 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 typecheckexited 0, andtsc --listFilesincludes 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/objectstackwith no paths, over 7 paths with the working tree clean, at 13407b7: 60 derived, 60 run, all exit 0.--ranprinted '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 lineClause-②: no (narrowing)is carried verbatim in the PR body and the changeset.",
"options": [
"A — keepno (narrowing): the method is @internal, has zero callers outside the package (measured), and is not a consumer contract.",
"B — regrade toyes (narrowing): #20984 declaredyes (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 afterpnpm exec turbo run build,pnpm check:type-check-debt,check:query-options-erasureandcheck: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
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsSeat answer — #21066's open question (report 5929893869): B,
Clause-②: yes (narrowing); answered in-seat, open to veto · the claim's line is amendeddomain: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:
InMemoryDrivergains one method,filterFieldDeclarations. It is tagged@internalbut appears in the published.d.ts, because the analytics face is another class. DoesClause-②stayno (narrowing)?Answer: B.
Clause-②: yes (narrowing).- Why it is in-seat: a declaration's grading under this lane's own landed precedent. [finding] driver-memory answers
$containson a stored array by substring per element (u1matches a row storingu10), where the SQL drivers answer membership; the spec docblock records the gap against a card that answers 404 #20874's changeset (PR fix(driver-memory): $contains on a multi-valued or JSON-stored field is membership, on every face #20984,f8178ffec) declaredyes (widening)for the analogous publicfilterContainsTest, the same "public for the analytics face" pattern. One pattern gets one grading. No runtime answer moves between A and B. - Level: unchanged.
minorwith BREAKING, and the ADR-0087 marker as written. - Execution: the dev edits the two lines (the PR body's line 2 and the changeset's
Clause-②line) on the same branch. The claim'sClause-②: no (narrowing)(5927990211) is amended toyes (narrowing)by this comment. - C, moving the analytics face out, is rejected. It would leave
find()refusing while the analytics face answers per element: two answers inside one package.
Generated by Claude Code
- Why it is in-seat: a declaration's grading under this lane's own landed precedent. [finding] driver-memory answers
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT — PR #21159 @
2eab11e4(driver-memoryrefuses the scalar-comparison family on a declared JSON-stored field, from the shared core set) ·Fixes #21066domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T11:19Z. Judged against GitHub, not the report (5929893869).-
Form: draft PR on
main. The body opensFixes #21066/Clause-②: yes (narrowing), per seat answer 5929927010 (B, following [finding] driver-memory answers$containson a stored array by substring per element (u1matches a row storingu10), where the SQL drivers answer membership; the spec docblock records the gap against a card that answers 404 #20874's grading in PR fix(driver-memory): $contains on a multi-valued or JSON-stored field is membership, on every face #20984). -
Scope: 7 files, +565 / −10, all in
packages/drivers/driver-memory. Not governed.filter-refusal.ts, the shape gate.memory-driver.ts, which passes the declarations.memory-analytics.ts, the second gate caller.- Pins, and the changeset.
The two files beyond the claim's list are named in the report and accepted by the review. The
@internalmethodfilterFieldDeclarationsappears in the published.d.ts; that is the reason foryes. -
Direction it executes: triage's 5925785069. Memory refuses what SQL refuses, from [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's shared home. -
Contract review: at tier, PASS on this head (5930161703). It found:
- the gate reads the shared set and sentence live from
@objectstack/core, with no third copy, so the message, code and status are byte-identical withdriver-sql; - the predicate (
STRUCTURED_JSON_TYPESorisMultiValueField, over shapes filled bysyncSchema) isdriver-sql's reading, and an unknown table is not judged on both drivers; - every query door and the analytics face are gated, and
$contains,$notContains,$null,$existsand$emptykeep answering; - the field and operator are withheld, and the diagnostic is logged at warn;
- the pin
memory-20444is flipped, not deleted; - the levels are right:
minor, BREAKING,yes (narrowing), and the markernot-required (no-migration-prescription). - Its one body-only defect: two prose sentences still read
no (narrowing). This seat corrected them through a PR-body edit (read back identical). The head is unchanged.
- the gate reads the shared set and sentence live from
-
CI on
2eab11e4: 48 check-runs. 41 succeeded and 7 were skipped, all on the roster (check-expected-skips --pr 21159: OK, exit 0). Not governed. The PR merges cleanly ontomainfde553c50(git merge-tree). -
Tests (dev's evidence):
driver-memory: 1703 passed, 89 of them in the new suite, each asserting code, status and message equal to the shared text.- Ablations A, B and C each turned their pins red: 73, 7 and 67 failed. The restores were proven clean.
- Gates: 60 of 60, exit 0.
- Driver conformance: 50 / 0 / 0 before and after.
-
Findings:
- [0]
turbowrites intoAGENTS.md→ 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. - [1] The AST comparison-node door still answers per element, on a direct call only (there is no emitter) → Acceptance notes.
- [2] The shared sentence's mechanism clause ("a field this driver stores as a JSON TEXT column") is untrue of memory and of the engine face → carried to [finding] driver-sql's JSON-column refusal is about 800 characters and is cut at the REST envelope's 500, so no caller reads the sentence saying the field and operator were withheld #21067 (5929942555), which rewrites that sentence.
- [3]
formula'sORDERING_OPERATORSdocblock is now true only of undeclared objects → Acceptance notes (comment drift). - Review residuals, both pre-existing with no public-door reach → Acceptance notes:
- a no-operator object comparand on a declared JSON field escapes on a direct call only; the engine's door refuses it first;
- the analytics face lowers type-blind.
- [0]
-
Landing: held behind [finding]
$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009, by this seat. Onmain, the search expander emits$inagainst a multi-valued field on a label match ([finding]$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009's report 5930193856). This PR would make the memory driver refuse that$inwith400. That is a search regression on memory-backed examples until the expander gives multi-valued fields membership. That fix rides PR fix(core)!: the JSON-column gate refuses $startsWith, $endsWith, $icontains and $like / $ilike as it refuses the equality family #21165 ([finding]$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009), by seat answer 5930243637. So:- this card goes
pm:dispatched→pm:blockedin the same act; - when [finding]
$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009 closes, the unlock scan returns it, and this seat mergesmaininto the branch through the dev, re-reads CI, and lands it. - Nothing in this record changes unless that merge moves the diff.
- this card goes
Blocked-by: #21009
Generated by Claude Code
-
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsUnlocked:
pm:blocked→pm:dispatched— #21009 closed as completed; PR #21159 lands nowdomain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T15:06Z.- The condition: this seat's hold 5930256504 blocked the landing on [finding]
$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009. That hold kept the memory driver from refusing the search expander's$inbefore the expander gave multi-valued fields membership. [finding]$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009 is closed: PR fix(core)!: the JSON-column gate refuses $startsWith, $endsWith, $icontains and $like / $ilike as it refuses the equality family #21165 merged as2c1cef334, and the expander now emits$contains. - The double check: the newest transition is 5930256504, and no PR has merged on this card since.
- Landing: ACCEPT 5930256504 stands, with review PASS 5930161703 @
2eab11e4and CI green on that head. The branch merges cleanly ontomain(git merge-tree). The merge queue tests the merged result, which now includes fix(core)!: the JSON-column gate refuses $startsWith, $endsWith, $icontains and $like / $ilike as it refuses the equality family #21165's five new set members; this driver's gate reads that set live. This seat readies and arms the PR in this act, in-flight p2 work that the maintainer ordered finished (5934179218).
Generated by Claude Code
- The condition: this seat's hold 5930256504 blocked the landing on [finding]
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded — PR #21159 as
45ce12a48· #21066 closed (completed)domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T16:03Z.- Merged through the merge queue at 2026-10-01T15:28Z as squash
45ce12a48onmain. It has one parent and is an ancestor oforigin/main. It carries the PR's 7 files (565 insertions, 10 deletions), as reviewed. TheFixes #21066keyword closed the card as completed. It landed after PR fix(core)!: the JSON-column gate refuses $startsWith, $endsWith, $icontains and $like / $ilike as it refuses the equality family #21165 ([finding]$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009), as the hold 5930256504 required. - Records it landed on: ACCEPT 5930256504, contract review PASS 5930161703 @
2eab11e4, seat answer 5929927010 (yes (narrowing)), and unlock 5934235936. - What changed for callers:
driver-memoryrefuses the shared JSON-column set (@objectstack/core, read live) plus implicit equality on a declared JSON-stored field. The refusal isINVALID_FILTER/400, withdriver-sql's sentence byte for byte. It applies on the query path and the analytics face. Since fix(core)!: the JSON-column gate refuses $startsWith, $endsWith, $icontains and $like / $ilike as it refuses the equality family #21165, that set includes the text operators, so memory refuses them too.$contains/$notContainsanswer membership. - Labels:
pm:dispatchedremoved in this act. - Unlock scan (run before this line was written): no open
pm:blockedcard namesBlocked-by: #21066.
Generated by Claude Code
- Merged through the merge queue at 2026-10-01T15:28Z as squash
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.findonInMemoryDriver, measured atorigin/maind1f8ce865by #21007's dev (os-dev-report5924484320 on #21007,out_of_scope_findings[0]). The REST door was not measured on memory, becausepackages/restdoes not depend ondriver-memory.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 fixture is #21004's:
ownersis amultiple: truelookup over six rows (d1…d6), andtagsis atagsfield.whereowners$eq 'u1'd1,d3(per element)INVALID_FILTERowners$in ['u1','u9']d1,d3owners$nin [...]d2,d4,d5,d6owners$gt 'u1'd1,d2,d3,d5tags$gt 'red'd3driver-sqlrefuses the whole family on a JSON column (JSON_COLUMN_INCOMPATIBLE_OPERATORS, #7398):$eq,$ne, ordering,$between,$inand$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 asfind(), or a refusal".The spec's
$containsdocblock (filter.zod.ts) names$containsas "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 readSET_MEMBER_DESCRIPTIONandFILTER_OPERATORSto confirm it.@objectstack/formula'sORDERING_OPERATORSdocblock records the per-element read as declared on #15104. Reconcile that record against this card.Scope for whoever takes it (⛔ not a ruling)
STRUCTURED_JSON_TYPESorisMultiValueField), with the same code. It reads the refused set from the shared home [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 is creating in@objectstack/core, if that has landed. ⛔ No third copy.$containscontrol (membership still answers).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:$containson a stored array by substring per element (u1matches a row storingu10), where the SQL drivers answer membership; the spec docblock records the gap against a card that answers 404 #20874 (closed) is$containssubstring on memory.max/minover a JSON-stored field answers per driver at the engine (memory an object, SQLite a string, PostgreSQL 500): the compatibility table refuses them, but the engine aggregate door enforces only itscount_distinctrow #20914 and analytics: on the ObjectQL strategy a$notover a multi-valued lookup ($contains) is refused 400, because the NULL-safe guard reaches driver-sql as$ne: nullon a JSON column, where the engine answers the rows #20918 (open) are aggregate and analytics faces.