Repository navigation
analytics 契约:AnalyticsResult.fields[] 增加可选 builtinAggregate 鉴别字段,服务端内置聚合默认 label("Count" 等 5 处硬编码)不再以英文字面量出线(objectui#7258 裁 B 的生产者半边) #14492
Description
Activity
Claim: epic cloud#1841 lane — spec seat taken over cross-lane by the epic PM per the owner's 2026-09-04 authorization (「你可以跨车道处理 epic 所有任务」)
Session: wonderful-driscoll-ac0895 (cloud PM session, Fable 5.1)
Branch: feat/analytics-builtin-aggregate-14492
Worktree: ~/Documents/GitHub/objectstack-issue-14492 (linked worktree off origin/main; primary checkout untouched)
Domain: spec (contracts/analytics-service) + service-analytics + kernel seeds + qa/downstream-contract fixtures
File surface: packages/spec/src/contracts/analytics-service.ts (+ zod/tests), packages/services/service-analytics/src/analytics-service.ts (inferMeasure ×2), packages/services/service-analytics/src/cube-registry.ts (inferFromObject), packages/spec/src/kernel/metadata-create-seeds.ts, packages/qa/downstream-contract/src/additional-domains.fixtures.ts, .changeset/*
Container & model: general-purpose subagent reading .claude/agents/os-dev.md, inherit tier
Clause-②: yes
Clause-② rationale: public spec surface grows by one optional field; contract review in-seat at fable tier (cloud scripts/pm/check-clause2-carriers.mjs --pair) before ACCEPT; consumer half = objectui#7258
Serial constraints: none on shared pin files; downstream cloud pin bump follows after merge (cloud#1841 lane)Generated by Claude Code
- added a commit that references this issue
on Sep 3, 2026 ACCEPT — PR #15017 verified in-seat: 9 files, all inside the Claim's surface (spec contract + zod mirror + seeds + service-analytics queryDataset + tests + changeset); all check runs terminal with 0 failures at 105ec6c; clause-② review PASS posted on the PR and the
needs:contract-reviewgate cleared on both carriers (predicate: legible, carriers agree). Marked ready and enqueued (--auto --squash). Downstream: cloud.objectstack-shabump (third in epic cloud#1841) → cloud#1933 label drop → objectui#7535 ready → cloud.objectui-shabump._Generated by Claude Code
os-dev-report
{ "issue": 14492, "status": "done", "branch": "feat/analytics-builtin-aggregate-14492", "pr": "https://github.com/objectstack-ai/objectstack/pull/15017", "premise_still_valid": true, "summary": "Ruling B implemented: AnalyticsResult.fields[] gains optional builtinAggregate (reusing the closed AggregationFunction enum from data/query.zod.ts), mirrored in AnalyticsResultResponseSchema (api/analytics.zod.ts — required by the spec's own compile-time parity guard), populated by queryDataset's measure enrichment exactly when the dataset measure declares an aggregate and NO authored label (judged on the authored key, so an unresolved locale-map label is still an author's label), and the dataset create seed drops its hardcoded label: 'Count' so a Studio-created dataset is a built-in default on the wire. Premise PARTIALLY refuted and stated in the PR body: fields[].label has exactly one producer (that enrichment, from the dataset measure), so the three Cube-metric sites (inferMeasure x2, cube-registry inferFromObject) never reach fields[] — they surface only via getMeta().title where CubeMeta.measures[].type already carries the aggregate — and populating them would mean widening the strict authorable MetricSchema with a key nothing reads; left untouched. The downstream-contract fixture is a Cube fixture with nothing to mirror (suite run green). The AI dashboard's actual 'Count' producer is cloud's dataset-derive.ts:206 (authored label), filed as cloud#1933 under epic #1841. Footer conflict: dispatch asked for the claude.com line, the role file for a session-URL footer — PR body carries both (bare form; session id not known to this seat). Verify lock reports no flock on this macOS host, so heavy runs went direct.", "tests": "Built closures first: pnpm --filter '@objectstack/service-analytics^...' build (exit 0) and '@objectstack/rest^...' build (exit 0). spec: check:generated exit 0 ('All 15 generated artifacts are up to date'); tsc --noEmit exit 0; check:test-typecheck exit 0 (54 files / 261 errors ledger held — the @ts-expect-error reverse pin compiled); vitest api/analytics + contracts/analytics-service + kernel/metadata-create-seeds: 3 files, 50 passed. service-analytics: typecheck exit 0; vitest query-dataset.test.ts 32 passed (+5 new: label-less count => 'count' and no label; authored 'Tasks' => no discriminator; locale-map label unresolved for locale 'fr' => no discriminator; sum/avg/count_distinct verbatim; derived => none). rest: typecheck (incl. test layer) exit 0; vitest analytics-routes.test.ts 14 passed (+1 passthrough). downstream-contract: typecheck exit 0, test 2 files / 19 passed. Reverse verification AFTER commit 105ec6c1b: scratch file in service-analytics assigning builtinAggregate: 'total' against the REBUILT dist d.ts => tsc exit 1, TS2322 naming the closed union; file removed, git diff HEAD empty. Gates derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (60 commands, all accounted for): spec check:api-surface/authorable-surface/docs/export-origins/liveness/strictness-ledger/skill-refs via check:generated exit 0; spec exported-any, dual-source-exports, empty-state, variant-docs, yaml-examples, llms-txt, entry-nameability, browser-reachable-entries, liveness all exit 0; root nul-bytes, cross-package-test-inputs, test-source-alias, type-check-coverage, spec-parsed-alias, changeset-gate-self-tests, objectui-changeset, published-files, doc-authoring, query-options-erasure, dispatcher-error-vocabulary, engine-double-contract, merge-driver, page-declaration-shape, slot-lookup, where-matcher, watch-hint-literal, refd-timer-probe, logger-receiver-detach, objectql-double-limit, type-source-resolution all exit 0; node scripts adr-0087-registration, changeset-no-major, ci-filter-parity, closing-keyword-parity, comment-mask-adoption, comment-mask-corpus, empty-changeset, keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, tenant-audit-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, lint check:doc-formula-expressions all exit 0. NOT MEASURED by their own verdict text (PREREQUISITE NOT MET, need whole-tree build / turbo test log): check:type-check-debt (3), check:dual-build-cjs-loads (3), check-dev-prereqs (1), check-test-completeness (3). Not run locally (network board sweeps): check:pm-half-states, scripts/pm/check-half-states.mjs. Head for every run: 105ec6c1b. CI: ALL TERMINAL on head 105ec6c1b — pass=30, skipping=5; 0 fail/cancel (skipped = Console Pin Gate, Build Docs, Packed-tarball smoke and the two other path-filtered jobs). Watched via a single until-condition wait (harness Monitor; foreground sleep is blocked here and macOS has no timeout binary); the monitor exited on ALL TERMINAL. Draft NOT flipped, nothing merged. Worktree cleaned up after the report (node_modules removed, git worktree remove without force).", "mcp_calls": "0 — REST (gh api) and git only for the whole run", "open_questions": [ { "question": "Should the Cube metric (MetricSchema, strict + authorable via defineCube/StackSchema) ALSO carry builtinAggregate so inferMeasure x2 and cube-registry inferFromObject populate it literally as the ruling's site list reads?", "options": [ "A — No (implemented): the discriminator lives only on the response, derived from 'aggregate present and label absent' on the dataset measure; the Cube-metric sites never reach fields[] and getMeta already exposes CubeMeta.measures[].type.", "B — Yes: widen MetricSchema with an optional builtinAggregate, populate the three Cube sites, and add a query()-path enrichment that copies cube metric label + builtinAggregate onto fields[] (new wire behaviour on /analytics/query, plus gen:schema/docs/authorable-surface regen)." ], "recommendation": "A. Long-term soundness: item 1 of the ruling names AnalyticsResult.fields[] as the contract change and a response-only member cannot be authored wrong; B declares an authorable key that no wire path reads today (check:liveness territory) and lets a metadata author or an AI stamp builtinAggregate beside a custom label, exactly the consumer-side tolerance the ruling refused. Real need: the measured producer of the reported chart is a dataset measure (cloud dataset-derive.ts), served by A. Scope: A is the smaller change. If the maintainer wants B, it is a separate card on top of this one, not a rework of it." } ], "out_of_scope_findings": [ "filed as objectstack-ai/cloud#1933 (sub-issue of cloud#1841, Blocked-by objectstack#14492 + pin bump): dataset-derive.ts:206 authors label: 'Count' on the AI-derived count measure, so the reported chart still carries an authored English label until that one line is dropped.", "filed as #15019 (finding): CubeRegistry.inferFromObject has no runtime caller on origin/main (only a unit test), so ruling site 3 (cube-registry.ts:80) is dead code and its composite LABEL (Sum) / LABEL (Avg) English defaults are unreachable; delete-vs-wire left to the maintainer." ] }Generated by Claude Code
PM ruling on the seat's open question: A (as implemented). The discriminator is a response-only member derived from "aggregate declared, no authored label" on the dataset measure; the three Cube-metric sites never reach
fields[]andCubeMeta.measures[].typealready exposes the aggregate on the meta path. Widening the authorableMetricSchema(B) would declare a key no wire path reads and let an author stamp it beside a custom label — the consumer-side tolerance ruling B refused. If the maintainer later wants the cubequery()path to carry it, that is a separate card on top of this one. objectstack#15019 (deadinferFromObject) stays with the maintainer.Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 29, 2026
背景(来自 objectstack-ai/objectui#7258 的诊断,维护者 2026-09-02 裁 B)
AI 搭建的客户仪表盘「按状态统计客户数量」柱状图,X 轴标题在中文界面显示英文 "Count"。objectui 侧 dev 证实:渲染端(
packages/core/src/utils/chart-series.ts的labelOf)把queryDataset()返回的fields[].label当作已解析的作者内容原样透传(objectui#4106 为真实自定义 measure label 钉了这一行为);英文字面量来自 objectstack 服务端内置聚合的硬编码默认 label,无 i18n 钩子,共 5 处:packages/services/service-analytics/src/analytics-service.ts:2045与:2300(inferMeasure)packages/services/service-analytics/src/cube-registry.ts:80(inferFromObject)packages/spec/src/kernel/metadata-create-seeds.ts:98packages/qa/downstream-contract/src/additional-domains.fixtures.ts:116裁决(维护者 2026-09-02,在场裁定):B — 契约加结构化鉴别字段
packages/spec/src/contracts/analytics-service.ts的AnalyticsResult.fields[]增加可选builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'(闭合枚举,与 spec 里既有的聚合枚举对齐,⛔ 不另造第二套拼写)。queryDataset的 REST 响应透传该字段。buildChartSeries()/labelOf()在字段带builtinAggregate时优先查 locale(复用/镜像现有report.aggregate.*键,zh 已有计数等),否则回落 label —— objectui 半边由 objectui#7258 的原认领分支跟进,本卡是生产者半边,PR 正文写明消费侧契约。packages/qa/downstream-contract的 fixture 同步。选项 A(objectui 按名字/文本猜)被否决:会误伤真的叫
count的作者字段、且文本匹配一换语言就失效——正是 AGENTS.md 契约优先禁止的消费端宽容。选项 C(服务端按 userLanguage 出译文)作为回退项未采用:把前端的语言判断复制到服务端,且没有鉴别字段供 per-widget 覆盖。契约面
触及
packages/spec/src/**(扩大公开面)⇒ Clause-②: yes,按 spec 席队列与契约复审档处理;本卡是 epic objectstack-ai/cloud#1841 的 sub-issue,落地后 objectui#7258 解除Blocked-by。验收
builtinAggregate: 'count',自定义 label 度量不带;