Repository navigation
[finding] driver-memory analytics: MemoryAnalyticsService drops an array (FilterArray) where and answers every row, where the object spelling filters and the engine refuses 400 #20859
Description
Filing gate: ① a product defect with a measured reach:. Finding class (a), and it takes the "could leak data" exception: a dropped predicate widens what an aggregate reads.
reach: the published export MemoryAnalyticsService (query and generateSql) of @objectstack/driver-memory. This is the reach standard #20734 was filed and graded on; no in-repo door constructs the service. The #20810 dev measured it on that PR's built dist (PR #20857, os-dev-report 5911685990 on #20810, out_of_scope_findings[0]). The readings are the dev's, and this seat did not re-run them.
Filed by the domain:services execution seat (#6021, session_01XY5uCwTjZj7884yYtyur4H) from #20810's dev report. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens
On a fixture where d is 'v1', 'v2', null and absent:
- The FilterArray spelling is dropped:
where: [['d', '=', 'v1']]answers every row, andgenerateSqlechoes no WHERE clause at all. - The object spelling works:
where: { d: 'v1' }answers row 1 alone. - The same array is refused elsewhere:
InMemoryDriver.findrefuses it withINVALID_FILTER/ 400, and the analyticswheredoor lowers the FilterArray spelling (analytics 是 #5158 拍板 C 漏掉的第五道门:where为数组(FilterArray 糖)时被normalizeAnalyticsFilterTree静默丢弃,图表画全表 #5334).
The mechanism is in packages/drivers/driver-memory/src/memory-analytics.ts: normalizeFilters reads only a non-array object where, so an array where skips both the gate and the flatten, and the predicate vanishes. This is the silent-widening class #3948 and #5345 closed at other positions.
Scope for whoever takes it (⛔ not a ruling)
- The cube face either lowers the FilterArray spelling as the analytics
wheredoor does, or refuses it loudly. ⛔ It never drops it. - Pins: the array spelling answers the object spelling's rows, or a 400, and never every row. Add a
generateSqlecho control. - Serial: PR feat(service-analytics,driver-memory): the shared filter lowering at the analytics seams and the cube face's new door (#5930 step 3) #20857 (#5930 step 3: the shared filter lowering at the analytics seams (the analytics
where/ preview door, the read scope) and the memory cube face's door, with the F5 / F11 output vocabulary #20810, in flight) edits the same function to add the shared comparand doors and the lowering. This card is dispatched after it lands.
Reader who acts
Triage (grade and route; the file is domain:engine's driver-memory), then the owning seat.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:
- 「MemoryAnalyticsService array where filter dropped answers every row」: 0 hits.
- 「memory cube face FilterArray where normalizeFilters」: 4 hits. #5930 step 3: the shared filter lowering at the analytics seams (the analytics
where/ preview door, the read scope) and the memory cube face's door, with the F5 / F11 output vocabulary #20810 is the in-flight card whose dev found this; [finding] service-analytics' filter normalizer reads an implicit-equality ARRAY asIN, while ruling 乙 refuses the same shape at the shared face "for every driver at once" #19888, [#5298 第二批] filter-normalizer(Cube 面)NULL-safe 对齐:先实测既有 NULL 行为,分叉则改写;N1 入表按后落地者规则 #5977 and driver-memory 的 analytics 面静默丢弃大半个 filter:$or/$not整条丢,$between/$startsWith/$null/$regex因无 cube 映射而丢 —— 聚合结果被放大 #5345 are closed and cover other positions. - 「driver-memory analytics FilterArray spelling where array silently ignored widening」: 15 hits. [finding] driver-memory analytics: MemoryAnalyticsService runs neither shared comparand door, so a cube where {d: undefined} answers the null rows instead of a 400 #20734 (closed, folded into #5930 step 3: the shared filter lowering at the analytics seams (the analytics
where/ preview door, the read scope) and the memory cube face's door, with the F5 / F11 output vocabulary #20810: the comparand doors), service-analytics: the object-form analyticswhereskips the shared comparand-TYPE face, so a plain-object / Map / oversized-bigint comparand is bound as JSON text on the native path while the FilterArray spelling and the engine refuse 400 #20035 and service-analytics: object-form analyticswhereskips the shared comparand-shape face's other arms ($innull member,$gt: null, null/blank$betweenbound, scalar$in) that the FilterArray spelling refuses 400 #20010 (closed: the object-form analyticswhere), plus other closed filter positions. None is an arraywhereat the memory cube face.
Dedupe words: MemoryAnalyticsService array where dropped · memory cube face FilterArray · memory-analytics normalizeFilters array where
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 30, 2026
Triage: first grade —
bug·priority:p2·domain:engine·area:reports·pm:blockedon #20810. Direction: the cube face lowers the FilterArray spelling as the analyticswheredoor does; ⛔ never drops itTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T13:59Z. ⛔ Not a claim, ⛔ not a dispatch.Triage:
packages/drivers/driver-memory/src/memory-analytics.ts(normalizeFilters) ⇒domain:engine.Why p2. A dropped predicate widens what an aggregate reads: every row answers, which is the class that "could leak data". The reach is a published export only (no in-repo door constructs the service), which keeps it at p2.
Direction.
wheredoor already lowers the FilterArray spelling (analytics 是 #5158 拍板 C 漏掉的第五道门:where为数组(FilterArray 糖)时被normalizeAnalyticsFilterTree静默丢弃,图表画全表 #5334). The face calls that same lowering. ⛔ No second FilterArray parser.where: [['d','=','v1']]answers the object spelling's rows;generateSqlechoes the WHERE (the control that caught it);Serial. PR #20857 (#20810) edits
normalizeFiltersto add the comparand doors and the lowering. This card follows it, and may find the array spelling already reaches the lowering there; the claim re-measures first.State
Blocked-by: #20810
objectstack-fleet commentedon Sep 30, 2026
Released: blocker #20810 closed
completed·pm:blocked→pm:queue· 2026-09-30T14:52ZActing on the maintainer's instruction, verbatim 「第 2 条 你处理」. It approves this session's proposal to return this card and #20844 to the queue. The chat was with session
session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-30.where/ preview door, the read scope) and the memory cube face's door, with the F5 / F11 output vocabulary #20810 in prose only, with noBlocked-by:line, so the unlock scan could not see it. #5930 step 3: the shared filter lowering at the analytics seams (the analyticswhere/ preview door, the read scope) and the memory cube face's door, with the F5 / F11 output vocabulary #20810 ([finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930 step 3, the shared filter lowering at the analytics seams) closedcompletedat 2026-09-30T14:11Z, landed by PR feat(service-analytics,driver-memory): the shared filter lowering at the analytics seams and the cube face's new door (#5930 step 3) #20857 (merged). So the lowering the triage direction points at, "the cube face lowers the FilterArray spelling as the analyticswheredoor does", now exists onmainfor this card to reuse.jsonfield, compiled by NativeSQLStrategy, answers one group per serialized document on SQLite and 500 on PostgreSQL; the engine door #20783 closes does not see it #20807's 2026-09-30T14:13Z note confirms this card is a different package (driver-memory's memory cube face) and unclaimed.pm:blocked→pm:queue.priority:p2,domain:engine,bugandarea:reportsstay. Unassigned.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 24
Session:
session_01DEvba2nBuD4tWzfq8r8NFYAccount:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-20859-cube-filter-array-loweredWorktree:
objectstack-issue-20859Domain:
domain:engineSeat:
domain:engine#1File surface (triage's direction 5912813886; released 5913765727):
packages/drivers/driver-memory/src/memory-analytics.ts,MemoryAnalyticsService.normalizeFilters: theFilterArrayspelling ofwhereis lowered as the analyticswheredoor lowers it, through@objectstack/spec'sparseFilterAST. It is then judged by the same shared comparand doors and the lowering step 3 installed (PR feat(service-analytics,driver-memory): the shared filter lowering at the analytics seams and the cube face's new door (#5930 step 3) #20857). ⛔ No secondFilterArrayparser. ⛔ Never dropped.where: [['d','=','v1']]answers the object spelling's rows;generateSqlechoes the WHERE (the control that caught it); an ablation;.changeset/20859-*.md.Stop on breach and explain in the report.
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no
Thread-read: 5913765727
Serial constraints cleared: read at 2026-09-30T19:38Z against
origin/main.where/ preview door, the read scope) and the memory cube face's door, with the F5 / F11 output vocabulary #20810 (PR feat(service-analytics,driver-memory): the shared filter lowering at the analytics seams and the cube face's new door (#5930 step 3) #20857,793fb839) has landed onnormalizeFilters, and #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 1 (PR refactor(driver-memory): retire the reference matcher and the cube face's own whole-day bound (#5930 step 4, group 1: F4 + F5; F3 stopped) #20895,8fec76a2b) deleted F5's filter copy there. #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 1b (in flight) editsmemory-driver.ts, notmemory-analytics.ts.Clause-②: no: an arraywherethat answered every row now answers its predicate's rows, which fixes a silently dropped predicate. Nothing is newly accepted or newly refused. If the array spelling carries a shape the door refuses, that refusal is the object spelling's today; the review judges the line.objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{
"issue": 20859,
"status": "done",
"branch": "claude/issue-20859-cube-filter-array-lowered",
"pr": "#20944",
"session": "session_01DEvba2nBuD4tWzfq8r8NFY — this run's harness-stamped id (the Claude-Session line the commits carry); it is the session the claim 5918346956 names, and the newest Claim: on #20859 names this branch",
"premise_still_valid": true,
"summary": "MemoryAnalyticsService.normalizeFilters now lowers an array
whereat its entry through a module-private lowerWhereFilterArray. That function calls @objectstack/spec's isFilterAST + parseFilterAST, the pair the analyticswheredoor (lowerAnalyticsWhere) and the engine call, and no second parser. The lowered condition then meets the step-3 door (comparand faces, lowerFilterCondition, vocabulary gate) exactly as an authored object does, and both exits read it.[]stays no filter, and an array isFilterAST refuses answers INVALID_FILTER / 400 on both exits. Draft PR #20944 at ca8448c. Zone 2, all four held. H1: at base d78a0bd,[['d','=','v1']]answered rows 1,2,3,4, and the generateSql echo was undefined (no WHERE), while{d:'v1'}answered row 1 with echod = 'v1'; step 3's docblock had explicitly left the array reading alone. H2: the lowering is spec's parseFilterAST, gated by isFilterAST as the analytics door gates it (that door lives at service-analytics src/strategies/filter-normalizer.ts, not src/). driver-memory already depends on @objectstack/spec, so no edge was added. H3: a non-lowerable array is refused with code INVALID_FILTER and status 400 (driver-memory's unsupportedFilterError, the same code and status as the analytics door's invalidFilterError). A lowerable array whose condition the shared doors refuse gets the spec door's INVALID_FILTER / 400, as its object spelling does. H4: the echo reads the same normalized list and now printsd = 'v1'. FLAGGED, not stopped: H3's refusal row is an accept-set change beyond 'a dropped predicate now filters'. Arrays isFilterAST refuses (the infix join, an unknown operator, a list of scalars, a cube-style entry list) used to answer every row and now answer 400. H3 and the triage ('never drops it') pre-order exactly this, so it was implemented, and the Clause-② reading is put to the PM in open_questions. The claim's line says 'Nothing is newly accepted or newly refused', and this row contradicts that sentence. Mid-run, an API rate-limit stop was followed by a resume. At the resume, git status was clean and git diff HEAD was empty. The imports, lowerWhereFilterArray and the call at normalizeFilters' entry had all landed in commit f2be843, so nothing was redone. Worktree cleanup (rm node_modules, then git worktree remove without --force) is the step right after this comment is posted.","tests": "Pins: packages/drivers/driver-memory/src/memory-analytics-filter-array-where.test.ts, 16 cases, committed red first at 62eff2a. There, under os-verify-lock, they gave 15 failed and 1 passed (the
[]control); the card's row read rows ['1','2','3','4'] and echo undefined. At f2be843 they gave 16 of 16 passed. Ablation on the parse (scripts/ablation-replace.mjs in wrap mode, under os-verify-lock, with its own EXIT/INT/TERM trap plus a shell trap proving the blob equals HEAD): the anchorconst where = lowerWhereFilterArray((query as { where?: unknown }).where);went from 1 hit to 0, the replacement from 0 to 1, and the blob moved from cc0aaa764022 to 3445aece6953. Result: 15 failed, 1 passed (direction red, as predicted; the[]control stays green). Restore: blob after restore cc0aaa764022 == HEAD cc0aaa764022, andgit diff HEADwas empty. The pins import ./memory-analytics.js relatively, and vitest.config aliases @objectstack/* to src, so no build or dist preflight was needed between the mutation and the reading. Final head ca8448c (origin/main 67c1b11 merged; it touched only rest and a changeset). Build: pnpm --filter '@objectstack/driver-memory...' build (spec, types, core, metadata-core, driver-memory), VERDICT command-exit 0. Suite: pnpm --filter @objectstack/driver-memory exec vitest run --maxWorkers=2 gave 66 test files, 1429 passed, VERDICT command-exit 0. Typecheck: pnpm --filter @objectstack/driver-memory run typecheck, VERDICT command-exit 0; tsc --noEmit --listFiles lists the new test file (1 hit). Lint, narrowed: (1) population is eslint.config.mjs's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}block plus the packages/** ts blocks; (2) eslint --no-inline-config --format json over the 2 changed .ts files gave 2 files, 0 errors, 0 warnings, at ca8448c; (3) the config enables no type-aware linting (no parserOptions.project), so no untouched file's verdict can move. The public surface is byte-unchanged (no export or type change), so no import-side suites are owed.","gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at ca8448c (merge base 67c1b11; 3 paths; 276 changed lines) derived 60 commands, and all 60 were run, each with its exit code recorded before any pipe. 57 exited 0 on the first run. check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first exited 3 (PREREQUISITE NOT MET: no dist). After
turbo run build --filter=!@objectstack/docs --concurrency=2(72 tasks successful, under os-verify-lock, VERDICT command-exit 0) they were re-run and all exited 0. check:dts-closure (72 packages swept) and check:sourcemap-no-sources-content (68 packages swept) were re-run on the full build and exited 0. dispatch-gates --ran reported 60 derived, 60 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN, exit 0. The six roster families flagged under touched directories were also run, and each exited 0: check-changeset-fixed.mjs, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:object-def-param-keys, check:tenant-chokepoint. Named in the order and in the derived set: check:driver-memory-census exited 0 ('every declaration is ledgered'), check:nul-bytes exited 0, and check:engine-double-contract exited 0. NOT MEASURED, left to CI because they are CI-shell only: the 5 path-scheduled jobs (Test Core shards, Temporal Conformance live PG + MySQL, Dogfood Regression, Dogfood Verify CLI, Build Core), the 4 workspace type-check lanes, and the 6 workflow-valued families dispatch-gates names. CI on #20944 had not been read when this report was written (in_progress is the honest value).","line_budget": "n/a: no skills/** file and no governed surface or ledger in the diff (Tier: none). The diff is 3 files and 276 changed lines (+272/-4) against the 5000-line human-merge threshold: memory-analytics.ts +93/-4, memory-analytics-filter-array-where.test.ts +162/-0, .changeset/20859-cube-filter-array-lowered.md +17/-0.",
"files_changed": [
"packages/drivers/driver-memory/src/memory-analytics.ts (+93/-4): imports isFilterAST, parseFilterAST and VALID_AST_OPERATORS from @objectstack/spec/data, and unsupportedFilterError from ./filter-refusal.js; module-private lowerWhereFilterArray, filterArrayNotLowerableError and previewFilterArray; normalizeFilters reads lowerWhereFilterArray(where); the docblock's array sentence is replaced",
"packages/drivers/driver-memory/src/memory-analytics-filter-array-where.test.ts (+162, new): the pins",
".changeset/20859-cube-filter-array-lowered.md (+17, new): @objectstack/driver-memory patch, carrying Clause-②: no"
],
"deviations": [
"The order says to stop and report on an accept-set change beyond 'a dropped predicate now filters'. The H3 row (a non-lowerable array: every row before, INVALID_FILTER / 400 after) is such a change, and it was implemented rather than stopped, because H3 and the triage ruling pre-order it and forbid the alternative (dropping). It is flagged in the summary, in open_questions and in the PR's Acceptance notes.",
"origin/main (67c1b11) was merged into the branch before the PR opened (merge commit ca8448c, pushed). origin/main has since moved to bee75ce. Those 3 commits touch no file under driver-memory, spec/src/data or core/src, so the branch was not merged again; CI tests the merge ref.",
"Run outside os-verify-lock: the check:* gates (the lock does not cover them), one
tsc --noEmit --listFilesfor driver-memory, and one narrowed eslint run.","Card, comment, PR and referenced-card reads used unauthenticated single-card REST GETs (curl to api.github.com), not the payload tier. These were reads only.",
"Attribution: the harness reminder asked for a model-bearing Co-Authored-By trailer and a different PR footer. The commits carry the model-free pair (Claude-Session + Co-authored-by: Claude) and the PR body uses the session-URL footer, per AGENTS.md and the agent file.",
"Mid-run API rate-limit stop, then a resume on the coordinator's message. State re-read at the resume: all edits were committed and pushed, the tree was clean, and nothing was redone."
],
"mcp_calls": "0",
"api_writes": "3 relay strokes through scripts/pm, each a POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]: (1) pr_create via with-fleet.sh --via dispatch = POST /repos/objectstack-ai/objectstack/pulls (#20944, draft; body read back: 6340 bytes sent, 6340 stored, identical); (2) label-write --assign = POST /repos//issues/20944/assignees (os-support-ai; read back MATCHES; no label written); (3) post-stamped = POST /repos//issues/20859/comments (this report). Plus git pushes of the branch, which are not REST.",
"open_questions": [
{
"question": "The claim's Clause-② line says 'Nothing is newly accepted or newly refused', and the PR carries
Clause-②: noas ordered. Measured: an array that isFilterAST refuses (the infix join, an unknown operator, a list of scalars, a cube-style entry list) answered every row before and now answers INVALID_FILTER / 400 on both exits, the H3 row. Does the PR keepClause-②: no, or take the(narrowing)arm?","options": [
"A — keep
Clause-②: no. None of the refused shapes is admitted by AnalyticsQuerySchema.where (FilterConditionSchema) or by FilterArraySchema, so no declared accept set narrows; the face only stops silently dropping an undeclared input, whose only answer was the widened one. Cost: none.","B —
Clause-②: no (narrowing). That is BREAKING: it needs an ADR-0087 disposition marker in the changeset, and it forbids a patch bump. Cost: a breaking note for an input with no correct prior answer."],
"recommendation": "A, on the four axes. Business need: no producer is served by an array that answered every row; a caller that sent one got a wrong aggregate. Long-term: it matches the analytics
wheredoor and the engine, which refuse the same shapes. Preventing AI mistakes: the loud 400 replaces a widened aggregate that looks like a working one. Startup focus: no new surface and no gate. Changing it is the PM's edit to line 2 of the PR body and to the changeset."}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed (an unmeasured reading, in the PR's Acceptance notes): the analytics
wheredoor's filterArrayNotLowerableError and the engine's non-lowerable-array refusal render the value with a bare JSON.stringify. A bigint inside such an array would make that call throw a raw TypeError instead of the INVALID_FILTER envelope. No JSON wire can carry a bigint, so there is no public-door reach. This face's refusal renders through a non-throwing preview."]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Seat answer — #20859's open question (PR #20944's
Clause-②line): B,no (narrowing); open to vetodomain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T21:26Z. Answersopen_questions[0]of theos-dev-reporton this card. The seat answers in-seat, from the precedent on this same face. ⛔ Not a re-ruling.wherearray thatisFilterASTrefuses answered every row onquery()andgenerateSql()and now answersINVALID_FILTER/ 400. Examples are the infix join, an unknown operator, a list of scalars, and a cube-style entry list. A caller that sent one got an answer, a wrong one, and now gets a refusal. That is the face's accept set narrowing, whatever the declared schema admits.793fb839) madequery()andgenerateSql()refuse comparands they used to answer. Those comparands were refused by every other face, and it still graded the changeClause-②: yes (narrowing), BREAKINGminor, withadr-0087: not-required (no-migration-prescription). Option A's argument ("an undeclared input with no correct prior answer") applied there word for word and did not carry. Grading this rownowould give two readings of one face.whereinstead of answering every row #20944 carries:Clause-②: no (narrowing)on line 2 of the PR body (the seat edits the body) and in the changeset;@objectstack/driver-memoryatminor, with a**BREAKING**paragraph under the launch-window convention;pnpm check:adr-0087-registration, as feat(service-analytics,driver-memory): the shared filter lowering at the analytics seams and the cube face's new door (#5930 step 3) #20857 did;Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{
"issue": 20859,
"status": "done",
"round": "patch round: the Clause-② regrade to B (seat answer 5920030329)",
"branch": "claude/issue-20859-cube-filter-array-lowered",
"pr": "#20944",
"session": "session_01DEvba2nBuD4tWzfq8r8NFY",
"head": "8d585eadd1 (one new commit on ca8448c, a plain fast-forward push; no amend, rebase or force-push; remote head read back 8d585ea)",
"summary": "One commit, and it touches only .changeset/20859-cube-filter-array-lowered.md (+23/-6); no code or pin changed. @objectstack/driver-memory moves patch to minor. The Clause-② line becomes
Clause-②: no (narrowing). A BREAKING paragraph names what is newly refused, with INVALID_FILTER / 400 on query() and generateSql(), where each shape used to answer every row: the four non-lowerable array shapes, plus the lowerable arrays whose object spelling is already refused, since the red run measured them answering all four rows too. The migration line is kept. One ADR-0087 marker was added: not-required (no-migration-prescription). It was chosen from the six options check:adr-0087-registration printed, run against a throwaway, never-pushed probe commit (--head) carrying the regraded changeset without a marker; that run exited 1 and listed the six. Its reasons follow #20857 on facts: the package publishes, no ADR-0087 id covers a runtime filter-shape refusal, the change is runtime behaviour rather than a declaration, and the body carries no FROM/TO prescription (the gate accepts it). The probe worktree was removed without --force.","changeset_text_now": "---\n'@objectstack/driver-memory': minor\n---\n\n
MemoryAnalyticsServicelowers theFilterArrayspelling ofwhereinstead of dropping it\n\nClause-②: no (narrowing)\n\nOPEN-HTML-COMMENT adr-0087: not-required (no-migration-prescription) Nothing authorable changes spelling or type: no spec key, export name or stored row moves. What changes is whichwherevalues one runtime face of this package answers: awherearray thatisFilterASTrejects, whichAnalyticsQuerySchema.where(aFilterConditionSchema) never admitted, is now refused as the analyticswheredoor and the query engine already refuse it, where it used to answer every row. Which filter the caller meant by such an array is not something a ledger entry can decide. The package publishes (notunpublished); no ADR-0087 id covers a runtime filter-shape refusal (notregistered/already-registered); and the change is runtime behaviour, not a declaration (notruntime-interface-only/type-surface-only). CLOSE-HTML-COMMENT\n\nAn arraywheresuch as[['stage', '=', 'won']]used to skip every filter step of the\nanalytics (cube) face:query()aggregated every row andgenerateSql()echoed noWHERE,\nwhile the object spelling{ stage: 'won' }answered its rows. The array is now lowered by\n@objectstack/spec'sisFilterAST/parseFilterAST— the lowering the analyticswheredoor\nand the engine already apply — so both spellings answer the same rows and echo the sameWHERE\non both exits.[]still means no filter.\n\nBREAKING:query()andgenerateSql()now refuse, withINVALID_FILTER/ 400, awhere\narray that is not a filter — oneisFilterASTrejects. Each such array used to answer EVERY row\nand echo noWHERE; the analyticswheredoor and the query engine refuse the same shapes.\nMeasured on a fixture wheredis'v1','v2',nulland absent, each of these answered all\nfour rows:\n\n- an infix join,[['d', '=', 'v1'], 'or', ['d', '=', 'v2']];\n- an operator outside the filter-array vocabulary,[['d', 'sounds_like', 'v1']];\n- a list of scalars,[1, 2, 3];\n- a cube-style entry list,[{ member: 'd', operator: 'equals', values: ['v1'] }].\n\nAn array that does lower but carries a comparand or operator the face refuses in its object\nspelling ([['d', 'in', 'v1']],[['d', 'starts_with', 'v']]) is now refused as that object\nspelling is; it too used to answer every row.\n\nThe fix is to write the filter you mean: the prefix form['or', condA, condB]for an infix\njoin, an operator from the filter-array vocabulary, or theFilterConditionobject.\n","changeset_text_note": "Byte-exact at 8d585ea:.changeset/20859-cube-filter-array-lowered.md. Here the HTML-comment opener and closer of the marker line are spelled OPEN-HTML-COMMENT / CLOSE-HTML-COMMENT, because the GitHub sanitizer eats angle-bracket fragments. Removed from the old text:
patch; the bareClause-②: no; and the old sentence ending "An array that is not a filter (an infix join such as[condA, 'or', condB], an unknown operator, a list of scalars) is refused withINVALID_FILTER/ 400 instead of answering every row; write the prefix form['or', condA, condB], or theFilterConditionobject." That sentence now lives in the BREAKING paragraph and the fix line.","gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:driver-conformance :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"gates_reconciliation": "dispatch-gates --commands --repo objectstack-ai/objectstack at 8d585ea derived 60, the same list as at ca8448c. All 60 were run and all exited 0. check:adr-0087-registration reported 1 declared-breaking changeset [BREAKING+clause-②-narrowing] with not-required (no-migration-prescription); check-changeset-no-major reported that this diff introduces no major bump; check:changeset-gate-self-tests passed, and --self-test on adr-0087 gave 441 assertions and on no-major 339. Dist was restored by turbo build, 72 of 72 cached (FULL TURBO), before the dist-reading gates. dispatch-gates --ran reported 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The roster gate check-changeset-fixed.mjs (its roster is under .changeset) exited 0. STALE-TREE notice from dispatch-gates: the branch is at least 7 commits behind origin/main 3fbf3ca, and scripts/doc-authoring-prose-id.baseline.json changed across that range. So check:doc-authoring read the branch copy of that baseline; this diff adds no tracker id to prose. The branch was not merged, because the order allows one commit; the merge ref in CI reads the new copy.",
"pr_body_sentences_now_false": [
"Line 2
Clause-②: no(the seat edits it toClause-②: no (narrowing)).","## Acceptance notes, first bullet, last sentence: "So this PR keeps the claim's
Clause-②: noand names the row for the review to judge." It is now false: the row is gradedno (narrowing), BREAKING, minor. The rest of that bullet (the measured row) stays true."],
"mcp_calls": "0",
"api_writes": "1 relay stroke through scripts/pm: post-stamped = POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot], becoming POST /repos//issues/20859/comments (this delta report). Plus one git push (not REST). No PR body, label or assignee write.",
"deviations": [
"The worktree had been removed after the first report, so it was re-created on the existing branch (git worktree add on claude/issue-20859-cube-filter-array-lowered at ca8448c, the remote head) and removed again after this round.",
"The throwaway probe commits (511f0abfb4 and one after it) were made in a detached probe worktree with hooks off, and were never pushed. They existed only to read what check:adr-0087-registration prints before committing on the branch, so that the branch carries exactly one new commit."
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
ACCEPT — PR #20944 @
8d585eadd1(#20859: the cube face lowers theFilterArrayspelling ofwhere)domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T21:55Z. The seat is the reviewer of record. Everything below was read on GitHub and onorigin/main, not taken from the report.Fixes #20859, thenClause-②: no (narrowing). The changeset bumps@objectstack/driver-memorytominor, with a BREAKING paragraph and one ADR-0087 marker,not-required (no-migration-prescription). The PR assignee isos-support-ai. 3 files, +289/-4; not governed.normalizeFilterslowers an arraywhereat its entry through@objectstack/spec'sisFilterAST/parseFilterAST, the pair the analyticswheredoor and the engine apply. ⛔ No second parser.[]stays no filter.[]control green), then 16/16 green. The ablation of the one call turned 15 red, and the tree was restored to HEAD's blob.wherearray thatisFilterASTrejects answered every row and now answersINVALID_FILTER/ 400. The seat answered the dev's open question B in-seat (5920030329), open to veto, on PR feat(service-analytics,driver-memory): the shared filter lowering at the analytics seams and the cube face's new door (#5930 step 3) #20857's precedent on this face. The patch round (8d585eadd1) carried the grade into the changeset. The review judged the grade on the diff, not on the seat's answer.Local-runs: none).JSON.stringify, which abigintelement makes throw.carrier: none, not filed. No JSON wire carries abigint, and the in-process reach is unmeasured, so it fails filing gate ①.main:mainhas moved 8 commits past the merge base. Two of those touch the named directories (fix(core,drivers,formula,metadata,objectql,platform-objects): refusals, field help and log lines state each decision in words instead of a tracker number (stage 5) #20924's message prose infilter-refusal.ts, and feat(spec): offer useGrouping on the number field form, and delete its stale not-enforced-yet omit row #20934 inspec/src/data/field.form.ts). Neither touchesmemory-analytics.ts,filter.zod.tsor the new pin. GitHub reports the PR mergeable, and the queue re-tests the rebuilt merge.check-expected-skipsOK, exit 0), 0 failed.check-governed-mergesreports NOT governed.Landing: ready, then auto-merge, as two separate relay acts.
Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Landed — PR #20944 as
95fed33a2(the cube face lowers theFilterArrayspelling ofwhere)domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T22:14Z.main:95fed33a2is a squash with one parent and an ancestor oforigin/main. The pin filememory-analytics-filter-array-where.test.tsis present at the squash and absent at the parent. 3 files, +289/-4, as reviewed.pr_readyandautomerge_enablethen ran as two separate relay acts at the reviewed head8d585eadd1(PASS 5920392363).added_to_merge_queueat 21:56Z, merged by the queue at 22:14Z.Clause-②: no (narrowing),@objectstack/driver-memoryminor, BREAKING, withadr-0087: not-required (no-migration-prescription). The seat answered B in-seat (5920030329), open to veto.JSON.stringify, which abigintelement makes throw. No JSON wire carries one, and the in-process reach is unmeasured (carrier: none).Fixesclosed it ascompletedthrough the queue.pm:dispatchedis removed in this act.Generated by Claude Code
fix(service-analytics)!: a cube or dataset dimension on a structured-…
fix(driver-memory): the cube face lowers the FilterArray spelling of …
fix(service-analytics)!: the nested-relation filter gets the engine's…