#3146 落地的 public-block-binding-reach.test.tsx 回答的是一个刻意窄的问题:声明了 objectName 的 public block,挂载后有没有一次数据调用带上这个对象名。它首跑抓到两个真缺陷(#3144 ,已由 #3147 修复),价值是实的。本 issue 记录它没有 覆盖的部分,以及每一块的处置决定。
console 的 curated 契约共 57 个 block(public-contract.test.ts 的 EXPECTED_COVERED)。当前行为覆盖 24 (两道探针的并集)。
层
范围
状态
1
候选筛选被 lazy stub 缩水
✅ #3153 ,8/14 → 14/14
2
已探块的非 objectName 绑定
✅ 已落地,但原方案不存在 ,见下
3a
record:* 家族(11 个,原文写 10)
✅ 已落地,8 个响应、3 个入台账,抓到 2 个缺陷
3b
纯展示原语(约 30 个)
❌ 决定不做 ,理由见下
第 1、2、3a 层已全部落地(#3153 / #3166 );3b 是明确的不做。除 3b 外无剩余项。
1. 候选筛选被 lazy stub 静默缩水 ✅ 已由 #3153 修复
> 更正(开 issue 后核实) :本条最初写的是"这批是 #4413 复发风险最高的一批"。那个判断是错的 ,记在这里免得下一个人照着它排优先级。逐个核过接线后:ObjectChart 自己读 context(ObjectChart.tsx:253:props.dataSource || context?.dataSource,所以裸注册在它这里安全),object-gantt / object-timeline / object-map / object-kanban / object-calendar 都有 context→prop wrapper。六个块接线本来就是对的,没有第三个 #3144 。
>
> 本条的性质因此不是"6 个高风险块没被探到",而是门禁静默少报了自己的覆盖面 ——价值仍在,但是止损一个和 objectstack#4472 同族的谎,只不过这次是新门禁对自己的作用域撒谎。
探针用 getPublicConfigs().filter(有 objectName 输入) 选候选,而 console 对这些块走 registerLazy,pending stub 不带 inputs。Registry.getMeta 的注释写得明明白白:
> A stub carries only what registerLazy was given, so inputs is usually absent until the chunk loads. Consumers should treat that as "not yet known", not as "declares no props".
探针恰恰犯了这条注释警告的错。这是 #2953 的形状(lazy 注册悄悄掉出契约)在消费端复发。原守卫 length > 0 + toContain('object-form') 在 8/14 时两条都是真的,看不见缩水。
#3153 的修法 :候选筛选前经注册表自己的 loadLazy 解析 pending 条目;守卫改成精确清单。
2. 已探块的非 objectName 绑定 ✅ 已落地——但原方案在代码库里不存在
> 更正(做的时候核实的) :本条原来提议的切片是"recordId 在 object-form / object-master-detail-form / embeddable-form 三个块上"。这三个块都没有声明 recordId 输入——事实上 57 个 public block 里没有任何一个声明 recordId。 全仓库只有两处 recordId/resourceId 输入声明,都在 plugin-detail/src/index.tsx(view:detail 与 detail-view),两个都不在 curated public 集合里。
>
> 这和 objectstack#4472 的方向 (d) 是同一类结果:一个从声明出发提出的切片,等真去看声明的时候发现它不在 。记在这里,因为这正是这条线要说的事——从声明推断能力,推错的成本就是这样。
代之以落地的是同机制、同成本、但确实存在的切片:record:related_list 和 record:line_items 各自把 required 的 relationshipField + childObject 绑进子查询,且只有在 record context 下才观察得到 ——也就是只有第 3a 层那道探针能看见。断言:两次 find() 都必须带上声明的子对象名和关系字段,并且 scope 到当前绑定的父记录 id。
结果:两个都成立 ,调用形如
find("probe_child__c", {"$filter":{"parent_probe_id":"probe-record-aaaa"},"$top":5,"$skip":0})
"同机制换断言"这个假设成立 ,而且成本是在本来就要发生的挂载上多加一条断言。原文担心的噪声比在这个切片上没有出现。
噪声比:更新后的账(这条更正会改变结论方向 )
> 本节先前写的是"2 个真缺陷 : 5 个探针自造假信号"。那是漏更新 ——假信号数加了第 3a 层新增的一个,真缺陷数却没跟着加。实际是:
数量
明细
真缺陷
4
list-view / embeddable-form 缺 dataSource 桥(#3144 )、record:activity 恒空(#3165 )、useMetadata 渲染死循环
探针自造假信号
5
Proxy 展开剥光方法、订阅方法返回 Promise 炸卸载、data 盖掉 objectName 绑定、object-map 的 maplibre teardown 抛错、sections 通用样本让 record:details 崩
从 1:2 变成接近 1:1 。原文"诊断尾巴不封顶、不建议全面铺开"的判断建立在前一个数字上,按新数字它站不住了 :外扩的性价比比当初估计的好得多。
真正该保留的谨慎不是"别扩",而是扩的方式 :每个假信号都是"给每个输入填了合理值 ≠ 配置整体合理"的一个实例,所以外扩要一次一个绑定、带负对照(第 3a 层那个"同一条记录挂两次必须逐字节相同"的仪器自检)地扩。有了负对照,假信号会当场暴露成仪器故障而不是伪装成发现。
3a. record:* 家族 ✅ 已落地(11 个,不是 10)
原文漏了 record:line_items。实际的 public record:* 是 11 个:details / highlights / related_list / path / line_items / activity / discussion / history / quick_actions / reference_rail / alert。(record:chatter 不是 public——它是 record:discussion 的同渲染器别名,public-contract.test.ts 按这个理由排除。)
探针 :apps/console/src/__tests__/record-block-record-reach.test.tsx。原文说"断言从有没有调 dataSource 变成渲染出的是记录内容还是 placeholder"——实际做成了差分 ,因为"是不是 placeholder"对 11 个块没有统一判据(path 渲染的是高亮哪一段、alert 渲染的是显不显示、history 渲染的是取数参数):
> 把 block 挂在 record context 下,绑两条不同的、同对象的 记录,各挂一次。DOM 或数据调用有没有变化 ?
几个刻意的设计决定,都是为了这道门禁不会变成它自己要消灭的东西:
两条记录,而不是"绑 vs 不绑" 。不绑的对照组树形不同(少一层 provider),React.useId 会整体偏移,于是每个块都"有差异"——探针自己制造绿。
差分,而不是"渲染非空" 。后者对一个无视全部输入的块也恒为真,正是 3b 那条不做的理由。
仪器本身也被断言 :每个块还会用记录 A 再挂一次 ,要求和第一次逐字节相同。这条一旦失败,"A 和 B 不同"就不再等于"记录到达了输出",整个文件的绿就是噪声——所以是断言,不是假设。
崩溃按崩溃报 。SchemaRenderer 会把渲染期抛错吃掉画成错误卡片,而崩了的块对两条记录渲染的是同一张卡片 ——不单独断言的话它会落进"无差异"那一档,被读成关于绑定的结论。(这条不是假想:sections 的样本值就把 record:details 送进了这个陷阱。)
无外网 。这家族里有块直接调 fetch('/api/v1/security/explain'),happy-dom 下相对路径会打到 localhost:3000——探针原本"能跑"只是因为连接被拒,每跑一次刷 24 行 ECONNREFUSED,而且谁本地起了 3000 端口结果就不一样。现在 fetch 立即 reject,URL 进同一份调用台账(走裸 fetch 绑定记录的块因此照样算数)。
结果:8 个响应绑定记录 (details / highlights / related_list / path / line_items / history / quick_actions / alert),3 个入 NO_RECORD_REACH 台账 ,每条都必须写明宿主路径——因为"宿主供数"只有在真有宿主在供的时候才是理由:
block
台账理由
核得住吗
record:discussion
items 来自 DiscussionContext,由 app-shell RecordDetailView 挂
✅ 宿主路径是真的
record:reference_rail
entries 由 buildDefaultPageSchema 注入,不是作者输入
✅ 宿主路径是真的
record:activity
渲染器把 items={[]} 写死,没有任何路径在供数
❌ 这就是 #4413,第三次
抓到的两个缺陷
record:activity 的 feed 结构上不可能有内容——11 个声明的输入全是空 feed 上的过滤器(objectstack#4413 的形状,第三次) #3165 — record:activity 的 feed 结构上不可能有内容。 渲染器 useRecordContext() 调了就丢,items={[]} 写死;底层组件不取数;synth 发出的节点不带 props 且触发它的 showActivity 生产代码从没设过 true。11 个声明的输入全是这个恒空 feed 上的过滤器和开关。它自己的文件头还写着 "Real data wiring … lives inside that component"——不在,那一句就是缺陷本身。 单独开 issue,因为修法是个功能(镜像 record:history 已有的 sys_activity 自取数),不是 list-view / embeddable-form 在 SDUI 注册表路径上拿不到 dataSource——声明为 required 的 objectName 是死的(objectstack#4413 同形) #3144 那种一行桥接。
useMetadata() 的"优雅兜底"是个无限渲染循环。 它每次调用都新建 兜底对象,于是 provider 外每渲染一次 getItem 就换一个身份;useMetadataItem 把 getItem 放在 effect deps 里、并在无 name 分支 setState 一个全新对象 → effect 重跑 → 新 state 对象 → 重渲染 → 新身份,同步到能把 render() 卡住 。注释里写它存在的理由是"让 provider 外的单测能正常渲染"——它恰恰让那些消费者根本挂不起来(record:alert / record:quick_actions 各自打满一个核、涨到 8.6 GB)。已由 test(console): record:* 家族第一次有了行为证据,抓到 record:activity 恒空与 useMetadata 的渲染死循环 (#3149) #3166 修掉:兜底改成冻结的模块级单例,并让清空分支在已清空时直接 bail。
3b. 纯展示原语(约 30 个)❌ 决定不做
flex / grid / card / text / image / badge / …,无数据绑定,唯一能断言的性质是"渲染非空 / 非 placeholder"。
这层不覆盖,理由是可断言的性质太弱 :对一个吐空 div 的块,"渲染非空"永远为真。加上去会得到一道报绿但什么都没检查 的门禁——正是 objectstack#4472 要消灭的东西,也正是"一道报绿的假门禁比没有门禁更危险"那句话的字面意思。与其加个橡皮图章让覆盖率数字好看,不如在这里显式记一句不做和为什么。
如果将来有人想覆盖这层,先要回答的是"对一个展示原语,什么性质值得断言"——在有答案之前,不覆盖是正确的状态,不是欠债。
(第 3a 层的差分设计其实给出了一条可能的路:变一个输入,看输出变不变 。但那要为每个原语挑一个"该被读到"的输入,是设计工作,不是套模板——所以仍然不做,只是把线索留在这里。)
边界,免得这个 issue 又被过度解读
到达数据层 / 记录到达输出 ≠ 渲染正确;这条链上仍然没有任何环节看渲染结果 (parity 门禁比声明、prop 门禁 parse JSX、契约从 schema 生成——ADR-0082 addendum 已写明)。本 issue 的范围是"绑定接没接上",不是视觉回归测试。
参考
#3146 落地的
public-block-binding-reach.test.tsx回答的是一个刻意窄的问题:声明了objectName的 public block,挂载后有没有一次数据调用带上这个对象名。它首跑抓到两个真缺陷(#3144,已由 #3147 修复),价值是实的。本 issue 记录它没有覆盖的部分,以及每一块的处置决定。console 的 curated 契约共 57 个 block(
public-contract.test.ts的EXPECTED_COVERED)。当前行为覆盖 24(两道探针的并集)。objectName绑定record:*家族(11 个,原文写 10)第 1、2、3a 层已全部落地(#3153 / #3166);3b 是明确的不做。除 3b 外无剩余项。
1. 候选筛选被 lazy stub 静默缩水✅ 已由 #3153 修复> 更正(开 issue 后核实):本条最初写的是"这批是 #4413 复发风险最高的一批"。那个判断是错的,记在这里免得下一个人照着它排优先级。逐个核过接线后:
ObjectChart自己读 context(ObjectChart.tsx:253:props.dataSource || context?.dataSource,所以裸注册在它这里安全),object-gantt/object-timeline/object-map/object-kanban/object-calendar都有 context→prop wrapper。六个块接线本来就是对的,没有第三个 #3144。>
> 本条的性质因此不是"6 个高风险块没被探到",而是门禁静默少报了自己的覆盖面——价值仍在,但是止损一个和 objectstack#4472 同族的谎,只不过这次是新门禁对自己的作用域撒谎。
探针用
getPublicConfigs().filter(有 objectName 输入)选候选,而 console 对这些块走registerLazy,pending stub 不带inputs。Registry.getMeta的注释写得明明白白:> A stub carries only what
registerLazywas given, soinputsis usually absent until the chunk loads. Consumers should treat that as "not yet known", not as "declares no props".探针恰恰犯了这条注释警告的错。这是 #2953 的形状(lazy 注册悄悄掉出契约)在消费端复发。原守卫
length > 0+toContain('object-form')在 8/14 时两条都是真的,看不见缩水。#3153 的修法:候选筛选前经注册表自己的
loadLazy解析 pending 条目;守卫改成精确清单。2. 已探块的非
objectName绑定 ✅ 已落地——但原方案在代码库里不存在> 更正(做的时候核实的):本条原来提议的切片是"
recordId在object-form/object-master-detail-form/embeddable-form三个块上"。这三个块都没有声明recordId输入——事实上 57 个 public block 里没有任何一个声明recordId。 全仓库只有两处recordId/resourceId输入声明,都在plugin-detail/src/index.tsx(view:detail与detail-view),两个都不在 curated public 集合里。>
> 这和 objectstack#4472 的方向 (d) 是同一类结果:一个从声明出发提出的切片,等真去看声明的时候发现它不在。记在这里,因为这正是这条线要说的事——从声明推断能力,推错的成本就是这样。
代之以落地的是同机制、同成本、但确实存在的切片:
record:related_list和record:line_items各自把 required 的relationshipField+childObject绑进子查询,且只有在 record context 下才观察得到——也就是只有第 3a 层那道探针能看见。断言:两次find()都必须带上声明的子对象名和关系字段,并且 scope 到当前绑定的父记录 id。结果:两个都成立,调用形如
"同机制换断言"这个假设成立,而且成本是在本来就要发生的挂载上多加一条断言。原文担心的噪声比在这个切片上没有出现。
噪声比:更新后的账(这条更正会改变结论方向)
> 本节先前写的是"2 个真缺陷 : 5 个探针自造假信号"。那是漏更新——假信号数加了第 3a 层新增的一个,真缺陷数却没跟着加。实际是:
list-view/embeddable-form缺 dataSource 桥(#3144)、record:activity恒空(#3165)、useMetadata渲染死循环data盖掉objectName绑定、object-map的 maplibre teardown 抛错、sections通用样本让record:details崩从 1:2 变成接近 1:1。原文"诊断尾巴不封顶、不建议全面铺开"的判断建立在前一个数字上,按新数字它站不住了:外扩的性价比比当初估计的好得多。
真正该保留的谨慎不是"别扩",而是扩的方式:每个假信号都是"给每个输入填了合理值 ≠ 配置整体合理"的一个实例,所以外扩要一次一个绑定、带负对照(第 3a 层那个"同一条记录挂两次必须逐字节相同"的仪器自检)地扩。有了负对照,假信号会当场暴露成仪器故障而不是伪装成发现。
3a.
record:*家族 ✅ 已落地(11 个,不是 10)原文漏了
record:line_items。实际的 publicrecord:*是 11 个:details / highlights / related_list / path / line_items / activity / discussion / history / quick_actions / reference_rail / alert。(record:chatter不是 public——它是record:discussion的同渲染器别名,public-contract.test.ts按这个理由排除。)探针:
apps/console/src/__tests__/record-block-record-reach.test.tsx。原文说"断言从有没有调 dataSource 变成渲染出的是记录内容还是 placeholder"——实际做成了差分,因为"是不是 placeholder"对 11 个块没有统一判据(path 渲染的是高亮哪一段、alert 渲染的是显不显示、history 渲染的是取数参数):> 把 block 挂在 record context 下,绑两条不同的、同对象的记录,各挂一次。DOM 或数据调用有没有变化?
几个刻意的设计决定,都是为了这道门禁不会变成它自己要消灭的东西:
React.useId会整体偏移,于是每个块都"有差异"——探针自己制造绿。sections的样本值就把record:details送进了这个陷阱。)fetch('/api/v1/security/explain'),happy-dom 下相对路径会打到localhost:3000——探针原本"能跑"只是因为连接被拒,每跑一次刷 24 行 ECONNREFUSED,而且谁本地起了 3000 端口结果就不一样。现在fetch立即 reject,URL 进同一份调用台账(走裸 fetch 绑定记录的块因此照样算数)。结果:8 个响应绑定记录(details / highlights / related_list / path / line_items / history / quick_actions / alert),3 个入
NO_RECORD_REACH台账,每条都必须写明宿主路径——因为"宿主供数"只有在真有宿主在供的时候才是理由:record:discussionRecordDetailView挂record:reference_railentries由buildDefaultPageSchema注入,不是作者输入record:activityitems={[]}写死,没有任何路径在供数抓到的两个缺陷
record:activity的 feed 结构上不可能有内容——11 个声明的输入全是空 feed 上的过滤器(objectstack#4413 的形状,第三次) #3165 —record:activity的 feed 结构上不可能有内容。 渲染器useRecordContext()调了就丢,items={[]}写死;底层组件不取数;synth 发出的节点不带 props 且触发它的showActivity生产代码从没设过 true。11 个声明的输入全是这个恒空 feed 上的过滤器和开关。它自己的文件头还写着 "Real data wiring … lives inside that component"——不在,那一句就是缺陷本身。 单独开 issue,因为修法是个功能(镜像record:history已有的sys_activity自取数),不是list-view/embeddable-form在 SDUI 注册表路径上拿不到 dataSource——声明为 required 的 objectName 是死的(objectstack#4413 同形) #3144 那种一行桥接。useMetadata()的"优雅兜底"是个无限渲染循环。 它每次调用都新建兜底对象,于是 provider 外每渲染一次getItem就换一个身份;useMetadataItem把getItem放在 effect deps 里、并在无 name 分支setState一个全新对象 → effect 重跑 → 新 state 对象 → 重渲染 → 新身份,同步到能把render()卡住。注释里写它存在的理由是"让 provider 外的单测能正常渲染"——它恰恰让那些消费者根本挂不起来(record:alert/record:quick_actions各自打满一个核、涨到 8.6 GB)。已由 test(console):record:*家族第一次有了行为证据,抓到record:activity恒空与useMetadata的渲染死循环 (#3149) #3166 修掉:兜底改成冻结的模块级单例,并让清空分支在已清空时直接 bail。3b. 纯展示原语(约 30 个)❌ 决定不做
flex / grid / card / text / image / badge / …,无数据绑定,唯一能断言的性质是"渲染非空 / 非 placeholder"。
这层不覆盖,理由是可断言的性质太弱:对一个吐空 div 的块,"渲染非空"永远为真。加上去会得到一道报绿但什么都没检查的门禁——正是 objectstack#4472 要消灭的东西,也正是"一道报绿的假门禁比没有门禁更危险"那句话的字面意思。与其加个橡皮图章让覆盖率数字好看,不如在这里显式记一句不做和为什么。
如果将来有人想覆盖这层,先要回答的是"对一个展示原语,什么性质值得断言"——在有答案之前,不覆盖是正确的状态,不是欠债。
(第 3a 层的差分设计其实给出了一条可能的路:变一个输入,看输出变不变。但那要为每个原语挑一个"该被读到"的输入,是设计工作,不是套模板——所以仍然不做,只是把线索留在这里。)
边界,免得这个 issue 又被过度解读
到达数据层 / 记录到达输出 ≠ 渲染正确;这条链上仍然没有任何环节看渲染结果(parity 门禁比声明、prop 门禁 parse JSX、契约从 schema 生成——ADR-0082 addendum 已写明)。本 issue 的范围是"绑定接没接上",不是视觉回归测试。
参考
list-view/embeddable-form在 SDUI 注册表路径上拿不到 dataSource——声明为 required 的 objectName 是死的(objectstack#4413 同形) #3144 / fix(plugin-list,plugin-form): 在注册表路径上把 dataSource 接到 list-view / embeddable-form (#3144) #3147(它抓到并修掉的两个缺陷)、fix(console): binding-reach 探针少报了自己 6 个块的覆盖面,而且是静默的 (#3149 第 1 层) #3153(第 1 层)、test(console):record:*家族第一次有了行为证据,抓到record:activity恒空与useMetadata的渲染死循环 (#3149) #3166(第 2 + 3a 层)、record:activity的 feed 结构上不可能有内容——11 个声明的输入全是空 feed 上的过滤器(objectstack#4413 的形状,第三次) #3165(第 3a 层抓到的缺陷)getPublicConfigs因此改为含 stub——第 1 层是同一形状在消费端复发)apps/console/src/__tests__/public-block-binding-reach.test.tsx、apps/console/src/__tests__/record-block-record-reach.test.tsx、apps/console/src/register-plugins.ts、packages/core/src/registry/Registry.ts(getMeta的 stub 语义注释)