Skip to content

refactor(app-shell): 把 DecisionOutputDef 收敛成 spec 的纯 re-export,并反转 parity pin - #3185

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-4562-decision-output-collapse
Aug 2, 2026
Merged

refactor(app-shell): 把 DecisionOutputDef 收敛成 spec 的纯 re-export,并反转 parity pin#3185
os-zhuang merged 3 commits into
mainfrom
claude/issue-4562-decision-output-collapse

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Fixes objectstack-ai/objectstack#4562

注:正文里的泛型都写成 Equal< Exclude< … > > —— 尖括号后故意加了空格。GitHub 会把「< 紧跟字母」的序列当成 HTML 标签吞掉,加空格后仍是合法 TypeScript,可直接复制。仓库里的真实代码没有这些空格。

背景

app-shell 的 DecisionOutputDef 一直是一个结构派生,带着 ONE documented divergence:

export interface DecisionOutputDef extends SpecDecisionOutputDef {
  required?: boolean;
}

required 之所以是本地加的,是因为服务端会强制它(decide() 在任何写入前就拒绝空的 required output),而 @objectstack/spec 当时还没建模这个字段。

现在这个前提没了:spec 在 cd6b9f202 采纳了 required,objectstack#4561 把它钉在 schema 层,而本仓在 #3178 升到 @objectstack/spec@17.0.0-rc.1 之后已经解析到含 required 的版本。派生因此变成纯冗余

改动(三项,一个 PR)

1. 收敛冗余派生

packages/app-shell/src/utils/decisionOutputParams.ts

export type DecisionOutputDef = SpecDecisionOutputDef;

保留 re-export(而不是让调用方直接 import spec),是因为这个模块是 approval 的 decision outputs 变成 action params 的唯一入口,它的消费者(useRecordApprovalsDeclaredActionsBarRecordDetailView)都从这里读类型。

2. 修掉过期的模块 TSDoc

原注释仍然断言 "the spec does not model it yet" —— 已经不成立,而且主动误导。这正是 objectstack#4115 那一类失败:下一个 agent 读到这句会把这个 divergence 当成地面真值并在其上继续构建(#2901 就是这么被填了个反的前提)。现已改写为当前事实,并说明 type 的闭合枚举收窄仍然是 load-bearing 的(它才是阻止拼错的 picker kind 静默降级成裸 record-id 文本框的东西 —— objectui#2955)。

3. 反转 parity pin

packages/app-shell/src/__tests__/spec-symbol-parity.test.ts

之前 —— required 是唯一的本地新增:

Equal< Exclude< keyof DecisionOutputDef, keyof SpecDecisionOutputDef >, 'required' >

现在 —— 排除集为空,外加一条精确同一性断言:

Equal< Exclude< keyof DecisionOutputDef, keyof SpecDecisionOutputDef >, never >
Equal< DecisionOutputDef, SpecDecisionOutputDef >

这就是原注释承诺的 "fails on that day" —— 今天就是那天。_TypeIsClosed 那条保持不变。文件头的计数也从「eighteen plain re-exports + ScreenSpec 和 DecisionOutputDef 各带一处 divergence」更新为「nineteen plain re-exports + ScreenSpec 仍带一处 divergence」。

⚠️ 关于 guard 状态(请 reviewer 注意)

这条 pin 目前不被任何 CI gate 编译。 packages/app-shell/tsconfig.jsonexclude**/*.test.ts,而 CI 的类型 gate 是 turbo 的逐包 tsc --noEmit,所以 spec-symbol-parity.test.ts 里所有 Assert< Equal< … > >从未进入编译单元。实测:往该文件追加 type _Probe = Assert< Equal< 1, 2 > > 之后 pnpm type-check 依然 exit 0

这一点已单独记录在 objectui#3181;本 PR 的反转断言要等 #3181 落地后才真正被 CI 强制。在那之前我用一个临时 tsconfig(include: ["src"]、去掉 test 的 exclude)显式编译验证,证据见下。这也解释了 objectstack#4562 的前提为何写成「parity 测试会在那天失败」—— 它其实永远不会失败。

验证(实测输出)

均在 merge 了 origin/main(含 #3178,spec 解析为 17.0.0-rc.1)之后运行。

类型检查

$ cd packages/app-shell && pnpm type-check
> tsc --noEmit
EXIT=0

反转后的断言显式编译(绕开 test exclude)

$ npx tsc --noEmit -p tsconfig.paritycheck.json | grep spec-symbol-parity
(无输出 = 干净)

负向对照 —— 旧的 'required' pin 现在必须失败

$ # 把旧断言重新加回去
src/__tests__/spec-symbol-parity.test.ts(293,3): error TS2344:
  Type 'false' does not satisfy the constraint 'true'.

说明反转不是把断言写松了,而是确实反了向。

测试

$ npx vitest run packages/app-shell
 Test Files  252 passed (252)
      Tests  2150 passed | 1 skipped (2151)
   Duration  180.18s

Lint

$ cd packages/app-shell && pnpm lint
✖ 2090 problems (0 errors, 2090 warnings)
EXIT=0

(2090 条全是既有 warning,与本 PR 无关。)

spec 符号 guard

$ pnpm check:spec-symbols
✅  spec symbol derivation: 1200 files scanned against 4344 spec export names;
    8 declared dialects, 75 untriaged collisions in 18 packages.
EXIT=0

changeset gate

$ pnpm changeset:check
✅  All workspace packages are in the changeset fixed group.

影响面

无行为变更。 DecisionOutputDef 是本包内部符号(src/index.ts re-export,包只有 . 一个 entry),收敛前后解析出的形状逐键相同,decisionOutputParams() 仍然读 d.required —— 只是现在读的是 spec 自己的字段。调用方(useRecordApprovals.tsDeclaredActionsBar.tsxRecordDetailView.tsx)一行都不用改,已确认没有 ripple。

changeset 标 patch(内部类型重构,无 API 变更;按 AGENTS.md 版本号策略,不声明 major)。

一点顺带的观察

#3178 只升了 @objectstack/spec,@objectstack/client / core / formula / lint 仍是 17.0.0-rc.0,而它们对 spec 的依赖是精确版本,所以图里现在同时存在两份 spec:

spec copies in graph: @objectstack+spec@17.0.0-rc.0, @objectstack+spec@17.0.0-rc.1
client / core / formula / lint  ->  都仍然看到 spec 17.0.0-rc.0

这不影响本 PR(app-shell 侧解析到 rc.1,收敛所需的 required 在),但对依赖引用同一性的 guard(spec-subschema-parity.test.ts、以及用 createRequire 解析 spec .d.ts 的两处)是潜在隐患。已记录在 objectui#3182,不在本 PR 范围内。

备注

分支上第一个 commit(139b9df)的 message 写着 "BLOCKED — does not compile against 17.0.0-rc.0" —— 那是 #3178 合并之前的状态,现已不成立。因为分支已推送且本仓禁止 force-push,该 commit message 原样保留,以此说明。


🤖 Generated with Claude Code

claude added 3 commits August 2, 2026 03:43
…port

BLOCKED — does not compile against the `@objectstack/spec` version this repo
currently resolves (17.0.0-rc.0). Pushed to preserve the prepared change; see
objectstack-ai/objectstack#4562 for the dependency decision it waits on.

- `DecisionOutputDef` was `extends SpecDecisionOutputDef { required?: boolean }`.
  The spec adopted `required` (cd6b9f202, schema-pinned by objectstack#4561), so
  the derivation adds nothing and collapses to `export type DecisionOutputDef =
  SpecDecisionOutputDef`.
- Rewrote the stale interface TSDoc, which still asserted "the spec does not
  model it yet".
- Inverted the parity pin in `spec-symbol-parity.test.ts`: the exclusion set
  `Exclude<keyof DecisionOutputDef, keyof SpecDecisionOutputDef>` is now `never`,
  plus an exact-identity assertion.

Verified against spec 17.0.0-rc.1 (which does model `required`): app-shell
`tsc --noEmit` clean, the new assertions compile, and the old `'required'`
assertion correctly fails (TS2344). Against the resolved 17.0.0-rc.0 the
collapse fails with TS2339 `Property 'required' does not exist`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5
@vercel

vercel Bot commented Aug 2, 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 2, 2026 8:20am

Request Review

@github-actions github-actions Bot added the tests label Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.45KB 3.08KB
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.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
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) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 475.95KB 104.41KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.20KB 34.74KB
fields (index.js) 223.45KB 54.66KB
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.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.44KB 10.66KB
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.05KB 1.53KB
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.90KB 12.35KB
plugin-charts (index.js) 60.53KB 17.12KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.87KB 28.82KB
plugin-designer (index.js) 210.51KB 42.50KB
plugin-detail (index.js) 224.52KB 54.98KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.38KB 26.93KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 184.69KB 48.91KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 104.87KB 25.31KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.48KB 10.57KB
plugin-timeline (index.js) 25.76KB 7.32KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.54KB 20.39KB
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
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.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
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

@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 08:27
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 5426cc7 Aug 2, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4562-decision-output-collapse branch August 2, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectui: DecisionOutputDef 派生分歧已纯冗余——塌缩为 re-export,反转 parity 钉子(#4525 收尾)

2 participants