Repository navigation
runtime: POST /analytics/query refuses the array where the objectui adapter now sends for every array-form filter — AnalyticsQueryRequestSchema.where is FilterConditionSchema, while lowerAnalyticsWhere (the gate ui#6302 measured) accepts filter AST #15828
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Sep 5, 2026 Triage routing:
domain:spec+needs-user-decision+bug+priority:p1+finding。分诊席(
session_01SwJQDFKe8tVit3BXQ9EfR5,R+164)。⛔ 本席不认领、不派工、不写代码、⛔ 不裁决。origin/main=cc5b3dd。复现:契约与路由两端都成立
packages/spec/src/data/analytics.zod.ts:299 where: FilterConditionSchema.optional().describe('Filtering criteria (canonical Query DSL FilterCondition)'), packages/runtime/src/domains/analytics.ts :49 function assertAnalyticsQueryBody(body: unknown): void { :62 const parsed = AnalyticsQueryRequestSchema.safeParse(body); :102 / :125 assertAnalyticsQueryBody(body); ← 唯一 HTTP 入口,先跑这一parse⇒
where声明为FilterConditionSchema(一个 record),而路由在任何归一化之前就用它safeParse。⇒ 数组形 filter AST 在进服务之前就被 400 掉。卡的四行 safeParse 结果(record ACCEPT / ast_tuple_array·ast_single·rule_array 全 REFUSE)与此一致。priority:p1(⛔ 高于本班次同类,判据写下)这不是"两份声明不一致"的整洁性问题,而是一条已发布的客户端路径今天就被服务端拒绝:
- 发送方已经上线 —— objectui
ObjectStackAdapter.aggregate()(pina472b07,PR objectui#6828 关闭 objectui#6302)把数组filter经translateFilterArray降成 filter AST 元组并作为where发出。 - 接收方拒绝它 —— 上面那条 parse。
- 而这个形状是被强制的:
element:number自 spec: two ComponentPropsMap entries contradict their ruled authorities —object-grid.datavs ViewDataSchema,element:number.filtervs the platform filter form (rulings ui#6207-A, ui#6206-B) #12039 起只接受数组。⇒ 作者按现行规范写出来的东西,在 runtime 路由上答400 Invalid AnalyticsQuery body: where: ...而不是聚合结果。 - 幸存的只有 MongoDB 式 record —— ⭐ 而那正是 ui#6206-B 已经退役的形式。⇒ 今天能跑通的,恰恰是被判退的那一种。
⇒ 已发布 × 已发布,方向明确,无需假设 ⇒ p1。
⛔ 不给 p0:无数据损失、无安全面、失败是响亮的 400(⛔ 不是静默错答),且卡诚实标注端到端未跑(「Not run end to end here … "Read, not run" on the route hop; the schema verdict is a run」)。⭐ 这个自我标注很准:schema 那一跳是跑出来的,路由那一跳是读出来的,而路由函数只有 15 行且直接调用被测的 schema。
⭐ 提级到 p0 的条件:任一真实部署经 runtime 路由服务 analytics 且已授权
element:number数组 filter ⇒ 那是在野破损,按 p0 论。为什么进决策箱(⛔ 不入队)
卡把决策写得很干净,本席原样转交、⛔ 不选边:
Contract-first: the spec says
whereis aFilterCondition; the service accepts a wider set than the spec declares。- (A)
AnalyticsQuerySchema.where也接受服务已经在降的FilterArray⇒ 声明=执行,且 objectui 已落地的降级变成正确的。落点packages/spec⇒ 本卡车道。 - (B) objectui 适配器把数组 filter 降成
FilterConditionrecord ⇒⚠️ 与该树里所有其他降级方向相反,且卡实测「nothing does it today:packages/core/src与packages/data-objectstack/src里不存在数组→record 的降级」。落点在 objectui ⇒ 若裁 B,车道应改判并转仓。
⇒ 本席按 (A) 的落点锚定
domain:spec(也是缺陷被观测到的契约面)。⚠️ 若裁决选 B,请把卡转到 objectui 并改判domain:ui—— ⛔ 别让它挂在 spec 上派给 spec 席去改 objectui。⭐ 一条无论裁哪边都要做的收尾(卡已写明,本席抬为验收项):
the
element-number-filter-rule-arrayD3 entry's runtime sentence should name the route hop it was measured at。⇒ 这条 D3 记录当初把"运行时前提"测到了服务门(
lowerAnalyticsWhere)就停了,而路由的请求 schema 断言在它前面一跳。⇒ 记录必须点名它测的是哪一跳,否则同样的漏测会再发生。⭐ 这与本班次反复出现的那条纪律同形:一个读数必须点名它取自哪棵树 / 哪一跳(见 #15357)。⚠️ 测试覆盖的缺口,请在裁决落地时一并补卡测得:
- 服务级测试(
service-analytics/src/__tests__/filter-array-lowering.test.ts:235、:332)直接调analyticsService.query并传 AST 数组 ⇒ 通过; - 没有任何测试把 AST 数组经路由 POST 进去;
- 也没有任何 spec 测试把这个拒绝钉成任一方向。
⇒ 所以这条分歧两侧都没有钉子,谁改都不会碰红。⛔ 裁决落地的 PR 必须补上路由级的那一跳,否则下一次它会再次悄悄分叉。
Generated by Claude Code
- 发送方已经上线 —— objectui
⚠️ 补一条对裁决的硬约束(分诊席,R+164 稍后测得)—— 选项 (A) 必须是"加法",⛔ 不能是"替换"。分诊席(
session_01SwJQDFKe8tVit3BXQ9EfR5)。上一条路由评论发出后,我路由同批的 #15829 时测到一件与本卡的裁决直接相关、而两张卡各自都没有写的事。读数
analytics.zod.ts:299 where: FilterConditionSchema.optional() ← 本卡想加宽的那道门 dashboard.zod.ts:387 filter: FilterConditionSchema.optional() ← 'Presentation-scope filter (runtimeFilter)' dashboard.zod.ts:720 filter: FilterConditionSchema.optional() ← 'Filter to apply to source object'而仪表盘那一侧有一个活的 record 生产者:objectui
packages/app-shell/src/views/metadata-admin/inspectors/datasetFilterCondition.ts的groupToCondition,从DashboardWidgetInspector发出{ [field]: { $op: value } }。⇒ 仪表盘 widget 的 filter 与 analytics 的
where是同一族 record 形,且仪表盘 widget 的下游正是/analytics/query。⇒ 约束
本卡的选项 (A)(「
AnalyticsQuerySchema.where也接受FilterArray」)必须实现为"record 或 array 都接受"。⛔ 不能实现为"把
where收窄成只接受 array" —— 那会打断上面这条今天正在运行的 record 生产链:设计器发出的仪表盘 widget filter 会开始被同一道门拒绝,而那条路径今天是好的。⚠️ 本卡正文说「the service accepts a wider set than the spec declares」,读起来自然会想到"让声明追上服务"。⭐ 但服务今天两种都吃(record 原生,array 经lowerAnalyticsWhere),⇒ 追上的目标是并集,⛔ 不是数组那一支。对选项 (B) 的影响
选项 (B)(让 objectui 适配器把数组降成 record)不受这条约束影响 —— 它反而与仪表盘一侧的现状一致(两边都发 record)。
⚠️ 但 (B) 的原有代价不变:卡实测「nothing does it today:packages/core/src与packages/data-objectstack/src里不存在数组→record 的降级」,且方向与该树里所有其他降级相反。⇒ 这条读数不选边,只是把 (A) 的可接受形状钉死为加法。
家族全景
ui#6206-B 这条裁决今天散在六张卡上(跨两仓、四车道):#15442 · #15449 · #15829(仪表盘,⭐
ComponentPropsMap之外的第三处)· 本卡 · objectui#7712 · objectui#7711。⇒ 完整清单我写在 #15829 的路由评论里,家族级 ADR-0087 D3 条目落地时建议照它逐项点名。
Generated by Claude Code
Maintainer ruling recorded — a frontend defect, not a protocol change; this card narrows to the spec-side documentation half
Director seat, summon #15,
session_01TezFG8ZMrNH6n5VTNpPpdH(os-zhuang), 2026-09-05T14:5xZ.Provenance (who / verbatim / where): maintainer, live director chat, 2026-09-05, after asking 「具体解释按照协议到底应该怎么设计,是协议设计的问题还是前端代码的错误」 and reading the seat's protocol analysis. Verbatim: 「15828 按前端缺陷处理」.
Freshness re-read before recording: two triage comments on the card (5551194378, 5551423990); the second adds that option (A) would have had to be additive because the dashboard side has a live record producer (
groupToCondition), and notes option (B) is consistent with that side. Nothing later.Why the protocol is right and the adapter is wrong (the reading the ruling rests on)
packages/spec/src/data/filter.zod.ts, theFilterArraydocblock (#5158 ruling C), verbatim: "AFilterArrayis not a storage shape and not a protocol shape. It is lowered to aFilterConditionat the single sinkparseFilterAST(@objectstack/spec/data) the moment it arrives, and only the loweredFilterConditiontravels any further.whereon a query is aFilterConditionand stays one … so nothing downstream — no driver, no transport, no stored row — ever has to understand two filter dialects." and "rather than widening the wire contract (rejected option A)". The declared doors where an array arrives and is lowered are React block props,FilterBuilder, and the REST$filterquery string (metadata-protocol runsisFilterAST+parseFilterAST, else400 INVALID_FILTER). A POST body'swhereis transport, not a door. The runtime's request-schema refusal is therefore the protocol working.objectui
packages/data-objectstack/src/index.ts(pina472b07):aggregate()'s analytics branch runstranslateFilterArray(rule objects → AST tuples — stillFilterArray, still input-only sugar) and posts the result aspayload.wherewithout the lowering the protocol mandates. The same file already importsisFilterASTfrom@objectstack/spec/data;parseFilterASTlives beside it. The branch's own comment shows the misreading: it tookservice-analytics'lowerAnalyticsWhere(the #5334 door for in-process callers) for the wire's door. objectui#6302's gate measured that service function, not the route, and the D3 migration entry recorded the runtime prerequisite at that same wrong hop.This card's option (A) — and the director seat's own earlier variant "analytics-only widening" — are both the #5158-rejected direction and are set aside. Option (B) is not "the reverse of every other lowering": it is the lowering, at the door the protocol names.
Execution (two halves, two repos)
- objectui (the fix) — rebuilt as objectui card (transfer is unavailable to this seat; provenance header on the new card, this card linked),
domain:ui, dispatched under the maintainer's direct instruction (the ui seat [PM seat] repo:objectui — 🔴 vacant · last shift: consolidated seat session_018rzQyhLGC5iVs11V3TzRs5 closed 2026-09-09T06:3xZ (brief on thread) · prev: os-sales session_014zHsbJoTkTZeJQ5DLbRXrE 收班 R26 #6025 is vacant):aggregate()lowers the array throughparseFilterAST(translateFilterArray(filter))before posting; the record path unchanged;aggregate-filter-lowering.test.tsre-pointed to assert the postedwhereis aFilterCondition; one test proves the payload passesAnalyticsQueryRequestSchema.safeParse(the route's own gate) — the route-hop pin both triage comments asked for.Clause-②: no. - objectstack (this card, narrowed) — spec documentation only, no accept-set change: the two D3 entries
18.element-number-filter-rule-arrayand18.element-record-picker-filter-rule-array(authored inpackages/spec/src/migrations/entries/semantic/, registry regenerated withgen:migration-registry, never by hand) name the correct hop — the adapter lowers withparseFilterASTbefore the wire; the route schema admits onlyFilterCondition; andAnalyticsQuerySchema.where's.describe()gains one sentence pointing array authors atFilterArray→parseFilterAST.Clause-②: no;--tierreads the spec surface as SUSPECT by path, judged from content as prose-only.
State transition, same stroke:
needs-user-decision→pm:queue(then claimed for direct dispatch). Labelsbug/priority:p1/finding/domain:specleft as triage set them; triage: the p1 user-visible half now lives on the objectui card — please re-grade this narrowed documentation half at your next pass. Ledger: director seat post #12708, summon #15.
Generated by Claude Code
- objectui (the fix) — rebuilt as objectui card (transfer is unavailable to this seat; provenance header on the new card, this card linked),
Claim: PM loop round — director seat summon #15, maintainer direct dispatch (verbatim 「15828 按前端缺陷处理」 replying to the seat's proposal 「按直派派发 objectui 修复与 spec 文档卡」, 2026-09-05, director chat); the spec seat #6017 is active and is told here rather than asked — this card is now documentation-only
Session:session_01TezFG8ZMrNH6n5VTNpPpdH
Branch:claude/issue-15828-d3-entry-route-hop
Worktree:objectstack-issue-15828
Domain:domain:spec
File surface:packages/spec/src/migrations/entries/semantic/18.element-number-filter-rule-array.tsand18.element-record-picker-filter-rule-array.ts(the runtime-prerequisite sentence) ·packages/spec/src/migrations/registry.ts(GENERATED —pnpm --filter @objectstack/spec gen:migration-registry, never by hand) ·packages/spec/src/data/analytics.zod.ts:299(where.describe()gains one sentence) · one.changeset/*.md(@objectstack/specpatch, docs-only text on a published package). ⛔ No schema shape, no accept-set, noFilterArraySchema, no runtime, no test-behaviour change (stop on breach; explain in the report)
Container & model: S docs-only,mode:subagent,model: opus—node scripts/pm/dispatch-gates.mjs --tier(14:53Z) reads no path mandate but flags all four paths as clause-② SUSPECT (packages/spec/src/**); judged from content: prose in.describe()and migration-entry text only ⇒ not clause-②; default tier kept rather than floor because the generated registry and the enqueue gate need care
Clause-②: no
Serial constraints cleared: spec seat's in-flight stack PR #15626 / #15814 / #15837 (#14478 duration keys) touchespackages/spec/src/api/**and regenerates docs baselines — this card touchesdata/analytics.zod.tsone describe line and two migration entries;registry.tsis a generated hot file shared by every migration-entry PR ⇒ landing goes throughscripts/pm/os-regen-merge.sh(regenerate, never hand-merge); #15829 (dashboard family) isneeds-user-decision, not in flight; none otherwise.
Generated by Claude Code
Claim (os-dev,
mode:subagent) — the narrowed spec-side DOCUMENTATION half of this card, per the director ruling comment 5552626017 (maintainer verbatim 「15828 按前端缺陷处理」). The user-visible adapter fix is objectui#7752 and is not touched here.Session:
session_01TezFG8ZMrNH6n5VTNpPpdH
Branch:claude/issue-15828-d3-entry-route-hop(pushed empty at claim time)
Worktree:objectstack-issue-15828, basecee39617
Domain:domain:specFile surface I intend to touch:
packages/spec/src/migrations/entries/semantic/18.element-number-filter-rule-array.ts— the runtime-prerequisite clause names the wrong hoppackages/spec/src/migrations/entries/semantic/18.element-record-picker-filter-rule-array.ts— read for the same claim; correct only if it makes itpackages/spec/src/data/analytics.zod.ts:299— thewhere.describe()gains one sentencepackages/spec/src/migrations/registry.ts— GENERATED, viapnpm --filter @objectstack/spec gen:migration-registry, never by hand; plus whatevercheck:generatedproves stale- one
.changeset/*.md—@objectstack/specpatch
Prose only. No schema shape, no accept-set, no
FilterArraySchema/FilterConditionSchema/AnalyticsQuerySchematype change, no runtime, no test-behaviour change.Clause-②: no.
Generated by Claude Code
- added a commit that references this issue
on Sep 5, 2026 Ruling basis, appended by the director seat (summon #15, 2026-09-05T15:2xZ) — the maintainer stated the principle behind today's ruling as a standing rule, verbatim: 「本项目以协议为基准。所以开发应该对其协议,协议有问题应该立卡修改协议」. Applied here:
whereis aFilterConditionon the wire because the protocol says so; the adapter conforms (objectui#7752); had the protocol been judged wrong, the remedy would have been a spec card to change it, not a widening option inside this bug card. Recorded for the skills lane on #15929.
Generated by Claude Code
os-dev-report
{ "issue": 15828, "status": "done", "branch": "claude/issue-15828-d3-entry-route-hop", "pr": "https://github.com/objectstack-ai/objectstack/pull/15941", "premise_still_valid": true, "summary": "The narrowed spec-side DOCUMENTATION half only, per ruling comment 5552626017 (maintainer verbatim: 15828 handled as a frontend defect). Prose only: the element-number D3 entry's reason clause now names the real path and the deciding hop (translateFilterArray yields AST tuples that are still a FilterArray, so the adapter must lower with parseFilterAST from @objectstack/spec/data BEFORE the wire; the runtime route POST /analytics/query parses where with AnalyticsQueryRequestSchema = FilterCondition only; lowerAnalyticsWhere is the in-process door #5334 for direct analyticsService.query callers, not the wire's), citing #15828 and objectui#7752. AnalyticsQuerySchema.where's .describe() gains one sentence pointing array authors at that lowering. Every other sentence and the entire acceptanceCriteria of the entry are byte-identical. The sibling entry 18.element-record-picker-filter-rule-array.ts was read for the same claim and DOES NOT make it (its measured path is the record picker's find() / convertQueryParams hop; grep for analytics / lowerAnalytics / wire returns zero matches in that file) so it is unchanged, as the dispatch directed. No FilterArraySchema, FilterConditionSchema or AnalyticsQuerySchema type change; no accept-set, runtime or test-behaviour change. Clause-2: no.", "premise_verification": "Verified against origin/main before editing: the FilterArray docblock (packages/spec/src/data/filter.zod.ts, #5158 ruling C) names parseFilterAST as the single lowering sink and explicitly records widening the wire contract as rejected option A; parseFilterAST is exported at packages/spec/src/data/filter.zod.ts:2108; AnalyticsQueryRequestSchema (packages/spec/src/api/analytics.zod.ts) is AnalyticsQuerySchema.extend(...).strict(), so where is inherited as FilterConditionSchema. The issue's own premise (that the D3 entry recorded the prerequisite at the wrong hop) holds.", "gates": { "derivation_command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands", "derived_at_commit": "9b431caf", "derived": 93, "run": 93, "unrun": 0, "green": 93, "red": 0, "reconciliation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran (file): '93 derived familie(s) accounted for - 93 run, 0 NOT-MEASURED'", "exit_code_capture": "every command run as: cmd > logfile 2>&1; ex=$? - redirect first, never through a pipe", "prerequisite_not_met_then_cleared": [ "node scripts/check-plugin-teardown-shape.mjs --self-test - EXIT=1, refused on a shallow clone (fixture pinned to commit 621a4876); cleared with git fetch --unshallow origin main, re-run EXIT=0", "pnpm --filter @objectstack/lint run check:doc-formula-expressions - EXIT=3 NOTHING MEASURED (@objectstack/lint, @objectstack/formula unbuilt); after turbo build, re-run EXIT=0", "pnpm --filter @objectstack/lint run check:doc-security-posture - EXIT=3 NOTHING MEASURED; after build, re-run EXIT=0", "pnpm --filter @objectstack/spec run check:skill-examples - EXIT=1, refused because packages/client-react/dist held no .d.ts; after build, re-run EXIT=0", "pnpm check:docs-transcript-drift - EXIT=3 PREREQUISITE NOT MET (@objectstack/lint unbuilt); after build, re-run EXIT=0" ], "still_not_measured": [ "pnpm check:dual-build-cjs-loads - EXIT=3 PREREQUISITE NOT MET: reads emitted bytes for 86 packages and needs a whole-monorepo pnpm build. Repo-level scan, CI-owned. Read as NOT MEASURED, not as green and not as red. This diff changes no package.json, no exports and no build config, so it cannot move that gate." ], "generated_artifact_check": "pnpm --filter @objectstack/spec check:generated after pnpm --filter @objectstack/spec build - 15 artifacts, exactly 1 stale (content/docs/references/**), regenerated with the repo's own gen:docs; re-run green. check:migration-registry was already green after gen:migration-registry. No skills/** file moved, so no landing path changed and no governed-surface merge is implied.", "ci_status_at_report_time": "in_progress - report is delivered at draft-PR time; CI convergence is the PM's to read" }, "tests": { "typecheck": "pnpm --filter @objectstack/spec typecheck - EXIT=0 (via scripts/pm/os-verify-lock.sh, VERDICT command-exit 0, held 311s)", "unit": "pnpm --filter @objectstack/spec test - Test Files 476 passed (476), Tests 12787 passed (12787), Duration 272.13s. Same locked run, VERDICT command-exit 0.", "build": "pnpm --filter '@objectstack/spec^...' build && pnpm --filter @objectstack/spec build - VERDICT command-exit 0; check-dts-emitted 34/34 declaration files present. Rebuilt again after the registry and describe edits before check:generated.", "affected_scope": "TURBO_SCM_BASE=cee3961759160ed72aacb407f15288cbc018d2eb pnpm exec turbo ls --affected lists 75 packages, because nearly every package depends on @objectstack/spec. The changed package's own suite is run in full above; the other 74 are declared to CI (see deviations).", "ablation": "not applicable - no new guard, gate or assertion was added. This card is prose-only by ruling; the only mechanical proof available is that the two generated artifacts reproduce byte-for-byte from the hand-edited sources, which check:migration-registry and check:docs assert and which both pass." }, "line_budget": "not applicable - no skills/** file is touched by this diff, so no published-skill line or token ratchet applies. Size for the record: 6 files, 49 insertions, 13 deletions vs base cee39617; of that, 2 files / 22 insertions / 6 deletions are GENERATED output.", "files_changed": [ { "path": "packages/spec/src/migrations/entries/semantic/18.element-number-filter-rule-array.ts", "generated": false, "change": "reason clause rewritten to name the route hop; +15/-5" }, { "path": "packages/spec/src/data/analytics.zod.ts", "generated": false, "change": "one sentence added to the where .describe(); type unchanged; +6/-1" }, { "path": "packages/spec/src/migrations/registry.ts", "generated": true, "generator": "pnpm --filter @objectstack/spec gen:migration-registry", "change": "regenerated; diff is exactly the entry clause above; +15/-5" }, { "path": "content/docs/references/api/analytics.mdx", "generated": true, "generator": "pnpm --filter @objectstack/spec gen:docs", "change": "one prop-table cell, the new describe sentence; +1/-1" }, { "path": "content/docs/references/data/analytics.mdx", "generated": true, "generator": "pnpm --filter @objectstack/spec gen:docs", "change": "one prop-table cell, the new describe sentence; +1/-1" }, { "path": ".changeset/analytics-where-names-the-route-hop.md", "generated": false, "change": "new; @objectstack/spec patch" } ], "files_not_changed_and_why": [ "packages/spec/src/migrations/entries/semantic/18.element-record-picker-filter-rule-array.ts - read for the same claim; it does not make it. Its reason clause measures the record picker's read path (renderer hands filter to query.$filter, adapter.find(), convertQueryParams lowers the array through translateFilterArray, engine lowers the tuples before the driver) and never mentions the analytics wire, POST /analytics/query or lowerAnalyticsWhere. Left byte-identical per the dispatch instruction." ], "deviations": [ "DECLARED NARROWING: pnpm check:dual-build-cjs-loads is left NOT MEASURED. It needs a whole-monorepo build (86 packages) - a repo-level scan CI owns, and outside the foreground time budget on a shared box. Not a green.", "DECLARED NARROWING: of the 75 packages turbo ls --affected reports, only @objectstack/spec (the one whose source changed) was tested and typechecked locally; the other 74 are affected solely because they depend on @objectstack/spec, and this diff moves only string-literal contents (no exported type, no export, no runtime path). check:api-surface, check:export-origins and check:declaration-map all pass, which is the mechanical support for that claim. The full matrix is CI's run.", "DERIVATION CAVEAT: origin/main advanced from cee396175 to ea03c7c16 while this branch was being deepened (the unshallow fetch moved the shared remote ref). dispatch-gates flags the tree as one commit stale on scripts/engine-double-contract.pinned.json. That roster belongs to check:engine-double-contract, which is outside this card's derived 93 and whose inputs this branch does not touch. The branch was NOT rebased or merged - the claim comment records that landing goes through scripts/pm/os-regen-merge.sh because registry.ts is a generated hot file shared by every migration-entry PR.", "CHANNEL: this seat's repo-scoped REST probe returned HTTP 200, so every GitHub read and write (issue body, all 4 comments, claim comment, PR create, PR body readback, report comment) went through REST curl. Zero MCP GitHub calls were made. No duplicate-search MCP call was needed either - see out_of_scope_findings." ], "mcp_calls": "0 - all GitHub I/O went through repo-scoped REST (probe returned HTTP 200 on this seat); no MCP GitHub tool was invoked for the whole run", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
Review: ACCEPT — PR #15941 (head
9b431caf), landing to follow when CI convergesDirector seat, summon #15,
session_01TezFG8ZMrNH6n5VTNpPpdH(os-zhuang), 2026-09-05T15:3xZ. Reviewing the os-dev report 5552844692 against the review checklist; every reading below was taken by this seat, not copied from the report.Independence pair (machine-readable):
Implemented-by:claude/issue-15828-d3-entry-route-hop(mode:subagentos-dev, no own session)
Reviewed-by:session_01TezFG8ZMrNH6n5VTNpPpdHPR shape and card relation
- Draft, base
main, first body lineFixes #15828— correct: this card's whole remaining scope is the documentation half; the user-visible fix is tracked by objectui#7752 / PR objectui#7754 on its own card. Body scanned: no other closing keyword adjacent to an open card number (#5158,#5334,#6302,#12039,objectui#7752appear as citations only).
Scope and changeset (from
get_files, not the report)- 6 files, all inside the claim's file surface:
18.element-number-filter-rule-array.ts(+15/−5, reason clause only),analytics.zod.ts(+5/−1,.describe()string only —FilterConditionSchema.optional()unchanged),registry.ts(generated, diff is exactly the entry clause), the twocontent/docs/references/**/analytics.mdxprop-table cells (generated), and one changeset. - No
content/docs/releases/change.@objectstack/specis published ⇒ changeset present,patch, correct for describe-text on a published package. - Sibling entry
18.element-record-picker-filter-rule-array.tsleft byte-identical; I read it onorigin/main— its reason clause measuresfind()/convertQueryParamsand does not name the analytics wire, so no correction was owed there. Dispatch was satisfied.
Clause-② and governed surface
Clause-②: no— concur from content: no new export, no accept-set movement,AnalyticsQuerySchema.whereis stillFilterConditionSchema. Carriers agree:node scripts/pm/check-clause2-carriers.mjs --pair 15941exit 0.node scripts/pm/check-governed-merges.mjs --test <6 paths>— 0 of 6 hit the register ⇒ ordinary queue landing.
Test and gate evidence
- Real commands with verdict lines: 93/93 derived gate families run at
9b431caf(= PR head, pinned),@objectstack/spectypecheck 0,476 files / 12787 tests passed,check:generatedgreen aftergen:docs,check:migration-registrygreen.check:dual-build-cjs-loadshonestly declared NOT MEASURED (whole-monorepo build) — CI's to close; this diff touches nopackage.json/exports. - Report contains no API reads git could have answered.
Generated-artifact landing (landing-operations §A)
content/docs/references/**is amerge=os-regenpath. Measured, not recalled:origin/mainadvanced one commit past the PR base (f1e91595→04679418), andgit log f1e91595..origin/main -- <all 6 PR paths>is empty — no path is changed on both sides, so the os-regen driver never runs on this merge and has nothing to drop.mergeable_stateisblocked(draft + running checks), notdirty. The four-step sync is therefore satisfied by measurement; post-landing I will re-readorigin/mainfor the new describe sentence in both mdx files, per the same section.Landing plan
CI at review time: 17 of 31 checks completed green (
Type Check · source gates,Check Changeset,Governed Surface Queue Guard, single-writer/single-issue guards, size, docs links), the restin_progresssince 15:30Z. Flip point set (~10 min): whenLint & Repo Gatesand allType Check ·jobs concludesuccessand no check is red, flip ready +auto-merge(SQUASH); on MERGED this card closes byFixesandpm:dispatchedis removed in the same stroke.
Generated by Claude Code
- Draft, base
MERGED confirmed — both halves of the 2026-09-05 ruling are on main. PR #15941 merged 16:44:57Z (squash
ceb48776, objectstackorigin/maintip); this card auto-closed byFixes #15828at 16:44:58Z. Read onorigin/main:packages/spec/src/data/analytics.zod.ts:301carries the newwheredescribe sentence ("lowered byparseFilterASTon the client before the wire; this field admits only the …"), the D3 entry18.element-number-filter-rule-array.tsnames the route hop, and the two generatedcontent/docs/references/**/analytics.mdxprop tables carry the sentence. The frontend half landed earlier as objectui PR #7754 (53ded82b, 15:58Z).pm:dispatchedremoved in the same stroke; ACCEPT was 5552877660. Director seat,session_01TezFG8ZMrNH6n5VTNpPpdH; ledger #12708.
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026
Filed unassigned from the #15442 / #15449 consumer census (PM session
session_01M59rPZZFzqhfMUPFqqZTkf, os-dev seat, 2026-09-05). Measured on objectstackorigin/mainfc20f7bdaand the objectui pina472b07(.objectui-sha).Measured
AnalyticsQueryRequestSchema(packages/spec/src/api/analytics.zod.ts:50,AnalyticsQuerySchema.extend(...);whereinherited frompackages/spec/src/data/analytics.zod.ts:299,FilterConditionSchema) —safeParse({ cube, measures, dimensions, where })with each shape (scratch tsx script against the spec source):The only HTTP entry runs exactly that parse first:
packages/runtime/src/dispatcher-plugin.ts:1152(server.post(prefix + '/analytics/query')) →handleAnalyticsRequest→assertAnalyticsQueryBody(packages/runtime/src/domains/analytics.ts:49-70, called at:102and:125) →AnalyticsQueryRequestSchema.safeParse(body)→validationFailure(...)on failure. No pre-parse normalisation exists (:49-61handles only thefilters→whererename refusal). The SDK posts the payload verbatim (packages/client/src/index.ts:1647-1650).Why it matters
ObjectStackAdapter.aggregate()(pina472b07,packages/data-objectstack/src/index.ts:4975-5003, PR objectui#6828 closing objectui#6302) lowers an ARRAYfilterthroughtranslateFilterArrayinto filter AST tuples and posts them aswhere. That was the runtime prerequisite recorded for theelement:number.filterconvergence (spec: two ComponentPropsMap entries contradict their ruled authorities —object-grid.datavs ViewDataSchema,element:number.filtervs the platform filter form (rulings ui#6207-A, ui#6206-B) #12039 Key 2, D3 entryelement-number-filter-rule-array: "authored array → adapter lowering → filter AST → accepted bylowerAnalyticsWhere"). That measurement stopped at the SERVICE gate (service-analytics/src/strategies/filter-normalizer.ts,lowerAnalyticsWhere); the route's request-schema assertion sits one hop earlier and refuses the same value.element:number(array-only since spec: two ComponentPropsMap entries contradict their ruled authorities —object-grid.datavs ViewDataSchema,element:number.filtervs the platform filter form (rulings ui#6207-A, ui#6206-B) #12039) or anobject-metricauthored with an arrayfilterplusaggregate(objectuiObjectMetricWidget.tsx:245-253→ds.aggregate(... filter)→ analytics branch) answers400 Invalid AnalyticsQuery body: where: ...instead of the aggregate. Only the MongoDB-style record survives that path — the exact form ui#6206-B retired. The service-level tests (packages/services/service-analytics/src/__tests__/filter-array-lowering.test.ts:235,:332) callanalyticsService.querydirectly with AST arrays and pass; no test posts an AST array through the route, and no spec test pins the refusal either way.The decision this needs (not taken here)
Contract-first: the spec says
whereis aFilterCondition; the service accepts a wider set than the spec declares. Either (A)AnalyticsQuerySchema.whereadmits theFilterArraythe service already lowers (declared = enforced, and the adapter's landed lowering becomes correct), or (B) the objectui adapter lowers array filters into aFilterConditionrecord for analytics — the reverse of every other lowering in that tree (nothing does it today; measured: no array-to-record lowering exists inpackages/core/srcorpackages/data-objectstack/src). Whichever lands, theelement-number-filter-rule-arrayD3 entry's runtime sentence should name the route hop it was measured at.Refs: #12039 (Key 2) · #15442 / #15449 (the census this fell out of) · objectui#6302 · objectui#6206 (ruling B, 2026-08-25).
Generated by Claude Code