#4340(由 #4353 关闭)settle 了 <RecordRelatedList objectName> 的语义。为了确认那个判定,我把 objectui(HEAD c785740)拉下来读了渲染器——判定是对的,但顺带发现一个更靠下的问题:record:* 这一族 block 在 kind:'react' 页面上根本不工作,而 react-tier 契约照样 publish 着它们的 objectName / recordId 绑定。
这是 AGENTS.md Prime Directive #10 的推论——「never advertise or demo a capability the runtime doesn't actually deliver」——的一个活实例。
证据链
-
react-page 的 scope 把 JSX props 摊进 SDUI schema bag,但只包了 SchemaRendererProvider,没有 RecordContextProvider。
objectui packages/components/src/renderers/layout/react-page.tsx → buildComponentScope:
React.createElement(SchemaRenderer, {
schema: { dataSource, ...props, type: tag },
})
全文件 grep RecordContextProvider / RecordContext 零命中。
-
RecordContextProvider 只由 record 详情路径挂载——app-shell 的 RecordDetailView,以及 metadata-admin 的 PagePreview。react 页面不经过任何一个。
-
useRecordContext() 在 provider 外返回 null,这是有意设计(objectui packages/react/src/context/RecordContext.tsx:190-198 的注释写明:让 record:* 渲染器在 Studio 设计器里能静态渲染而不抛错)。
-
于是在 react 页面上:
| block |
渲染器取 objectName |
取父记录 |
实际结果 |
<RecordDetails> |
ctx?.objectName || '' (record-details.tsx:53) |
ctx |
作者写的两个 prop 都到不了 → 空 |
<RecordHighlights> |
ctx?.objectName || '' (record-highlights.tsx:37) |
ctx(data/objectSchema 也来自 ctx) |
同上 → 空 |
<RecordPath> |
ctx |
ctx |
同上 → 空 |
<RecordRelatedList> |
schema.objectName ✅ (record-related-list.tsx:64) |
ctx?.recordId ❌ |
parentLinkValue = null → 拒绝 fetch |
最后一行有 objectui 自己的测试钉着:packages/plugin-detail/src/__tests__/RelatedList.parentscope.test.tsx:58 — "refuses to fetch when parentId is missing"。
注意四个 block 里只有 <RecordRelatedList> 从 schema 读 objectName;另外三个连对象名都走 context。也就是说这一族内部本身就不一致。
活体影响
examples/app-showcase/src/ui/pages/renewals-pipeline.page.ts 的 <RecordHighlights> 和 <RecordRelatedList> 现在渲染出来是空的——在 #4353 之前和之后都是。#4353 修正的是元数据的正确性(objectName 该写子对象),它没有、也不可能在 framework 侧让渲染器去读它。
那个 PR 的正文写了「the parent record stays bound by recordId」。那是契约这么说,渲染器并不读 schema.recordId。#4353 里 relationshipValueField 被 ledger 为 skipped-with-reason,理由写的是「react 面绑父记录但不绑父对象」——实际情况更糟:父记录也没绑上。
但 #4340 的 objectName 判定本身是确认无误的,不受此影响:
<RelatedList
api={objectName} // 被查询/列出的对象 = 子对象
referenceField={schema.relationshipField} // 该对象上的外键
parentId={parentLinkValue} // 父值,另走 RecordContext
columns={filteredColumns} // objectName 的列
/>
columns/sort 确实解析到 objectName 命名的那个对象,所以 #4353 里的 lint 校验的是正确的对象。
修的方向(以及它的真实成本)
直觉方案是:让 react-page 的 scope 对 record:* 类型用 block 自己的 objectName/recordId props 包一层 RecordContextProvider。但 RecordContextProvider(RecordContext.tsx:167)是个纯值 provider,自己不取数——它要 data / objectSchema,RecordDetailView 是自己 fetch 完再喂进去的。所以真正要加的是一个会取数的 wrapper,这不是接线,是新组件。
分档看成本:
<RecordRelatedList> 在默认 relationshipValueField: 'id' 下只需要 recordId,不需要 data —— 这一档最便宜,单独修就能让 showcase 那个页面活过来。
<RecordDetails> / <RecordHighlights> / <RecordPath> 需要 data + objectSchema,必须真取数。
另一条路是承认这四个 block 本就是 record-detail 语境的组件,把它们从 react-tier 契约里摘掉(REACT_BLOCKS),而不是让 react 面去伪造一个 record context。ADR-0080「capability ≠ contract」支持这种取舍。两条路都是产品决策,不是纯实现问题。
不论选哪条,当前状态不能留:契约 publish 了四个 block 的绑定 prop,渲染器一个都不读(related-list 只读一半),而 os validate 现在还会认真校验这些 prop 指向的字段——lint 在为一个不会执行的绑定把关。
验收
record:* 的每个 block 要么在 react 页面上真的按自己的 props 绑定并渲染,要么从 react-tier 契约里移除;showcase 的 Renewals Pipeline 页面能真的渲染出 highlights 和 invoice 关联列表(或者不再声称能)。
/cc 需要 objectui 侧改动(objectstack-ai/objectui)。
#4340(由 #4353 关闭)settle 了
<RecordRelatedList objectName>的语义。为了确认那个判定,我把 objectui(HEADc785740)拉下来读了渲染器——判定是对的,但顺带发现一个更靠下的问题:record:*这一族 block 在kind:'react'页面上根本不工作,而 react-tier 契约照样 publish 着它们的objectName/recordId绑定。这是 AGENTS.md Prime Directive #10 的推论——「never advertise or demo a capability the runtime doesn't actually deliver」——的一个活实例。
证据链
react-page 的 scope 把 JSX props 摊进 SDUI schema bag,但只包了
SchemaRendererProvider,没有RecordContextProvider。objectui packages/components/src/renderers/layout/react-page.tsx→buildComponentScope:全文件 grep
RecordContextProvider/RecordContext零命中。RecordContextProvider只由 record 详情路径挂载——app-shell的RecordDetailView,以及 metadata-admin 的PagePreview。react 页面不经过任何一个。useRecordContext()在 provider 外返回null,这是有意设计(objectui packages/react/src/context/RecordContext.tsx:190-198的注释写明:让 record:* 渲染器在 Studio 设计器里能静态渲染而不抛错)。于是在 react 页面上:
objectName<RecordDetails>ctx?.objectName || ''(record-details.tsx:53)<RecordHighlights>ctx?.objectName || ''(record-highlights.tsx:37)data/objectSchema也来自 ctx)<RecordPath><RecordRelatedList>schema.objectName✅ (record-related-list.tsx:64)ctx?.recordId❌parentLinkValue = null→ 拒绝 fetch最后一行有 objectui 自己的测试钉着:
packages/plugin-detail/src/__tests__/RelatedList.parentscope.test.tsx:58— "refuses to fetch when parentId is missing"。注意四个 block 里只有
<RecordRelatedList>从 schema 读objectName;另外三个连对象名都走 context。也就是说这一族内部本身就不一致。活体影响
examples/app-showcase/src/ui/pages/renewals-pipeline.page.ts的<RecordHighlights>和<RecordRelatedList>现在渲染出来是空的——在 #4353 之前和之后都是。#4353 修正的是元数据的正确性(objectName该写子对象),它没有、也不可能在 framework 侧让渲染器去读它。需要更正 #4353 的一句话
那个 PR 的正文写了「the parent record stays bound by
recordId」。那是契约这么说,渲染器并不读schema.recordId。#4353 里relationshipValueField被 ledger 为 skipped-with-reason,理由写的是「react 面绑父记录但不绑父对象」——实际情况更糟:父记录也没绑上。但 #4340 的
objectName判定本身是确认无误的,不受此影响:columns/sort确实解析到objectName命名的那个对象,所以 #4353 里的 lint 校验的是正确的对象。修的方向(以及它的真实成本)
直觉方案是:让 react-page 的 scope 对
record:*类型用 block 自己的objectName/recordIdprops 包一层RecordContextProvider。但RecordContextProvider(RecordContext.tsx:167)是个纯值 provider,自己不取数——它要data/objectSchema,RecordDetailView是自己 fetch 完再喂进去的。所以真正要加的是一个会取数的 wrapper,这不是接线,是新组件。分档看成本:
<RecordRelatedList>在默认relationshipValueField: 'id'下只需要recordId,不需要data—— 这一档最便宜,单独修就能让 showcase 那个页面活过来。<RecordDetails>/<RecordHighlights>/<RecordPath>需要data+objectSchema,必须真取数。另一条路是承认这四个 block 本就是 record-detail 语境的组件,把它们从 react-tier 契约里摘掉(
REACT_BLOCKS),而不是让 react 面去伪造一个 record context。ADR-0080「capability ≠ contract」支持这种取舍。两条路都是产品决策,不是纯实现问题。不论选哪条,当前状态不能留:契约 publish 了四个 block 的绑定 prop,渲染器一个都不读(related-list 只读一半),而
os validate现在还会认真校验这些 prop 指向的字段——lint 在为一个不会执行的绑定把关。验收
record:*的每个 block 要么在 react 页面上真的按自己的 props 绑定并渲染,要么从 react-tier 契约里移除;showcase 的 Renewals Pipeline 页面能真的渲染出 highlights 和 invoice 关联列表(或者不再声称能)。/cc 需要 objectui 侧改动(
objectstack-ai/objectui)。