fix(fields): 退役 capability-multiselect widget 名 —— 删注册面与最后一个 hint 消费方,组件保留 (#3308) - #3793
Conversation
…,组件保留
`field:capability-multiselect` 只由 `registerFields()` 注册,而该函数唯一调用方
是文档站,所以这个键从未存在于活路径(`registerAllFields()`,模块加载即执行,
遍历 `fieldWidgetMap` —— 表里没有它)。作者在字段上写
`widget: 'capability-multiselect'`,在文档站以外的任何应用里都解析不到组件,而
注册处上方的注释把它描述成可从记录表单使用的 widget:注释承诺了一个不存在的能力。
也没有任何生产方 stamp 这个 hint。ADR-0056 P1 经
`ObjectStackAdapter.getObjectSchema` 这一个 choke point,给
`sys_permission_set` 六个 facet(含 `system_permissions`)stamp 的是
`permission-facet-link`;P2 把能力编辑器放进了 Studio。该 widget 名是那次分期
落地中间态的残留。
按 ADR-0049 enforce-or-remove 删除,四处各留 tombstone:
- `fields` —— `field:capability-multiselect` 注册与那段注释。语义上是
breaking,但仅止于名字:该键在文档站以外不可达,没有应用能解析到它。仍带该
hint 的字段现在退化为其声明的 `type` 渲染器,即未注册 widget 的既定行为。
- `plugin-detail` —— `InlineFieldInput` 的 `widget === 'capability-multiselect'`
分支,该 hint 最后一个 honoring surface。给一个无人 emit、无表单能解析的名字
留一个特例,正是本 issue 要消灭的声明/实际背离,只是方向相反。相邻的
`permission-facet-link` 分支未动,并新增断言钉住。
- `components` —— 表单渲染器 `DATA_SOURCE_FIELD_TYPES` 里那条死条目,它永远
匹配不到一个可解析的 widget。
- `plugin-form` —— 一处把 `capability-multiselect` 说成 stamp 到
`sys_permission_set.system_permissions` 的注释,改为实际 stamp 的
`permission-facet-link`。
`CapabilityMultiSelectField` 组件本身不动、仍然导出:Studio 的
`PermissionMatrixEditor` 直接 import 渲染它,这就是 ADR-0056 P2 的设计。只退役
widget 名;组件的 doc 注释现在如实写明它不是注册表 field widget。
`registerFields()` 一并保留,但把 `@deprecated Use registerAllFields() instead`
这句改写为如实说明:两者不可互换 —— 它注册的是 `createFieldRenderer(widget)`,
额外合成 label、description 与本地 `value`/`onChange` 状态,正是这套 chrome 让
裸字段节点(`{ type: 'currency', label: 'Amount' }`)能在文档站 demo 里独立渲染。
实测:改走 `registerAllFields()` 后 `FieldWidgetComponentProps.onChange` 是必填
且 widget 无保护地调用它,demo 输入框每次击键抛
`TypeError: onChange is not a function`(CurrencyField.tsx:79)。退役它需要先决定
demo chrome 的归属,故留给 #3308 的后续裁决,注释就地记录,不再暗示是 drop-in
替换。
## 验证
- 引用面扫描(`origin/main` = 00b9451):`git grep -n "capability-multiselect"`
改后仅剩 tombstone/pin-test/changeset 与历史 CHANGELOG。
- `pnpm vitest run packages/fields`:65 files / 978 tests passed。
- `pnpm vitest run packages/plugin-detail packages/plugin-form`:88 / 785 passed。
- `pnpm vitest run packages/components`:95 / 698 passed。
- `pnpm vitest run packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor`:
7 / 25 passed —— Studio 侧组件消费方未受影响。
- 四包 `type-check` 与 `build` 全绿;`app-shell` `type-check` 全绿(它直接
import 该组件);四包 `lint` 0 errors。
- 反向自证:把删掉的两处肢体加回,两个 pin 测试按预期变红
(`field:capability-multiselect` 返回 FieldCarrierAdapter;inline 分支渲染出 2
个 chip button)。值得记录的是 fields pin 的第一条(只看活路径)在肢体加回后
仍然绿 —— 只钉活路径的测试在 `origin/main` 上就会通过、什么也钉不住,故 pin
显式覆盖文档站路径。变异不入 commit。
- `pnpm check:control-bytes` OK;改动文件另做
`grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'` 自扫,零控制字节。
- `node scripts/check-changeset-no-major.mjs` 通过(changeset 标 minor,按
AGENTS.md 版本号策略:fixed 组不声明 major,破坏性语义写在正文)。
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。 裁决落实方式的裁定:裁决两条各有一处前提被 dev 实测证伪 —— (1)「删组件」: git 实物核验( 衍生:objectstack#6773(liveness 账本失真用例)、objectui#3794(ADR-0056 Consequences 自相矛盾)两条 finding 立单边界正确,归分诊席。 Generated by Claude Code |
Fixes #3308
按 2026-08-06 维护者裁决(该死 —— 删除,ADR-0049 enforce-or-remove)落地。核验后裁决三条里第 1、3 条按裁决执行,第 2 条的前提被证伪,原因与证据见下,未强推。
裁决逐条落实
1. 删
capability-multiselect组件与承诺不存在能力的注释 —— 已删,但删的是 widget 名而非组件文件前提复核(
origin/main=00b9451d8)确认 issue 的注册表事实成立:field:capability-multiselect只由registerFields()注册,该函数唯一调用方是文档站,所以键从未存在于活路径(registerAllFields(),模块加载即执行,遍历fieldWidgetMap—— 表里没有它)。但引用面扫描发现 issue 未列的两个直接 import 消费方,组件本身并没有死:
packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx:72,788—— Studio 权限矩阵编辑器直接 import 并渲染它(不经注册表),而且在活跃维护中(就地注释引用 objectui#3332 的 chip wall 高度修复)。这正是 ADR-0056 P2 的设计:"能力编辑器本身位于 Studio"。packages/plugin-detail/src/InlineFieldInput.tsx:108—— 也直接 import,在widget === 'capability-multiselect'分支里渲染。所以字面执行"删组件"会打掉一个活的 Studio 编辑器。真正死的是 widget 名:注册面 + hint 分支。裁决意图(退役这个不可达的 field widget)完整保留,删除面为:
packages/fields/src/index.tsxfield:capability-multiselect注册 + 那段承诺注释 + 已无用的静态 importpackages/plugin-detail/src/InlineFieldInput.tsxwidget === 'capability-multiselect'分支(该 hint 最后一个 honoring surface)+ importpackages/components/src/renderers/form/form.tsx:234DATA_SOURCE_FIELD_TYPES里的死条目(永远匹配不到可解析 widget)packages/plugin-form/src/ObjectForm.tsx:585system_permissions—— 改为实际 stamp 的permission-facet-linkCapabilityMultiSelectField组件不动、仍从@object-ui/fields导出,doc 注释改为如实写明"它不是注册表 field widget"。没有任何生产方 stamp 过这个 hint —— 这条是删
InlineFieldInput分支的依据。packages/data-objectstack/src/index.ts:2486-2501(applyFieldWidgetOverrides,ADR-0056 P1 的单一 choke point)给sys_permission_set六个 facet(含system_permissions)stamp 的是permission-facet-link。全仓 + framework 仓零命中 stamp 处。留一个特例给无人 emit、无表单能解析的名字,正是本 issue 要消灭的声明/实际背离,只是方向相反。2. 收敛
registerFields()废弃路径、文档站改走registerAllFields()—— 前提证伪,未执行裁决(与 issue)把
registerFields()当成"废弃的重复注册路径"。它不是。它是文档站的 demo host adapter,以注册表覆写的形式实现:registerAllFields()注册withFieldCarrier(lazy(widget))—— 裸 widget,只渲染控件。label / description / value 状态归 host(表单渲染器、FieldEditWidget、action dialog)。registerFields()注册createFieldRenderer(widget)—— 额外合成 label、description 和本地useState+onChange。正是这套 chrome 让裸字段节点能被
SchemaRenderer独立渲染,而这就是文档站 demo 每一个content/docs/fields/*.mdx例子的方式:apps/site/app/components/InteractiveDemo.tsx只render一个SchemaRenderer,自己不传value/onChange;examples/schema-catalog里的例子形如{ "type": "currency", "name": "amount", "label": "Amount", "placeholder": "$0.00" }。实测(一次性 probe,未入 commit):按 demo 路径渲染
withFieldCarrier(CurrencyField),只给 schema 派生的 props 而不给value/onChange——FieldWidgetComponentProps.onChange是必填(widgets/types.ts:122)且 widget 无保护地调用它。方向值得如实记录:错误确实抛出,但发生在 React 事件系统内部,fireEvent不会同步 rethrow —— 浏览器里的表现是 demo 输入框失灵 + 每次击键一条未捕获错误,而非组件崩溃边界。同时 label chrome 消失(queryByText('Amount')为 null)。受影响面是content/docs/fields/下 26 个字段文档页里走裸键的约 24 个类别。所以"文档站改走
registerAllFields()"不是一次等价替换,它需要先决定 demo chrome 归谁。这一条留给维护者,本 PR 不猜。作为替代,本 PR 把引发误读的那句声明就地改对:@deprecated Use registerAllFields() instead改写为如实说明两者不可互换、为什么文档站不能直接切、以及退役门槛,并注明 apps 一律用registerAllFields()。选项与建议见文末。裁决里预设的 semver 冲突点因此没有发生:
registerFields导出保留,没有公开导出被删,check-changeset-no-major.mjs无需对抗。apps/site一行未改(ObjectUIProvider.tsx未动),emitted dts 里export declare function registerFields(): void;仍在,故文档站编译面无风险。3. 存量引用留 tombstone —— 已留
四处删除点各有一段 tombstone 注释,写明:键曾在哪里、为什么死、谁 stamp 的是什么、组件为何还活着、以及"不要重新加回来"。CHANGELOG 里的历史提及按退役纪律原样保留,未改。
引用面扫描
origin/main=00b9451d8。词界扫描防registerAllFields子串误命中。git grep -n "capability-multiselect" origin/main—— 11 命中:4 处历史 CHANGELOG(不动)、1 处注册 + 1 处注释(删)、1 处组件 doc 注释(改写)、plugin-detailhint 分支(删)、components死条目(删)、plugin-form注释(改)。git grep -nw "registerFields" origin/main—— 3 命中:定义 1 处 +apps/site/app/components/ObjectUIProvider.tsx:5,13。issue 所列的消费方完整。跨仓:
git -C /home/user/objectstack grep -nw "capability-multiselect" origin/main唯一命中是packages/spec/liveness/field.json:109的一条 liveness note,把该 widget 记作sys_permission_set在用 —— 那条记录已陈旧(实际 stamp 的是permission-facet-link)。属 framework 仓文档面、非本 PR 范围,已作为 observation-class finding 记入报告交 PM。改后
git grep --untracked -n "capability-multiselect"仅剩 tombstone / pin test / changeset / 历史 CHANGELOG。semver 与 changeset 依据
minor,四个包(
fields/plugin-detail/components/plugin-form)。依据 AGENTS.md「版本号策略」:objectui 的 major 跟随@objectstack节奏,fixed 组(39 包)任一major都会把全组推上去,因此自身破坏性变更也标minor,破坏语义写在正文 —— 这是仓内明写的约定,不是偷偷降档。scripts/check-changeset-no-major.mjs已跑,通过。破坏性实际范围:仅"名字层面"。
field:capability-multiselect在文档站以外不可达,没有应用能解析到它;仍带该 hint 的字段现在退化为其声明的type渲染器,即未注册 widget 的既定行为(framework liveness note 原文:"Degrades to thetyperenderer when the widget is unregistered")。没有公开导出被删。验证证据
引用面扫描见上。测试与门禁:
type-check(fields/plugin-detail/plugin-form/components)全Done;app-shelltype-check全绿(它是组件的直接消费方)。四包build全Done。四包lint0 errors(warnings 是仓库既有基线)。首轮
plugin-formtype-check 报 9 条 TS2307Cannot find module '@object-ui/fields'—— 新 worktree 未建依赖产物所致(AGENTS.md §9 陷阱),pnpm --filter '@object-ui/plugin-form^...' --filter '@object-ui/plugin-detail^...' build后全绿,非本改动引起。注册行为自证(
packages/fields/src/__tests__/capability-multiselect-retired.test.ts):import '@object-ui/fields'后ComponentRegistry.get('field:capability-multiselect')与裸键均为undefined;且registerFields()(文档站路径)跑完后仍为undefined;同时逐条断言该路径其余 38 个键全部仍注册 —— 退役恰好减一个名字,没有顺手削薄文档站路径。反向自证:把删掉的两处肢体加回,两个 pin 按预期变红 —— fields pin 第 77 行拿到
[Function FieldCarrierAdapter](期望undefined),InlineFieldInputpin 渲染出 2 个 chip button(期望 0)。值得单独记一笔:fields pin 的第一条(只看活路径)在肢体加回后仍然绿 —— 只钉活路径的测试在origin/main上就会通过、什么也钉不住,所以 pin 显式覆盖了文档站路径。变异未入 commit。字节纪律:
pnpm check:control-bytesOK(3718 tracked text files);改动文件另做一次grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'自扫,零控制字节。content/docs/releases/**未触。待裁决:
registerFields()的最终归属(裁决第 2 条)三个选项,按「本项目长期健康」与「让 AI 写的元数据难写错」两轴评估:
apps/site—— 从@object-ui/fields删掉registerFields+createFieldRenderer,文档站自己拥有一个 demo 包装器。库侧只剩一条路,文档不坏。代价:第二个包装器仍然存在(只是换了归属),文档 demo 仍渲染出真实应用不会产生的样子,"文档是错误权威"这个失效类没被消掉 —— 而这恰是本 issue 的病根。fields-*catalog 例子改成表单托管({ "type": "form", "fields": [...] }),两个包装器都删掉。文档从此展示真实应用的渲染结果(真表单渲染器拥有 label 与状态)。代价:约 60 个 catalog JSON + 26 个文档页要重验,PR 体量大,且改变了文档教什么,应当是维护者决定并单开一单。capability-multiselectwidget 在实际注册路径上根本不存在 —— 代码注释却说它可从记录表单用 #3308 发现的东西。建议 B,单开一单;不建议 A。 两轴同向:长期健康上,A 是 patch —— 它把"文档 demo 与真实渲染不一致"这个二元契约原样保留下来,只换了持有者;B 消灭它。对"让 AI 写的元数据难写错"这一轴,差别更大:模型读文档看到裸字段节点渲染成带 label 的输入框,就会生成同样的裸节点,而在真实应用里它渲染不出 label、也没有 host 提供
onChange—— 文档正在充当一份错误权威,与本 issue 抱怨的注释是同一类缺陷。B 让文档只能展示真实可行的写法。本 PR 已按 C 把误导性声明改对,作为 B 落地前的安全垫。Generated by Claude Code