fix(components): div 废弃提示按 provenance 收窄到 JSON 作者面 (#4000) - #4916
Conversation
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>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
PM 验收:ACCEPT(#4000,批次 17,PM 会话 裁定执行:废弃警告按 provenance 拆分 —— 判据由生产者确立(sdui-parser 造节点时打标),而非消费端启发式猜测,方向正确。机制的三个承重点全部想透并钉住:① 实物核验(已过):8 文件 +378/−9 全在声明面;标识 0;releases 0;examples 零触碰(#3965 清扫按裁定排后)。前提验证以「基线上先跑新钉见红」开场(2 failed | 2 passed,且渲染对照先行通过排除整页编译失败假红)—— 教科书式。 反向验证(双面):A 撤豁免 → 预判钉 1 红 + 钉 2 连带红(warn-once latch),实测逐字命中,且暴露「生产者侧 6 钉看不见消费者不读标记」的事实 —— 消费端钉不可省,已记录;B 豁免扩到作者节点 → 钉 2 红,实测命中 —— B 面专防豁免写反,单面钉守不住的正是这个。 有据偏离(接受):①div.mdx 补作用域说明 —— 手册复述同一契约承诺,只改代码文案会让手册继续说假话,属验收点③同面;②新增包级导出 CI(亲读终态):20 项全 completed,18 success + 2 skipped,零失败。 附带产出:新 finding #4917(span 落后 div 两步:逐次告警 + 同样误伤 html-tier;实测今日零 catalog 命中,本 PR 的 → undraft + auto-merge (SQUASH)。 Generated by Claude Code |
Fixes #4000
基 sha
5ffcc1432。按 2026-08-10 维护者裁定实施:废弃提示按 provenance 拆分 ——kind:'html'tier 自己的解析器产出的节点豁免,JSON 作者面的节点照旧告警。前提验证(先跑,后写实现)
新钉子在未改任何实现的基线上先跑了一遍,证实卡面立论成立:
第一条正是 issue 描述的误伤:一个
kind:'html'页面里的盒子标签,编译渲染后收到一条提示。这一条的对照断言先于警告断言通过(页面确实渲染出了.outer/.inner与文本,没有落到 "failed to compile" 面板)—— 否则「零提示」可能只是因为整页编译失败,那是空绿。第二条红是第一条的连带:warn-once 的 Set 是模块级的,前一条已经 latch,所以作者节点那一条反而观测到 0。修好后两条同时转绿,这个互相牵制正是钉子的设计(见文件头注释)。
机制
判据是来源,由生产者确立,不是消费端按节点形状猜。
生产者(
packages/sdui-parser)。解析器在造出每个元素节点时打一个标记 ——parse.ts里markHtmlTierNode({ type: tag, ...props })。新文件src/provenance.ts持有标记与谓词isHtmlTierNode,并从包入口导出。标记是一个 symbol,而且是
Symbol.for注册键(@object-ui/sdui-parser.html-tier-node),三点都是有意的:JSON.stringify/Object.keys/for...in不可见。 于是它落不进被持久化的文档 —— 这是要点而不是限制:「本进程里我们的解析器产出了这个节点」不是保存文档的属性,保存下来的文档也不该能重放它。若写成_provenance: 'html'这样的字符串键,一份手写或 AI 生成的 JSON 元数据把它抄进去,就给自己买到了本该冲它来的那条提示的豁免。作者面能伪造的 provenance 不是 provenance。同理它也漏不到 DOM 上变成野生属性。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 条,顺序是断言的一部分):PageRenderer编译 + 渲染,虚拟机内 Set 尚为处女态,所以「零」不可能由「之前有人 latch 过」解释。含渲染对照断言(防空绿)。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 绿;解析器侧文件整体保持全绿 —— 生产者钉子看不见「消费者不读了」,这正是组件侧那条钉子存在的理由。
实测与预判一致:
B. 把豁免扩到作者节点(条件恒假,任何节点都不报)。
预判:钉子 1 保持绿(仍然静默);钉子 2 转红(期望 1 条、实得 0);3、4 绿。这一面专门防豁免面写反 —— 若两面都只由同一条钉子把守,把判断写反会两面都「过」。
实测与预判一致:
验证
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