Skip to content

fix(components): div 废弃提示按 provenance 收窄到 JSON 作者面 (#4000) - #4916

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4000-div-warning-provenance
Aug 17, 2026
Merged

fix(components): div 废弃提示按 provenance 收窄到 JSON 作者面 (#4000)#4916
yinlianghui merged 1 commit into
mainfrom
claude/issue-4000-div-warning-provenance

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4000

基 sha 5ffcc1432。按 2026-08-10 维护者裁定实施:废弃提示按 provenance 拆分 —— kind:'html' tier 自己的解析器产出的节点豁免,JSON 作者面的节点照旧告警。


前提验证(先跑,后写实现)

新钉子在未改任何实现的基线上先跑了一遍,证实卡面立论成立:

FAIL packages/components/src/__tests__/div-deprecation-provenance.test.tsx
  × stays silent for nodes the html tier compiled from its own source
    AssertionError: expected [ Array(1) ] to have a length of +0 but got 1
  × still reports a JSON-authored node, exactly once, …
    AssertionError: expected [] to have a length of 1 but got +0
 Test Files  1 failed (1)   Tests  2 failed | 2 passed (4)

第一条正是 issue 描述的误伤:一个 kind:'html' 页面里的盒子标签,编译渲染后收到一条提示。这一条的对照断言先于警告断言通过(页面确实渲染出了 .outer / .inner 与文本,没有落到 "failed to compile" 面板)—— 否则「零提示」可能只是因为整页编译失败,那是空绿。

第二条红是第一条的连带:warn-once 的 Set 是模块级的,前一条已经 latch,所以作者节点那一条反而观测到 0。修好后两条同时转绿,这个互相牵制正是钉子的设计(见文件头注释)。

机制

判据是来源,由生产者确立,不是消费端按节点形状猜。

生产者(packages/sdui-parser)。解析器在造出每个元素节点时打一个标记 —— parse.tsmarkHtmlTierNode({ type: tag, ...props })。新文件 src/provenance.ts 持有标记与谓词 isHtmlTierNode,并从包入口导出。

标记是一个 symbol,而且是 Symbol.for 注册键(@object-ui/sdui-parser.html-tier-node),三点都是有意的:

  • symbol 对 JSON.stringify / Object.keys / for...in 不可见。 于是它落不进被持久化的文档 —— 这是要点而不是限制:「本进程里我们的解析器产出了这个节点」不是保存文档的属性,保存下来的文档也不该能重放它。若写成 _provenance: 'html' 这样的字符串键,一份手写或 AI 生成的 JSON 元数据把它抄进去,就给自己买到了本该冲它来的那条提示的豁免。作者面能伪造的 provenance 不是 provenance。同理它也漏不到 DOM 上变成野生属性。
  • 但它是 enumerable 的,这一条比看上去关键:SchemaRenderer 交给渲染器的是节点的浅拷贝({ ...schema }),对象展开会带走可枚举的 symbol 键而丢掉不可枚举的。写成不可枚举,标记会在那次拷贝里消失,原缺陷只在渲染路径上、悄无声息地回来。这一条由 provenance.test.ts 的「survives the shallow copy」和组件侧走真实 SchemaRenderer 的钉子共同压住。
  • Symbol.for 而非模块内 Symbol() 本包同时出 ESM/CJS 且被多个包消费,存在两份模块实例的可能;两个局部 symbol 不相等,而这种不匹配的失败形态就是原缺陷无声回归。注册键不会漂移。

花括号属性里夹带的 JSON(body={[{"type":"div"}]})不打标记 —— 那段 JSON 是手写的,JSON 作者面的建议对它成立。豁免只覆盖解析器真正产出的节点,不多一寸。

消费者(packages/components/src/renderers/basic/div.tsx)。渲染器读谓词决定是否告警。顺序#3998 的 production 提前返回同一纪律:豁免判断在 warn-once 的 Set 标记之前 —— html-tier 节点先渲染不得 latch 守卫,否则它会吞掉同一页里 JSON 作者节点应得的那一条,恰好把这条提示唯一该服务的读者静音掉。

文案。 迁移建议两行逐字节未动(#3998 的断言仍绿);首行改为「deprecated for JSON-authored pages」,并补一句说明:在 kind:'html' 页面里该标签是那一层词表的一等成员,原样编译,不在此报告。一条说「这个类型废弃了,句号」的提示,在另一层把它当作永久词表成员的情况下对它的两类读者之一就是假的 —— 而一直收到它的恰恰是无能为力的那一类。content/docs/components/basic/div.mdx 同步补了同一句话的作用域说明。

钉子

packages/components/src/__tests__/div-deprecation-provenance.test.tsx(4 条,顺序是断言的一部分):

  1. html tier 自产节点零提示 —— 走真实 PageRenderer 编译 + 渲染,虚拟机内 Set 尚为处女态,所以「零」不可能由「之前有人 latch 过」解释。含渲染对照断言(防空绿)。
  2. JSON 作者节点仍报,且恰好一条 —— 只有在第 1 条没有标记 Set 的前提下才成立;顺带断言迁移建议原样保留、文案写明了作用域(验收点 ③)。
  3. 后续渲染不再报(warn-once 语义保持,验收点 ②)。
  4. provenance 不落到 DOM、不进序列化 —— 断言元素上没有相关属性。

packages/sdui-parser/src/__tests__/provenance.test.ts(6 条):逐层打标记、活过对象展开、对 JSON 不可见且往返后为 false(反伪造)、花括号夹带的 JSON 不打标记、非对象输入不抛、注册键稳定。

反向验证(两面各证一次,先书面预判后执行)

变异前已 commit;还原用 git checkout --(未用 stash)。

A. 撤掉 provenance 豁免(改回无条件告警)。
预判:钉子 1 转红(1 条提示);钉子 2 连带转红(0 条,守卫被 latch);3、4 绿;解析器侧文件整体保持全绿 —— 生产者钉子看不见「消费者不读了」,这正是组件侧那条钉子存在的理由。
实测与预判一致:

× stays silent for nodes the html tier compiled from its own source
    AssertionError: expected [ Array(1) ] to have a length of +0 but got 1
× still reports a JSON-authored node, exactly once, …
    AssertionError: expected [] to have a length of 1 but got +0
 Test Files  1 failed | 1 passed (2)   Tests  2 failed | 8 passed (10)

B. 把豁免扩到作者节点(条件恒假,任何节点都不报)。
预判:钉子 1 保持绿(仍然静默);钉子 2 转红(期望 1 条、实得 0);3、4 绿。这一面专门防豁免面写反 —— 若两面都只由同一条钉子把守,把判断写反会两面都「过」。
实测与预判一致:

× still reports a JSON-authored node, exactly once, …
    AssertionError: expected [] to have a length of 1 but got +0
 Test Files  1 failed | 1 passed (2)   Tests  1 failed | 9 passed (10)

验证

  • pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build —— 绿(新 worktree 先建依赖)。
  • pnpm exec vitest run packages/sdui-parser packages/components/src/__tests__ examples/schema-catalog --maxWorkers=2 —— Test Files 98 passed (98) / Tests 2359 passed (2359)
  • pnpm exec turbo run type-check --concurrency=2 —— Tasks: 81 successful, 81 total
  • pnpm exec turbo run lint --filter=@object-ui/components --filter=@object-ui/sdui-parser —— 0 errors(849 条既有 warning,与本改动无关)。
  • node scripts/check-control-bytes.mjs —— OK (scanned 4397 tracked text file(s));改动文件另做了越过门禁盲区的自查(grep -naP),零命中。
  • check-changeset-presence / check-changeset-no-major / check-phantom-deps / check-self-import / check-doc-types —— 均绿。

消费半径清点:@object-ui/sdui-parser 的消费者是 page.tsx、codegen、manifest-dump 与四处测试;div 渲染面另含 examples/schema-catalog(189 个作者面节点,行为不变)与 html-* 组件测试 —— 都在上面这轮里跑到了。

边界

只动了解析器的 provenance 面、basic/div.tsx、两个测试、一个 changeset 与 components/basic/div.mdx 的一段作用域说明。没有content/docs/releases/,没有examples/schema-catalog/** 的示例(#3965 的清扫排在本卡之后)。

basic/span.tsx 是同族但未动:它比 div 还早一步 —— 至今仍是逐次渲染告警(#3998 把 warn-once 留作后续),叠加同一条 provenance 误伤。按不扩围原则另立 finding 卡,不在本 PR 处理。


Generated by Claude Code

kind:'html' tier 的页面由引擎自己的解析器编译,标签名原样映射成节点 ——
作者在那一层写下的盒子标签是该 tier 词表的一等成员,没有别的拼法可迁移,
提示却照旧对他们开火并给出 JSON 作者面的替代建议。一条谁都无法执行的提示
不是废弃而是噪声,同时意味着这个类型永远退不掉。

判据是来源、由生产者确立:解析器给它产出的每个节点打一个 symbol 标记
(Symbol.for 注册键,跨模块实例不漂移),渲染器读这个标记。symbol 对
JSON.stringify / Object.keys / DOM 均不可见 —— 不落进持久化文档,也就无法
被一份 JSON 元数据复制回来给自己买到豁免;花括号属性夹带的 JSON 不打标记。

豁免判断放在 warn-once 的 Set 标记之前,html-tier 节点先渲染不会吞掉之后
JSON 作者节点应得的那一条。迁移建议一字未改,文案补上它针对哪个作者面。

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
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) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
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) 5.02KB 0.88KB
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) 498.61KB 111.16KB
core (index.js) 4.06KB 1.61KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 157.05KB 43.28KB
fields (index.js) 231.73KB 57.60KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.86KB 10.83KB
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) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
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.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 127.85KB 32.73KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 239.81KB 59.97KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 120.43KB 29.04KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 197.59KB 53.01KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.23KB 26.97KB
plugin-map (index.js) 17.91KB 5.72KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.97KB 11.33KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
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.66KB 3.50KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.53KB 9.41KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.55KB 2.07KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.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 (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 3.05KB 1.52KB
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

Copy link
Copy Markdown
Collaborator Author

PM 验收:ACCEPT(#4000,批次 17,PM 会话 session_01GTRjn8xBqp75dk7kFupVRt)

裁定执行:废弃警告按 provenance 拆分 —— 判据由生产者确立(sdui-parser 造节点时打标),而非消费端启发式猜测,方向正确。机制的三个承重点全部想透并钉住:①Symbol.for 标记对 JSON 序列化/Object.keys/for...in 不可见 —— 手写或 AI 生成的 JSON 元数据无法抄袭豁免(字符串键 _provenance 就可以被抄);②刻意 enumerable —— SchemaRenderer 的 {...schema} 浅拷贝路径上标记存活,否则原缺陷在渲染路径无声回归;③豁免判断在 warn-once 的 Set 标记之前 —— html-tier 节点先渲染不吞掉同页作者节点应得的告警(循 #3998 同纪律)。花括号属性夹带的 JSON 不打标记(那段是手写的)—— 豁免面边界划得准。

实物核验(已过):8 文件 +378/−9 全在声明面;标识 0;releases 0;examples 零触碰(#3965 清扫按裁定排后)。前提验证以「基线上先跑新钉见红」开场(2 failed | 2 passed,且渲染对照先行通过排除整页编译失败假红)—— 教科书式。

反向验证(双面):A 撤豁免 → 预判钉 1 红 + 钉 2 连带红(warn-once latch),实测逐字命中,且暴露「生产者侧 6 钉看不见消费者不读标记」的事实 —— 消费端钉不可省,已记录;B 豁免扩到作者节点 → 钉 2 红,实测命中 —— B 面专防豁免写反,单面钉守不住的正是这个。

有据偏离(接受):①div.mdx 补作用域说明 —— 手册复述同一契约承诺,只改代码文案会让手册继续说假话,属验收点③同面;②新增包级导出 HTML_TIER_NODE/isHtmlTierNode/markHtmlTierNode —— 生产者侧建判据的必然,已标注可低成本收窄(维护者偏好 internal 时只留谓词),否决窗在此。

CI(亲读终态):20 项全 completed,18 success + 2 skipped,零失败。

附带产出:新 finding #4917(span 落后 div 两步:逐次告警 + 同样误伤 html-tier;实测今日零 catalog 命中,本 PR 的 isHtmlTierNode 可直接照抄)。

→ undraft + auto-merge (SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 05:35
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit 40d3a33 Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4000-div-warning-provenance branch August 17, 2026 05:36
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.

div 的废弃警告会对 kind: 'html' tier 自己解析出的节点开火 —— 作者无法消除,且意味着 div 结构上退不掉

2 participants