Repository navigation
finding(data-objectstack): aggregate()'s spec-shape branch sends where unlowered, so one chart's filter is lowered or not depending on which aggregation shape it uses #6825
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 29, 2026 - addedbugSomething isn't workingSomething isn't workingand removed
on Aug 30, 2026 os-project-manager commented
on Aug 30, 2026 CollaboratorMore actionsTriage 审计 · R+34
定级:
bug·domain:ui·priority:p2·pm:queue· typeBug
(finding已摘除 = 已完成首次定级)⚠️ 派发的是度量,不是修法 —— 这是本卡最重要的一条口径卡自己划得很清楚,本席照单批准并加固:
- 认领者的第一件、也可能是唯一一件工作 = 普查:已发布的元数据里,有没有任何真实图表把规则形数组带进 spec-shape 分支。
- ⛔ 在普查出结果之前,不得改
aggregate()的任何一行。 - ⛔ 修法方向(分支 2 也降解 vs. 在生产端拒绝)是一次语义裁决 —— 它决定"一个 filter 在一条其声明契约另有说法的线上意味着什么"。这落在人工底线(契约/协议变更),保留给维护者,⛔ 不是执行席可以自决的,也 ⛔ 不是本席可以代裁的。ObjectStackAdapter.aggregate() must lower rule-shaped filter arrays before the wire — the analytics path skips the lowering find() already performs (prerequisite of the ruled element:number.filter convergence) #6302 的派发口径("No ruled semantic changes — only ordering")当初把它留出来是对的,不要在这里把它捡回去。
普查必须带正对照(零命中纪律)
如果普查结论是"没有任何图表踩中",那个零只有在同一条查询里有一个必然命中的正对照命中时才算读数。具体地:
- 目标查询:带
groupBy数组 或aggregations数组、且where为规则形数组的图表节点; - 正对照:带
groupBy/aggregations数组、where为合法 FilterNode AST 的图表节点 —— 这个必须命中,否则说明查询压根没找到 spec-shape 图表,零是假的。
⛔ 一个没有正对照的零,在本仓最近四张卡(objectstack#13293 / #13304 / #13305 / #13306)里已经连续翻车四次。本卡的普查是同一个陷阱的第五个入口。
为什么 p2,以及升 p1 的条件
- 不是 p1:没有测到任何真实图表踩中。卡自己诚实标了 ⛔ Not established —— 采信。
- 不是 p3:这是一条真实的、已从源码读出的不对称。同一个
filterForRun,经ObjectChart.tsx:483(where:,走 spec-shape 分支)与:491(filter:,走 analytics 分支)进入同一个适配器,却得到不同的线上语义,而决定走哪条分支的属性(有没有groupBy/aggregations)与 filter 毫无关系。这种"作者意图相同、结果不同,且分岔点与该意图无关"的形状,是回归的温床。 ⚠️ 升 p1 的条件,写死:普查一旦找到一例已发布图表踩中,立即升 p1 —— 因为那一例的表现是过滤条件被静默丢弃、图表照常渲染出错误数据,用户看不出来。
定
bug而不是enhancement,理由有人会说"没测到实例,算不上 bug"。本席不采纳:缺陷是已存在于源码中的不对称,普查决定的是它的严重度,不是它的存在性。而且接收端并不更宽容 —— objectstack 的
packages/objectql/src/engine-filter-array-lowering.test.ts钉死了引擎会用同一句 "is not a filter" 拒绝parseFilterAST表达不了的数组形状。⇒ 踩中即失败,不是踩中即降级。锚定:
domain:ui保留两个候选落点 —— 适配器
packages/data-objectstack/src/index.ts、生产端packages/plugin-charts/src/ObjectChart.tsx—— 都在 objectui。原标签正确,不动。⚠️ 但注意:若维护者裁决为"在生产端按 spec 拒绝",那么packages/spec/src/data/query.zod.ts里where的 FilterNode 声明可能需要一句更明确的合同文本,那一半会落到 objectstack 的domain:spec,届时另立卡,⛔ 不要跨仓塞进本卡的 PR。Refs: #6302(analytics 路径的降解修复)· objectstack#12039(Key-2 排序裁决)
Generated by Claude Code
claude commented
on Aug 30, 2026 claudeboton Aug 30, 2026 – with ClaudeContributorAuthorMore actionsClaim: PM loop round R25
Session:session_013hfmP9hoMd3dJwTh85J4yB
Branch:claude/issue-6825-specshape-where-census
Worktree:objectui-issue-6825
Domain:domain:ui
File surface: READ-ONLY census first. ⛔ Per triage, not one line ofpackages/data-objectstack/src/index.tsorpackages/plugin-charts/src/ObjectChart.tsxmay change before the census returns — and the repair direction is a maintainer ruling, so the expected deliverable of this dispatch is a measurement posted to this card, very possibly with no PR at all.
Container & model:M,mode:subagent,model: opus
Clause-②: no — this dispatch changes no contract behaviour because it changes no code.⚠️ The repair would be clause-② territory (it decides what a filter means on a wire whose declared contract says otherwise), which is exactly why triage reserved it for the maintainer and why this claim is scoped to measurement only.
Serial constraints cleared:packages/data-objectstack/**— PR #6828 (#6302, the analytics-branch lowering this card was severed from) MERGED this round, so the card's premise now describes shippedmain. No other open PR touchespackages/data-objectstackorpackages/plugin-charts.
Generated by Claude Code
zhuangjianguo commented
on Aug 30, 2026 CollaboratorMore actions⚖️ RULED — 维护者,2026-08-30,第 5 场总监席决裁批 #4,verbatim「同意」
裁定:采选项 A ——
aggregate()的 spec-shape 分支保持严格,在生产端响亮拒绝未 lower 的where。⛔ 不选 B(在分支 2 也 lower=宽容消费端方向,且祝福一个连AggregateParams都没声明的形状);采 A 而非 D(retire 分支)是保守选择——D 会删一条今天仓内无生产者、但props: any那半测不到、仓外 host 可能已在用的分支,A 保留分支并让它响亮拒绝,不赌仓外无用途。- 裁据:普查(带正对照 + 生产者枚举第二方法)证本仓结构性零能踩中(无生产者/fixture/测试),接收端引擎已钉死
is not a filter拒绝(engine-filter-array-lowering.test.ts:308/313/317/318)⇒ 踩中即失败非降级。现状失败形态是静默(过滤丢、图表照渲、数字错、作者无信号),A 让失败响亮、契约面在生产端拒绝。 - 落点:
packages/data-objectstack/src/index.tsspec-shape 分支;⛔ 不 lower。 - ⛔ 不并入本卡的两项:①
AggregateParams契约缺口(packages/types/src/data.ts:1248声明groupBy:string无where/aggregations)——objectui 自己的契约缺陷,A/B 都不解决,已另立卡独立派发;② finding(data-objectstack): aggregate()'s spec-shape branch never readsfilter/field/function, so an arraygroupByon the LEGACY shape silently drops both the filter and the measure #6864(同looksLikeSpecShape谓词第二种更坏失败:legacy 形状带数组groupBy落进分支 2,filter 与度量一起静默丢——症状正是 triage 写死的 p1 条件)待分诊定级。
性质:已发布契约语义(生产端拒绝行为)⇒ 人工地板。席位边界(裁不派):录裁 + 转
pm:queue。同笔摘needs-user-decision。裁决入本场追认摘要表。
Generated by Claude Code
- 裁据:普查(带正对照 + 生产者枚举第二方法)证本仓结构性零能踩中(无生产者/fixture/测试),接收端引擎已钉死
claude commented
on Aug 30, 2026 claudeboton Aug 30, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{ "issue": 6825, "status": "done", "branch": "claude/issue-6825-aggregate-spec-shape-strict-where", "pr": "https://github.com/objectstack-ai/objectui/pull/6911", "premise_still_valid": true, "summary": "Executes the 2026-08-30 ruling (option A) at packages/data-objectstack/src/index.ts. aggregate()'s spec-shape branch now calls assertSpecShapeWhereIsFilterAst(params.where, resource) before assigning queryAst.where, throwing the new exported UnloweredAggregateWhereError instead of posting an array the spec's own isFilterAST gate rejects. The gate IS isFilterAST, imported as a value from @objectstack/spec/data, so the producer-side refusal and the ingress refusal cannot drift; it is scoped to arrays with two carve-outs measured against the receiving door rather than assumed (a non-array where is the declared FilterCondition, and [] is 'no filter' the engine accepts). The error carries MalformedFilterError's INVALID_FILTER/400 pair so isMalformedFilterError() recognises it and a failed widget renders 'this filter is malformed' rather than 'check your connection'. Base moved under the dispatch: the prompt named fab4802e3, origin/main was dbd519450 at worktree time and that is the merge-base used throughout. Second commit updates packages/data-objectstack/README.md, whose filter section claimed 'Both read paths lower that array to the same AST' - true of find()'s $filter and aggregate()'s analytics filter, false of aggregate({ where }), so a reader following it would have written the exact shape that now throws.", "tests": "All figures from 64fb1689b (the branch head), suites run from the repo root, never a package-filtered test script. UNION at that commit: pnpm exec vitest run --maxWorkers=2 packages/data-objectstack/ => 'Test Files 50 passed (50)', 'Tests 691 passed (691)'. New file packages/data-objectstack/src/aggregate-spec-shape-where.test.ts carries 22 of those. TYPECHECK: pnpm --filter @object-ui/data-objectstack type-check clean, and proven to actually cover the edit - tsc --noEmit --listFiles lists both src/index.ts and the new test file (tsconfig includes src/**/* with no test exclusion), so this is not the 'ran but measured nothing' case. RED FIRST, two independent neutralisations, IDENTICAL row for row at 'Tests 10 failed | 12 passed (22)': (1) index.ts reverted to the merge-base dbd519450, base version proven on disk via hash-object equal to the base blob plus zero mentions of the guard; (2) surgical ablation neutralising only the guard body to an early return, mutation proven on disk via an anchored marker count of exactly 1 plus a blob differing from the HEAD blob. PREDICTED vs OBSERVED, every row: 'throws UnloweredAggregateWhereError' RED/RED; 'sends NOTHING to any of the three doors' RED/RED; 'the message tells an author what arrived, what is expected, and where to fix it' RED/RED; the 7 refusal-parity rows RED/RED (7 of 7); 'the refused params take the SAME branch as the observed control' predicted GREEN because it asserts branch selection, which does not depend on the refusal - observed GREEN (the discriminating row); the 3 branch-on-the-wire rows GREEN/GREEN; the 8 pass-through rows GREEN/GREEN. The failure text of 'sends NOTHING' is the card's silent pass-through measured rather than argued: 'AssertionError: expected spec-shape to be null' - with the refusal removed the unlowered rule array lands on client.data.query and the call returns rows. RESTORE PROVEN BOTH WAYS ON BOTH LEGS: git hash-object equal to the HEAD blob AND path-scoped git diff HEAD empty AND zero residual markers. Both scripts carried trap RESTORE_FN on EXIT INT TERM with absolute paths resolved from git rev-parse --show-toplevel. My first ablation script's restore assertion was tree-wide and so reported RESTORE FAILURE on an unrelated pending README edit; the target path itself was verified clean immediately and the second script scoped the assertion to the mutated path. NO REBUILD NEEDED OR SKIPPED on either leg: the test imports the subject by relative source path ('./index'), no dist is in the resolution - which the ablation itself proves, since neutralising the source changed the observed result. LINT against the base versions of the same files, not a bare count: eslint . --format json, 56 files in both runs, src/index.ts 121 warnings at base and 121 at head (delta 0), 0 errors throughout; the new test file contributes 11 warnings, all @typescript-eslint/no-explicit-any from the shared fetch-mock idiom, the identical count both siblings carry (aggregate-filter-lowering.test.ts 11, aggregate-capability.test.ts 11). GATES, quoting each gate's own verdict line rather than a bare exit code: check:control-bytes OK (5756 tracked text files), check:doc-fences OK (224 documents), check:changeset-presence OK (1 changeset for 2 changed source files of 1 released package), check:changeset-no-major OK, check:spec-symbols OK, check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK, check:vi-mock-inherit OK, check:esm-specifiers OK, check:skills-paths OK. check:spec-floors and check:readme-exports are PREREQUISITE NOT MET locally and are reported as neither red nor green: both read built artifacts and 16 of 39 packages were unbuilt here, so every finding is 'no-artifact' with the gate itself printing 'Build the workspace before running this gate'. The one real risk they cover, a README naming a non-existent export, was checked directly instead: dist/index.d.ts after pnpm --filter @object-ui/data-objectstack build carries UnloweredAggregateWhereError, MalformedFilterError and isMalformedFilterError. CI runs both over a full build. Note objectui has no scripts/pm/dispatch-gates.mjs (that script lives only in objectstack and answers only about its own tree), so the gate family was derived by hand from this repo's package.json and .github/workflows/. All heavy runs went through the shared lock; one attempt returned exit 99 (queue timeout, 540s) and is recorded as NOT MEASURED, not as a red - it was re-run with the same OS_VERIFY_LOCK_SLOT and acquired.", "branch_selection_assertion": "The two branches post to different endpoints, so the wire proves the branch for anything that reaches it: spec-shape params with a lowered where land on http://localhost:3000/api/v1/data/opportunity/query with ZERO calls to /analytics/query; the same rule array under 'filter' with legacy params lands on /analytics/query and is still lowered to ['stage','=','won']; and adding a where key to those same legacy params flips them onto the spec-shape door, which is the asymmetry the card reported, asserted on the wire. A refusal reaches no wire, so for the refusing inputs the branch is proven a second way: looksLikeSpecShape is transcribed into the test as branchSelectors(), and the refused params are asserted to yield the SAME selector triple [true, true, true] as a control observed landing on the spec-shape door inside that same test. That row stays GREEN under both neutralisations, which is what shows it asserts branch selection rather than riding on the behaviour under test.", "green_control": "'everything the receiving door accepts still passes through untouched' - 8 tests, all green before and after and under both neutralisations. A properly-lowered where reaches client.data.query byte-unchanged for a comparison tuple, a logical AST node, an or node and a legacy nested array of nodes; plus the FilterCondition object { stage: 'won' }, the empty array [], the no-where case (no where key on the body, groupBy and aggregations forwarded), and limit forwarded alongside a lowered where. Separately, the analytics branch still lowers the very rule array the spec-shape branch refuses, so 'we now refuse' cannot be confused with 'we now refuse everything'.", "refusal_message": "aggregate('opportunity'): the spec-shape branch received a `where` array that is not a filter - [{\"field\":\"stage\",\"operator\":\"equals\",\"value\":\"won\"}]. This branch posts `where` to POST /data/:object/query verbatim, so it must ALREADY be lowered: either a FilterCondition object (`QuerySchema.where`, @objectstack/spec data/query.zod.ts), or a FilterArray the ingress can lower - a comparison tuple (['stage','=','won']), a logical node (['and',[..],[..]]), or an array of nodes (data/filter.zod.ts, gate `isFilterAST`). A ViewFilterRule[] ([{ field, operator, value }, ...]) is authoring sugar, not a filter: lower it in the producer that built these aggregate params, not here. Nothing was sent to the server, so no unfiltered numbers came back.", "why_an_author_can_act_on_it": "Four acts, each pinned by an assertion in the test: the value received VERBATIM, so the producer is identifiable from a log line alone; the shapes accepted AND where the spec declares them (QuerySchema.where in data/query.zod.ts, the FilterArray forms in data/filter.zod.ts), so the fix does not require guessing; where to fix it - the producer that built the params, not this adapter, which is the contract-first direction AGENTS.md #0.1 mandates and the direction the ruling chose; and that nothing was sent, so the author knows no wrong number was produced and no partial state exists. It also names the branch, so a reader of the log knows which of the two ran - the branch being invisible is what made the original defect hard to see.", "mcp_calls": "0 - a repo-scoped REST read probe returned HTTP 200 on this seat, so the whole run used REST (reads, PR creation, comment); `gh` is absent in this container. No MCP GitHub tool was called and no dedupe search was needed, as both adjacent findings were already carded by the ruling itself.", "ruling_compliance": { "did_not_touch_AggregateParams": "CONFIRMED - packages/types is not in the diff at all. git diff --name-only dbd519450 HEAD is exactly: .changeset/6825-aggregate-spec-shape-strict-where.md, packages/data-objectstack/README.md, packages/data-objectstack/src/aggregate-spec-shape-where.test.ts, packages/data-objectstack/src/index.ts. Nothing needed widening it to typecheck; the guard's parameters are (where: unknown, resource: string), so it never touches the params type.", "did_not_lower_on_branch_2": "CONFIRMED - the spec-shape branch calls no lowering function. translateFilterArray is not referenced anywhere in that branch; the guard's only outcomes are 'return' (leave the value exactly as received) and 'throw'. The pass-through tests assert byte-equality with what the caller passed, so a lowering slipped in later turns them red.", "did_not_retire_the_branch": "CONFIRMED - the branch is intact, still selected by the same unchanged looksLikeSpecShape predicate, and still posts groupBy, aggregations, where and limit to client.data.query. 12 of the 22 new tests exercise it end to end on the wire, including every shape the receiving door accepts." }, "landing_path": "Human floor, NOT clause 2, per the ruling's own classification. The PR body states the absence of the clause-2 review as deliberate and why, following PR #6897. PR #6911 is a DRAFT.", "open_questions": [], "out_of_scope_findings": [] }PR: #6911 (draft) - branch
claude/issue-6825-aggregate-spec-shape-strict-whereat64fb1689b.Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Sep 8, 2026 - added a commit that references this issue
on Sep 9, 2026
Observation from the objectui execution seat while implementing #6302 (the analytics-path lowering). NOT fixed there, deliberately: fixing it would decide what a filter MEANS on a wire whose declared contract says otherwise, which #6302's dispatch order expressly reserves for a ruling.
What was measured (source reads, on objectui
26896c689and the objectstack tree in the same container)ObjectStackAdapter.aggregate()(packages/data-objectstack/src/index.ts) has TWO branches, and they now differ in how an array filter reaches the server:field/function/groupByparams) —payload.whereis lowered throughtranslateFilterArrayand posted toclient.analytics.query. This is what ObjectStackAdapter.aggregate() must lower rule-shaped filter arrays before the wire — the analytics path skips the lowering find() already performs (prerequisite of the ruled element:number.filter convergence) #6302 fixed.looksLikeSpecShape:groupByis an array, oraggregationsis an array, orwhereis defined) —queryAst.where = params.whereverbatim, thenclient.data.query. No lowering, before or after ObjectStackAdapter.aggregate() must lower rule-shaped filter arrays before the wire — the analytics path skips the lowering find() already performs (prerequisite of the ruled element:number.filter convergence) #6302.The two branches are reachable from ONE authored chart.
packages/plugin-charts/src/ObjectChart.tsxpasses the SAME resolved value to both:where: filterForRun(spec-shape branch)filter: filterForRun(analytics branch)Which branch runs depends only on whether the chart carries
groupBy+aggregationsor the legacyfield/functionpair. So after #6302 one chart schema's filter is lowered and the other's is not, decided by a property that has nothing to do with the filter.The receiving door is not more forgiving than the analytics one: objectstack's
packages/objectql/src/engine-filter-array-lowering.test.tspins that the engine refuses array shapesparseFilterASTcannot express, with the same "is not a filter" refusal.Why this was NOT folded into #6302
The
whereon branch 2 is the spec Query DSL'swhere—packages/spec/src/data/query.zod.tsdeclares it a FilterNode AST, so a caller sending rule objects there is off-spec, and AGENTS.md #0.1 (contract-first) says the repair belongs at the producer, not as a tolerant lowering in the adapter. #6302's ruling was explicitly scoped: "No ruled semantic changes — only ordering." Deciding branch 2 is a semantic call, so it is reported rather than taken.What is NOT established here
No measured case of a real authored chart carrying a rule-shaped array into branch 2. The asymmetry is read from source; whether any shipped metadata exercises it is exactly what triage should establish before anything changes. If nothing does, the honest outcome may be to leave branch 2 strict and reject at the producer instead.
Refs: #6302 (the analytics-path fix) - objectstack#12039 (the Key-2 sequencing ruling).
Generated by Claude Code
Generated by Claude Code