Skip to content

fix(data-objectstack): 把 queryDataset(selection) 直接声明为 spec 的 DatasetSelection - #3753

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3613-dataset-selection-spec-type
Aug 8, 2026
Merged

yinlianghui merged 1 commit into
mainfrom
claude/issue-3613-dataset-selection-spec-type

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3613

前提核验(issue 只是线索)

在 origin/main @ 28364bd 上逐条核过,前提成立:

  • packages/data-objectstack/src/index.ts:3202 确实仍是 compareTo?: { kind: 'previousPeriod' | 'previousYear'; dimension: string } —— dimension 必填。
  • packages/data-objectstack/package.json pin 的是 @objectstack/spec@^17.0.0-rc.5,lockfile 解析到 17.0.0-rc.5(注意:共享 checkout 当前分支的 lockfile 还停在 rc.2,所以这一条必须按 origin/main 读)。
  • rc.5 里 DatasetCompareTo.dimension 确为可选,注释原文:**Optional since #5011, resolved by the EXECUTOR — not by any consumer.**

一处与 issue 正文的出入,已按裁定 1「READ the pinned spec package first」核实后照实处理:issue 说该类型在 contracts/analytics-service.ts,但发布出来的包里没有 src/contracts/(源码只部分随包)。实际导出名与入口是 DatasetSelection / DatasetCompareTo,从 @objectstack/spec/contracts 子路径导出(本包 spec-symbol-batch6.test.ts 已在用这个子路径)。名字与 issue 一致,入口按实际的写。

改了什么

手写的 selection 内联类型换成对 spec 类型的直接引用:

import type { DatasetSelection } from '@objectstack/spec/contracts';

async queryDataset(
  dataset: Record< string, unknown > | string,
  selection: DatasetSelection,
): Promise< { … } >

删掉的那份副本一共漂了三处(裁定 2 的 timeDimensions 只是其中之一):

字段 删掉的副本 spec rc.5
compareTo { kind; dimension: string }(必填) DatasetCompareTo,dimension?
timeDimensions unknown[] AnalyticsQuery['timeDimensions']
runtimeFilter Record< string, unknown > FilterCondition
dateGranularity 整个键都没有 'day'|'week'|'month'|'quarter'|'year'

前两项 issue 点到了。后两项是核验时才浮出来的:runtimeFilter 放宽成 Record< string, unknown > 抹掉了服务端要解析的 $and/$or/$not 词汇;而 dateGranularity(framework#3588)是副本从来没长出来过的键 —— 这条最能说明问题:重述一份契约不只会在它有的字段上漂移,它还会停在写它那天的样子。这也正是 issue 自己的结论(「重述一份就是给同一契约开第二份方言」),所以这里做的是消灭方言而不是重新同步一遍。

为什么这不是纸面问题:dimension 必填等于让编译器向每个按类型调用的作者索要一个执行器本该自己决议的维度 —— 恰好是 objectstack#5011 与 AGENTS.md #0.1 明令禁止的消费端猜维度,把「零个/多个候选时响亮报错并列出候选」换成一个安静的错窗口。今天没有运行时路径撞上(DatasetWidget 走 selection: unknown),但类型声明不是 dormant code。

钉子(裁定 4)

加在 queryDataset.test.ts(与既有运行时用例同处),沿用本包 spec-symbol-batch6.test.ts 的 Assert/Equal/HasKey idiom:

  • 主钉:Assert< Equal< Parameters< ObjectStackAdapter['queryDataset'] >[1], SpecDatasetSelection > > —— 用 Equal 而非双向 extends 是刻意的:必填的 dimension 仍然可赋给可选的,双向 extends 在漂移方向上也是绿的,结构恒等才是手写副本漂移时过不去的那一条。
  • 三处漂移各自单独钉一条(附注释说明该漂移的代价),外加一条运行时用例:compareTo: { kind: 'previousPeriod' }(不带 dimension)必须原样送到服务端。这一条守的是 adapter 永远不会替执行器"帮忙"补一个 dimension。

key-vs-value: 这里守的是 compareTo 省略 dimension 是否为合法的授权/编写形状(一个 key/shape 事实),所以用编译期可赋值性断言;真正的 value 判决(哪个维度被平移)在执行器,本仓无从断言。

反向验证 —— 方向是先预测再跑的

预测:把旧内联方言塞回去 → type-check 变红,而 vitest 保持绿。 因为 vitest 不做类型检查,而那条运行时透传断言测的是 JSON 序列化,类型声明动不了它。这个钉子的承重门是 tsc --noEmit,不是 vitest —— 写在这里免得后来的人误以为跑一遍 vitest 就守住了。

两个方向都如预测:

type-check 红,13 条错误,每条都对得上一处具名漂移:

src/queryDataset.test.ts(168,34): error TS2344: Type 'false' does not satisfy the constraint 'true'.   # 主钉 _IsTheSpecType
src/queryDataset.test.ts(185,38): error TS2344: Type 'false' does not satisfy the constraint 'true'.   # _DimensionOptional
src/queryDataset.test.ts(205,50): error TS2339: Property 'dimension' does not exist on type 'unknown'. # timeDimensions 退化成 unknown[]
src/queryDataset.test.ts(228,24): error TS2339: Property 'dateGranularity' does not exist on type '{ … }'.
src/queryDataset.test.ts(248,7):  error TS2741: Property 'dimension' is missing in type '{ kind: "previousPeriod"; }' but required in type '{ kind: …; dimension: string; }'.

vitest 同时仍是绿的(Test Files 1 passed (1) / Tests 12 passed (12)),与预测一致。随后已复原。

消费者核查(裁定 3)

queryDataset 不在 @object-ui/types 的 DataSource 接口上,只在本包具体类上,所以类型面只有直接调用方会碰到。全仓调用点三处,全部经 any/局部 unknown 取值,因此没有类型耦合:

  • packages/plugin-charts/src/ObjectChart.tsx:452 —— ds: any
  • packages/app-shell/…/previews/DatasetPreview.tsx:73 —— 自行 cast 成 (d: unknown, s: unknown) => …
  • packages/app-shell/…/previews/ReportPreview.tsx —— 同上

--filter "...@object-ui/data-objectstack" type-check(本包 + 33 个下游)全绿,exit 0,零 error,无需任何消费端改动 —— 文件面因此严格停在 packages/data-objectstack/。

(首轮跑出的 Cannot find module '@object-ui/data-objectstack' 是新 worktree 没建产物的 §9 陷阱镜像,先 --filter "...@object-ui/data-objectstack..." build 后即消失,不是本改动引起。)

验证

pnpm --filter @object-ui/data-objectstack type-check          # tsc --noEmit,绿
pnpm --filter "...@object-ui/data-objectstack" type-check     # 34 个包全绿,exit 0
pnpm exec vitest run packages/data-objectstack/               # 23 files / 347 tests passed
pnpm --filter @object-ui/data-objectstack lint                # 0 errors(310 warnings 均为既存 no-explicit-any)
node scripts/check-control-bytes.mjs                          # OK(3690 files)
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' <改动文件>           # 无控制字节
node scripts/check-changeset-no-major.mjs                     # OK

Changeset:.changeset/dataset-selection-spec-type-3613.md,patch(公开类型面被修正),英文正文。

刻意没做的事


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

…etSelection

The adapter restated the selection contract inline, field by field, and the
copy had drifted three ways from the pinned @objectstack/spec@17.0.0-rc.5:

- compareTo.dimension was required. It has been optional since
  objectstack#5011 precisely because the EXECUTOR resolves it (exactly one
  time dimension carrying a dateRange is shifted; zero or several raises a
  loud error naming the candidates). Requiring it made the compiler demand
  from every typed caller the consumer-side dimension guess that change
  forbids, trading a loud error for a silently wrong comparison window.
- timeDimensions was widened to unknown[], erasing the entry shape the
  executor's resolution reads, and runtimeFilter to Record<string, unknown>,
  erasing the $and/$or/$not vocabulary the server parses.
- dateGranularity was missing entirely: the copy had stopped at whatever the
  contract looked like the day it was written.

The parameter is now the spec type by reference, so there is nothing left to
re-sync. Removing the dialect rather than correcting it is the point --
restating a contract owned elsewhere opens a second de-facto dialect of it
(AGENTS.md #0/#0.1), and this issue is the instance.

queryDataset.test.ts pins structural identity with DatasetSelection plus each
of the three drifts individually, checked by this package's tsc --noEmit (its
tsconfig includes the whole src tree). A runtime test pins that a
dimension-less compareTo reaches the server untouched, so the adapter can
never start guessing on the executor's behalf.

The response type is deliberately untouched -- it is the REST envelope
(object / dimensionFields / drillRawRows), not a restatement of
AnalyticsResult. Its own fields[] drift is filed separately as #3752.

Fixes #3613
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 8, 2026 11:59am

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CtzM40k0.js —
Status PASS —

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 480.72KB 105.64KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.84KB 3.18KB
data-objectstack (index.js) 138.22KB 35.46KB
fields (index.js) 230.90KB 56.84KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 115.50KB 29.96KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.79KB 57.42KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 186.61KB 49.34KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 12:05
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 5f08c05 Aug 8, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3613-dataset-selection-spec-type branch August 8, 2026 12:06
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…alyticsResult element (objectstack-ai#3752) (objectstack-ai#3817)

The return-value half of the drift objectstack-ai#3613/objectstack-ai#3753 fixed on the parameter side. The
adapter hand-listed five keys for a result column (name/type/label/format/
currency) and, like every restatement, stopped at the contract of the day it was
written: it never grew percentScale, which @objectstack/spec@17.0.0-rc.5 carries
on AnalyticsResult.fields[] and documents as mandatory reading — "renderers that
receive it must scale by it instead of guessing from the value" (objectstack-ai#3136).

Three in-repo consumers (plugin-dashboard, plugin-report, DatasetPreview) read
the key through @object-ui/core's local DatasetResultField, so nothing was red
here; but an author reading columns through the adapter's DECLARED return type
got "Property 'percentScale' does not exist" — the declaration steering them
back to the guess-by-magnitude objectstack-ai#3136 banned.

fields is now the spec element by reference, additively for existing consumers.
The rest of the envelope stays locally declared on purpose: the route adds
ADR-0021 D2 drill metadata on top of AnalyticsResult, and this method rebuilds
its own object without copying sql (index.ts:3355), so an intersection with
AnalyticsResult would advertise a key the adapter structurally cannot return.

queryDataset.test.ts pins structural identity with the spec element, pins
percentScale as the 'fraction' | 'whole' union rather than a widened string,
keeps a negative pin against the five-key shape, pins the envelope as NOT an
AnalyticsResult, and reads percentScale off a column through the declared type
at runtime.

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 17, 2026
…ields[] (objectstack-ai#3815) (objectstack-ai#4128)

`packages/core/src/utils/dataset-format.ts` declared its own six-key interface
for the analytics result column under a doc comment describing the server's
contract. The key set matched the spec today, so nothing was broken — but this
was the last surviving member of the derive-don't-restate family (objectstack-ai#3613/objectstack-ai#3753
parameter side, objectstack-ai#3752 adapter return side), and the member with no compile-time
tripwire: three surfaces (DatasetWidget, DatasetReportRenderer, DatasetPreview)
consume this name AS the real column type, so the next spec column key would
simply never appear here and no build would complain.

It is now `AnalyticsResult['fields'][number]`. The exported name is unchanged
and the `PercentScale` re-export is untouched.

`type` is required again, as the contract has it. The investigation the card
asked for found no construction site that could not supply it: every value of
this type originates in `ObjectStackAdapter.queryDataset`, which already
declares the spec element (objectstack-ai#3752), and the whole downstream type-check closure
of `@object-ui/core` (68 tasks) is green with no fixture change. No consumer
reads `.type` at all, so the relaxation had bought no caller anything while
advertising a `string | undefined` the wire never produces.

`packages/core/tsconfig.typetests.json`, chained off the package's `type-check`,
compiles the new parity test — core's test tree is still in the type-check
coverage script's TEST_DEBT, so without it these pins would be the
"declared != enforced" landmine they exist to remove (objectui#3009). Reverting
the interface turns five of them red, including the negative pin against the
hand-written shape and the `ChartResultField` superset asymmetry.


Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

data-objectstack 的 queryDataset 仍把 compareTo.dimension 声明为必填,与 spec rc.5(#5011 起可选)相左

2 participants