Skip to content

fix(driver-memory): the cube face lowers the FilterArray spelling of where instead of answering every row - #20944

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20859-cube-filter-array-lowered
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20859-cube-filter-array-lowered

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20859
Clause-②: no (narrowing)

What changed

MemoryAnalyticsService.normalizeFilters (packages/drivers/driver-memory/src/memory-analytics.ts) read only a non-array object where. A FilterArray where therefore skipped the shared comparand doors, the shared lowering, the vocabulary gate and the flatten, so its predicate vanished.

The array is now lowered at the method's entry by a module-private lowerWhereFilterArray, which calls the spec's own pair: isFilterAST gates the shape and parseFilterAST lowers it. That is the pair the analytics where door (lowerAnalyticsWhere, see #5334) and the engine's lowerWhereFilterArray call. The face adds no parser of its own. The lowered condition then enters the step-3 door (#20857) exactly as an authored object does. So both spellings meet the same comparand doors, the same lowerFilterCondition and the same gate, and both exits (query() and generateSql()) read the same normalized list.

The three arrival answers are the analytics where door's:

arrival answer
[] no filter (unchanged: every row, no WHERE)
an array isFilterAST accepts lowered by parseFilterAST, then the door, the lowering and the gate, like the object spelling
any other array (infix join, unknown operator, list of scalars, a cube-style entry list) refused INVALID_FILTER / 400 on both exits

Measured

On the fixture from the card (d is 'v1', 'v2', null, absent):

where before (base d78a0bda07) after (ca8448c005)
{ d: 'v1' } rows 1; echo d = 'v1' rows 1; echo d = 'v1' (control, unchanged)
[['d', '=', 'v1']] rows 1, 2, 3, 4; echo has no WHERE rows 1; echo d = 'v1'
[] rows 1–4; no WHERE rows 1–4; no WHERE (unchanged)
[['d', '=', 'v1'], 'or', ['d', '=', 'v2']] (infix) rows 1–4 INVALID_FILTER / 400
[['d', 'in', 'v1']] (scalar in) rows 1–4 INVALID_FILTER / 400, as { d: { $in: 'v1' } } is

Tests

New pin file packages/drivers/driver-memory/src/memory-analytics-filter-array-where.test.ts, 16 cases:

  • the card's row, rows and echo;
  • seven spellings, each answering the rows and the echo of the FilterCondition it lowers to (a comparison, a flat list, an and group, an or group, a negative-polarity leaf, is_null, is_not_null);
  • the [] control;
  • four non-lowerable arrays refused on both exits;
  • three lowerable arrays that are refused exactly as their object spelling is refused.

Refusals assert the envelope (code + status), not the prose.

  • Red first: at 62eff2a8f9 (pins only, source at the base), the file gave 15 failed, 1 passed. The one pass is the [] control. The card's row read rows ['1','2','3','4'] and echo undefined.
  • Green: 16 of 16 passed at f2be8432cd.
  • Ablation on the parse: scripts/ablation-replace.mjs replaced const where = lowerWhereFilterArray((query as { where?: unknown }).where); with the pre-fix read. The anchor went from 1 to 0, and the blob moved cc0aaa764022 to 3445aece6953. Result: 15 failed, 1 passed (the [] control). The restore was proven: the blob equals HEAD cc0aaa764022, and git diff HEAD is empty. The pins import ./memory-analytics.js by relative path, and the package's vitest config aliases @objectstack/* to source, so no build sat between the mutation and the reading.
  • At ca8448c005, after pnpm --filter '@objectstack/driver-memory...' build, the driver-memory suite gave 66 files and 1429 tests passed. typecheck exited 0, and tsc --noEmit --listFiles includes the new test file.
  • Gates at ca8448c005: dispatch-gates --commands derived 60 families, and all 60 were run.
    • Three first answered PREREQUISITE NOT MET (exit 3): check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt. After a full turbo run build (72 tasks) they were re-run, and all three passed.
    • dispatch-gates --ran reported 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN.
    • The six roster families under touched directories were also run, and all passed: check-changeset-fixed, authz-resolver, error-code-casing, filter-alias-parity, object-def-param-keys, tenant-chokepoint.
  • Lint, narrowed and measured:
    • Population: eslint.config.mjs's **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} block.
    • eslint --no-inline-config --format json over the two changed TypeScript files: 2 files, 0 errors, 0 warnings, at ca8448c005.
    • The config enables no type-aware linting, so no untouched file's verdict can move.
  • NOT MEASURED, left to CI: the path-scheduled CI jobs (Test Core shards, Dogfood, Temporal Conformance, Build Core) and the workspace type-check lanes.

Acceptance notes

  • Accept-set reading. A well-formed array that answered every row now answers its predicate's rows. An array whose lowered condition the doors refuse is refused as that object spelling already is. One row goes beyond "a dropped predicate now filters": an array isFilterAST refuses (the infix join, an unknown operator, a list of scalars, a cube-style entry list) used to answer every row and now answers INVALID_FILTER / 400. None of those shapes is admitted by AnalyticsQuerySchema.where (FilterConditionSchema) or by FilterArraySchema, and the triage direction rules out dropping. Answered by the seat (5920030329, B, open to veto): the face's accept set narrows, as it did 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 ([finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930 step 3) on this same face, so this PR takes Clause-②: no (narrowing). The changeset is minor with a BREAKING paragraph and one ADR-0087 marker; the claim's "nothing is newly refused" was the seat's error.
  • On an array, the shared comparand-shape and comparand-type faces run twice: inside parseFilterAST, then at the door. Both are idempotent (an assertion, and a copy-on-write narrowing), so the verdict is the same one the first pass gives.
  • normalizeFilters' object branch still carries its !Array.isArray(where) test. After the lowering it is always true for anything that reaches it, and it is kept as the object branch's own statement of what it reads.
  • Unmeasured reading, not filed (carrier: none): the analytics where door's and the engine's non-lowerable-array refusals 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 envelope. This face's message renders through a non-throwing preview. No JSON wire can carry a bigint.

Generated by Claude Code

An array `where` at MemoryAnalyticsService answers every row and the
generateSql echo carries no WHERE, while the object spelling of the same
filter answers its rows. These pins hold the lowering the next commit adds:
both spellings answer the same rows and echo, `[]` stays no filter, and an
array the face cannot lower is refused INVALID_FILTER / 400 on both exits.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
…`where`

MemoryAnalyticsService.normalizeFilters read only a non-array object `where`,
so `[['d', '=', 'v1']]` skipped the comparand doors, the shared lowering, the
vocabulary gate and the flatten: query() aggregated every row and the
generateSql() echo carried no WHERE, while `{ d: 'v1' }` answered its row.

The array is now lowered at the method's entry by the spec's own pair, the one
the analytics `where` door and the engine call: isFilterAST gates the shape and
parseFilterAST lowers it, so the array reaches the doors as the
FilterCondition it spells. `[]` stays "no filter"; an array with no lowering
(the infix join, an unknown operator, a list of scalars, a cube-style entry
list) is refused INVALID_FILTER / 400 on both exits instead of answering every
row.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3fbf3ca6173e80dc515448fd0df497dfb3aed29c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eb65f705ae7da7b9de4649e9931b515ab32645b5 — the merge of head 8d585eadd1eeae6355cd641d08d54418b6e21576 into base 3fbf3ca6173e80dc515448fd0df497dfb3aed29c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin eb65f705ae7da7b9de4649e9931b515ab32645b5 && git checkout eb65f705ae7da7b9de4649e9931b515ab32645b5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3fbf3ca6173e80dc515448fd0df497dfb3aed29c 8d585eadd1eeae6355cd641d08d54418b6e21576 && git checkout -B drift-repro 3fbf3ca6173e80dc515448fd0df497dfb3aed29c && git merge --no-ff 8d585eadd1eeae6355cd641d08d54418b6e21576

node scripts/docs-audit/affected-docs.mjs --json 3fbf3ca6173e80dc515448fd0df497dfb3aed29c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…wing

A `where` array isFilterAST rejects used to answer every row on query() and
generateSql() and now answers INVALID_FILTER / 400, so the driver-memory
changeset moves to minor with a BREAKING paragraph naming the refused shapes,
the `Clause-②: no (narrowing)` line, and one ADR-0087 disposition
(not-required, no-migration-prescription). No code or pin changes.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8d585eadd1eeae6355cd641d08d54418b6e21576
Local-runs: none

Inputs: card #20859 (body and its six comments: triage 5912813886, release 5913765727, claim 5918346956, os-dev-report 5920005720, seat answer 5920030329, delta report 5920225694); PR #20944 body, file list and the net diff against main at the head (3 files, +289/-4: .changeset/20859-cube-filter-array-lowered.md, packages/drivers/driver-memory/src/memory-analytics.ts, packages/drivers/driver-memory/src/memory-analytics-filter-array-where.test.ts); precedent PR #20857 (#20810, 793fb839) and its two changesets; the head's check-runs. The head was still 8d585eadd1 when read (one changeset-only commit on ca8448c005; the merge base with main is 67c1b11a20).

Check-runs on the head, read once at 2026-09-30T21:53Z and not polled: 40 runs, none failed. In progress at that moment: Lint & Repo Gates, TypeScript Type Check, Test Core (1/6), Test Core (4/6). Success: Build Core, Dogfood Regression Gate and its three shards, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Test Core (2/6, 3/6, 5/6, 6/6), the four Type Check · lanes (workspace, source gates, consumer gates, debt ledger), Check Changeset (both runs), Dogfood Verify CLI, the claim, single-writer and part-of guards, Check Documentation Links, Flag docs affected by code changes, filter. Skipped: Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in), one Auto Label and one Check PR Size run. The commit status Vercel is pending. Of the seven required contexts, four are green and three were still running when read; nothing is red. No governed surface is in the file list.

① Derived judgments

Read against the spec pair (packages/spec/src/data/filter.zod.ts: isFilterAST, parseFilterAST, VALID_AST_OPERATORS), the analytics where door (packages/services/service-analytics/src/strategies/filter-normalizer.ts: lowerAnalyticsWhere, filterArrayNotLowerableError) and the engine door (packages/objectql/src/engine.ts: lowerWhereFilterArray), all at the head.

  1. The lowering is the same one, with no second parser — RIGHT. lowerWhereFilterArray in memory-analytics.ts gives the three arrival answers lowerAnalyticsWhere and the engine door give, in the same order: [] is no filter; isFilterAST gates before parseFilterAST (so the lenient $op fallback in convertComparison is never reached by an unknown operator); anything else array-shaped is refused. The function contains no parsing logic of its own; the operator list in its message is derived from VALID_AST_OPERATORS, the set the spec derives from its one operator table. The "unreachable" branch is unreachable by construction (an array isFilterAST accepts lowers, by induction over the three node shapes, to a non-empty object), and it is kept loud exactly as the other two doors keep theirs.
  2. [] still means no filter — RIGHT. It returns undefined, the object branch is skipped, and normalizeFilters returns an empty list: no $match stage and no WHERE. At the base an array of any length skipped the branch, so the [] answer is unchanged, and it is the reading parseFilterAST([]) and both other doors give.
  3. Both exits read the lowered list — RIGHT. query() and generateSql() each call this.normalizeFilters(query), and the lowering sits at that method's entry; the reassigned where is the only read of query.where in the file.
  4. No path still drops an array silently — RIGHT. Every array is one of: empty (no filter, by contract), lowered and handed to the step-3 door, or thrown. The object branch's !Array.isArray(where) test is therefore always true for what reaches it, as the acceptance note says. A non-array where is untouched by this diff: an object meets the step-3 door as before; undefined / null is no filter. (A scalar where such as a string is still read as no filter — pre-existing, outside this card, and the same reading lowerAnalyticsWhere gives a non-object where; noted, not filed here.)
  5. The refusal row — RIGHT: same envelope and same shapes as the analytics door. The face refuses through unsupportedFilterError (filter-refusal.ts: code INVALID_FILTER, status 400), the analytics door through invalidFilterError (same code, same status), and all three doors gate on the same isFilterAST, so the refused set is identical by construction. The message renders through previewFilterArray, which wraps JSON.stringify in try/catch and falls back to String(value) or a kind word, so a bigint element, undefined, or a cyclic value cannot turn the refusal into a raw TypeError. The lowerable-but-refused arrays are refused where their object spelling is: a scalar in comparand and an undefined comparand by the shared shape and type faces inside parseFilterAST (INVALID_FILTER / 400 from the spec); starts_with by this face's own gate ($startsWith is not a key of MONGO_TO_CUBE_OPERATOR, so assertFilterConditionShape refuses it, 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 left it).
  6. The two shared comparand faces run twice on an array — RIGHT, and harmless. parseFilterAST runs assertListComparandShapes then normalizeFilterComparandTypes; the step-3 door runs them again on the returned condition. The first is an assertion, the second is copy-on-write narrowing that is a no-op on its own output, so the second pass cannot move a verdict.
  7. Public surface — unchanged, RIGHT. No export, type or schema moves; the three new functions are module-private; the added imports come from @objectstack/spec/data, which driver-memory already depends on (workspace:*) and whose barrel re-exports filter.zod wholesale. No new dependency edge, no spec artefact to regenerate.
  8. Pins — RIGHT as written. The file carries 16 cases (the card's row, seven spellings each paired with the object it lowers to, the [] control, four non-lowerable arrays refused on both exits, three lowerable arrays refused as their object spelling is); refusals assert code + status only; the echo control is the one the card asked for; the file imports ./memory-analytics.js relatively while vitest.config.ts aliases every @objectstack/spec/* namespace to spec/src/*/index.ts, so the ablation sentence "no build sat between the mutation and the reading" is true. The ablation's restored blob cc0aaa764022 is the head's blob of memory-analytics.ts. The run counts (15/1 red, 16/16 green, 66 files / 1429 passed) are the dev's readings and were not re-run here; 66 test files exist under driver-memory at the head, and the Test Core shards are the gate.

② Semver level

  • Changeset: @objectstack/driver-memory (published: publishConfig.access: public, no private flag) at minor, carrying Clause-②: no (narrowing), a **BREAKING** paragraph, the migration line, and one ADR-0087 marker not-required (no-migration-prescription) inside an HTML comment. The PR body's line 2 carries the same Clause-②: no (narrowing). Under check-changeset-no-major.mjs a declared (narrowing) is BREAKING and ships minor in the launch window; Check Changeset is green on the head.
  • The grade, judged on the diff — RIGHT. The widening question is truthfully no: no new key lands on a published payload, and a lowerable array was already accepted (answered, wrongly) before, so its corrected answer widens nothing. The narrowing arm is real: on a published export, a non-lowerable array where that used to be answered (every row) is now refused, and a lowerable array whose condition the doors refuse is now refused. That the declared schema never admitted those arrays does not change the face's measured accept set, and the precedent on this same face (feat(service-analytics,driver-memory): the shared filter lowering at the analytics seams and the cube face's new door (#5930 step 3) #20857: Clause-②: yes (narrowing), minor, BREAKING, the same marker) graded the identical situation — comparands the schema never admitted — as a narrowing; grading this no would give one face two readings. The precedent's yes came from its widenings ($or, $null, $between); this diff has none, so no (narrowing) is the right arm. The first round's patch / bare no was wrong for the same reason, and the regrade commit touches the changeset alone.
  • The marker's stated facts, each judged: nothing authorable changes spelling or type — true (no spec key, export or stored row moves). AnalyticsQuerySchema.where is FilterConditionSchema.optional() (analytics.zod.ts), a z.record(...) intersected with a z.object(...), neither of which admits an array — true, it never admitted the refused shapes; FilterArraySchema (a union of comparison tuple, logic group and non-empty list) admits none of the four either. The analytics door and the engine already refuse the same shapes with the same envelope — true. The package publishes — true. No ADR-0087 conversion covers a runtime filter-shape refusal, and the change is runtime behaviour rather than a declaration — consistent with the precedent's identical marker on this face. The gate's one mechanical check for this category (no migration prescription in the body) reads the same remedy-sentence shape the precedent shipped; the dev's check:adr-0087-registration run reported exit 0, and CI's Lint & Repo Gates was still running when read.
  • The BREAKING paragraph's list — complete and true. The first class, "a where array that is not a filter — one isFilterAST rejects", is the exact complement of the lowered set, so it is complete by construction; each of the four examples fails isFilterAST for the stated reason (an infix join's first element is neither a keyword nor a field, and not every element is a node; sounds_like is outside VALID_AST_OPERATORS; a scalar list has no node; a cube-style entry list has object elements). The second class, "an array that does lower but carries a comparand or operator the face refuses in its object spelling", covers every remaining newly refused arrival, including the pin file's undefined-comparand row; both named examples are true (point 5 above). "It too used to answer every row" is true at the base for any array. [] unchanged: true.
  • The review faces, sentence by sentence: every factual sentence of the changeset and of the PR body ("What changed", the three-answer table, "Measured", "Tests", "Acceptance notes") is true against the head and the code named above, including the two the seat edited after the regrade (line 2, and the accept-set bullet's last sentence, which now states the seat's answer and the changeset's shape). The measured counts (run totals, 60 gate families, and the base blob d78a0bda07, which is an ancestor of the head) are the dev's readings; nothing in the artefacts contradicts them. No sentence found false.

③ Boundary flags

  1. The stop clause not stopped on — answered, accepted. The order (claim 5918346956) says the array spelling is never dropped, and the card's scope says it is lowered or refused loudly and never dropped. For an array isFilterAST rejects there is no lowering, so the refusal is the only order-compliant answer and the alternative is the forbidden one. The dev implemented it, named it in the report's summary, deviations and open_questions, and the seat answered the grade (B). The code did not move on the regrade; only the changeset did.
  2. Behind main — answered, with an update. When the first report was written, main was at bee75cebe6, three commits past the merged 67c1b11a20, and none of the three touches driver-memory, spec/src/data or core/src — true as written. As of this read, main is eight commits past the merge base, and two of the later five do 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 (3fbf3ca617) edits driver-memory/src/filter-refusal.ts (message prose only; unsupportedFilterError, the helper this diff calls, is byte-identical on both sides) and core/src/*; feat(spec): offer useGrouping on the number field form, and delete its stale not-enforced-yet omit row #20934 (31c39964fc) edits spec/src/data/field.form.ts. Neither touches memory-analytics.ts, filter.zod.ts or the new pin; the diff's added lines carry no tracker number (the thing 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 stripped); GitHub reports the PR mergeable; the delta report itself recorded the stale-tree notice. The queue rebuilds on current main, which is the re-verification this needs. No re-merge is owed for this record.
  3. The out-of-scope note — verified, escalated to the seat. filterArrayNotLowerableError in the analytics door and the engine door's !isFilterAST refusal both interpolate a bare JSON.stringify(where), which throws a TypeError on a bigint element, so an in-process caller sending a non-lowerable array holding a bigint would get a raw throw instead of the envelope. No JSON wire can carry one, so "carrier: none" is right for the wire; both services are published exports, so an in-process reach exists. The facts are correct and the note is in the shape Prime Directive chore: version packages #10 asks for; whether it is filed is the seat's call, not this record's.
  4. The rate-limit stop and resume — answered. The branch carries exactly the commits the narrative names, each with the file set it claims: 62eff2a8f9 (the pin file alone), f2be8432cd (memory-analytics.ts alone), 0d27cf3465 (the changeset), ca8448c005 (the merge of 67c1b11a20), 8d585eadd1 (the changeset regrade alone). Nothing was redone and nothing is missing.
  5. open_questions[0] (keep no, or take (narrowing)) — answered B by the seat; judged B on the diff in ② above.

Implemented-by: claude/issue-20859-cube-filter-array-lowered
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 21:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 95fed33 Sep 30, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20859-cube-filter-array-lowered branch September 30, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants