Parcourir la source

refactor(plugins): retain bundle management and existing row toggles

Yichen Jiang il y a 1 semaine
Parent
commit
95e2da7416
43 fichiers modifiés avec 127 ajouts et 461 suppressions
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md
  4. 0 6
      .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.i18n.yaml
  5. 6 0
      .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.i18n.yaml
  6. 7 9
      .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.md
  7. 7 9
      .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.zh.md
  8. 1 1
      docs/config-catalog.i18n.yaml
  9. 1 1
      docs/config-catalog.md
  10. 1 1
      docs/event-producer-consumer.i18n.yaml
  11. 2 2
      docs/event-producer-consumer.md
  12. 2 2
      docs/subsystems/core.i18n.yaml
  13. 0 15
      docs/subsystems/core.md
  14. 0 15
      docs/subsystems/core.zh.md
  15. 2 2
      packages/boot/app-boot/README.i18n.yaml
  16. 1 1
      packages/boot/app-boot/README.md
  17. 1 1
      packages/boot/app-boot/README.zh.md
  18. 5 35
      packages/boot/app-boot/src/package-metadata.ts
  19. 13 13
      packages/boot/app-boot/tests/package-metadata.spec.ts
  20. 2 2
      packages/boot/plugin-manager/README.i18n.yaml
  21. 3 4
      packages/boot/plugin-manager/README.md
  22. 3 4
      packages/boot/plugin-manager/README.zh.md
  23. 1 2
      packages/boot/plugin-manager/package.json
  24. 0 77
      packages/boot/plugin-manager/src/manager.ts
  25. 0 48
      packages/boot/plugin-manager/src/modules.ts
  26. 4 31
      packages/boot/plugin-manager/src/types.ts
  27. 0 2
      packages/boot/plugin-manager/src/view.ts
  28. 34 77
      packages/boot/plugin-manager/tests/plugin-manager.spec.ts
  29. 0 3
      packages/boot/plugin-manager/tsconfig.json
  30. 2 21
      packages/extensions/tool-cordis/src/api-catalog.ts
  31. 2 2
      packages/host/plugin-manager/README.i18n.yaml
  32. 4 4
      packages/host/plugin-manager/README.md
  33. 4 4
      packages/host/plugin-manager/README.zh.md
  34. 1 27
      packages/host/plugin-manager/src/index.ts
  35. 1 3
      packages/host/plugin-manager/src/types.ts
  36. 5 10
      packages/host/plugin-manager/tests/plugin-manager.spec.ts
  37. 2 2
      packages/util/package-manifest/README.i18n.yaml
  38. 0 1
      packages/util/package-manifest/README.md
  39. 0 1
      packages/util/package-manifest/README.zh.md
  40. 0 1
      packages/util/package-manifest/src/index.ts
  41. 0 12
      packages/util/package-manifest/src/types.ts
  42. 4 3
      pnpm-lock.yaml
  43. 0 1
      scripts/gen-cordis-catalog.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
-2026-09-04-plugin-manager-over-the-profile-runtime.md: c68d7aedcd93ca4f179ab9df97e157ff5388b407
-2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: e01301a5f9e1f35e07471e86fb0df0e223c7f965
+2026-09-04-plugin-manager-over-the-profile-runtime.md: cfe1c63fc450761f4f740509f24e8ec64c804acd
+2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: ff83ea3dfa342b51cad19127bf62b9600eddf78e

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md

@@ -16,9 +16,9 @@ The CLI and Web need the same installation checks, while only a running applicat
 
 
 **pnpm retains its normal launch environment.** The installer uses `node:child_process` and Windows shell handling for the `.cmd` shim. Registry credentials and proxy configuration remain available. Output streams as `plugins/install-log` under a job id, with terminal colours when requested; spawn failures, timeouts and nonzero exits report `plugins/install-failed` with a bounded log tail. A failed add restores the saved manifest. Static declaration and bundle-conflict checks follow installation; unknown packages remain installed, and new bundles remain disabled unless the caller requests enablement. No discovery-cache directory is owned.
 **pnpm retains its normal launch environment.** The installer uses `node:child_process` and Windows shell handling for the `.cmd` shim. Registry credentials and proxy configuration remain available. Output streams as `plugins/install-log` under a job id, with terminal colours when requested; spawn failures, timeouts and nonzero exits report `plugins/install-failed` with a bounded log tail. A failed add restores the saved manifest. Static declaration and bundle-conflict checks follow installation; unknown packages remain installed, and new bundles remain disabled unless the caller requests enablement. No discovery-cache directory is owned.
 
 
-**Enablement selects a layer; diagnostics describe actual rows.** Bundle enablement, nontransactional recomposition and explicit `dsh.plugins` discovery follow [native entry diagnostics and static declarations](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md). The manager keeps per-row issues distinct from the enabled choice. `retry` removes the complete layer, awaits cleanup, and adds it again; unchanged row options alone do not restart a failed plugin. `list` derives each package view from the manifest, static declarations and current Loader state.
+**Enablement selects a layer; diagnostics describe actual rows.** Bundle enablement and nontransactional recomposition follow [native entry diagnostics](2026-09-11-native-entry-diagnostics.md). The manager keeps per-row issues distinct from the enabled choice. `retry` removes the complete layer, awaits cleanup, and adds it again; unchanged row options alone do not restart a failed plugin. `list` derives each package view from the manifest, static declarations and current Loader state.
 
 
-**Rows go through the patch-file writer.** `addRow` inserts the declared module and config into the profile user patch. `setRowDisabled` writes or removes `disabled: true`, restoring the author's original gate when the user withdraws a denial. Global changes recompose the live tree.
+**Rows go through the patch-file writer.** `setRowDisabled` writes or removes `disabled: true`, restoring the author's original gate when the user withdraws a denial. Global changes recompose the live tree.
 
 
 **Runtime notifications follow diagnostics.** Native entry and fiber events share one pending reader. It waits for Loader and profile recomposition, then compares row identity, module, fiber phase, failure stage and message. Only a changed diagnostic set emits `plugins/changed` with reason `runtime`; healthy status churn and repeated identical failures stay silent. Management operations retain their own completion notifications. Events arriving during a read request another pass, and disposing the adapter cancels publication.
 **Runtime notifications follow diagnostics.** Native entry and fiber events share one pending reader. It waits for Loader and profile recomposition, then compares row identity, module, fiber phase, failure stage and message. Only a changed diagnostic set emits `plugins/changed` with reason `runtime`; healthy status churn and repeated identical failures stay silent. Management operations retain their own completion notifications. Events arriving during a read request another pass, and disposing the adapter cancels publication.
 
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md

@@ -16,9 +16,9 @@ CLI 与 Web 需要相同的安装检查,而只有运行中的应用才能应
 
 
 **pnpm 保留正常的启动环境。** 安装器使用 `node:child_process`,并在 Windows 上通过 shell 处理 `.cmd` 垫片。Registry 凭据与代理配置保持可用。输出以 job id 下的 `plugins/install-log` 流式发送,按请求保留终端颜色;启动失败、超时与非零退出以 `plugins/install-failed` 和有界日志尾部报告。add 失败时恢复保存的清单。安装后进行静态声明和组合包冲突检查;未知包保留安装,新组合包保持禁用,除非调用方请求启用。没有需要维护的发现缓存目录。
 **pnpm 保留正常的启动环境。** 安装器使用 `node:child_process`,并在 Windows 上通过 shell 处理 `.cmd` 垫片。Registry 凭据与代理配置保持可用。输出以 job id 下的 `plugins/install-log` 流式发送,按请求保留终端颜色;启动失败、超时与非零退出以 `plugins/install-failed` 和有界日志尾部报告。add 失败时恢复保存的清单。安装后进行静态声明和组合包冲突检查;未知包保留安装,新组合包保持禁用,除非调用方请求启用。没有需要维护的发现缓存目录。
 
 
-**启用选择层,诊断描述实际行。** 组合包启用、非事务重组和显式 `dsh.plugins` 发现遵循[原生条目诊断与静态声明](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md)。管理器区分逐行问题与启用选择。`retry` 移除完整层,等待清理,再将其加入;仅保留不变的行选项不会重启失败插件。`list` 从清单、静态声明和当前 Loader 状态派生每个包的视图。
+**启用选择层,诊断描述实际行。** 组合包启用和非事务重组遵循[原生条目诊断](2026-09-11-native-entry-diagnostics.zh.md)。管理器区分逐行问题与启用选择。`retry` 移除完整层,等待清理,再将其加入;仅保留不变的行选项不会重启失败插件。`list` 从清单、静态声明和当前 Loader 状态派生每个包的视图。
 
 
-**行通过 patch-file 写入器修改。** `addRow` 将声明的模块与配置插入 profile 用户 patch。`setRowDisabled` 写入或移除 `disabled: true`,用户撤销禁用时恢复作者原来的条件。全局修改重组运行中的树。
+**行通过 patch-file 写入器修改。** `setRowDisabled` 写入或移除 `disabled: true`,用户撤销禁用时恢复作者原来的条件。全局修改重组运行中的树。
 
 
 **运行时通知跟随诊断。** 原生条目与 fiber 事件共用一次待完成的读取。读取等待 Loader 和 profile 重组完成,再比较行身份、模块、fiber 阶段、失败阶段与消息。只有诊断集合变化才发送原因是 `runtime` 的 `plugins/changed`;健康状态波动和重复的相同失败保持安静。管理操作仍保留自己的完成通知。读取期间到来的事件请求再次读取,适配器销毁时取消发布。
 **运行时通知跟随诊断。** 原生条目与 fiber 事件共用一次待完成的读取。读取等待 Loader 和 profile 重组完成,再比较行身份、模块、fiber 阶段、失败阶段与消息。只有诊断集合变化才发送原因是 `runtime` 的 `plugins/changed`;健康状态波动和重复的相同失败保持安静。管理操作仍保留自己的完成通知。读取期间到来的事件请求再次读取,适配器销毁时取消发布。
 
 

+ 0 - 6
.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md
-2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md: 0c83dc5e23515708d78747b1489847ac4f229705
-2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md: badb759753229def9e29c8b706b1dab03a798bb5

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.md
+2026-09-11-native-entry-diagnostics.md: 3f8e8eea3f9d436170579b7e77878e827fd76f55
+2026-09-11-native-entry-diagnostics.zh.md: f14430c3afab00ec1bb1f2301b494769ebbe0c3e

+ 7 - 9
.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md → .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.md

@@ -1,8 +1,8 @@
-# Agent Note: Native entry diagnostics and static plugin declarations
+# Agent Note: Native entry diagnostics
 
 
 Status: implemented
 Status: implemented
 
 
-English | [中文](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md)
+English | [中文](2026-09-11-native-entry-diagnostics.zh.md)
 
 
 ## Problem
 ## Problem
 
 
@@ -12,13 +12,13 @@ Third-party plugin failures must leave the application’s management endpoints
 
 
 **Startup strictness remains consumer-owned.** App-boot uses the [global required entry ids](2026-09-09-consumer-owned-startup-strictness.md) and the bootstrap Include identity. Package origin does not change the audit: all other rows may fail with a warning. Bundle metadata has no trust or stage policy. Row ownership only attributes diagnostics and management operations; nested Includes inherit their owning entry’s package without matching unrelated root ids.
 **Startup strictness remains consumer-owned.** App-boot uses the [global required entry ids](2026-09-09-consumer-owned-startup-strictness.md) and the bootstrap Include identity. Package origin does not change the audit: all other rows may fail with a warning. Bundle metadata has no trust or stage policy. Row ownership only attributes diagnostics and management operations; nested Includes inherit their owning entry’s package without matching unrelated root ids.
 
 
-**Bundle enablement selects a whole patch layer.** Native rows and author-written groups retain their explicit ids and parents. Anonymous bundle rows receive deterministic ids on detached execution copies. Disabling a bundle removes its inserts and overrides across every target group; persistent user row overrides survive re-enablement. Ownership is checked before loading: strict layers claim ids first, optional collisions omit the whole losing bundle, and conflicting user inserts are omitted per row. Prefixing explicit ids would break author expressions and user patches; relying on authors to avoid collisions permits silent Loader reparenting.
+**Bundle enablement selects a whole patch layer.** Native rows and author-written groups retain their explicit ids and parents. Anonymous bundle rows receive deterministic ids on detached execution copies. Disabling a bundle removes its inserts and overrides across every target group; persistent user row overrides survive re-enablement. Ownership is checked before loading: bundle layers claim ids in their listed order, collisions omit the whole losing bundle, and conflicting user inserts are omitted per row. Prefixing explicit ids would break author expressions and user patches; relying on authors to avoid collisions permits silent Loader reparenting.
 
 
 **Diagnostics describe attempts and running instances separately.** `Entry.lastFailure` passively retains import, activation or update errors where the Loader already catches or propagates them. It does not restore transactions or change native continuation. Asynchronous activation failures come from `fiber.await()`, pending dependencies from injection state, and disabled-expression failures from evaluating the effective gate. An active old fiber can coexist with an update failure. Removed entries need no separate failure registry. Nested continuation failures remain advisory; readiness does not promise every nested plugin callback succeeded.
 **Diagnostics describe attempts and running instances separately.** `Entry.lastFailure` passively retains import, activation or update errors where the Loader already catches or propagates them. It does not restore transactions or change native continuation. Asynchronous activation failures come from `fiber.await()`, pending dependencies from injection state, and disabled-expression failures from evaluating the effective gate. An active old fiber can coexist with an update failure. Removed entries need no separate failure registry. Nested continuation failures remain advisory; readiness does not promise every nested plugin callback succeeded.
 
 
 **Recomposition is serialized and nontransactional.** The launcher provides `ProfileRuntime` before startup auditing. Preparation rejects malformed files before mutation. Accepted updates settle current Loader work and await removed fibers before publishing composition facts and returning issues. Old fiber references are necessary because removed entries are absent from Loader task enumeration. Successful sibling changes remain applied after another entry fails. Fresh patch copies prevent a later override from mutating data reused by another composition. `ProfileRuntime` declares Loader as a required injection so both root and plugin-context handles can reconcile and inspect the tree.
 **Recomposition is serialized and nontransactional.** The launcher provides `ProfileRuntime` before startup auditing. Preparation rejects malformed files before mutation. Accepted updates settle current Loader work and await removed fibers before publishing composition facts and returning issues. Old fiber references are necessary because removed entries are absent from Loader task enumeration. Successful sibling changes remain applied after another entry fails. Fresh patch copies prevent a later override from mutating data reused by another composition. `ProfileRuntime` declares Loader as a required injection so both root and plugin-context handles can reconcile and inspect the tree.
 
 
-**Discovery reads declarations without executing modules.** `readPackageMetadata` reads package identity, bundle patches and explicit `dsh.plugins` entries. `.` names the main export; subpaths and default configs remain available for composition editors. Undeclared packages remain installed and unknown. A declaration does not certify importability, Config validity or activation. Physical Cordis peer resolution is advisory and cannot detect an inlined copy. Runtime validation belongs to each actual mounted row, including separate mounts of one module in different presets.
+**Metadata discovery does not execute installed code.** `readPackageMetadata` reads package identity and bundle patch rows. A bundle’s metadata describes its declared composition; packages without a bundle patch remain installed and unknown. Physical Cordis peer resolution is advisory and cannot detect an inlined copy. Config validation and execution diagnostics belong to actual mounted rows.
 
 
 **Unhandled process failures stay fatal.** `installFailLoud` remains until shutdown. Only duplicate rejections already observed by an entry audit are coalesced through its process checkpoint. Unrelated detached rejections retain master’s teardown-and-exit policy. No in-process group protects against `process.exit`, a blocked event loop, native crashes or OOM. Preset generation owners retain their own strict mount-and-cleanup behavior.
 **Unhandled process failures stay fatal.** `installFailLoud` remains until shutdown. Only duplicate rejections already observed by an entry audit are coalesced through its process checkpoint. Unrelated detached rejections retain master’s teardown-and-exit policy. No in-process group protects against `process.exit`, a blocked event loop, native crashes or OOM. Preset generation owners retain their own strict mount-and-cleanup behavior.
 
 
@@ -26,7 +26,7 @@ Third-party plugin failures must leave the application’s management endpoints
 
 
 ## Alternatives considered
 ## Alternatives considered
 
 
-**Keep contained groups and child probes.** Contained groups protected against transactional Loader rollback and supplied failure records for removed rows. Native entries retain both siblings and failed rows, so wrapping adds identities without supplying bundle-wide enablement that layer composition lacks. Child probes bounded discovery-time execution and discovered undeclared exports and schemas. Static declarations avoid that execution; automatic main-export detection and pre-mount schemas are given up. A future execution sandbox needs its own process ownership and teardown design, not a discovery probe presented as runtime isolation.
+**Wrap every bundle in a synthetic parent group.** Native entries retain both successful siblings and failed rows. A wrapper adds another entry identity, while whole-bundle enablement already follows the selected patch layers. Preserving author-written ids keeps their expressions and user patches valid.
 
 
 **Import inside the Host with try/catch.** Rejected because discovery could execute irreversible side effects, exit the Host or block its event loop. Catching exceptions cannot restore that isolation. Parsing source to infer plugin exports would add another JavaScript interpreter with incomplete results.
 **Import inside the Host with try/catch.** Rejected because discovery could execute irreversible side effects, exit the Host or block its event loop. Catching exceptions cannot restore that isolation. Parsing source to infer plugin exports would add another JavaScript interpreter with incomplete results.
 
 
@@ -36,10 +36,8 @@ Third-party plugin failures must leave the application’s management endpoints
 
 
 ## Consequences
 ## Consequences
 
 
-Installed metadata, enabled layer selection and current runtime health are independent. An enabled bundle may be partially running, pending or failed; updating a row may leave old behavior active with a visible diagnostic. Whole-bundle disable removes its patch effects without requiring a synthetic parent group. Startup can still fail when an optional provider leaves a required consumer pending. Old probe records are not activation evidence, and plugins need explicit `dsh.plugins` declarations for discovery as addable modules.
+Installed metadata, enabled layer selection and current runtime health are independent. An enabled bundle may be partially running, pending or failed; updating a row may leave old behavior active with a visible diagnostic. Whole-bundle disable removes its patch effects without requiring a synthetic parent group. Startup can still fail when an optional provider leaves a required consumer pending.
 
 
 ## Verification
 ## Verification
 
 
-`packages/boot/app-boot/tests/entry-issues.spec.ts` covers required-id policy, import/config/apply/disabled/pending failures, nested-entry and anonymous-row bundle ownership, active old config after failed updates, recovery, and awaited removed-fiber teardown. Composition tests cover duplicate ownership and unchanged parents. Static metadata tests use an import-time file-writing fixture and assert no execution, retain unknown packages, and reject malformed declarations. Inventory tests verify actual Loader failures and composition conflicts without a historical failure registry. Process-guard and user-patch tests retain fatal detached failures and best-effort live reload diagnostics.
-
-The [manager/Remote split](2026-09-04-plugin-manager-over-the-profile-runtime.md) remains in force. Manager integration tests cover retained unknown packages, static declarations, per-entry enable results, failed overrides with original ownership, and runtime-change notifications after dependency recovery.
+`packages/boot/app-boot/tests/entry-issues.spec.ts` covers required-id policy, import/config/apply/disabled/pending failures, nested-entry and anonymous-row bundle ownership, active old config after failed updates, recovery, and awaited removed-fiber teardown. Composition tests cover duplicate ownership and unchanged parents. Static metadata tests use an import-time file-writing fixture and assert no execution, retain unknown packages, and ignore unrelated manifest fields. Inventory tests verify actual Loader failures and composition conflicts without a historical failure registry. Process-guard and user-patch tests retain fatal detached failures and best-effort live reload diagnostics.

+ 7 - 9
.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md → .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.zh.md

@@ -1,8 +1,8 @@
-# Agent Note: 原生条目诊断与静态插件声明
+# Agent Note: 原生条目诊断
 
 
 Status: implemented
 Status: implemented
 
 
-[English](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md) | 中文
+[English](2026-09-11-native-entry-diagnostics.md) | 中文
 
 
 ## Problem
 ## Problem
 
 
@@ -12,13 +12,13 @@ Status: implemented
 
 
 **启动严格程度仍由消费者负责。** App-boot 使用[全局必需条目 id](2026-09-09-consumer-owned-startup-strictness.zh.md) 和启动 Include 的身份。包来源不改变审查规则:其他行失败只产生警告。组合包元数据没有 trust 或 stage 策略。行归属只用于诊断和管理操作;嵌套 Include 继承所属条目的包,不能匹配无关的根级 id。
 **启动严格程度仍由消费者负责。** App-boot 使用[全局必需条目 id](2026-09-09-consumer-owned-startup-strictness.zh.md) 和启动 Include 的身份。包来源不改变审查规则:其他行失败只产生警告。组合包元数据没有 trust 或 stage 策略。行归属只用于诊断和管理操作;嵌套 Include 继承所属条目的包,不能匹配无关的根级 id。
 
 
-**组合包启停选择整份 patch 层。** 原生行和作者声明的组保留显式 id 与父组。匿名组合包行在独立执行副本中获得确定性的 id。禁用组合包会从所有目标组中移除其插入与覆盖;持久化的用户行覆盖在重新启用后保留。加载前检查所有权:严格层优先占有 id,可选层冲突时整包排除,用户插入冲突时逐行排除。为显式 id 加前缀会破坏作者表达式与用户 patch;依赖作者自行避免冲突则允许 Loader 静默改变父组。
+**组合包启停选择整份 patch 层。** 原生行和作者声明的组保留显式 id 与父组。匿名组合包行在独立执行副本中获得确定性的 id。禁用组合包会从所有目标组中移除其插入与覆盖;持久化的用户行覆盖在重新启用后保留。加载前检查所有权:组合包层按声明顺序占有 id,冲突时整包排除,用户插入冲突时逐行排除。为显式 id 加前缀会破坏作者表达式与用户 patch;依赖作者自行避免冲突则允许 Loader 静默改变父组。
 
 
 **诊断分别描述操作尝试与运行实例。** `Entry.lastFailure` 在 Loader 已有的捕获或传播位置被动保留导入、激活或更新错误。它不恢复事务,也不改变原生继续加载行为。异步激活失败取自 `fiber.await()`,等待依赖取自注入状态,disabled 表达式失败取自有效条件的求值。活跃的旧 fiber 可以与更新失败同时存在。移除的条目无需独立失败注册表。嵌套延续任务失败仍为提示信息;就绪不保证每个嵌套插件回调均成功。
 **诊断分别描述操作尝试与运行实例。** `Entry.lastFailure` 在 Loader 已有的捕获或传播位置被动保留导入、激活或更新错误。它不恢复事务,也不改变原生继续加载行为。异步激活失败取自 `fiber.await()`,等待依赖取自注入状态,disabled 表达式失败取自有效条件的求值。活跃的旧 fiber 可以与更新失败同时存在。移除的条目无需独立失败注册表。嵌套延续任务失败仍为提示信息;就绪不保证每个嵌套插件回调均成功。
 
 
 **重组串行执行且不具备事务性。** launcher 在启动审计前提供 `ProfileRuntime`。准备阶段在修改运行树前拒绝无效文件。已接受的更新等待当前 Loader 工作与已移除 fiber 完成后,发布组合信息并返回问题。必须保留旧 fiber 引用,因为已移除的条目不在 Loader 任务枚举中。某行失败后,其他行已成功的变化保留。新的 patch 副本避免后续覆盖修改其他重组会复用的数据。 `ProfileRuntime` 将 Loader 声明为必需注入,使根上下文与插件上下文取得的句柄都能重组并检查配置树。
 **重组串行执行且不具备事务性。** launcher 在启动审计前提供 `ProfileRuntime`。准备阶段在修改运行树前拒绝无效文件。已接受的更新等待当前 Loader 工作与已移除 fiber 完成后,发布组合信息并返回问题。必须保留旧 fiber 引用,因为已移除的条目不在 Loader 任务枚举中。某行失败后,其他行已成功的变化保留。新的 patch 副本避免后续覆盖修改其他重组会复用的数据。 `ProfileRuntime` 将 Loader 声明为必需注入,使根上下文与插件上下文取得的句柄都能重组并检查配置树。
 
 
-**发现阶段只读声明,不执行模块。** `readPackageMetadata` 读取包身份、组合包 patch 和显式 `dsh.plugins` 条目。`.` 表示主入口;子路径与默认配置继续供组合编辑器使用。未声明的包保持已安装和未知状态。声明不证明可导入、Config 有效或激活成功。Cordis 物理 peer 解析仅供提示,无法识别内联副本。运行校验属于每个实际挂载行,包括同一模块在不同预设中的独立挂载。
+**发现元数据时不执行安装包代码。** `readPackageMetadata` 读取包身份和组合包 patch 行。组合包元数据描述其声明的组合;没有组合包 patch 的包保持已安装和未知状态。Cordis 物理 peer 解析仅供提示,无法识别内联副本。Config 校验和执行诊断属于实际挂载的行。
 
 
 **未处理的进程级失败仍然致命。** `installFailLoud` 保持到应用关闭。只有条目审计已观测到的重复 rejection 会在其进程检查点内合并。无关的脱离管理 rejection 保留 master 的清理退出策略。进程内的组无法防护 `process.exit`、事件循环阻塞、原生崩溃或 OOM。预设代际的拥有者保留独立的严格挂载与清理行为。
 **未处理的进程级失败仍然致命。** `installFailLoud` 保持到应用关闭。只有条目审计已观测到的重复 rejection 会在其进程检查点内合并。无关的脱离管理 rejection 保留 master 的清理退出策略。进程内的组无法防护 `process.exit`、事件循环阻塞、原生崩溃或 OOM。预设代际的拥有者保留独立的严格挂载与清理行为。
 
 
@@ -26,7 +26,7 @@ Status: implemented
 
 
 ## Alternatives considered
 ## Alternatives considered
 
 
-**保留 contained group 和子进程 probe。** contained group 防止事务式 Loader 回滚,并为已移除行提供失败记录。原生条目会同时保留成功行与失败行,包装只增加身份,并未提供整层组合所缺少的整包启停能力。子进程 probe 为发现阶段的执行设定边界,并发现未声明导出与 schema。静态声明避免这次执行,同时放弃自动主入口识别和挂载前 schema。未来的执行沙箱需要独立设计进程归属与清理,不能把发现 probe 视为运行隔离。
+**为每个组合包包装一个合成父组。** 原生条目同时保留成功行与失败行。包装会增加条目身份,而整包启停已经由选中的 patch 层决定。保留作者声明的 id,才能继续使用其表达式和用户 patch。
 
 
 **在 Host 内 import 并 try/catch。** 拒绝,因为发现阶段可能产生不可逆副作用、退出 Host 或阻塞事件循环。捕获异常无法恢复隔离。通过解析源码推断插件导出会增加一个结果不完整的 JavaScript 解释器。
 **在 Host 内 import 并 try/catch。** 拒绝,因为发现阶段可能产生不可逆副作用、退出 Host 或阻塞事件循环。捕获异常无法恢复隔离。通过解析源码推断插件导出会增加一个结果不完整的 JavaScript 解释器。
 
 
@@ -36,10 +36,8 @@ Status: implemented
 
 
 ## Consequences
 ## Consequences
 
 
-已安装元信息、启用层选择与当前运行健康相互独立。启用的组合包可能部分运行、等待依赖或失败;行更新可能保留旧行为并显示诊断。整包禁用移除其 patch 效果,无需合成父组。可选提供方失败导致必需消费方等待时,启动仍可能失败。旧 probe 记录不是激活证据,插件需要显式 `dsh.plugins` 声明才能被发现为可添加模块。
+已安装元信息、启用层选择与当前运行健康相互独立。启用的组合包可能部分运行、等待依赖或失败;行更新可能保留旧行为并显示诊断。整包禁用移除其 patch 效果,无需合成父组。可选提供方失败导致必需消费方等待时,启动仍可能失败。
 
 
 ## Verification
 ## Verification
 
 
-`packages/boot/app-boot/tests/entry-issues.spec.ts` 覆盖必需 id 策略、导入/配置/apply/disabled/pending 失败、嵌套条目与匿名行的组合包归属、失败更新后的活跃旧配置、恢复及已移除 fiber 的清理等待。组合测试覆盖重复归属与父组保持。静态元信息测试使用导入时写文件的 fixture,确认未执行、保留未知包并拒绝无效声明。inventory 测试直接验证 Loader 失败与组合冲突,不依赖历史失败注册表。进程处理与用户 patch 测试保留脱离管理失败的致命策略,以及尽力执行的实时重载诊断。
-
-[管理器与 Remote 的拆分](2026-09-04-plugin-manager-over-the-profile-runtime.zh.md)继续适用。管理器集成测试覆盖未知包保留、静态声明、逐行启用结果、保留原归属的失败覆盖,以及依赖恢复后的运行变化通知。
+`packages/boot/app-boot/tests/entry-issues.spec.ts` 覆盖必需 id 策略、导入/配置/apply/disabled/pending 失败、嵌套条目与匿名行的组合包归属、失败更新后的活跃旧配置、恢复及已移除 fiber 的清理等待。组合测试覆盖重复归属与父组保持。静态元信息测试使用导入时写文件的 fixture,确认未执行、保留未知包并忽略无关的 manifest 字段。inventory 测试直接验证 Loader 失败与组合冲突,不依赖历史失败注册表。进程处理与用户 patch 测试保留脱离管理失败的致命策略,以及尽力执行的实时重载诊断。

+ 1 - 1
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 7b4bab0c7d9db6854e41eaf6543b36dd321790db
+config-catalog.md: 8fc5193e93a233b4cf23e5b9ca241f5a6eb9dff8
 config-catalog.zh.md: 6beb0e9d3f58efd63f5c2684adf9f66dd3c8150f
 config-catalog.zh.md: 6beb0e9d3f58efd63f5c2684adf9f66dd3c8150f

+ 1 - 1
docs/config-catalog.md

@@ -942,7 +942,7 @@ export interface Config {
 }
 }
 ```
 ```
 
 
-Source: [`packages/host/plugin-manager/src/index.ts:40`](../packages/host/plugin-manager/src/index.ts)
+Source: [`packages/host/plugin-manager/src/index.ts:39`](../packages/host/plugin-manager/src/index.ts)
 
 
 <a id="deepseek-aidsh-host-webserver"></a>
 <a id="deepseek-aidsh-host-webserver"></a>
 
 

+ 1 - 1
docs/event-producer-consumer.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
 #   pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
-event-producer-consumer.md: f30ae258881eee0cf3b220aaf760e17ac1a900dd
+event-producer-consumer.md: abfc811e02271eace2ee19965c76b6ad7caeef07
 event-producer-consumer.zh.md: 02645d1b5ba78a23f75a3d4c7e81fea2c2809f3a
 event-producer-consumer.zh.md: 02645d1b5ba78a23f75a3d4c7e81fea2c2809f3a

+ 2 - 2
docs/event-producer-consumer.md

@@ -48,8 +48,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:72`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:72`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:44`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
 | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:44`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
-| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:233`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:239`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:206`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:212`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |

+ 2 - 2
docs/subsystems/core.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/core.md
 #   pnpm run verify-translation-pairing --write docs/subsystems/core.md
-core.md: 9ffdfdac6bc4262a541eed214386d8649ff041c1
-core.zh.md: 45298191ecaf3d03894638e3167712bf6fe39a2a
+core.md: fcc2d3300e1e9fe7014f5c7f8a566b3b0a80f47a
+core.zh.md: e1879ce32a4f9b2ed301aa808cb12759e43e09a5

+ 0 - 15
docs/subsystems/core.md

@@ -948,21 +948,6 @@ The row injects only the Loader; the profile runtime and the agent registry are
  */
  */
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 
 
-/**
- * Add a row naming one of the package's modules to the profile user layer.
- * @param packageName - the installed package.
- * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
- * `id` overrides the derived row id, `config` overrides the declared default.
- * @returns where the row landed.
- */
-@Remote('addRow') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
-
-/**
- * Remove a row a user layer inserted.
- * @param rowId - the inserted row's id.
- */
-@Remote('removeRow') async removeRow(rowId: string): Promise<void>
-
 /**
 /**
  * Switch one row off or on in a user layer; deny-only.
  * Switch one row off or on in a user layer; deny-only.
  * @param rowId - the row's id as the composition declares it.
  * @param rowId - the row's id as the composition declares it.

+ 0 - 15
docs/subsystems/core.zh.md

@@ -958,21 +958,6 @@ The row injects only the Loader; the profile runtime and the agent registry are
  */
  */
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 
 
-/**
- * Add a row naming one of the package's modules to the profile user layer.
- * @param packageName - the installed package.
- * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
- * `id` overrides the derived row id, `config` overrides the declared default.
- * @returns where the row landed.
- */
-@Remote('addRow') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
-
-/**
- * Remove a row a user layer inserted.
- * @param rowId - the inserted row's id.
- */
-@Remote('removeRow') async removeRow(rowId: string): Promise<void>
-
 /**
 /**
  * Switch one row off or on in a user layer; deny-only.
  * Switch one row off or on in a user layer; deny-only.
  * @param rowId - the row's id as the composition declares it.
  * @param rowId - the row's id as the composition declares it.

+ 2 - 2
packages/boot/app-boot/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
 #   pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
-README.md: e070f187379638dff75e1bcfc3bfe835195d08c0
-README.zh.md: 9d56700ca22743c7fc484bba113b718751f935e0
+README.md: b34d38cc928e01c320b502560ea82fa7817fa5c7
+README.zh.md: 4ba7b4d34f3aa80e505ac61798ffcf10a078dcc2

+ 1 - 1
packages/boot/app-boot/README.md

@@ -201,7 +201,7 @@ These limits describe when this boot library is a poor fit or needs special care
 - **Overrides retain the target’s owner** — changing a built-in row can make that required row fail; successful updates to other rows remain applied. Disabling the bundle removes its whole patch layer, including overrides.
 - **Overrides retain the target’s owner** — changing a built-in row can make that required row fail; successful updates to other rows remain applied. Disabling the bundle removes its whole patch layer, including overrides.
 - **A conflict is decided by order, not merit** — among external bundles the earlier layer in `dsh.profile.bundles` keeps a contested id, so uninstalling that bundle lets the later one mount on the next boot; the plugin list shows which bundle lost and to whom.
 - **A conflict is decided by order, not merit** — among external bundles the earlier layer in `dsh.profile.bundles` keeps a contested id, so uninstalling that bundle lets the later one mount on the next boot; the plugin list shows which bundle lost and to whom.
 - **The nested-fiber audit is advisory** — a failed `ctx.inject()` continuation under a built-in entry is reported, not fatal, until shipped compositions are known clean.
 - **The nested-fiber audit is advisory** — a failed `ctx.inject()` continuation under a built-in entry is reported, not fatal, until shipped compositions are known clean.
-- **Declarations are not activation results** — `readPackageMetadata` reads `dsh.plugins` without importing modules. Declare `plugins: [{ name: "." }]` for a main export, or subpaths such as `./tools`; undeclared packages remain `unknown`. Config validation and execution diagnostics appear only after an actual mount. Physical Cordis resolution cannot detect an inlined copy.
+- **Metadata is not an activation result** — `readPackageMetadata` reads package identity and bundle patch rows without importing modules. Packages without a bundle patch remain `unknown`. Config validation and execution diagnostics appear only after an actual mount. Physical Cordis resolution cannot detect an inlined copy.
 
 
 <a id="dev-note"></a>
 <a id="dev-note"></a>
 ### Dev Note
 ### Dev Note

+ 1 - 1
packages/boot/app-boot/README.zh.md

@@ -201,7 +201,7 @@ Loader 结算后,app-boot 将 optional 失败报告为警告;若已启用的
 - **覆盖保留目标的所有者**——修改内置行可能使该必需行失败;其他行成功的更新仍然生效。禁用组合包移除其整份 patch 层,包括覆盖。
 - **覆盖保留目标的所有者**——修改内置行可能使该必需行失败;其他行成功的更新仍然生效。禁用组合包移除其整份 patch 层,包括覆盖。
 - **冲突按顺序判定,不看是非**——外部组合包之间,`dsh.profile.bundles` 里靠前的那层保住争议 id,卸掉它之后靠后的那层在下次启动时挂上;插件列表显示谁输给了谁。
 - **冲突按顺序判定,不看是非**——外部组合包之间,`dsh.profile.bundles` 里靠前的那层保住争议 id,卸掉它之后靠后的那层在下次启动时挂上;插件列表显示谁输给了谁。
 - **嵌套 fiber 审计只是提示**——内置条目下失败的 `ctx.inject()` 延续会被报告而非致命,直到确认随附组合都没有这类失败。
 - **嵌套 fiber 审计只是提示**——内置条目下失败的 `ctx.inject()` 延续会被报告而非致命,直到确认随附组合都没有这类失败。
-- **声明不代表激活结果**——`readPackageMetadata` 读取 `dsh.plugins`,不导入模块。主入口声明为 `plugins: [{ name: "." }]`,子路径可写为 `./tools`;未声明的包保持 `unknown`。Config 校验和执行诊断只在实际挂载后产生。Cordis 物理路径解析无法识别被内联打包的副本。
+- **元数据不代表激活结果**——`readPackageMetadata` 读取包身份和组合包 patch 行,不导入模块。没有组合包 patch 的包保持 `unknown`。Config 校验和执行诊断只在实际挂载后产生。Cordis 物理路径解析无法识别被内联打包的副本。
 
 
 <a id="dev-note"></a>
 <a id="dev-note"></a>
 ### 开发备注
 ### 开发备注

+ 5 - 35
packages/boot/app-boot/src/package-metadata.ts

@@ -6,7 +6,6 @@ import { fileURLToPath } from 'node:url'
 import { loadOverlayPatches } from './index.ts'
 import { loadOverlayPatches } from './index.ts'
 import { readProfileManifest, resolveBundleDir } from './profile.ts'
 import { readProfileManifest, resolveBundleDir } from './profile.ts'
 import { visitPatchRows } from './patch-rows.ts'
 import { visitPatchRows } from './patch-rows.ts'
-import type { DshPluginDeclaration } from '@deepseek-ai/dsh-package-manifest'
 
 
 /** One configured bundle row; conditions remain unevaluated until it mounts. */
 /** One configured bundle row; conditions remain unevaluated until it mounts. */
 export interface PackageMetadataRow {
 export interface PackageMetadataRow {
@@ -21,13 +20,12 @@ export interface PackageMetadata {
   readonly version?: string
   readonly version?: string
   readonly description?: string
   readonly description?: string
   readonly title?: string
   readonly title?: string
-  readonly kind: 'bundle' | 'plugin' | 'unknown'
+  readonly kind: 'bundle' | 'unknown'
   readonly enginesDsh?: string
   readonly enginesDsh?: string
   /** Physical resolution only; null when unavailable, not an execution guarantee. */
   /** Physical resolution only; null when unavailable, not an execution guarantee. */
   readonly cordisSameCopy: boolean | null
   readonly cordisSameCopy: boolean | null
   readonly rows: readonly PackageMetadataRow[]
   readonly rows: readonly PackageMetadataRow[]
   readonly overrides: readonly string[]
   readonly overrides: readonly string[]
-  readonly addable: readonly DshPluginDeclaration[]
 }
 }
 
 
 /** Locations needed to read one installed package. */
 /** Locations needed to read one installed package. */
@@ -39,39 +37,15 @@ export interface PackageMetadataOptions {
 }
 }
 
 
 /**
 /**
- * Read package identity, bundle rows and explicitly addable modules without loading code.
+ * Read package identity and bundle rows without loading code.
  * @param options - installation and profile resolution anchors.
  * @param options - installation and profile resolution anchors.
- * @returns declared metadata; an undeclared main export remains unknown.
+ * @returns declared metadata; packages without a bundle patch remain unknown.
  * @throws when the package or its declaration cannot be read or parsed.
  * @throws when the package or its declaration cannot be read or parsed.
  */
  */
 export function readPackageMetadata(options: PackageMetadataOptions): PackageMetadata {
 export function readPackageMetadata(options: PackageMetadataOptions): PackageMetadata {
   const { binName, packageName, installAnchor, profileDir } = options
   const { binName, packageName, installAnchor, profileDir } = options
   const dir = resolveBundleDir(binName, packageName, installAnchor, profileDir)
   const dir = resolveBundleDir(binName, packageName, installAnchor, profileDir)
   const manifest = readProfileManifest(binName, dir)
   const manifest = readProfileManifest(binName, dir)
-  const declarations: unknown = manifest.dsh?.plugins
-  const addable: DshPluginDeclaration[] = []
-  if (declarations !== undefined) {
-    if (!Array.isArray(declarations)) throw new TypeError(`${packageName}: dsh.plugins must be an array`)
-    const names = new Set<string>()
-    for (const value of declarations as unknown[]) {
-      if (!isRecord(value) || typeof value.name !== 'string' || value.name.length === 0
-        || (value.title !== undefined && typeof value.title !== 'string')) {
-        throw new TypeError(`${packageName}: dsh.plugins entries require a name and an optional string title`)
-      }
-      const name = value.name
-      const subpath = name.replace(/^\.\//, '')
-      if (name !== '.' && (subpath.startsWith('/') || subpath.split('/').some(part => part === '..' || part === '.' || part === '')
-        || /[:\\]/.test(subpath))) throw new TypeError(`${packageName}: invalid plugin subpath ${JSON.stringify(name)}`)
-      const key = name === '.' ? '.' : subpath
-      if (names.has(key)) throw new TypeError(`${packageName}: repeated plugin subpath ${JSON.stringify(name)}`)
-      names.add(key)
-      addable.push({
-        name,
-        ...value.title === undefined ? {} : { title: value.title },
-        ...value.config === undefined ? {} : { config: value.config },
-      })
-    }
-  }
   const rows: PackageMetadataRow[] = []
   const rows: PackageMetadataRow[] = []
   const own = new Set<string>()
   const own = new Set<string>()
   const declaredBundle = manifest.dsh?.bundle?.patch
   const declaredBundle = manifest.dsh?.bundle?.patch
@@ -88,15 +62,11 @@ export function readPackageMetadata(options: PackageMetadataOptions): PackageMet
     ...manifest.description === undefined ? {} : { description: manifest.description },
     ...manifest.description === undefined ? {} : { description: manifest.description },
     ...manifest.dsh?.title === undefined ? {} : { title: manifest.dsh.title },
     ...manifest.dsh?.title === undefined ? {} : { title: manifest.dsh.title },
     ...manifest.engines?.dsh === undefined ? {} : { enginesDsh: manifest.engines.dsh },
     ...manifest.engines?.dsh === undefined ? {} : { enginesDsh: manifest.engines.dsh },
-    kind: declaredBundle !== undefined ? 'bundle' : addable.length > 0 ? 'plugin' : 'unknown',
-    cordisSameCopy, rows, overrides, addable,
+    kind: declaredBundle !== undefined ? 'bundle' : 'unknown',
+    cordisSameCopy, rows, overrides,
   }
   }
 }
 }
 
 
-function isRecord(value: unknown): value is Record<string, unknown> {
-  return typeof value === 'object' && value !== null && !Array.isArray(value)
-}
-
 /** Resolve physical package identity through a packaged-runtime module proxy. */
 /** Resolve physical package identity through a packaged-runtime module proxy. */
 function cordisPackageDir(resolved: string): string | undefined {
 function cordisPackageDir(resolved: string): string | undefined {
   let dir = dirname(resolved.startsWith('file:') ? fileURLToPath(resolved) : resolved)
   let dir = dirname(resolved.startsWith('file:') ? fileURLToPath(resolved) : resolved)

+ 13 - 13
packages/boot/app-boot/tests/package-metadata.spec.ts

@@ -63,20 +63,18 @@ describe('readPackageMetadata', () => {
     expect(metadata.cordisSameCopy).toBe(kind === 'foreign' ? false : kind === 'proxy' ? true : null)
     expect(metadata.cordisSameCopy).toBe(kind === 'foreign' ? false : kind === 'proxy' ? true : null)
   })
   })
 
 
-  it('reads explicit main and subpath entries without importing them or reading a probe cache', () => {
+  it('reads package identity without executing the main export', () => {
     const staged = stage({ pkg: {
     const staged = stage({ pkg: {
       main: "import { writeFileSync } from 'node:fs'; writeFileSync(new URL('./executed', import.meta.url), 'bad'); throw new Error('must never execute')",
       main: "import { writeFileSync } from 'node:fs'; writeFileSync(new URL('./executed', import.meta.url), 'bad'); throw new Error('must never execute')",
-      manifest: { dsh: { title: 'Package', plugins: [{ name: '.', title: 'Main', config: { a: 1 } }, { name: './tools', title: 'Tools' }] } },
+      manifest: { dsh: { title: 'Package' } },
     } })
     } })
     const options = { ...staged, binName: NAME, packageName: 'pkg' }
     const options = { ...staged, binName: NAME, packageName: 'pkg' }
     const result = readPackageMetadata(options)
     const result = readPackageMetadata(options)
-    expect(result).toMatchObject({ kind: 'plugin', title: 'Package', version: '1.0.0',
-      addable: [{ name: '.', title: 'Main', config: { a: 1 } }, { name: './tools', title: 'Tools' }],
-    })
+    expect(result).toMatchObject({ kind: 'unknown', title: 'Package', version: '1.0.0' })
     const dir = join(staged.profileDir, 'node_modules/pkg')
     const dir = join(staged.profileDir, 'node_modules/pkg')
     expect(existsSync(join(dir, 'executed'))).toBe(false)
     expect(existsSync(join(dir, 'executed'))).toBe(false)
     writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'pkg', version: '2.0.0' }))
     writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'pkg', version: '2.0.0' }))
-    expect(readPackageMetadata(options)).toMatchObject({ kind: 'unknown', version: '2.0.0', addable: [] })
+    expect(readPackageMetadata(options)).toMatchObject({ kind: 'unknown', version: '2.0.0' })
   })
   })
   it('lists bundle rows, nested groups, unevaluated gates and external overrides', () => {
   it('lists bundle rows, nested groups, unevaluated gates and external overrides', () => {
     const staged = stage({ pkg: {
     const staged = stage({ pkg: {
@@ -98,7 +96,7 @@ describe('readPackageMetadata', () => {
     expect(readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' })).toMatchObject({
     expect(readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' })).toMatchObject({
       kind: 'bundle', description: 'Bundle', enginesDsh: '>=0.1.0',
       kind: 'bundle', description: 'Bundle', enginesDsh: '>=0.1.0',
       rows: [{ id: 'group', name: 'cordis:group', gated: false }, { id: 'tool', name: 'pkg/tool', gated: true }],
       rows: [{ id: 'group', name: 'cordis:group', gated: false }, { id: 'tool', name: 'pkg/tool', gated: true }],
-      overrides: ['webserver'], addable: [],
+      overrides: ['webserver'],
     })
     })
   })
   })
   it.each(['export default () => {}', 'export function apply() {}', 'throw new Error("broken")'])('keeps an undeclared package unknown: %s', (main) => {
   it.each(['export default () => {}', 'export function apply() {}', 'throw new Error("broken")'])('keeps an undeclared package unknown: %s', (main) => {
@@ -106,11 +104,13 @@ describe('readPackageMetadata', () => {
     expect(readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' }).kind).toBe('unknown')
     expect(readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' }).kind).toBe('unknown')
     expect(existsSync(join(staged.profileDir, 'node_modules/pkg/package.json'))).toBe(true)
     expect(existsSync(join(staged.profileDir, 'node_modules/pkg/package.json'))).toBe(true)
   })
   })
-  it.each([
-    'invalid', [{}], [{ name: '' }], [{ name: '../outside' }], [{ name: '/absolute' }],
-    [{ name: './tools' }, { name: 'tools' }], [{ name: '.', title: 1 }],
-  ])('rejects malformed or ambiguous plugin declarations: %j', (plugins) => {
-    const staged = stage({ pkg: { manifest: { dsh: { plugins } } } })
-    expect(() => readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' })).toThrow()
+  it('ignores unrelated metadata while reading a bundle patch', () => {
+    const staged = stage({ pkg: {
+      manifest: { dsh: { bundle: { patch: './patch.yml' }, plugins: 'unrecognized metadata' } },
+      files: { 'patch.yml': '- insert:\n    - id: tool\n      name: pkg/tool\n' },
+    } })
+    expect(readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' })).toMatchObject({
+      kind: 'bundle', rows: [{ id: 'tool', name: 'pkg/tool', gated: false }],
+    })
   })
   })
 })
 })

+ 2 - 2
packages/boot/plugin-manager/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/plugin-manager/README.md
 #   pnpm run verify-translation-pairing --write packages/boot/plugin-manager/README.md
-README.md: 538f837dbaaec0b9b89d0ba2a70a44d5f67cf32f
-README.zh.md: 803dbd572b735f95f828260290fd7d39dd4e6fbe
+README.md: af12af8fb8f7839c9a2ec34275b8fbf65a21126e
+README.zh.md: a6569cfb5af5fc08f1d2595c48e3eb57f49e20af

+ 3 - 4
packages/boot/plugin-manager/README.md

@@ -70,17 +70,17 @@ const manager = new PluginManager(ctx, {
 console.log(await manager.list())
 console.log(await manager.list())
 ```
 ```
 
 
-`list` reports package identity, `bundle` / `plugin` / `unknown` classification, installation and enablement. Runtime rows carry actual phases and failures; disabled bundles show static patch declarations. `addable` comes only from `dsh.plugins`, including `.` for the main export and declared defaults. An active row can retain its previous config after an update fails. Package `issues` also names failed rows its patch overrides, without transferring their ownership.
+`list` reports package identity, `bundle` / `unknown` classification, installation and enablement. Runtime rows carry actual phases and failures; disabled bundles show static patch declarations. An active row can retain its previous config after an update fails. Package `issues` also names failed rows its patch overrides, without transferring their ownership.
 
 
 `enable` selects the whole bundle layer and recomposes live profiles. Per-row failures retain the enabled choice and successful siblings; results report `issues`, and the list can show `partial` or `failed`. Preparation failures revert the enable selection and raise `plugins/enable-failed`. `disable` removes the whole layer, including overrides. `retry` disables, awaits cleanup, and enables again. Startup-only profiles report `effect: restart`. Unapplied selections on live profiles report `failed`, preserving the rows actually running; `restart-required` is reserved for startup-only profiles. `uninstall` disables the bundle, removes user-inserted references, and runs pnpm remove.
 `enable` selects the whole bundle layer and recomposes live profiles. Per-row failures retain the enabled choice and successful siblings; results report `issues`, and the list can show `partial` or `failed`. Preparation failures revert the enable selection and raise `plugins/enable-failed`. `disable` removes the whole layer, including overrides. `retry` disables, awaits cleanup, and enables again. Startup-only profiles report `effect: restart`. Unapplied selections on live profiles report `failed`, preserving the rows actually running; `restart-required` is reserved for startup-only profiles. `uninstall` disables the bundle, removes user-inserted references, and runs pnpm remove.
 
 
-`addRow` writes an explicitly declared `dsh.plugins` module to the profile’s global `cordis.patch.yml`. It preserves declared defaults, checks the target row id, and performs no pre-mount import. `removeRow` removes a user insert. `setRowDisabled` writes or removes `disabled: true`, preserving the bundle’s own condition. Global edits recompose immediately on live profiles. `dependents` reports injection dependents and user-layer module references.
+`setRowDisabled` writes or removes `disabled: true`, preserving the bundle’s own condition. Global edits recompose immediately on live profiles. `dependents` reports injection dependents and user-layer module references.
 
 
 The manager runs one mutation at a time — a second call while one runs fails with `plugins/busy` naming the operation in flight — and `add` and `uninstall` refuse to change `node_modules` while a session is running, with `plugins/agents-running`. Every change is followed by a `plugins/changed` event on the context, and an install run emits pnpm's output as `plugins/install-log` chunks, each naming the command line it ran and the profile directory it ran in, and, with colour on, carrying pnpm's SGR escapes.
 The manager runs one mutation at a time — a second call while one runs fails with `plugins/busy` naming the operation in flight — and `add` and `uninstall` refuse to change `node_modules` while a session is running, with `plugins/agents-running`. Every change is followed by a `plugins/changed` event on the context, and an install run emits pnpm's output as `plugins/install-log` chunks, each naming the command line it ran and the profile directory it ran in, and, with colour on, carrying pnpm's SGR escapes.
 
 
 ### Failures
 ### Failures
 
 
-Every refusal or failure is a `PluginOperationError` with a stable `code` and `details` typed by it: `plugins/unavailable` (no profile runtime), `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/row-conflict`, `plugins/busy`, `plugins/agents-running`, and `plugins/bad-request` for a request that names nothing the profile has. `pluginOperationFailureOf` narrows a caught value to the code-discriminated union.
+Every refusal or failure is a `PluginOperationError` with a stable `code` and `details` typed by it: `plugins/unavailable` (no profile runtime), `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/busy`, `plugins/agents-running`, and `plugins/bad-request` for a request that names nothing the profile has. `pluginOperationFailureOf` narrows a caught value to the code-discriminated union.
 
 
 -----
 -----
 
 
@@ -114,7 +114,6 @@ The manager reads Loader entries, the reflect store and entry-owned diagnostics.
 | [`src/installer.ts`](src/installer.ts) | `PluginInstaller`: pnpm runs with streamed output, static declarations, and the post-install checks |
 | [`src/installer.ts`](src/installer.ts) | `PluginInstaller`: pnpm runs with streamed output, static declarations, and the post-install checks |
 | [`src/manager.ts`](src/manager.ts) | `PluginManager`: every operation over the booted profile, the one-at-a-time mutex, user-layer edits, and dependents |
 | [`src/manager.ts`](src/manager.ts) | `PluginManager`: every operation over the booted profile, the one-at-a-time mutex, user-layer edits, and dependents |
 | [`src/view.ts`](src/view.ts) | The view fold: manifest, declarations, and live-tree rows into one `PluginPackageView`, and the row-ownership walk |
 | [`src/view.ts`](src/view.ts) | The view fold: manifest, declarations, and live-tree rows into one `PluginPackageView`, and the row-ownership walk |
-| [`src/modules.ts`](src/modules.ts) | Declared `dsh.plugins` modules: row naming, derived row ids, and their wire view |
 | [`src/helpers.ts`](src/helpers.ts) | Shared vocabulary: the diagnostic prefix, tooling bounds, the spawn seam, and the manifest readers |
 | [`src/helpers.ts`](src/helpers.ts) | Shared vocabulary: the diagnostic prefix, tooling bounds, the spawn seam, and the manifest readers |
 | [`src/types.ts`](src/types.ts) | Payloads, the `plugins/changed` and `plugins/install-log` events, and the `plugins/*` failure codes with their details |
 | [`src/types.ts`](src/types.ts) | Payloads, the `plugins/changed` and `plugins/install-log` events, and the `plugins/*` failure codes with their details |
 | [`src/errors.ts`](src/errors.ts) | `PluginOperationError` and the code-discriminated failure union |
 | [`src/errors.ts`](src/errors.ts) | `PluginOperationError` and the code-discriminated failure union |

+ 3 - 4
packages/boot/plugin-manager/README.zh.md

@@ -70,17 +70,17 @@ const manager = new PluginManager(ctx, {
 console.log(await manager.list())
 console.log(await manager.list())
 ```
 ```
 
 
-`list` 报告包身份、`bundle` / `plugin` / `unknown` 分类、安装与启用状态。运行行携带实际阶段与失败;禁用组合包显示静态 patch 声明。`addable` 只来自 `dsh.plugins`,包括表示主入口的 `.` 和声明的默认配置。行更新失败后,活跃实例可能保留先前配置。包的 `issues` 还报告其 patch 覆盖的失败行,但不转移这些行的归属。
+`list` 报告包身份、`bundle` / `unknown` 分类、安装与启用状态。运行行携带实际阶段与失败;禁用组合包显示静态 patch 声明。行更新失败后,活跃实例可能保留先前配置。包的 `issues` 还报告其 patch 覆盖的失败行,但不转移这些行的归属。
 
 
 `enable` 选择整份组合包层,并在实时 profile 中重组。逐行失败保留启用选择与成功的其他行;结果报告 `issues`,列表可显示 `partial` 或 `failed`。准备失败会撤销启用选择,并抛出 `plugins/enable-failed`。`disable` 移除整层,包括覆盖。`retry` 先禁用并等待清理,再启用。仅启动时生效的 profile 报告 `effect: restart`。实时 profile 中尚未应用的选择报告 `failed`,并保留实际运行的行;`restart-required` 只用于仅启动时生效的 profile。`uninstall` 禁用组合包、删除用户插入的引用、执行 pnpm remove。
 `enable` 选择整份组合包层,并在实时 profile 中重组。逐行失败保留启用选择与成功的其他行;结果报告 `issues`,列表可显示 `partial` 或 `failed`。准备失败会撤销启用选择,并抛出 `plugins/enable-failed`。`disable` 移除整层,包括覆盖。`retry` 先禁用并等待清理,再启用。仅启动时生效的 profile 报告 `effect: restart`。实时 profile 中尚未应用的选择报告 `failed`,并保留实际运行的行;`restart-required` 只用于仅启动时生效的 profile。`uninstall` 禁用组合包、删除用户插入的引用、执行 pnpm remove。
 
 
-`addRow` 将显式声明的 `dsh.plugins` 模块写入 profile 的全局 `cordis.patch.yml`。它保留声明的默认配置,检查目标行 id,不在挂载前 import。`removeRow` 删除用户插入。`setRowDisabled` 写入或删除 `disabled: true`,保留组合包自己的条件。全局编辑在实时 profile 中立即重组。`dependents` 报告注入依赖方与用户层模块引用。
+`setRowDisabled` 写入或删除 `disabled: true`,保留组合包自己的条件。全局编辑在实时 profile 中立即重组。`dependents` 报告注入依赖方与用户层模块引用。
 
 
 管理器一次只跑一个变更——上一个还在跑时再调用会以 `plugins/busy` 失败并点名正在进行的操作——`add` 与 `uninstall` 在有会话运行时拒绝改动 `node_modules`,报 `plugins/agents-running`。每次变更之后在上下文上发出 `plugins/changed` 事件,安装运行把 pnpm 的输出以 `plugins/install-log` 分块发出,每块都写明所跑的命令行与所在的 profile 目录,开了颜色时还带着 pnpm 的 SGR 转义。
 管理器一次只跑一个变更——上一个还在跑时再调用会以 `plugins/busy` 失败并点名正在进行的操作——`add` 与 `uninstall` 在有会话运行时拒绝改动 `node_modules`,报 `plugins/agents-running`。每次变更之后在上下文上发出 `plugins/changed` 事件,安装运行把 pnpm 的输出以 `plugins/install-log` 分块发出,每块都写明所跑的命令行与所在的 profile 目录,开了颜色时还带着 pnpm 的 SGR 转义。
 
 
 ### 失败
 ### 失败
 
 
-每次拒绝或失败都是一个 `PluginOperationError`,带稳定的 `code` 与按码定型的 `details`:`plugins/unavailable`(没有 profile runtime)、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/row-conflict`、`plugins/busy`、`plugins/agents-running`,以及请求点名了 profile 没有的东西时的 `plugins/bad-request`。`pluginOperationFailureOf` 把捕获到的值收窄为按码区分的联合。
+每次拒绝或失败都是一个 `PluginOperationError`,带稳定的 `code` 与按码定型的 `details`:`plugins/unavailable`(没有 profile runtime)、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/busy`、`plugins/agents-running`,以及请求点名了 profile 没有的东西时的 `plugins/bad-request`。`pluginOperationFailureOf` 把捕获到的值收窄为按码区分的联合。
 
 
 -----
 -----
 
 
@@ -114,7 +114,6 @@ subprocess seam 会清洗形似密钥的变量且没有 shell 模式,而 pnpm
 | [`src/installer.ts`](src/installer.ts) | `PluginInstaller`:pnpm 流式输出、静态声明与装后检查 |
 | [`src/installer.ts`](src/installer.ts) | `PluginInstaller`:pnpm 流式输出、静态声明与装后检查 |
 | [`src/manager.ts`](src/manager.ts) | `PluginManager`:已启动 profile 上的每项操作、一次一个的互斥、用户层编辑与依赖查询 |
 | [`src/manager.ts`](src/manager.ts) | `PluginManager`:已启动 profile 上的每项操作、一次一个的互斥、用户层编辑与依赖查询 |
 | [`src/view.ts`](src/view.ts) | 将 manifest、声明与实际行状态组合为 `PluginPackageView`,并查询行归属 |
 | [`src/view.ts`](src/view.ts) | 将 manifest、声明与实际行状态组合为 `PluginPackageView`,并查询行归属 |
-| [`src/modules.ts`](src/modules.ts) | 声明的 `dsh.plugins` 模块:行命名、派生行 id 及其 wire 视图 |
 | [`src/helpers.ts`](src/helpers.ts) | 共享词汇:诊断前缀、工具边界、spawn 测试缝与 manifest 读取器 |
 | [`src/helpers.ts`](src/helpers.ts) | 共享词汇:诊断前缀、工具边界、spawn 测试缝与 manifest 读取器 |
 | [`src/types.ts`](src/types.ts) | 载荷、`plugins/changed` 与 `plugins/install-log` 事件,以及 `plugins/*` 失败码及其 details |
 | [`src/types.ts`](src/types.ts) | 载荷、`plugins/changed` 与 `plugins/install-log` 事件,以及 `plugins/*` 失败码及其 details |
 | [`src/errors.ts`](src/errors.ts) | `PluginOperationError` 与按码区分的失败联合 |
 | [`src/errors.ts`](src/errors.ts) | `PluginOperationError` 与按码区分的失败联合 |

+ 1 - 2
packages/boot/plugin-manager/package.json

@@ -43,7 +43,6 @@
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
-    "@deepseek-ai/dsh-package-manifest": "workspace:^",
-    "@deepseek-ai/dsh-util-values": "workspace:^"
+    "@deepseek-ai/dsh-package-manifest": "workspace:^"
   }
   }
 }
 }

+ 0 - 77
packages/boot/plugin-manager/src/manager.ts

@@ -13,25 +13,21 @@ import {
   healProfilesModuleFallback,
   healProfilesModuleFallback,
   loadProfile,
   loadProfile,
   readProfileManifest,
   readProfileManifest,
-  rootIncludeEntry,
   inspectEntryIssues,
   inspectEntryIssues,
   type EntryIssue,
   type EntryIssue,
   type readPackageMetadata,
   type readPackageMetadata,
   type ProfileRuntime,
   type ProfileRuntime,
 } from '@deepseek-ai/dsh-app-boot'
 } from '@deepseek-ai/dsh-app-boot'
 import { mutatePatchFile, readPatchListFile, type PatchRow } from '@deepseek-ai/dsh-app-boot/patch-file'
 import { mutatePatchFile, readPatchListFile, type PatchRow } from '@deepseek-ai/dsh-app-boot/patch-file'
-import type { JsonValue } from '@deepseek-ai/dsh-util-values'
 import { PluginOperationError } from './errors.ts'
 import { PluginOperationError } from './errors.ts'
 import { bundlesOf, dependenciesOf, messageOf, NAME, optional, type PluginToolingConfig, type SpawnLike } from './helpers.ts'
 import { bundlesOf, dependenciesOf, messageOf, NAME, optional, type PluginToolingConfig, type SpawnLike } from './helpers.ts'
 import { PluginInstaller } from './installer.ts'
 import { PluginInstaller } from './installer.ts'
-import { derivedRowId, moduleSpecifier } from './modules.ts'
 import type {
 import type {
   PluginChangeReason,
   PluginChangeReason,
   PluginDependents,
   PluginDependents,
   PluginEnableResult,
   PluginEnableResult,
   PluginInstallResult,
   PluginInstallResult,
   PluginPackageView,
   PluginPackageView,
-  PluginRowAddition,
   PluginRowIssue,
   PluginRowIssue,
   PluginRowReference,
   PluginRowReference,
   PluginServiceDependent,
   PluginServiceDependent,
@@ -328,72 +324,6 @@ export class PluginManager {
     return result
     return result
   }
   }
 
 
-  /**
-   * Add a row naming one of the package's modules to the profile's `cordis.patch.yml`.
-   * @param packageName - the installed package.
-   * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
-   * `id` overrides the derived row id, `config` overrides the declared default.
-   * @returns where the row landed.
-   * @throws {PluginOperationError} `plugins/not-installed`, `plugins/not-enableable`
-   * when the module is absent from `dsh.plugins`, or `plugins/row-conflict`.
-   */
-  async addRow(
-    packageName: string,
-    options?: { module?: string; id?: string; config?: JsonValue },
-  ): Promise<PluginRowAddition> {
-    return this.exclusive('addRow', packageName, () => this.addRowNow(packageName, options))
-  }
-
-  private async addRowNow(
-    packageName: string,
-    options?: { module?: string; id?: string; config?: JsonValue },
-  ): Promise<PluginRowAddition> {
-    const runtime = this.runtime()
-    const installer = this.installer(runtime)
-    installer.assertInstalled(packageName)
-    const metadata = installer.metadata(packageName)
-    const declared = options?.module ?? '.'
-    const addable = metadata.addable.find(entry => entry.name === declared)
-    if (addable === undefined) {
-      const reason = `${declared} is not a module ${packageName} declares addable`
-      throw new PluginOperationError('plugins/not-enableable', `${NAME}: ${reason}`, { packageName, reason })
-    }
-    const moduleName = moduleSpecifier(packageName, declared)
-    const rowId = options?.id ?? derivedRowId(packageName, declared)
-    if (rowId.trim() === '' || rowId.includes(':')) {
-      throw new PluginOperationError('plugins/bad-request', `${NAME}: row ids must be nonempty and cannot contain ':'`, {})
-    }
-    const config = options?.config !== undefined ? options.config : addable.config !== undefined ? addable.config : {}
-    const row: PatchRow = { id: rowId, name: moduleName, config }
-    if (await this.rowExists(runtime, rowId)) {
-      throw new PluginOperationError('plugins/row-conflict', `${NAME}: a row ${JSON.stringify(rowId)} already exists`, { rowId })
-    }
-    const file = await this.editLayer(runtime, (document) => { document.appendInsert(row) })
-    this.changed('row', packageName)
-    return { rowId, file }
-  }
-
-  /**
-   * Remove a row a user layer inserted.
-   * @param rowId - the inserted row's id.
-   * @throws {PluginOperationError} `plugins/bad-request` when the layer inserts no such row.
-   */
-  async removeRow(rowId: string): Promise<void> {
-    return this.exclusive('removeRow', rowId, () => this.removeRowNow(rowId))
-  }
-
-  private async removeRowNow(rowId: string): Promise<void> {
-    const runtime = this.runtime()
-    // A holder rather than a `let`: the assignment happens inside the edit
-    // callback, which control-flow narrowing does not see.
-    const outcome = { removed: false }
-    await this.editLayer(runtime, (document) => { outcome.removed = document.removeInsert(rowId) })
-    if (!outcome.removed) {
-      throw new PluginOperationError('plugins/bad-request', `${NAME}: the layer inserts no row ${JSON.stringify(rowId)}`, {})
-    }
-    this.changed('row')
-  }
-
   /**
   /**
    * Switch one row off or on in a user layer. Deny-only: `true` writes
    * Switch one row off or on in a user layer. Deny-only: `true` writes
    * `disabled: true` for the row, `false` removes that key, so a bundle's
    * `disabled: true` for the row, `false` removes that key, so a bundle's
@@ -470,13 +400,6 @@ export class PluginManager {
     return found
     return found
   }
   }
 
 
-  /** Whether the profile's composition already carries a row with `rowId`. */
-  private async rowExists(runtime: ProfileRuntime, rowId: string): Promise<boolean> {
-    const tree = rootIncludeEntry(this.ctx.root)?.subtree
-    return [...this.ctx.loader.entries()].some(entry => entry.parent.tree === tree && entry.options.id === rowId)
-      || (await readPatchListFile(NAME, runtime.patchPath, 'patches') ?? []).some(patch => patch.insert?.some(row => row.id === rowId))
-  }
-
   /** Edit one user layer file and, for the live global layer, recompose. */
   /** Edit one user layer file and, for the live global layer, recompose. */
   private async editLayer(
   private async editLayer(
     runtime: ProfileRuntime,
     runtime: ProfileRuntime,

+ 0 - 48
packages/boot/plugin-manager/src/modules.ts

@@ -1,48 +0,0 @@
-/**
- * The modules a package offers a composition: how a declared `dsh.plugins`
- * entry is named as a row and derived into a row id, and its wire view.
- * @module @deepseek-ai/dsh-plugin-manager/modules
- */
-
-import type { PackageMetadata } from '@deepseek-ai/dsh-app-boot'
-import type { JsonValue } from '@deepseek-ai/dsh-util-values'
-import { optional } from './helpers.ts'
-import type { PluginPackageAddableView } from './types.ts'
-
-/**
- * The row `name` for one declared addable module.
- * @param packageName - the package.
- * @param declared - the `dsh.plugins[].name`, `.` for the main export.
- * @returns the bare package for `.`, else `<package>/<subpath>`.
- */
-export function moduleSpecifier(packageName: string, declared: string): string {
-  if (declared === '.') return packageName
-  return `${packageName}/${declared.replace(/^\.\//, '')}`
-}
-
-/**
- * The row id derived from a package name and module: the unscoped name, then the subpath.
- * @param packageName - the package.
- * @param declared - the `dsh.plugins[].name`, `.` for the main export.
- * @returns the row id.
- */
-export function derivedRowId(packageName: string, declared: string): string {
-  const base = packageName.replace(/^@/, '')
-  return declared === '.' ? base : `${base}/${declared.replace(/^\.\//, '')}`
-}
-
-/**
- * The wire view of one declared addable module.
- * @param packageName - the package.
- * @param entry - the installed manifest’s module declaration.
- * @returns the view.
- */
-export function addableView(packageName: string, entry: PackageMetadata['addable'][number]): PluginPackageAddableView {
-  return {
-    moduleName: moduleSpecifier(packageName, entry.name),
-    declaredName: entry.name,
-    ...optional('title', entry.title),
-    // Config comes from the installed JSON manifest.
-    ...optional('config', entry.config as JsonValue | undefined),
-  }
-}

+ 4 - 31
packages/boot/plugin-manager/src/types.ts

@@ -5,10 +5,8 @@
  * @module @deepseek-ai/dsh-plugin-manager/types
  * @module @deepseek-ai/dsh-plugin-manager/types
  */
  */
 
 
-import type { JsonValue } from '@deepseek-ai/dsh-util-values'
-
-/** What an installed package is: a bundle layer, declared plugin modules, or an unknown package. */
-export type PluginPackageKind = 'bundle' | 'plugin' | 'unknown'
+/** What an installed package is: a bundle layer or an unknown package. */
+export type PluginPackageKind = 'bundle' | 'unknown'
 
 
 /**
 /**
  * The state one package is in, folded from the manifest, declarations, and current entries:
  * The state one package is in, folded from the manifest, declarations, and current entries:
@@ -19,7 +17,7 @@ export type PluginPackageKind = 'bundle' | 'plugin' | 'unknown'
  * - `disabled`: installed and not in the layer list;
  * - `disabled`: installed and not in the layer list;
  * - `not-enableable`: bundle declarations cannot be read;
  * - `not-enableable`: bundle declarations cannot be read;
  * - `restart-required`: its manifest state and the live tree disagree, which a profile without live reload resolves at the next start;
  * - `restart-required`: its manifest state and the live tree disagree, which a profile without live reload resolves at the next start;
- * - `plain`: a bundle-less package; explicitly declared modules can be added to a composition.
+ * - `plain`: a package without a bundle patch.
  */
  */
 export type PluginPackageStatus =
 export type PluginPackageStatus =
   | 'running'
   | 'running'
@@ -51,19 +49,6 @@ export interface PluginPackageRowView {
   readonly failure?: { readonly stage: string; readonly message: string }
   readonly failure?: { readonly stage: string; readonly message: string }
 }
 }
 
 
-/** One module a package declares addable to the profile. */
-export interface PluginPackageAddableView {
-  /** The module as it is named in a row: the bare package for `.`, else `<package>/<subpath>`. */
-  readonly moduleName: string
-  /** The `dsh.plugins[].name` the package declared. */
-  readonly declaredName: string
-  /** Display title, when declared. */
-  readonly title?: string
-  /** Default row config, when declared. */
-  readonly config?: JsonValue
-
-}
-
 /** One package as the manager sees it. */
 /** One package as the manager sees it. */
 export interface PluginPackageView {
 export interface PluginPackageView {
   /** The installed package name. */
   /** The installed package name. */
@@ -92,8 +77,6 @@ export interface PluginPackageView {
   readonly issues?: readonly PluginRowIssue[]
   readonly issues?: readonly PluginRowIssue[]
   /** Ids of built-in rows the bundle's patch overrides. */
   /** Ids of built-in rows the bundle's patch overrides. */
   readonly overrides: readonly string[]
   readonly overrides: readonly string[]
-  /** Modules the package declares addable. */
-  readonly addable: readonly PluginPackageAddableView[]
   /** Whether the profile applies user patch files while running; false means changes wait for a restart. */
   /** Whether the profile applies user patch files while running; false means changes wait for a restart. */
   readonly liveReload: boolean
   readonly liveReload: boolean
 }
 }
@@ -162,14 +145,6 @@ export interface PluginDependents {
   readonly references: readonly PluginRowReference[]
   readonly references: readonly PluginRowReference[]
 }
 }
 
 
-/** Where a row was added. */
-export interface PluginRowAddition {
-  /** The id the row was given. */
-  readonly rowId: string
-  /** The user layer file the row was written to. */
-  readonly file: string
-}
-
 /** Why the manager changed something, for a listener deciding what to refresh. */
 /** Why the manager changed something, for a listener deciding what to refresh. */
 export type PluginChangeReason = 'install' | 'uninstall' | 'enable' | 'disable' | 'retry' | 'row' | 'runtime'
 export type PluginChangeReason = 'install' | 'uninstall' | 'enable' | 'disable' | 'retry' | 'row' | 'runtime'
 
 
@@ -200,14 +175,12 @@ export interface PluginOperationDetailsMap {
   'plugins/unavailable': { readonly reason: string }
   'plugins/unavailable': { readonly reason: string }
   /** The package is not a profile dependency. */
   /** The package is not a profile dependency. */
   'plugins/not-installed': { readonly packageName: string }
   'plugins/not-installed': { readonly packageName: string }
-  /** The package cannot be enabled or added, with the declaration or request error. */
+  /** The package cannot be enabled, with the declaration or request error. */
   'plugins/not-enableable': { readonly packageName: string; readonly reason: string }
   'plugins/not-enableable': { readonly packageName: string; readonly reason: string }
   /** Preparation or the root Include rejected enablement; the layer selection was reverted. */
   /** Preparation or the root Include rejected enablement; the layer selection was reverted. */
   'plugins/enable-failed': { readonly packageName: string; readonly reason: string }
   'plugins/enable-failed': { readonly packageName: string; readonly reason: string }
   /** pnpm exited non-zero, could not be spawned, or timed out. */
   /** pnpm exited non-zero, could not be spawned, or timed out. */
   'plugins/install-failed': { readonly spec: string; readonly exitCode: number | null; readonly log: string }
   'plugins/install-failed': { readonly spec: string; readonly exitCode: number | null; readonly log: string }
-  /** The row id is already taken in the target user layer. */
-  'plugins/row-conflict': { readonly rowId: string }
   /** Another mutation is still running; the manager runs one at a time and refuses rather than queues. */
   /** Another mutation is still running; the manager runs one at a time and refuses rather than queues. */
   'plugins/busy': {
   'plugins/busy': {
     readonly operation: string
     readonly operation: string

+ 0 - 2
packages/boot/plugin-manager/src/view.ts

@@ -10,7 +10,6 @@ import type { Entry } from '@deepseek-ai/cordis-plugin-loader'
 import { rootIncludeEntry, type EntryIssue, type PackageMetadata, type ProfileManifest, type ProfileRuntime } from '@deepseek-ai/dsh-app-boot'
 import { rootIncludeEntry, type EntryIssue, type PackageMetadata, type ProfileManifest, type ProfileRuntime } from '@deepseek-ai/dsh-app-boot'
 import { bundlesOf, dependenciesOf, messageOf, optional } from './helpers.ts'
 import { bundlesOf, dependenciesOf, messageOf, optional } from './helpers.ts'
 import type { PluginInstaller } from './installer.ts'
 import type { PluginInstaller } from './installer.ts'
-import { addableView } from './modules.ts'
 import type { PluginPackageRowView, PluginPackageStatus, PluginPackageView, PluginRowPhase } from './types.ts'
 import type { PluginPackageRowView, PluginPackageStatus, PluginPackageView, PluginRowPhase } from './types.ts'
 
 
 /** Runtime mirror: FiberState is a cross-package const enum. */
 /** Runtime mirror: FiberState is a cross-package const enum. */
@@ -104,7 +103,6 @@ export function packageView(
     cordisSameCopy: metadata?.cordisSameCopy ?? null,
     cordisSameCopy: metadata?.cordisSameCopy ?? null,
     rows,
     rows,
     overrides: metadata?.overrides ?? [],
     overrides: metadata?.overrides ?? [],
-    addable: (metadata?.addable ?? []).map(entry => addableView(name, entry)),
     ...affected.length === 0 ? {} : { issues: affected.map(issue => ({
     ...affected.length === 0 ? {} : { issues: affected.map(issue => ({
       entryId: issue.entry.id, moduleName: issue.entry.options.name, stage: issue.stage, message: issue.message,
       entryId: issue.entry.id, moduleName: issue.entry.options.name, stage: issue.stage, message: issue.message,
     })) },
     })) },

+ 34 - 77
packages/boot/plugin-manager/tests/plugin-manager.spec.ts

@@ -99,7 +99,6 @@ interface StagedPackage {
   /** `index.js` text, exported as the package main. */
   /** `index.js` text, exported as the package main. */
   main?: string
   main?: string
   version?: string
   version?: string
-  plugins?: { name: string; title?: string; config?: unknown }[]
   files?: Record<string, string>
   files?: Record<string, string>
 }
 }
 
 
@@ -116,7 +115,6 @@ function stagePackage(profileDir: string, name: string, staged: StagedPackage):
     dsh: {
     dsh: {
       title: `Title of ${name}`,
       title: `Title of ${name}`,
       ...staged.patch === undefined ? {} : { bundle: { patch: './cordis.patch.yml' } },
       ...staged.patch === undefined ? {} : { bundle: { patch: './cordis.patch.yml' } },
-      ...staged.plugins === undefined ? {} : { plugins: staged.plugins },
     },
     },
   }, null, 2))
   }, null, 2))
   if (staged.patch !== undefined) writeFileSync(join(dir, 'cordis.patch.yml'), staged.patch)
   if (staged.patch !== undefined) writeFileSync(join(dir, 'cordis.patch.yml'), staged.patch)
@@ -330,27 +328,16 @@ describe('PluginManager', () => {
     expect(ctx.loader.resolve('include:hello').fiber?.state).toBe(2)
     expect(ctx.loader.resolve('include:hello').fiber?.state).toBe(2)
   })
   })
 
 
-  it('does not execute a declared main module during install or list and refuses an undeclared entry', async () => {
+  it('installs and lists a non-bundle package without executing its main module', async () => {
     const staged = await stageHome()
     const staged = await stageHome()
     const marker = join(staged.profileDir, 'executed')
     const marker = join(staged.profileDir, 'executed')
-    stagePackage(staged.profileDir, 'declared', {
-      plugins: [{ name: '.', config: { custom: 1 } }],
+    stagePackage(staged.profileDir, 'plain', {
       main: `import { writeFileSync } from 'node:fs'; writeFileSync(${JSON.stringify(marker)}, 'bad'); export function apply() {}`,
       main: `import { writeFileSync } from 'node:fs'; writeFileSync(${JSON.stringify(marker)}, 'bad'); export function apply() {}`,
     })
     })
-    stagePackage(staged.profileDir, 'unknown', { main: 'export function apply() {}' })
     const { manager } = await bootProfile(staged, { spawn: recordingPnpm(staged.profileDir) })
     const { manager } = await bootProfile(staged, { spawn: recordingPnpm(staged.profileDir) })
-    await manager.add('declared')
-    await manager.add('unknown')
-    const views = await manager.list()
-    expect(views.find(view => view.name === 'declared')?.addable[0]).toMatchObject({ moduleName: 'declared', config: { custom: 1 } })
+    await manager.add('plain')
+    expect((await manager.list()).find(view => view.name === 'plain')?.kind).toBe('unknown')
     expect(existsSync(marker)).toBe(false)
     expect(existsSync(marker)).toBe(false)
-    for (const id of ['', 'nested:child']) {
-      await expect(manager.addRow('declared', { id })).rejects.toMatchObject({ code: 'plugins/bad-request' })
-    }
-    const added = await manager.addRow('declared', { config: null })
-    expect(readFileSync(added.file, 'utf8')).toContain('config: null')
-    expect(views.find(view => view.name === 'unknown')?.kind).toBe('unknown')
-    await expect(manager.addRow('unknown')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
   })
   })
 
 
   it('reports plugins/unavailable without a profile runtime', async () => {
   it('reports plugins/unavailable without a profile runtime', async () => {
@@ -363,9 +350,9 @@ describe('PluginManager', () => {
   describe('list', () => {
   describe('list', () => {
     it('folds installed, enabled, declared and running facts into one view per package', async () => {
     it('folds installed, enabled, declared and running facts into one view per package', async () => {
       const staged = await stageHome()
       const staged = await stageHome()
-      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW, plugins: [{ name: './extra.js', title: 'Extra' }], files: { 'extra.js': 'export const name = "extra"\nexport function apply() {}\n' } })
+      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW, files: { 'extra.js': 'export const name = "extra"\nexport function apply() {}\n' } })
       stagePackage(staged.profileDir, 'ext-lib', { main: 'export const x = 1\n' })
       stagePackage(staged.profileDir, 'ext-lib', { main: 'export const x = 1\n' })
-      stagePackage(staged.profileDir, 'ext-plugin', { plugins: [{ name: '.' }], main: 'export const name = "p"\nexport function apply() {}\n' })
+      stagePackage(staged.profileDir, 'ext-plugin', { main: 'export const name = "p"\nexport function apply() {}\n' })
       addDependency(staged.profileDir, 'ext-bundle')
       addDependency(staged.profileDir, 'ext-bundle')
       addDependency(staged.profileDir, 'ext-lib')
       addDependency(staged.profileDir, 'ext-lib')
       addDependency(staged.profileDir, 'ext-plugin')
       addDependency(staged.profileDir, 'ext-plugin')
@@ -376,13 +363,12 @@ describe('PluginManager', () => {
       expect(views.map(view => [view.name, view.kind, view.status, view.installed, view.enabled])).toEqual([
       expect(views.map(view => [view.name, view.kind, view.status, view.installed, view.enabled])).toEqual([
         ['ext-bundle', 'bundle', 'disabled', true, false],
         ['ext-bundle', 'bundle', 'disabled', true, false],
         ['ext-lib', 'unknown', 'plain', true, false],
         ['ext-lib', 'unknown', 'plain', true, false],
-        ['ext-plugin', 'plugin', 'plain', true, false],
+        ['ext-plugin', 'unknown', 'plain', true, false],
       ])
       ])
       const bundle = views[0]
       const bundle = views[0]
       expect(bundle).toMatchObject({ version: '1.0.0', title: 'Title of ext-bundle', description: 'staged ext-bundle', liveReload: true })
       expect(bundle).toMatchObject({ version: '1.0.0', title: 'Title of ext-bundle', description: 'staged ext-bundle', liveReload: true })
       // Rows come from static declarations while the bundle is not composed, under the ids the patch declares.
       // Rows come from static declarations while the bundle is not composed, under the ids the patch declares.
       expect(bundle?.rows).toEqual([{ entryId: 'hello', rowId: 'hello', moduleName: 'cordis:good', enabled: true, phase: null }])
       expect(bundle?.rows).toEqual([{ entryId: 'hello', rowId: 'hello', moduleName: 'cordis:good', enabled: true, phase: null }])
-      expect(bundle?.addable).toEqual([{ moduleName: 'ext-bundle/extra.js', declaredName: './extra.js', title: 'Extra' }])
     })
     })
 
 
     it('reads a composed bundle\'s rows from the live tree with their failures', async () => {
     it('reads a composed bundle\'s rows from the live tree with their failures', async () => {
@@ -475,7 +461,7 @@ describe('PluginManager', () => {
       const { manager } = await bootProfile(staged, {
       const { manager } = await bootProfile(staged, {
         metadata: ({ packageName }) => {
         metadata: ({ packageName }) => {
           if (packageName === 'ext-odd') throw new Error('invalid declarations')
           if (packageName === 'ext-odd') throw new Error('invalid declarations')
-          return { packageName, kind: 'bundle', cordisSameCopy: false, rows: [], overrides: [], addable: [] }
+          return { packageName, kind: 'bundle', cordisSameCopy: false, rows: [], overrides: [] }
         },
         },
       })
       })
 
 
@@ -877,67 +863,37 @@ describe('PluginManager', () => {
   })
   })
 
 
   describe('rows in user layers', () => {
   describe('rows in user layers', () => {
-    it('adds, disables, re-enables, and removes a row in the live global layer', async () => {
-      const staged = await stageHome()
-      stagePackage(staged.profileDir, '@acme/ext-plugin', { plugins: [{ name: '.' }], main: 'export const name = "p"\nexport function apply() {}\n' })
-      addDependency(staged.profileDir, '@acme/ext-plugin')
-      const { ctx, manager, changes } = await bootProfile(staged)
-
-      const added = await manager.addRow('@acme/ext-plugin')
-
-      expect(added).toEqual({ rowId: 'acme/ext-plugin', file: join(staged.profileDir, 'cordis.patch.yml') })
-      expect(readFileSync(added.file, 'utf8')).toBe('- insert:\n    - id: acme/ext-plugin\n      name: "@acme/ext-plugin"\n      config: {}\n')
-      // Composed live: the row is in the tree, though its module cannot import from the temp home.
-      expect(entryIds(ctx)).toContain('include:acme/ext-plugin')
-      await expect(manager.addRow('@acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/row-conflict' })
-
-      await manager.setRowDisabled('acme/ext-plugin', true)
-      expect(ctx.loader.resolve('include:acme/ext-plugin')?.disabled).toBe(true)
-      expect(readFileSync(added.file, 'utf8')).toContain('- id: acme/ext-plugin\n  disabled: true\n')
-      await manager.setRowDisabled('acme/ext-plugin', false)
-      expect(ctx.loader.resolve('include:acme/ext-plugin')?.disabled).toBe(false)
-      expect(readFileSync(added.file, 'utf8')).not.toContain('disabled')
-
-      await manager.removeRow('acme/ext-plugin')
-      expect(entryIds(ctx)).not.toContain('include:acme/ext-plugin')
-      await expect(manager.removeRow('acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/bad-request' })
-      expect(changes.map(change => change.reason)).toEqual(['row', 'row', 'row', 'row'])
-    })
-
-    it('detects a conflict through the layer file when the tree was not recomposed', async () => {
+    it('persists a startup profile row toggle without changing its mounted tree', async () => {
       const staged = await stageHome('startup')
       const staged = await stageHome('startup')
-      stagePackage(staged.profileDir, 'ext-plugin', { plugins: [{ name: '.' }], main: 'export const name = "p"\nexport function apply() {}\n' })
-      addDependency(staged.profileDir, 'ext-plugin')
+      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW })
+      addDependency(staged.profileDir, 'ext-bundle')
+      const manifest = manifestOf(staged.profileDir)
+      manifest.dsh.profile.bundles.push('ext-bundle')
+      writeFileSync(join(staged.profileDir, 'package.json'), JSON.stringify(manifest))
       const { ctx, manager } = await bootProfile(staged)
       const { ctx, manager } = await bootProfile(staged)
 
 
-      await manager.addRow('ext-plugin')
+      await manager.setRowDisabled('hello', true)
 
 
-      expect(entryIds(ctx)).not.toContain('include:ext-plugin')
-      await expect(manager.addRow('ext-plugin')).rejects.toMatchObject({ code: 'plugins/row-conflict' })
+      expect(readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')).toContain('disabled: true')
+      expect(ctx.loader.resolve('include:hello').disabled).toBe(false)
     })
     })
 
 
-    it('adds a declared addable module with its default config and an explicit id, and refuses the rest', async () => {
+    it('disables and restores a bundle row through the live user layer', async () => {
       const staged = await stageHome()
       const staged = await stageHome()
-      stagePackage(staged.profileDir, 'ext-bundle', {
-        patch: BUNDLE_ONE_ROW,
-        plugins: [
-          { name: './tools/sql.js', title: 'SQL', config: { dsn: 'sqlite://' } },
-          { name: './missing.js' },
-        ],
-        files: { 'tools/sql.js': 'export const name = "sql"\nexport function apply() {}\n' },
-      })
+      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW })
       addDependency(staged.profileDir, 'ext-bundle')
       addDependency(staged.profileDir, 'ext-bundle')
-      const { manager } = await bootProfile(staged)
+      const { ctx, manager, changes } = await bootProfile(staged)
+      await manager.enable('ext-bundle')
+      const file = join(staged.profileDir, 'cordis.patch.yml')
 
 
-      const added = await manager.addRow('ext-bundle', { module: './tools/sql.js', id: 'sql' })
-      expect(added.rowId).toBe('sql')
-      expect(readFileSync(added.file, 'utf8')).toContain('- id: sql\n      name: ext-bundle/tools/sql.js\n      config:\n        dsn: sqlite://\n')
-      await expect(manager.addRow('ext-bundle')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
-      await expect(manager.addRow('ext-bundle', { module: './missing.js' })).resolves.toMatchObject({ rowId: 'ext-bundle/missing.js' })
-      await expect(manager.addRow('absent')).rejects.toMatchObject({ code: 'plugins/not-installed' })
+      await manager.setRowDisabled('hello', true)
+      expect(ctx.loader.resolve('include:hello').disabled).toBe(true)
+      expect(readFileSync(file, 'utf8')).toContain('disabled: true')
+      await manager.setRowDisabled('hello', false)
+      expect(ctx.loader.resolve('include:hello').disabled).toBe(false)
+      expect(readFileSync(file, 'utf8')).not.toContain('disabled')
+      expect(changes.map(change => change.reason)).toEqual(['enable', 'row', 'row'])
     })
     })
-
-
   })
   })
 
 
   describe('dependents and uninstall', () => {
   describe('dependents and uninstall', () => {
@@ -986,14 +942,15 @@ describe('PluginManager', () => {
       expect((await manager.dependents('ext-lib')).references).toEqual([])
       expect((await manager.dependents('ext-lib')).references).toEqual([])
     })
     })
 
 
-    it('disables, drops references, removes the package, and forgets its probe', async () => {
+    it('disables a bundle and removes references from a handwritten user patch before uninstalling', async () => {
       const staged = await stageHome()
       const staged = await stageHome()
-      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW, plugins: [{ name: './extra.js' }], files: { 'extra.js': 'export function apply() {}\n' } })
+      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW, files: { 'extra.js': 'export function apply() {}\n' } })
       addDependency(staged.profileDir, 'ext-bundle')
       addDependency(staged.profileDir, 'ext-bundle')
       const calls: string[][] = []
       const calls: string[][] = []
       const { ctx, manager, changes } = await bootProfile(staged, { spawn: recordingPnpm(staged.profileDir, calls) })
       const { ctx, manager, changes } = await bootProfile(staged, { spawn: recordingPnpm(staged.profileDir, calls) })
       await manager.enable('ext-bundle')
       await manager.enable('ext-bundle')
-      await manager.addRow('ext-bundle', { module: './extra.js' })
+      writeFileSync(join(staged.profileDir, 'cordis.patch.yml'), '- insert:\n    - id: ext-bundle/extra.js\n      name: ext-bundle/extra.js\n')
+      await ctx.profileRuntime.recompose()
       expect(entryIds(ctx)).toEqual(expect.arrayContaining(['include:hello', 'include:ext-bundle/extra.js']))
       expect(entryIds(ctx)).toEqual(expect.arrayContaining(['include:hello', 'include:ext-bundle/extra.js']))
 
 
       await manager.uninstall('ext-bundle')
       await manager.uninstall('ext-bundle')
@@ -1003,7 +960,7 @@ describe('PluginManager', () => {
       expect(entryIds(ctx)).not.toContain('include:hello')
       expect(entryIds(ctx)).not.toContain('include:hello')
       expect(entryIds(ctx)).not.toContain('include:ext-bundle/extra.js')
       expect(entryIds(ctx)).not.toContain('include:ext-bundle/extra.js')
       expect(readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')).toBe('[]\n')
       expect(readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')).toBe('[]\n')
-      expect(changes.map(change => change.reason)).toEqual(['enable', 'row', 'disable', 'uninstall'])
+      expect(changes.map(change => change.reason)).toEqual(['enable', 'disable', 'uninstall'])
       await expect(manager.uninstall('ext-bundle')).rejects.toMatchObject({ code: 'plugins/not-installed' })
       await expect(manager.uninstall('ext-bundle')).rejects.toMatchObject({ code: 'plugins/not-installed' })
     })
     })
   })
   })

+ 0 - 3
packages/boot/plugin-manager/tsconfig.json

@@ -20,9 +20,6 @@
     {
     {
       "path": "../app-boot"
       "path": "../app-boot"
     },
     },
-    {
-      "path": "../../util/values"
-    },
     {
     {
       "path": "../../util/package-manifest"
       "path": "../../util/package-manifest"
     }
     }

+ 2 - 21
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -1423,17 +1423,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         parameters: [{ name: 'packageName', description: 'the enabled bundle.' }],
         parameters: [{ name: 'packageName', description: 'the enabled bundle.' }],
         returns: 'the enable outcome of the second step.',
         returns: 'the enable outcome of the second step.',
       },
       },
-      {
-        signature: '@Remote(\'addRow\') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>',
-        description: 'Add a row naming one of the package\'s modules to the profile user layer.',
-        parameters: [{ name: 'packageName', description: 'the installed package.' }, { name: 'options', description: '`module` selects a declared `dsh.plugins[]` name (default `.`), `id` overrides the derived row id, `config` overrides the declared default.' }],
-        returns: 'where the row landed.',
-      },
-      {
-        signature: '@Remote(\'removeRow\') async removeRow(rowId: string): Promise<void>',
-        description: 'Remove a row a user layer inserted.',
-        parameters: [{ name: 'rowId', description: 'the inserted row\'s id.' }],
-      },
       {
       {
         signature: '@Remote(\'setRowDisabled\') async setRowDisabled(rowId: string, disabled: boolean): Promise<void>',
         signature: '@Remote(\'setRowDisabled\') async setRowDisabled(rowId: string, disabled: boolean): Promise<void>',
         description: 'Switch one row off or on in a user layer; deny-only.',
         description: 'Switch one row off or on in a user layer; deny-only.',
@@ -4951,13 +4940,9 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'PluginInstallResult',
     name: 'PluginInstallResult',
     declaration: 'export interface PluginInstallResult {\n    readonly installed: readonly string[];\n    readonly removed: readonly PluginInstallRejection[];\n    readonly enabled: readonly string[];\n    readonly installedOnly: readonly string[];\n    readonly plain: readonly string[];\n    readonly jobId: string;\n}',
     declaration: 'export interface PluginInstallResult {\n    readonly installed: readonly string[];\n    readonly removed: readonly PluginInstallRejection[];\n    readonly enabled: readonly string[];\n    readonly installedOnly: readonly string[];\n    readonly plain: readonly string[];\n    readonly jobId: string;\n}',
   },
   },
-  {
-    name: 'PluginPackageAddableView',
-    declaration: 'export interface PluginPackageAddableView {\n    readonly moduleName: string;\n    readonly declaredName: string;\n    readonly title?: string;\n    readonly config?: JsonValue;\n}',
-  },
   {
   {
     name: 'PluginPackageKind',
     name: 'PluginPackageKind',
-    declaration: 'export type PluginPackageKind = \'bundle\' | \'plugin\' | \'unknown\';',
+    declaration: 'export type PluginPackageKind = \'bundle\' | \'unknown\';',
   },
   },
   {
   {
     name: 'PluginPackageRowView',
     name: 'PluginPackageRowView',
@@ -4969,11 +4954,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   },
   {
   {
     name: 'PluginPackageView',
     name: 'PluginPackageView',
-    declaration: 'export interface PluginPackageView {\n    readonly name: string;\n    readonly version?: string;\n    readonly title?: string;\n    readonly description?: string;\n    readonly kind: PluginPackageKind;\n    readonly installed: boolean;\n    readonly enabled: boolean;\n    readonly status: PluginPackageStatus;\n    readonly reason?: string;\n    readonly enginesDsh?: string;\n    readonly cordisSameCopy: boolean | null;\n    readonly rows: readonly PluginPackageRowView[];\n    readonly issues?: readonly PluginRowIssue[];\n    readonly overrides: readonly string[];\n    readonly addable: readonly PluginPackageAddableView[];\n    readonly liveReload: boolean;\n}',
-  },
-  {
-    name: 'PluginRowAddition',
-    declaration: 'export interface PluginRowAddition {\n    readonly rowId: string;\n    readonly file: string;\n}',
+    declaration: 'export interface PluginPackageView {\n    readonly name: string;\n    readonly version?: string;\n    readonly title?: string;\n    readonly description?: string;\n    readonly kind: PluginPackageKind;\n    readonly installed: boolean;\n    readonly enabled: boolean;\n    readonly status: PluginPackageStatus;\n    readonly reason?: string;\n    readonly enginesDsh?: string;\n    readonly cordisSameCopy: boolean | null;\n    readonly rows: readonly PluginPackageRowView[];\n    readonly issues?: readonly PluginRowIssue[];\n    readonly overrides: readonly string[];\n    readonly liveReload: boolean;\n}',
   },
   },
   {
   {
     name: 'PluginRowIssue',
     name: 'PluginRowIssue',

+ 2 - 2
packages/host/plugin-manager/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/host/plugin-manager/README.md
 #   pnpm run verify-translation-pairing --write packages/host/plugin-manager/README.md
-README.md: 1eea9fe8e3a0d785a52e4109716ea8d4bb698c34
-README.zh.md: afbdc28c8021c4e5260f7d1ab8f510d02c6299d2
+README.md: b1cab6b904de1cc3c46884db2683abf1c1d6482d
+README.zh.md: af9338467c015239b1e4363c07ff13bc874b65d6

+ 4 - 4
packages/host/plugin-manager/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 
 ## Summary
 ## Summary
 
 
-`dsh-host-plugin-manager` mounts `pluginManager` as a Typert service and exposes the `plugins` Remote — `list`, `add`, `uninstall`, `enable`, `disable`, `retry`, `addRow`, `removeRow`, `setRowDisabled`, `dependents` — each relaying to the [`dsh-plugin-manager`](../../boot/plugin-manager/README.md) `PluginManager` built over this context, with every `plugins/*` failure crossing the wire as a `RemoteError` of the same code. What the operations do, and the `plugins/changed` and `plugins/install-log` events that follow them, belong to that package; this one reads the profile runtime and the agent registry off the context and hands them over. Clients consume the Remote through the [`api-remotes`](../../api/remotes/README.md) assembly.
+`dsh-host-plugin-manager` mounts `pluginManager` as a Typert service and exposes the `plugins` Remote — `list`, `add`, `uninstall`, `enable`, `disable`, `retry`, `setRowDisabled`, `dependents` — each relaying to the [`dsh-plugin-manager`](../../boot/plugin-manager/README.md) `PluginManager` built over this context, with every `plugins/*` failure crossing the wire as a `RemoteError` of the same code. What the operations do, and the `plugins/changed` and `plugins/install-log` events that follow them, belong to that package; this one reads the profile runtime and the agent registry off the context and hands them over. Clients consume the Remote through the [`api-remotes`](../../api/remotes/README.md) assembly.
 
 
 ## Table of Contents
 ## Table of Contents
 
 
@@ -29,11 +29,11 @@ Mount the row in a host composition beside the plugin inventory; the web bundle
 
 
 ### The Remote
 ### The Remote
 
 
-`plugins/list`, `plugins/add`, `plugins/uninstall`, `plugins/enable`, `plugins/disable`, `plugins/retry`, `plugins/addRow`, `plugins/removeRow`, `plugins/setRowDisabled`, and `plugins/dependents` carry the arguments and answers the manager's methods of the same names define; the [manager README](../../boot/plugin-manager/README.md#use-this-package) documents each. The `./types` export re-exports the manager's payload types unchanged, so a client imports one vocabulary.
+`plugins/list`, `plugins/add`, `plugins/uninstall`, `plugins/enable`, `plugins/disable`, `plugins/retry`, `plugins/setRowDisabled`, and `plugins/dependents` carry the arguments and answers the manager's methods of the same names define; the [manager README](../../boot/plugin-manager/README.md#use-this-package) documents each. The `./types` export re-exports the manager's payload types unchanged, so a client imports one vocabulary.
 
 
 ### Failure codes
 ### Failure codes
 
 
-A manager failure reaches the client as a `RemoteError` with the same `code` and `details`: `plugins/unavailable`, `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/row-conflict`, `plugins/busy`, and `plugins/agents-running`, each declared in the Remote failure map with the manager's details type. The manager's generic refusal, `plugins/bad-request`, crosses as the Gateway's `gateway/bad-request`. Any other error the manager throws propagates untouched, which the Gateway reports as `gateway/internal`.
+A manager failure reaches the client as a `RemoteError` with the same `code` and `details`: `plugins/unavailable`, `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/busy`, and `plugins/agents-running`, each declared in the Remote failure map with the manager's details type. The manager's generic refusal, `plugins/bad-request`, crosses as the Gateway's `gateway/bad-request`. Any other error the manager throws propagates untouched, which the Gateway reports as `gateway/internal`.
 
 
 ### Configuration
 ### Configuration
 
 
@@ -59,7 +59,7 @@ The adapter supplies per-call profile and agent readers to one `PluginManager`,
 
 
 | File | Role |
 | File | Role |
 |---|---|
 |---|---|
-| [`src/index.ts`](src/index.ts) | `PluginManagerRemote`: the `pluginManager` service, the ten `plugins` Remote methods, and `remoteErrorOf` |
+| [`src/index.ts`](src/index.ts) | `PluginManagerRemote`: the `pluginManager` service, the eight `plugins` Remote methods, and `remoteErrorOf` |
 | [`src/types.ts`](src/types.ts) | The manager's types re-exported, and the `plugins/*` codes declared in the Remote failure map |
 | [`src/types.ts`](src/types.ts) | The manager's types re-exported, and the `plugins/*` codes declared in the Remote failure map |
 | — | No runtime invariant companion is published; the row holds no state of its own. |
 | — | No runtime invariant companion is published; the row holds no state of its own. |
 
 

+ 4 - 4
packages/host/plugin-manager/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 
 ## 概述
 ## 概述
 
 
-`dsh-host-plugin-manager` 把 `pluginManager` 挂载为 Typert 服务并暴露 `plugins` Remote——`list`、`add`、`uninstall`、`enable`、`disable`、`retry`、`addRow`、`removeRow`、`setRowDisabled`、`dependents`——每个方法都转给建立在本上下文之上的 [`dsh-plugin-manager`](../../boot/plugin-manager/README.zh.md) `PluginManager`,每个 `plugins/*` 失败以同码的 `RemoteError` 过线。操作做什么、以及随之而来的 `plugins/changed` 与 `plugins/install-log` 事件属于那个包;本包只是从上下文读取 profile runtime 与 agent 注册表并交给它。客户端经 [`api-remotes`](../../api/remotes/README.zh.md) 装配消费该 Remote。
+`dsh-host-plugin-manager` 把 `pluginManager` 挂载为 Typert 服务并暴露 `plugins` Remote——`list`、`add`、`uninstall`、`enable`、`disable`、`retry`、`setRowDisabled`、`dependents`——每个方法都转给建立在本上下文之上的 [`dsh-plugin-manager`](../../boot/plugin-manager/README.zh.md) `PluginManager`,每个 `plugins/*` 失败以同码的 `RemoteError` 过线。操作做什么、以及随之而来的 `plugins/changed` 与 `plugins/install-log` 事件属于那个包;本包只是从上下文读取 profile runtime 与 agent 注册表并交给它。客户端经 [`api-remotes`](../../api/remotes/README.zh.md) 装配消费该 Remote。
 
 
 ## 目录
 ## 目录
 
 
@@ -29,11 +29,11 @@ kind: "package-reference"
 
 
 ### Remote
 ### Remote
 
 
-`plugins/list`、`plugins/add`、`plugins/uninstall`、`plugins/enable`、`plugins/disable`、`plugins/retry`、`plugins/addRow`、`plugins/removeRow`、`plugins/setRowDisabled` 与 `plugins/dependents` 携带管理器同名方法定义的参数与答复;[管理器 README](../../boot/plugin-manager/README.zh.md#use-this-package) 逐一说明。`./types` 导出原样 re-export 管理器的载荷类型,客户端只需导入一套词汇。
+`plugins/list`、`plugins/add`、`plugins/uninstall`、`plugins/enable`、`plugins/disable`、`plugins/retry`、`plugins/setRowDisabled` 与 `plugins/dependents` 携带管理器同名方法定义的参数与答复;[管理器 README](../../boot/plugin-manager/README.zh.md#use-this-package) 逐一说明。`./types` 导出原样 re-export 管理器的载荷类型,客户端只需导入一套词汇。
 
 
 ### 失败码
 ### 失败码
 
 
-管理器的失败以同样的 `code` 与 `details` 作为 `RemoteError` 到达客户端:`plugins/unavailable`、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/row-conflict`、`plugins/busy` 与 `plugins/agents-running`,各自以管理器的 details 类型声明在 Remote 失败表里。管理器的通用拒绝 `plugins/bad-request` 以 Gateway 的 `gateway/bad-request` 过线。管理器抛出的其他错误原样传播,由 Gateway 报为 `gateway/internal`。
+管理器的失败以同样的 `code` 与 `details` 作为 `RemoteError` 到达客户端:`plugins/unavailable`、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/busy` 与 `plugins/agents-running`,各自以管理器的 details 类型声明在 Remote 失败表里。管理器的通用拒绝 `plugins/bad-request` 以 Gateway 的 `gateway/bad-request` 过线。管理器抛出的其他错误原样传播,由 Gateway 报为 `gateway/internal`。
 
 
 ### 配置
 ### 配置
 
 
@@ -59,7 +59,7 @@ kind: "package-reference"
 
 
 | 文件 | 职责 |
 | 文件 | 职责 |
 |---|---|
 |---|---|
-| [`src/index.ts`](src/index.ts) | `PluginManagerRemote`:`pluginManager` 服务、十个 `plugins` Remote 方法与 `remoteErrorOf` |
+| [`src/index.ts`](src/index.ts) | `PluginManagerRemote`:`pluginManager` 服务、八个 `plugins` Remote 方法与 `remoteErrorOf` |
 | [`src/types.ts`](src/types.ts) | re-export 管理器的类型,并把 `plugins/*` 码声明进 Remote 失败表 |
 | [`src/types.ts`](src/types.ts) | re-export 管理器的类型,并把 `plugins/*` 码声明进 Remote 失败表 |
 | — | 不发布运行时不变量伴随件;该行不持有自己的状态。 |
 | — | 不发布运行时不变量伴随件;该行不持有自己的状态。 |
 
 

+ 1 - 27
packages/host/plugin-manager/src/index.ts

@@ -20,11 +20,10 @@ import {
   type PluginInstallResult,
   type PluginInstallResult,
   type PluginOperationFailure,
   type PluginOperationFailure,
   type PluginPackageView,
   type PluginPackageView,
-  type PluginRowAddition,
   type SpawnLike,
   type SpawnLike,
 } from '@deepseek-ai/dsh-plugin-manager'
 } from '@deepseek-ai/dsh-plugin-manager'
 import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
 import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
-import { assertNever, type JsonValue } from '@deepseek-ai/dsh-util-values'
+import { assertNever } from '@deepseek-ai/dsh-util-values'
 import type {} from './types.ts'
 import type {} from './types.ts'
 
 
 export type * from './types.ts'
 export type * from './types.ts'
@@ -180,30 +179,6 @@ export class PluginManagerRemote extends TypertRemoteService {
     return relay(() => this.manager.retry(packageName))
     return relay(() => this.manager.retry(packageName))
   }
   }
 
 
-  /**
-   * Add a row naming one of the package's modules to the profile user layer.
-   * @param packageName - the installed package.
-   * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
-   * `id` overrides the derived row id, `config` overrides the declared default.
-   * @returns where the row landed.
-   */
-  @Remote('addRow')
-  async addRow(
-    packageName: string,
-    options?: { module?: string; id?: string; config?: JsonValue },
-  ): Promise<PluginRowAddition> {
-    return relay(() => this.manager.addRow(packageName, options))
-  }
-
-  /**
-   * Remove a row a user layer inserted.
-   * @param rowId - the inserted row's id.
-   */
-  @Remote('removeRow')
-  async removeRow(rowId: string): Promise<void> {
-    return relay(() => this.manager.removeRow(rowId))
-  }
-
   /**
   /**
    * Switch one row off or on in a user layer; deny-only.
    * Switch one row off or on in a user layer; deny-only.
    * @param rowId - the row's id as the composition declares it.
    * @param rowId - the row's id as the composition declares it.
@@ -253,7 +228,6 @@ export function remoteErrorOf(failure: PluginOperationFailure): RemoteError {
     case 'plugins/not-enableable': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/not-enableable': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/enable-failed': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/enable-failed': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/install-failed': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/install-failed': return new RemoteError(failure.code, failure.message, failure.details, options)
-    case 'plugins/row-conflict': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/busy': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/busy': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/agents-running': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/agents-running': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/bad-request': return new RemoteError('gateway/bad-request', failure.message, {}, options)
     case 'plugins/bad-request': return new RemoteError('gateway/bad-request', failure.message, {}, options)

+ 1 - 3
packages/host/plugin-manager/src/types.ts

@@ -16,14 +16,12 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
     'plugins/unavailable': PluginOperationDetailsMap['plugins/unavailable']
     'plugins/unavailable': PluginOperationDetailsMap['plugins/unavailable']
     /** The package is not a profile dependency. */
     /** The package is not a profile dependency. */
     'plugins/not-installed': PluginOperationDetailsMap['plugins/not-installed']
     'plugins/not-installed': PluginOperationDetailsMap['plugins/not-installed']
-    /** The package cannot be enabled or added, with the declaration or request error. */
+    /** The package cannot be enabled, with the declaration or request error. */
     'plugins/not-enableable': PluginOperationDetailsMap['plugins/not-enableable']
     'plugins/not-enableable': PluginOperationDetailsMap['plugins/not-enableable']
     /** Preparation or the root Include rejected enablement; the layer selection was reverted. */
     /** Preparation or the root Include rejected enablement; the layer selection was reverted. */
     'plugins/enable-failed': PluginOperationDetailsMap['plugins/enable-failed']
     'plugins/enable-failed': PluginOperationDetailsMap['plugins/enable-failed']
     /** pnpm exited non-zero, could not be spawned, or timed out. */
     /** pnpm exited non-zero, could not be spawned, or timed out. */
     'plugins/install-failed': PluginOperationDetailsMap['plugins/install-failed']
     'plugins/install-failed': PluginOperationDetailsMap['plugins/install-failed']
-    /** The row id is already taken in the target user layer. */
-    'plugins/row-conflict': PluginOperationDetailsMap['plugins/row-conflict']
     /** Another mutation is still running; the manager runs one at a time and refuses rather than queues. */
     /** Another mutation is still running; the manager runs one at a time and refuses rather than queues. */
     'plugins/busy': PluginOperationDetailsMap['plugins/busy']
     'plugins/busy': PluginOperationDetailsMap['plugins/busy']
     /** `node_modules` cannot change while a session runs; `running` counts the agents in `running` status. */
     /** `node_modules` cannot change while a session runs; `running` counts the agents in `running` status. */

+ 5 - 10
packages/host/plugin-manager/tests/plugin-manager.spec.ts

@@ -249,7 +249,7 @@ describe('PluginManagerRemote', () => {
     const remote = await mount()
     const remote = await mount()
     expect(remote.typertRemote).toMatchObject({ serviceKey: 'pluginManager', namespace: 'plugins' })
     expect(remote.typertRemote).toMatchObject({ serviceKey: 'pluginManager', namespace: 'plugins' })
     expect(remoteMethods(remote).map(marker => marker.method)).toEqual([
     expect(remoteMethods(remote).map(marker => marker.method)).toEqual([
-      'list', 'add', 'uninstall', 'enable', 'disable', 'retry', 'addRow', 'removeRow', 'setRowDisabled', 'dependents',
+      'list', 'add', 'uninstall', 'enable', 'disable', 'retry', 'setRowDisabled', 'dependents',
     ])
     ])
   })
   })
 
 
@@ -274,8 +274,6 @@ describe('PluginManagerRemote', () => {
     await remote.enable('pkg')
     await remote.enable('pkg')
     await remote.disable('pkg')
     await remote.disable('pkg')
     await remote.retry('pkg')
     await remote.retry('pkg')
-    await remote.addRow('pkg', { module: './x.js', id: 'x', config: { a: 1 } })
-    await remote.removeRow('x')
     await remote.setRowDisabled('x', true)
     await remote.setRowDisabled('x', true)
     await remote.dependents('pkg')
     await remote.dependents('pkg')
 
 
@@ -286,8 +284,6 @@ describe('PluginManagerRemote', () => {
       ['enable', 'pkg'],
       ['enable', 'pkg'],
       ['disable', 'pkg'],
       ['disable', 'pkg'],
       ['retry', 'pkg'],
       ['retry', 'pkg'],
-      ['addRow', 'pkg', { module: './x.js', id: 'x', config: { a: 1 } }],
-      ['removeRow', 'x'],
       ['setRowDisabled', 'x', true],
       ['setRowDisabled', 'x', true],
       ['dependents', 'pkg'],
       ['dependents', 'pkg'],
     ])
     ])
@@ -306,7 +302,7 @@ describe('PluginManagerRemote', () => {
       whenIdle: async () => {},
       whenIdle: async () => {},
     } as never)
     } as never)
     const metadata: typeof readPackageMetadata = options => ({
     const metadata: typeof readPackageMetadata = options => ({
-      packageName: options.packageName, kind: 'plugin', cordisSameCopy: null, rows: [], overrides: [], addable: [{ name: '.' }],
+      packageName: options.packageName, kind: 'unknown', cordisSameCopy: null, rows: [], overrides: [],
     })
     })
     const spawn: SpawnLike = () => { throw new Error('this test spawns nothing') }
     const spawn: SpawnLike = () => { throw new Error('this test spawns nothing') }
     class SeamedRemote extends PluginManagerRemote {
     class SeamedRemote extends PluginManagerRemote {
@@ -323,7 +319,7 @@ describe('PluginManagerRemote', () => {
       // The agent-count reader: a running session refuses an install before pnpm runs.
       // The agent-count reader: a running session refuses an install before pnpm runs.
       ctx.provide('agents', { list: () => [{ status: 'running' }, { status: 'idle' }] } as never)
       ctx.provide('agents', { list: () => [{ status: 'running' }, { status: 'idle' }] } as never)
       await expect(remote.add('anything')).rejects.toMatchObject({ code: 'plugins/agents-running', details: { operation: 'add', running: 1 } })
       await expect(remote.add('anything')).rejects.toMatchObject({ code: 'plugins/agents-running', details: { operation: 'add', running: 1 } })
-      await expect(remote.addRow('pkg', { module: './unknown' })).rejects.toMatchObject({ code: 'plugins/not-enableable' })
+      await expect(remote.enable('pkg')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
     } finally {
     } finally {
       rmSync(profileDir, { recursive: true, force: true })
       rmSync(profileDir, { recursive: true, force: true })
     }
     }
@@ -352,7 +348,6 @@ describe('remoteErrorOf', () => {
       new PluginOperationError('plugins/not-enableable', 'm', { packageName: 'p', reason: 'r' }),
       new PluginOperationError('plugins/not-enableable', 'm', { packageName: 'p', reason: 'r' }),
       new PluginOperationError('plugins/enable-failed', 'm', { packageName: 'p', reason: 'r' }),
       new PluginOperationError('plugins/enable-failed', 'm', { packageName: 'p', reason: 'r' }),
       new PluginOperationError('plugins/install-failed', 'm', { spec: 's', exitCode: 1, log: 'l' }),
       new PluginOperationError('plugins/install-failed', 'm', { spec: 's', exitCode: 1, log: 'l' }),
-      new PluginOperationError('plugins/row-conflict', 'm', { rowId: 'x' }),
       new PluginOperationError('plugins/busy', 'm', { operation: 'add', subject: 'y', active: { operation: 'add', subject: 'x' } }),
       new PluginOperationError('plugins/busy', 'm', { operation: 'add', subject: 'y', active: { operation: 'add', subject: 'x' } }),
       new PluginOperationError('plugins/agents-running', 'm', { operation: 'add', running: 1 }),
       new PluginOperationError('plugins/agents-running', 'm', { operation: 'add', running: 1 }),
     ]
     ]
@@ -368,7 +363,7 @@ describe('remoteErrorOf', () => {
 
 
 describe('RemoteError codes', () => {
 describe('RemoteError codes', () => {
   it('declare their details', () => {
   it('declare their details', () => {
-    const error = new RemoteError('plugins/row-conflict', 'taken', { rowId: 'x' })
-    expect(error.details).toEqual({ rowId: 'x' })
+    const error = new RemoteError('plugins/not-installed', 'absent', { packageName: 'x' })
+    expect(error.details).toEqual({ packageName: 'x' })
   })
   })
 })
 })

+ 2 - 2
packages/util/package-manifest/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/util/package-manifest/README.md
 #   pnpm run verify-translation-pairing --write packages/util/package-manifest/README.md
-README.md: 081b07d9a9123d688b288f1ad30dc9465b7dcbe9
-README.zh.md: d9edc50e6979a1d6664076c977ca0ad62b27dc6b
+README.md: ca14a4bd1929faa3d2009ba6c6d2c0bb331a5b37
+README.zh.md: e52f0699f225925eb99dcbfad8b4c00e72a52e72

+ 0 - 1
packages/util/package-manifest/README.md

@@ -38,7 +38,6 @@ const manifest: DshPackageManifest = {
   dsh: {
   dsh: {
     manifestVersion: 1,
     manifestVersion: 1,
     title: 'Example plugin',
     title: 'Example plugin',
-    plugins: [{ name: './tool.js', config: { enabled: true } }],
     bundle: { patch: './cordis.patch.yml' },
     bundle: { patch: './cordis.patch.yml' },
     client,
     client,
   },
   },

+ 0 - 1
packages/util/package-manifest/README.zh.md

@@ -38,7 +38,6 @@ const manifest: DshPackageManifest = {
   dsh: {
   dsh: {
     manifestVersion: 1,
     manifestVersion: 1,
     title: 'Example plugin',
     title: 'Example plugin',
-    plugins: [{ name: './tool.js', config: { enabled: true } }],
     bundle: { patch: './cordis.patch.yml' },
     bundle: { patch: './cordis.patch.yml' },
     client,
     client,
   },
   },

+ 0 - 1
packages/util/package-manifest/src/index.ts

@@ -9,7 +9,6 @@ export type {
   DshEnginesManifest,
   DshEnginesManifest,
   DshManifest,
   DshManifest,
   DshPackageManifest,
   DshPackageManifest,
-  DshPluginDeclaration,
   DshProfileManifest,
   DshProfileManifest,
   ProfilePatchReload,
   ProfilePatchReload,
 } from './types.ts'
 } from './types.ts'

+ 0 - 12
packages/util/package-manifest/src/types.ts

@@ -36,8 +36,6 @@ export interface DshManifest {
   client?: DshClientManifest
   client?: DshClientManifest
   /** Display title of the package, read without executing package code. */
   /** Display title of the package, read without executing package code. */
   title?: string
   title?: string
-  /** Agent-plane modules the package declares addable to a composition, read without executing package code. */
-  plugins?: DshPluginDeclaration[]
 }
 }
 
 
 /** Runtime version requirements under `package.json.engines`. */
 /** Runtime version requirements under `package.json.engines`. */
@@ -52,16 +50,6 @@ export interface DshEnginesManifest {
   [engine: string]: string | undefined
   [engine: string]: string | undefined
 }
 }
 
 
-/** One agent-plane module a package declares addable to a composition. */
-export interface DshPluginDeclaration {
-  /** The package's export subpath, with `.` naming its main entry and `./tools` a subpath. */
-  name: string
-  /** Display title. */
-  title?: string
-  /** Default row config. */
-  config?: unknown
-}
-
 /** The configuration layer exported by a bundle package. */
 /** The configuration layer exported by a bundle package. */
 export interface DshBundleManifest {
 export interface DshBundleManifest {
   /** Patch file path relative to the declaring package root. */
   /** Patch file path relative to the declaring package root. */

+ 4 - 3
pnpm-lock.yaml

@@ -1358,6 +1358,10 @@ importers:
         version: 15.0.0
         version: 15.0.0
 
 
   packages/boot/plugin-manager:
   packages/boot/plugin-manager:
+    dependencies:
+      '@deepseek-ai/dsh-util-values':
+        specifier: workspace:^
+        version: link:../../util/values
     devDependencies:
     devDependencies:
       '@deepseek-ai/cordis':
       '@deepseek-ai/cordis':
         specifier: workspace:^
         specifier: workspace:^
@@ -1374,9 +1378,6 @@ importers:
       '@deepseek-ai/dsh-package-manifest':
       '@deepseek-ai/dsh-package-manifest':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../util/package-manifest
         version: link:../../util/package-manifest
-      '@deepseek-ai/dsh-util-values':
-        specifier: workspace:^
-        version: link:../../util/values
 
 
   packages/bundle/acp-app:
   packages/bundle/acp-app:
     dependencies:
     dependencies:

+ 0 - 1
scripts/gen-cordis-catalog.ts

@@ -721,7 +721,6 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
   PluginPackageView: 'plugin manager package views are owned by packages/host/plugin-manager/README.md',
   PluginPackageView: 'plugin manager package views are owned by packages/host/plugin-manager/README.md',
   PluginInstallResult: 'plugin manager install results are owned by packages/host/plugin-manager/README.md',
   PluginInstallResult: 'plugin manager install results are owned by packages/host/plugin-manager/README.md',
   PluginEnableResult: 'plugin manager enable results are owned by packages/host/plugin-manager/README.md',
   PluginEnableResult: 'plugin manager enable results are owned by packages/host/plugin-manager/README.md',
-  PluginRowAddition: 'plugin manager row additions are owned by packages/host/plugin-manager/README.md',
   PluginDependents: 'plugin manager dependents are owned by packages/host/plugin-manager/README.md',
   PluginDependents: 'plugin manager dependents are owned by packages/host/plugin-manager/README.md',
   AgentPresetRoster: 'path-free preset roster is owned by packages/preset/agent-presets/README.md',
   AgentPresetRoster: 'path-free preset roster is owned by packages/preset/agent-presets/README.md',
   AgentPresetDocument: 'preset composition view is owned by packages/preset/agent-presets/README.md',
   AgentPresetDocument: 'preset composition view is owned by packages/preset/agent-presets/README.md',