Skip to content

fix(app-shell): 检查器读写 { dialect, source } 表达式信封 (#3218) - #3228

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-3218-hook-inspector-expression-envelope
Aug 2, 2026
Merged

fix(app-shell): 检查器读写 { dialect, source } 表达式信封 (#3218)#3228
os-zhuang merged 1 commit into
mainfrom
claude/issue-3218-hook-inspector-expression-envelope

Conversation

@os-zhuang

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

Copy link
Copy Markdown
Contributor

Fixes #3218

缺陷

HookSchema.condition 用的是 ExpressionInputSchema(和 FlowEdgeSchema.condition 同一个 pipe),裸字符串在 parse 时被规范化成 { dialect: 'cel', source } —— 信封才是平台自己产出的形状。Hook 检查器只认裸字符串,于是「Run only when (optional CEL)」渲染为

空框不是外观问题:ConditionBuilderemit 只编译当前 UI 里的行,所以作者在这个看起来空的构建器里下一次编辑,提交的是替换掉那条他从没看见过的守卫(清空则提交 condition: undefined)。仅仅打开面板是安全的(onCommit 只在真实编辑时触发),但诱导那次编辑的正是这个空框。

读侧:走既有的那一个读法

全部经 conditionText(#3216 沉淀下来的唯一读法),包在共享的 expressionSource / writeExpressionSource 里。没有写出第四份 typeof c === 'string' —— previews/flow-canvas-layout.ts 未被改动,只被读取。

写侧:按本单裁决(选项 B),含 ast 必须丢弃那一条

原先提交路径唯一保住的键就是 source;其余全丢,因为它发的是裸字符串,而 spec 的 pipe 把 dialect 硬编码cel。也就是说,改一个 dialect: 'cron' / dialect: 'template' 守卫的一个字符,就把它换到了另一个求值引擎上,并顺手丢掉 ast 与 ADR-0089 的 meta(rationale / generatedBy —— AI 作者最常填、也最没人会手工补回来的那份)。

现在编辑 source 时:

行为
dialect 保留
meta 保留
source 替换
ast 丢弃 —— 它是旧 source 的派生物。留着就是「引擎按旧 AST 求值,界面显示新 source」,比原缺陷更难查;objectstack compile 会重新填,且 source 在,ExpressionSchemasource || ast refine 仍满足。

没有原信封可保留时(值缺失、裸字符串、或 action.disabled 上的布尔),提交仍是裸字符串简写 —— spec 的 pipe 把它规范化成的正是 { dialect: 'cel', source },一分不少。这条同时让同一个 helper 对普通 string 谓词字段也安全:给什么形状,还什么形状。这一点是有意为之,因为通用 SchemaForm 条件控件同时服务这两类字段。

普查结果:同族有四处,不止一处

位置 症状
inspectors/HookDefaultInspector.tsx condition —— 本单报告的缺陷
inspectors/ActionDefaultInspector.tsx visible / disabled(spec 是 boolean | ExpressionInput),同样的空框读法
widgets.tsxConditionWidget SchemaForm每一个谓词名字段(visible / hidden / disabled / condition / predicate / *When)都路由到这里,而它做的是 String(value) —— 信封进到编辑器里是字面量 [object Object],比空框更糟
studio-design/ObjectValidationsPanel.tsx 规则 condition;另外类型切换器里还有第三份窄读法(typeof cur.condition === 'string'),信封守卫会被直接丢掉、留下骨架里那条永不命中的 'false'ValidationRuleDraft.condition 的本地类型由 string 改为 ExpressionInput

外加 inspectors/FlowEdgeInspector.tsx:#3216 只收敛了它的,仍是裸字符串,是上面那条 dialect 改写的同一个口子,一并按同一规则修掉。

未改动 inspectors/PageBlockInspector.tsxhidden。它不是本族:PageComponentSchema.strict() 且根本没有 hidden 这个键(canonical 是 visibleWhen)。那是另一个真缺陷,已按 Prime Directive #10 单独立单:#3229

测试(先写,改前失败)

新增两个文件,固件一律不手写信封 —— 按 #3216 的做法把作者输入喂给 HookSchema.parse 再断言,固件因此不可能与 spec 漂移。

  • inspectors/HookDefaultInspector.condition.test.tsx —— 真渲染检查器:① 信封条件显示出 source;② 编辑后 dialect / meta 原样保留;③ 原来带 ast 的,编辑后 ast 没了(并再跑一次 HookSchema.parse 证明结果仍然合法);④ dialect: 'template' 的条件编辑后仍是 template(A/B 分界线);外加清空仍提交 undefined
  • inspectors/expression-envelope.test.ts —— 共享读写对的单元测试(其余调用点靠这一份契约覆盖),含 cron、AST-only 信封、布尔 disabled、不可变性。

改前:5 failed | 1 passed,失败原因正是守卫渲染为空(找不到条件行、找不到删除按钮、raw 编辑器值为空串)。改后 15 passed;packages/app-shell 全量 257 files / 2203 tests 通过,type-checklint(0 errors)通过。

)

`HookSchema.condition` is `ExpressionInputSchema`, so a persisted hook carries
`{ dialect, source }` — the Hook inspector's guard box read a bare string only
and rendered empty, and `ConditionBuilder.emit` compiles only the rows on
screen, so the author's next edit replaced a guard they were never shown.

Read side converges on `conditionText` (the one reader #3216 settled on) via a
shared `expressionSource` / `writeExpressionSource` pair — no fourth
`typeof c === 'string'`. Write side follows the #3218 ruling: preserve
`dialect` and `meta`, replace `source`, DISCARD `ast` (derived from the old
source; `objectstack compile` refills it). With no prior envelope the commit
stays the bare-string shorthand, which the spec's pipe normalizes to
`{ dialect: 'cel', source }`.

Swept the rest of the family: action `visible` / `disabled`, the generic
SchemaForm condition widget (which put a literal `[object Object]` in the
editor), the object-validations rule `condition` plus its type-switch carry
(a third narrow read that dropped the guard), and the flow-edge inspector's
write, whose read #3216 had already fixed.

Fixtures are authored input fed through `HookSchema.parse`, never hand-written
envelopes, so they cannot drift from the spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRJtkgUAaVG11FsJQbvZWA
@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:14pm

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-B76hz360.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
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) 476.12KB 104.47KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 223.43KB 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) 37.91KB 10.53KB
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) 230.54KB 56.77KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.39KB 26.94KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 185.04KB 49.00KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 104.86KB 25.30KB
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
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.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 20:25
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

PM 验收:ACCEPT,放行合并队列

CI 15 项 13 绿 2 skipped、零红,已独立复核。

裁决(选项 B + ast 丢弃)全部落地,读侧确实没有出现第四份 typeof c === 'string',flow-canvas-layout.ts 只读未改。测试先写、改前 5 failed | 1 passed 且失败原因正是"守卫渲染为空",这是行为变更该有的证明方式。

普查是本 PR 最大的价值,远超原单

派发时那句"ConditionBuilder 的其余调用点一并普查"换回了四处,而原单只报告了一处——报告的那处还是最轻的:

位置 症状
HookDefaultInspector 空框(原单报告)
ActionDefaultInspector visible / disabled 同样空框
widgets.tsxConditionWidget String(value) → 编辑器里显示字面量 [object Object],而 SchemaForm每一个谓词名字段(visible/hidden/disabled/condition/predicate/*When)都路由到它
ObjectValidationsPanel 规则 condition,外加类型切换器里第三份窄读法——信封守卫被直接丢弃,留下骨架里那条永不命中的 'false'

外加 FlowEdgeInspector侧:#3216 只收敛了它的读,dialect 改写的口子还开着。

也就是说,我在裁决里判断"A 会静默改写 dialect"的那个口子,在仓里有五个入口。只修报告的那一处,等于修好一扇门、留着四扇。

边界守得对

没有顺手改 PageBlockInspectorhidden——PageComponentSchema 根本没有 hidden 这个键,那不是本族而是另一个真缺陷,按 Prime Directive #10 单独立单。这个判断是对的:同族的判据是"spec 声明了这个键、本地读法过窄",不是"看起来像条件字段"。

一处设计选择我确认一下,认为正确

没有原信封可保留时仍提交裸字符串简写(而不是强行构造 {dialect:'cel', source})。这是对的:spec 的 pipe 把裸字符串规范化成的正是那个形状,一分不少;而且这让同一个 helper 对普通 string 谓词字段也安全——给什么形状还什么形状。通用 SchemaForm 条件控件同时服务两类字段,这个性质是必需的,不是巧合。


Generated by Claude Code

Merged via the queue into main with commit b06f78a Aug 2, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-3218-hook-inspector-expression-envelope branch August 2, 2026 20:26
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.

Hook 检查器把 { dialect, source } 信封形式的 condition 读成空字符串,提交时会清掉它

2 participants