Files
Workspace/Docs/Archive/CORDIS_MIGRATION.completed.md
T
cneicy d74c2f08ca feat(packages): 内置 SDK 包并完善 ContextLoader 集成
- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档
- 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理
- 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言
- 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则
- 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护
- 新增 Network 语义扫描、模板保护和 App 组件生命周期测试
- 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试
- 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
2026-08-18 18:06:34 +08:00

331 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ShrinkSDK × Cordis 组织形式改造方案(已归档)
> 归档说明:阶段 0 至 5 已完成并合入当前架构。本文只保存迁移论证、过程决策和历史验收数据;当前状态以仓库根 `DESIGN.md` 为准。
> 生成日期:2026-08-16;独立重读与阶段 5 路线更新:2026-08-17
> 参考文献:《Cordis: A Programming Paradigm for Spatiotemporal Composability》(北京大学 / DeepSeek-AI88 页,`C:\Users\im\Downloads\Cordis_paper_zh-CN.pdf`
> 本文档回答一个问题:**如果按 Cordis 的"时空可组合性"范式重组 ShrinkSDK,目标形态是什么、差距在哪、如何分阶段走过去。**
> 现状基线见仓库根目录 `DESIGN.md`。
---
## 1. Cordis 论文核心思想提炼
### 1.1 两个正交维度
| 维度 | 定义 | 失败症状(现状普遍存在) |
|---|---|---|
| **时间可组合性** | 移除组件时,其对共享环境的全部修改被**完全、有序、安全地回滚** | VSCode 式困境:禁用扩展要重启宿主;deactivate 钩子与 activate 分离,靠作者自律清理 |
| **空间可组合性** | 组件**声明式**表达依赖,运行时**响应式**解析:依赖出现→激活,消失→停用,换提供者→重载 | 服务定位器/Spring getBean:依赖隐式分散,空检查遍布;依赖消失只能崩或靠手写适配 |
### 1.2 两大运行时机制
**可逆效应(revertible effect**——时间维度的原语:
- 每个上下文变换 `e : Γ → Γ × (Γ→Γ)` 在执行时**返回自己的逆函数**
- 运行时把逆函数累积到累加器 φ,卸载 = 执行 φ(LIFO);
- 复合效应的逆由组合自动推导——**组件的拆卸从其加载过程推导出来,而非并列手写**;
- 实现形态就是一个 API`ctx.effect(callback)` 返回 `dispose` 闭包。
**响应式余效应(reactive coeffect)**——空间维度的原语:
- 依赖是键值表 `Σ : K ⇀ V``ctx.set(key, value)` 提供依赖、`ctx.get(key)` 消费依赖;
- 组件声明规范 `d ⊆ K`inject),满足谓词 `σ ⊨ d` 可判定;
- 每次上下文变化被分类为 activating / deactivating / neutral,自动驱动组件激活/停用/重载;
- `set` 本身是效应 → 提供依赖自动可逆(卸载提供者 = 撤回绑定 = 依赖者自动停用)。
**统一上下文 Γ∞**× (Γ→Γ) × Σ` 递归自相似——上下文树。父子层级由"实例化子组件是父组件的一个效应"天然形成:**卸载父级级联卸载子级**。
### 1.3 组件与纤程(fiber
```
组件 component = ( inject: 依赖键集合 d, provide: 供给键集合 p, apply: 效应函数 e )
纤程 fiber = 组件的运行时实例化 ⟨d, p, e, π(父), σ, τ(退役), θ(生命周期)⟩
```
- **供给不相交**:同一注册表中两个纤程的 provide 不得相交(每键唯一提供者);
- 生命周期由 **target(目标提供者集合)+ 惯性状态机** 驱动:target 变化 → refresh → reload(执行 apply,追踪效应)或 unload(先 drain 依赖者,再 LIFO 执行逆操作);
- 转换中途 target 再变 → 链式再转换(惯性),转换内每步迭代边界检查 target(部分回滚);
- 提供者以 **uid(永不复用)** 标识——同值不同提供者的替换也能被观察到。
### 1.4 三层实现架构(这是"组织形式"的直接参照)
| 层 | 职责 | Cordis 内容 |
|---|---|---|
| **① 核心库** | 效应追踪 + 余效应解析 + fiber 生命周期 | `ctx.effect / get / set / isolate / intercept / use`、refresh/reload/unload 算法 |
| **② 组件加载器** | 声明式配置协调 + 热模块替换 | 配置树(entry: id/url/isolate/intercept/config/disabled)、增量协调、@cordisjs/group、HMR 三阶段(模块分类→陈旧项检测→事务性重载) |
| **③ 应用框架** | 领域词汇 | Koishi(4000+ 插件的聊天机器人框架);Web 控制台是同一模型的第二个 Cordis 应用 |
关键设计取向:
- **元框架不规定领域**——只提供通用动态组合语义,领域词汇留给上层;
- **隔离域**(isolate):同一逻辑键在不同上下文解析为不同绑定(多租户/测试/沙箱);
- **拦截**(intercept):访问时按元数据调整依赖的使用方式(细粒度访问控制,如只读降权);
- 依赖访问经代理介导:**未声明的访问在访问点直接拒绝**(能力安全)。
### 1.5 本次独立重读后的校准
1. **Cordis 不是“DI + Dispose”**。核心纪律是组件对环境的读写都归属于自己的上下文:写入才能被归因并自动求逆,读取才能由余效应介导。静态服务定位器只能作为系统边界或兼容门面;经它发生的隐式访问不在主保证内。
2. **时间可组合性不等于有 cleanup 钩子**。逆操作必须和前向效应局部绑定、按应用顺序记录并 LIFO 回放。若两个组件会以任意顺序卸载,它们的效应要么在观察等价下独立/可交换,要么把顺序敏感性显式变成余效应依赖。
3. **空间可组合性的关键是两步 Leave/Unload**:提供者先停止对外供给并排空所有消费者;消费者卸载期间仍通过 committed view 读取旧提供者;最后才执行提供者逆操作。仅在每次 `Inject` 变化后重新检查是否满足,并不足以保证安全卸载。
4. **提供者身份不是值相等**。fiber uid 必须进入目标视图;即便新旧服务值相同,只要提供者变了,消费者也要重载。异步转换遵循惯性:已经开始的一步要落地,再根据新 target 转入后续 unload,不能在任意 await 点宣称“取消即回滚”。
5. **失败是纤程局部事务**。apply 失败必须回滚已累积的部分效应,进入失败/非活动状态,不能自动无限重试;显式配置或依赖变化再触发下一次尝试。
6. **加载器条目 id 是双向绑定的稳定主键**。配置变更应优先交给组件做最小 diff,而不是永久依赖统一重建。HMR 还必须是事务:模块分类、陈旧条目计算、缓存备份/失效、替换;新模块导入失败时完整恢复缓存与旧条目。只有 `dispose + use` 不能称为论文语义的 HMR。
7. **系统边界决定“可逆”的上限**。获取 socket、注册 handler、订阅事件可以回滚;已经发出的网络字节、写入的文件/存档不能被真正撤回,只能在提交前扣留输出,或执行领域补偿。Network/DataSaver 的组件说明和阶段 4 验收必须区分这两类效应。
8. **跨领域集成组件不是过渡瑕疵,而是论文 §6.5 的正式答案**。Command-Network、模块-EventBus 这类双向/循环关系应由一个注入双方服务键的薄组件承载,核心包保持互不依赖。真正应淘汰的是隐式反射接桥和安装器包装,不是所有 Integration 包。
9. **字符串键仍有碰撞与接口漂移风险**。当前先使用命名空间化键;后续应把包版本约束、结构兼容检查或强类型生成键纳入加载器边界。
### 1.6 对 ShrinkSDK 的工程判断
综合论文第 3~6 节与当前实现,我对 Cordis 在 ShrinkSDK 中的定位是:
1. **它应当是生命周期与组合语义的唯一地基,不是新的业务框架。** EventBus、DataSaver、Network、Command、Tutorial 仍然保留自己的领域 API;Cordis 只负责回答“谁拥有这些副作用、依赖变化时谁该运行、组件退出时如何恢复”。后续不能再新增第三套启动/卸载模型。
2. **阶段 0~4 已经覆盖论文最关键的正确性路径。** 当前 `ShrinkContextRuntime` 已具备 provider uid、committed view、惯性转换、部分回滚和 drain-before-inverse`ShrinkModContextHost` 已把外部 DLL 的组件源、revision 缓存与旧组合恢复纳入同一事务。接下来主要是把这些保证变得可约束、可诊断、可运营,而不是重新实现 fiber。
3. **当前最大语义缺口是“声明没有被完全强制”。** `IShrinkComponent.Provide` 目前仍主要依赖作者纪律,运行时没有拒绝组件写入未声明键;字符串键也只提供名义连接,没有类型和版本兼容保证。若不先固定访问契约,intercept 只能成为一层不可靠的包装。
4. **当前最大工程缺口是“发生问题时看不见”。** fiber 已有状态和 `LastError`,但缺少稳定的运行时快照、事务阶段、依赖等待链和 revision 常驻统计。动态组合系统一旦发生等待、回滚或程序集常驻增长,必须能从诊断面直接回答原因。
5. **intercept 是访问介导,不是安全沙箱。** 它适合表达“社区模组只能只读 DataSaver”“某组件不能访问某类网络能力”等宿主可控策略;但外部 DLL 仍可绕开 ctx 直接调用 CLR/Unity API。不可信代码必须另用进程、WebAssembly 或其他执行隔离,不能把语言层访问控制写成安全边界。
6. **程序集不可卸载必须转化为容量策略。** Mono 下 revision 替换只能撤回实例和效应,旧 `Assembly` 仍然常驻。正确做法不是伪装成完全卸载,而是统计历史 revision、估算常驻量、设阈值并给出重启建议。
因此,阶段 5 的顺序应为:先收紧访问契约和诊断,再实现 isolate/intercept,之后补程序集常驻策略和配置/调试工具。这样后续能力建立在可验证的边界上,而不是继续扩大隐式行为。
---
## 2. 概念对照:Cordis ↔ ShrinkSDK 现状
| Cordis 概念 | ShrinkSDK 现有对应 | 差距评估 |
|---|---|---|
| `ctx`(一等上下文) | `ShrinkAppContext`Host+Settings+Services | 形似神不似:无效应追踪、无响应式,启动后只读 |
| `ctx.effect(cb)` → dispose | **无统一机制**。最接近的孤例:`IShrinkEventSubscription : IDisposable`(订阅=效应,Dispose=逆) | 全仓库最大空白 |
| fiber(组件实例+生命周期) | `IShrinkAppModuleInstaller` 实例(只有启动半程) | 现状只有 Load,没有 Unload/Reload;无 uid、无 target、无惯性 |
| `inject`(依赖键集合) | `installer.DependsOn`(模块 ID 字符串) | 依赖对象是"模块"而非"服务键";只在启动时校验一次,不响应变化 |
| `provide`(供给键集合) | `context.Services.Register<T>(service)` | 有键值语义(Type→实例),但无声明、无双向绑定、撤回不级联 |
| 满足谓词 + notify/refresh | **无** | 依赖缺失时抛异常(`SortInstallers` 直接报错),而非等待/停用 |
| 声明式配置树 + 协调器 | `ShrinkAppSettings.disabledModuleIds`(一维开关) | 无条目树、无 config 变更分发、无增量协调 |
| 上下文树/父子级联 | 无(安装器扁平列表拓扑排序) | — |
| isolate / intercept | 无 | 多租户测试、按组件降权等能力缺失 |
| HMRdispose + re-use | ModFramework 外部 DLL **只增不减**热载 | 有"加载"半程,无替换/回滚半程 |
| 组件加载器层 | ShrinkApp.BootstrapBeforeSceneLoad 一次性跑完) | 无持续协调循环 |
| 应用框架层(Koishi 位) | EventBus/DataSaver/Network/Command/Tutorial | 这层不需要"改造",需要的是**改坐在新地基上** |
**结构性发现(校准后)**`Integration.App` 的一次性安装器包装应被原生组件和组合根取代;`Integration.EventBus` / Command-Network 则应收缩为注入双方服务键的薄集成组件。范式切换消除的是隐式启动、反射发现和手写依赖编排,不要求把具有真实跨领域职责的 Integration 包全部删除。
---
## 3. Unity / C# 特有约束与对策
Cordis 原文(§6.4)已论证范式语言无关,并给出各语言的接入条件。针对 Unity 2022.3 / C#
| 约束 | 论文对应讨论 | 对策 |
|---|---|---|
| **程序集不可卸载**Mono/IL2CPP 无 collectible ALC;论文:托管运行时需模块注册表可驱逐) | §6.4 时间可组合性第二要求 | **降级语义**:可回滚的是"组件实例 + 其效应",不是"类型/程序集"。外部 DLL 模组卸载 = dispose 全部 fiber + 程序集留内存(与 ModFramework 现有边界一致,但把"实例级回滚"补全)。IL2CPP 下外部 DLL 动态加载本就不可用 |
| **无 JS Proxy** | §6.4:无拦截原语时用运行时反射或**编译期元编程** | 已有 `ShrinkShared.CodeGen`Cecil ILPostProcessor)正好承担"编译期元编程"角色:生成 inject/provide 声明、强类型键访问器(对应论文中 Rust 过程宏的位置) |
| **异步模型** | create_task / Promise | `UniTask` 一一对应:`apply : Func<ShrinkCtx, UniTask>`fiber.inertia = `UniTask` 句柄;逆操作 `Func<UniTask>` |
| **Domain Reload / Enter Play Mode** | —(Node 单进程长生) | 上下文树挂静态根 + `[SubsystemRegistration]` 重置(沿用 `ShrinkApp.ResetStaticState` 模式) |
| **静态门面传统**EventBus.TriggerEvent 等静态单例) | §3.3.3 批评服务定位器;§6.7 指出库实现中上下文可被闭包误用 | 允许"默认上下文"静态门面作为过渡兼容层,但新代码一律显式 ctx;EventBus 实例成为默认上下文上的一个键 |
| **主线程亲和**Unity API 只能在主线程) | — | 现有 `ShrinkNetworkDispatchQueue` 模式保留;fiber 转换全部调度到主线程泵(`PlayerLoop`/`UniTask` 主线程) |
---
## 4. 目标组织形式
### 4.1 模块拓扑(改造后)
```text
Assets/Modules/
├── ① 元框架层(新增,Cordis 位)
│ ├── ShrinkContext.Core/ # ctx / effect / coeffect store / fiber / 生命周期
│ │ ├── Runtime/Context/ # ShrinkCtx、EffectAccumulator、上下文树
│ │ ├── Runtime/Coeffects/ # get/set/isolate/intercept、notify
│ │ ├── Runtime/Fibers/ # Fiber、refresh/reload/unload、惯性状态机
│ │ └── Runtime/Loader/ # 声明式配置树、增量协调器、条目双向绑定
│ ├── ShrinkContext.UnityHost/ # Unity 接地:主线程泵、Domain Reload 重置、场景桥
│ └── ShrinkContext.CodeGen/ # 并入现有 ShrinkShared.CodeGeninject/provide 织入、访问器生成
├── ② 加载器配置层
│ └── ShrinkApp.Core 演进为加载器壳 + 兼容适配;Starter 演进为配置树生成器)
├── ③ 应用框架层(现有功能模块,改造为组件)
│ ├── ShrinkEventBus/ # 保留;另提供 ShrinkContext 适配(订阅=效应)
│ ├── ShrinkDataSaver/ ShrinkNetwork/ ShrinkCommand/ ShrinkTutorial/
│ │ # 各自成为组件:声明 inject/provideapply=原初始化逻辑
│ └── ShrinkModFramework/ # 与 Fiber 模型合流:外部 DLL=延迟组件源;热载=dispose+re-use
└── *.Integration.* 七个桥接包 → 大部分废弃,逻辑并入对应组件或核心键适配)
```
### 4.2 组件形态(C# 化的目标 API 草案)
```csharp
// 组件 = (inject, provide, apply)
[ShrinkComponent("shrink.network")] // 由 ShrinkContext.CodeGen 织入注册表
public sealed class ShrinkNetworkComponent : IShrinkComponent
{
public static readonly string[] Inject = { ShrinkKeys.EventBus, ShrinkKeys.Config }; // d:依赖键
public static readonly string[] Provide = { ShrinkKeys.NetworkService }; // p:供给键
public async UniTask ApplyAsync(ShrinkCtx ctx, JsonNode config) // e:效应函数
{
var service = new ShrinkNetworkService(...);
ctx.Set(ShrinkKeys.NetworkService, service); // set 是效应:自动追踪、卸载自动撤回
await using var sub = ctx.Effect(() => // 显式可逆效应
UniTask.FromResult(() => service.Shutdown()));
// ... bind transport 等
}
}
// 编排 = 声明式配置树(持久化为 JSON/YAML 资产)
// entry: { id, component, isolate, intercept, config, disabled }
await loader.ApplyConfigAsync(configTree); // 增量协调:diff → reload/unload/原地更新
```
要点:
- `ctx.Effect(callback)` 中 callback 返回逆操作(`Func<UniTask>``IAsyncEnumerable<Func<UniTask>>` 支持分步效应);
- `IShrinkEventSubscription``IDisposable` 天然是逆操作,写一行适配即可纳入;
- 供给键建议 `static class ShrinkKeys`(或 CodeGen 从 `Provide` 生成常量),解决论文 §6.6 的键命名空间问题(`"shrink.network/service"` 风格);
- 静态门面(`ShrinkNetworkRuntime.Default` 等)在过渡期指向默认上下文的键解析结果。
### 4.3 各现有模块的具体去向
| 现模块 | 去向 |
|---|---|
| `ShrinkApp.Core` | Host/Services 退役;`ShrinkAppSettings.disabledModuleIds` → 配置树 disabled 字段;拓扑排序 → 依赖驱动的响应式激活(不再需要启动期全排序,缺依赖的组件静默等待) |
| `*.Integration.App` ×3 | **废弃**——其内容(把服务放进容器)= 组件的 `ctx.Set` |
| `*.Integration.EventBus` ×3 | 收缩为一层薄适配:DataSaver 事件→EventBus 键上的发布;Network 事件桥同理。桥的"自动接入反射探测"被键解析取代 |
| `ShrinkCommand.Integration.Network` | 保留语义,实现改为:Network 组件与 Command 组件之上的一个普通集成组件(论文 §6.5 的"集成组件"模式) |
| `ShrinkModFramework` | 模组 = 延迟组件源(外部 DLL 扫描产出组件注册);`IShrinkMod` 四阶段生命周期映射为 apply 内部分步效应;Harmony 补丁声明为效应(PatchAll 的逆 = UnpatchAll——Harmony 本身支持,正好构成可逆对);目录监听热载 = dispose 旧 fiber + `use` 新组件 |
| `ShrinkShared.CodeGen` | 扩展为 `ShrinkContext.CodeGen` 的宿主:新增 `[ShrinkComponent]` 注册表织入、`Inject/Provide` 校验、强类型访问器生成 |
| `GeneratedServers` 独立服务器 | 独立宿主用自己的 `ShrinkContext` 实例(论文 §6.2 跨进程:每进程一个上下文树);服务器模块体系 `IShrinkServerModule` 与 fiber 合流 |
| EventBus / DataSaver / Tutorial 内核 | **基本不动**——它们是被组合的领域库;改动集中在"谁调用初始化"(从 Bootstrap/Installer 改为组件 apply |
---
## 5. 分阶段实施路线(每阶段独立可验证)
### 阶段 0:纸面与原型(不改生产代码)✅ 已完成(2026-08-16
**产出:`Assets/Modules/ShrinkContext.Core/`(包 `com.cneicy.shrink-context-core` 0.1.0+ 21 个 EditMode 测试全部通过。**
- 核心库落地:`ShrinkCtx`(效应累积/隔离派生/介导访问)、`ShrinkEffectHandle`(算法 1armed 单次执行、LIFO、dispose 等待进行中前向)、`ShrinkContextRuntime`(算法 2-5set 可逆绑定、notify、Use/Retire、惯性状态机 RunTransitionAsync、drain-before-inverse)、`ShrinkFiber`uid 目标、提交视图、Retired)、`IShrinkComponent` / `IShrinkIterativeComponent``ShrinkContextDefaults`Domain Reload 重置)。
- 测试对照论文元理论:定理 7/16/20 行为(恢复初始、选择性撤销、LIFO)、算法 6 介导纪律(未声明/未激活拒绝)、定义 46 uid 语义(同值替换仍重载)、L-Unload 守卫(依赖者先排空)、4.3.3 惯性(装载中途退役永不 Active、链式卸载)、定理 64(步进边界部分回滚)、L-Raise(apply 失败部分回滚)、循环依赖静止、供给冲突、父子级联、隔离域丢弃恢复、Domain Reset。
- **实现教训(已记入模块 README**:Mono 的 `IAsyncEnumerable`/`ValueTask` 迭代器消费端恢复经同步上下文调度、不内联,破坏确定性语义——分步效应改用自定义 `IShrinkStepEffectEnumerator`(UniTask 步进协议)后恢复全链路内联。
- 验证:`ShrinkContext.Core.Tests` 21/21 通过;全仓 EditMode 138 个测试中 8 个失败**全部位于既有 `ShrinkDataSaver.Tests`**(缺 `LogAssert.Expect``ShrinkSettings.SaveAsync` 空引用等测试侧问题),与本次纯新增改动无关(git 确认无既有文件修改)。
**下一步决策点**:原型语义已验证,是否进入阶段 1/2(加载器 + ShrinkApp 迁移)取决于产品侧对运行时重配(热插拔/热替换)的需求确认。
### 阶段 1:核心库落地
- `ShrinkCtx`get/set/isolate/intercept/effect/use)、`Fiber`、notify/refresh/reload/unload。
- 验收:两个玩具组件 A(provide `k`)、Binject `k`)——加载 A→B 激活;卸载 A→B 自动停用且上下文恢复;B 换提供者→B 重载。
### 阶段 2:加载器 + ShrinkApp 迁移 ✅ 完成(2026-08-16
**已完成:**
- **声明式加载器**`ShrinkContext.Core/Runtime/Loader/`):`ShrinkLoaderEntry`id/component/config/disabled/isolate+ `ShrinkComponentCatalog`(名字→工厂,不做全域反射)+ `ShrinkContextLoader.ApplyAsync` 增量协调——条目消失→退役(依赖者响应式停用但保持被管理,依赖回归自动复活);disabled→卸载/重载;组件/配置变化→重建;无变化→幂等不扰。10 个测试覆盖含隔离域条目(同键不同 realm 共存)。
- **ShrinkApp 适配器**(新包 `ShrinkContext.AppAdapter`):`ShrinkAppInstallerComponent` 把现有 `IShrinkAppModuleInstaller` 包装为组件——`DependsOn` 映射为 `app.module.*` 注入键,**依赖缺失从"排序期抛错"变为"非活动等待"**;重复 ModuleId 从"宿主级异常"退化为"该纤程供给冲突失败";激活次序由依赖关系结构性保证。5 个测试验证行为差异。
- **加载器宿主与模式切换**ShrinkApp.Core 0.1.1 + 适配器):`ShrinkAppSettings.hostingMode`ClassicHost/ContextLoader)双模式并存,经典路径零行为变化;`ShrinkAppLoaderHost` + `ShrinkAppLoaderBootstrapper` 自动引导并接回 `ShrinkApp.Services``SetModuleDisabledAsync` 运行中按模块开关。LoaderHost 6 个测试。
- **PlayMode 实测**Starter 调试台场景 ShrinkAppEntryContextLoader 模式):加载器宿主激活全部 3 个真实模块(shrink.command / shrink.datasaver / shrink.network),经典宿主无双启动、无错误日志;运行中 disable→enable shrink.network 增量生效,宿主与其余模块不重启。
- 验证:全仓 EditMode **159/159 全绿**(含修复后的 DataSaver 83 个)。
**顺带修复的既有隐患**`Assets/Resources/ShrinkAppSettings.asset` 的脚本链接曾为 `m_Script: fileID:0`,运行时 `Resources.Load` 会静默走默认配置。本次接手时发现问题仍存在,已通过 Unity Editor API 删除无效主对象并在同路径重建;MCP 读回确认为 `ShrinkApp.ShrinkAppSettings``hostingMode = ContextLoader`
**阶段 2 原边界的关闭情况**:阶段 3 已补 `ShrinkAppServices.TryUnregister(instance)`,原生组件门面可逆注销且不会误删新替代者;`ShrinkApp.IsRunning` 已接入外部 LoaderHost 状态。仍保留的边界是 config 变化统一重建、编排配置仍由代码构建,持久化配置资产(ScriptableObject/JSON)与调试台 UI 开关按钮属后续打磨。
### 阶段 3:功能模块组件化 ✅ 默认组合主路径完成(2026-08-16)
**已完成:**
- **核心 API 补充**`ShrinkCtx.EffectInverse(inverse)`——前向已在外部执行完毕的效应只登记逆操作,供桥接适配器把既有"注册/注销"对包装为可逆效应。
- **EventBus 订阅=效应**(新包 `ShrinkContext.EventBusAdapter`):`EffectSubscribe<TEvent>``IShrinkEventSubscription.Dispose` 即逆操作,ctx 卸载自动退订)与 `EffectRegister(target)`(实例对象注册/注销对)。3 个测试:上下文回滚自动退订、手动退订选择性、实例注册随回滚注销。
- **Network 组件化**`ShrinkNetwork.Integration.App` 新增 `ShrinkNetworkAppComponent`):提供 `shrink.service.network` 余效应键(依赖者按键注入);loopback 传输绑定为**可逆效应**——停用时会话关闭、传输解绑(挂起 RPC 失败、会话清理由 BindTransport(null) 路径触发)、服务键撤回、依赖者停用,**全部为效应回滚而非手写卸载**。与经典 installer 二选一(同键供给冲突保护)。3 个测试:绑定+键、停用回滚+依赖者停用、重启再绑定(相对会话数断言,避免静态单例污染)。
- **PlayMode 热插拔实测**(验收项):Use→Active(自建会话+1)→ Retire→Inactive(会话-1、服务键撤回,与 Starter 演示自身的绑定互不干扰)→ 再 Use 恢复。无错误日志。
- 验证:全仓 EditMode **165/165 全绿**
**阶段 3 第二片(同日)**
- `ShrinkCommandAppComponent``shrink.service.command` 键)与 `ShrinkDataSaverAppComponent``shrink.service.datasaver` 键;初始化为前向,停用补偿式收尾=触发式设置落盘,不阻塞停用链;EditMode 下 DontDestroyOnLoad 驱动按 `Application.isPlaying` 关闭)。
- `ShrinkCommandNetworkIntegrationComponent`(论文 6.5 集成组件模式):注入双服务键(command+network),双提供者齐备才激活,任一退役即停用、回归即复活——依赖接线由响应式余效应结构性完成。已知边界:ShrinkNetworkService 无 handler 注销 API,停用不撤销 command/execute 处理器(传输解绑后不可达)。
- 4 个集成测试(含"仅命令就绪仍等待网络"的半就绪断言)。全仓 EditMode **169/169 全绿**
**阶段 3 第三片(本次接手)**
- **默认组合根落地**`ShrinkAppLoaderBootstrapper.DefaultComposition` 在宿主启动前接入 Starter 装配表;`ShrinkAppBasicContextComposition` 用三个原生 App 组件覆盖旧 installer 条目,并通过 `AddModuleComponent` 加入 Command-Network 与三个 EventBus 薄适配,共 7 个声明式模块。
- **兼容门面可逆化**Command/DataSaver/Network 原生组件把 `ShrinkApp.Services` 注册视为效应并登记逆操作;模块退役时按实例注销。LoaderHost 同时接回 `ShrinkApp.IsRunning`,加载器模式不再出现双重状态口径。
- **EventBus 桥收缩**:新增 Command/DataSaver/Network 三个按服务键注入的集成组件;移除 Command/Network 的程序集加载时默认注册与 Network Core 的可选程序集反射接桥;DataSaver 改为显式、可注销桥。仅安装桥包不再产生全局业务副作用。
- **旧安装器降级为兼容面**:三个 `*.Integration.App` installer 标记 `[Obsolete]`,仅供 ClassicHost 继续发现;ContextLoader 默认路径不再执行 installer 包装。
- **端到端测试升级**`NativeWiringTests` 直接调用 Basic Starter 组合根,覆盖 7 模块/7 键/3 门面、Command 停用导致两个依赖集成先退出、门面撤回、重新启用恢复、Shutdown 全清退。全仓 EditMode **171/171 全绿**Unity 编译 **0 error / 0 warning**
- **真实 Play Mode**`Assets/Scenes/ShrinkAppEntry.unity`、有效 ContextLoader 设置下启动为 **7 Active / 0 Waiting**;运行中禁用 Command 后保留 4 Active、两个 Command 集成 Waiting,三个相关键与门面均撤回;重新启用恢复 **7 Active / 0 Waiting**。控制台无错误,仅有与本迁移无关的缺省 `ShrinkModFrameworkSettings` 警告。
**阶段 3 保留边界**ClassicHost 与静态服务门面仍作为兼容层存在;Command-Network 的服务 handler 暂无逐项注销 API;全局静态桥在多根上下文并行时仍需引用计数或实例化。这些不阻塞单默认根主路径,但不能误写成完全隔离保证。
### 阶段 4ModFramework 合流 + 热替换 ✅ 完成(2026-08-16
- 新增 `ShrinkModComponentSource` / `ShrinkModContextHost`:将 `IShrinkMod` 的构造、注册、初始化、Ready 生命周期纳入 Cordis apply;模组效应通过本地逆操作登记,Registry、Network handler、Harmony lease 均可随组件退役清理。
- 事务单位固定为 `ModId + revision`:同一 revision 幂等复用;替换失败时保留 `LastError` 并重新应用上一组 source,旧模组实例、generation 与已登记内容恢复。
- 外部 DLL 按 SHA-256 扫描当前 revision;文件内容变化创建新的组件源,旧程序集仍驻留但不会继续被发现,坏 DLL 不覆盖当前有效 revision。扫描状态随 Host 事务一起快照/恢复,失败替换不会把失败 revision 留在当前缓存中。
- `ShrinkModLoader` 默认走 `ShrinkModContextHost``useContextHost=false` 保留旧加载路径作为兼容边界。Harmony 在 ContextHost 路径取得可逆 lease,旧路径仍是一次性应用。
- `ShrinkModRuntimeDriver` 监听新增、修改、删除、重命名,并用独立 debouncer 把 burst 事件折叠为一次主线程 reload;删除文件会提交完整期望组合,依赖缺失在协调前拒绝并保留旧组合。
- 真实 fixture 验收覆盖:编译 DLL `valid → changed(fail) → restore`、损坏字节不污染当前 revision、有效 revision 恢复、watcher burst 去抖、删除卸载、依赖 DLL 删除/恢复。`ShrinkModFramework.Tests` **9/9**,全仓 EditMode **180/180**
- 真实 Starter PlayMode 确认 `modCordis=true``modLoaded=true`、宿主 7 个模块 active 且无错误。Unity 程序集不可卸载,因此回滚单位仍是组件实例与效应,而不是类型本身。
### 阶段 5:生产化边界、隔离与拦截 ✅ 完成(2026-08-17
#### 阶段 5A:访问契约、诊断与通知索引 ✅ 首批完成(2026-08-17
- 运行时强制组件只能向 `Provide` 声明的键写入;根上下文的 ambient 绑定保留为显式系统边界。
- 提供稳定的 runtime/fiber 诊断快照:uid、组件名、状态、target、committed provider、realm、是否退役、是否转换中、最近错误。
- 提供依赖等待关系与加载器事务诊断,使失败能定位到条目、fiber、阶段和恢复结果。
- 将 notify 从遍历全部 fiber 改为 `key → inject fibers` 倒排索引;realm 仍在触发时精确过滤,复杂度由 O(all fibers) 收敛为 O(affected-by-key)。
- 强类型/版本键以新增契约逐步引入,不在本阶段强行改写全部现有字符串键调用点。
**验收:** 未声明供给在 apply 阶段被拒绝并完整回滚;诊断快照能解释 Active/Waiting/Failed/Unloading;索引不会遗漏不同 realm 下的合法依赖者;既有生命周期测试全部保持通过。
**已落地:** `Provide` 写入约束、`ShrinkKey<T>` 版本键、runtime/fiber/依赖/notify 诊断快照、loader 事务与恢复结果、`key → inject fibers` 倒排索引均已实现。target 仍是 provider uid 视图,但现在保留 inject 声明顺序和重复键,不再退化成无序 provider 集合。
#### 阶段 5Bisolate 隔离验证与 intercept ✅ 首批完成(2026-08-17
- loader 支持隔离域条目;同一组件在多个 realm 中独立运行,替换其中一个 realm 的提供者不扰动另一个 realm。
-`ctx.intercept(key, metadata)` 的派生上下文与元数据合并;策略改变依赖的使用方式,不改变满足关系,也不因策略变化触发 fiber 重载。
- 访问介导使用明确的服务策略/包装接口,不把 Harmony 或通用反射 AOP 当成默认实现。
- 以“社区模组只读 DataSaver”为真实验收:读取允许,写入拒绝,核心组件仍保留完整能力。
**验收:** 多 realm 激活/替换/卸载互不串扰;intercept 更新不改变 provider uid 和 fiber generation;未声明访问、未激活访问与策略拒绝三类错误可以区分;文档明确其不是不可信代码沙箱。
**已落地:** loader 条目可携带 isolate 与 interceptintercept 元数据按上下文链合并并在条目原位更新,不触发 fiber 重载。DataSaver 提供 `Reader / Writer` 能力拆分,社区上下文可取得只读包装,写接口与具体服务请求被策略拒绝。当前 isolate 条目变化仍通过重建该条目生效;运行中 fiber 原位迁移 realm 未实现,也不计入本批完成项。
#### 阶段 5C:外部 DLL 常驻 revision 策略 ✅ 首批完成(2026-08-17
- 暴露当前 revision、历史程序集数量、来源路径与累计载入字节。
- 增加软阈值和明确告警;超过阈值时建议 Domain Reload/进程重启,不尝试在 Mono 上伪造程序集卸载。
- 长时间回归覆盖连续有效替换、失败恢复、损坏 DLL、删除/恢复与历史 revision 增长。
**验收:** 每次替换后当前 revision 与生效组件一致;失败 revision 不成为 current;常驻增长可查询、可告警且不影响旧组合恢复。
**已落地:** `ShrinkModDiagnostics.CaptureExternalAssemblies` 暴露 current/history、路径、程序集、revision、载入字节与软阈值状态;达到 `externalAssemblyRevisionSoftLimit` 时只给出 Domain Reload/进程重启建议。真实 DLL fixture 覆盖有效、失败、再有效三个 revision,确认失败程序集可以常驻但不会成为 current。
#### 阶段 5D:配置与调试体验 ✅ 完成(2026-08-17
- 将代码构建的默认组合逐步映射到 ScriptableObject/JSON 条目,并保持代码目录作为组件工厂来源。
- 提供 Editor/运行时调试面,查看 active/waiting/failed fiber、provider target、最近事务和常驻程序集统计。
- 建立 notify、重载延迟、失败恢复和常驻内存的基准,避免仅以功能测试替代容量判断。
**已落地:** `ShrinkAppCompositionProfile / Document` 将条目启用、显式排除、isolate 与 intercept 映射为 ScriptableObject/JSON;组件工厂继续由代码组合根注册,配置不能反射实例化任意类型。Basic Starter 提交 `Resources/ShrinkAppComposition.asset`,生成器可创建或修复该资产。`ShrinkSDK/Cordis/诊断与组合` 在 Play Mode 展示 fiber、等待依赖、target、最近事务和外部 Assembly 常驻快照,并提供 Profile JSON 导入/校验/写回/导出。Editor 基准可重复测 notify/reload 与失败恢复;耗时和 GC 堆差值只作同机对比,自动测试断言结构性规模。
**阶段 5 明确非目标:** 不在本阶段实现不可信 DLL 沙箱;不承诺已发出的网络数据或外部文件写入可以真正撤回;不删除 ClassicHost 兼容面,除非其调用方已完成独立迁移验证。
**2026-08-17 验证基线:** Unity 编译 **0 error / 0 warning**;全仓 EditMode **193/193**PlayMode Test Runner 当前没有测试项,因此另行启动真实 `Assets/Scenes/ShrinkAppEntry.unity` 验收:`hostingMode=ContextLoader`、Profile 已应用且显式列出 7 个条目、7 个模块全部 Active、0 Waiting/Failed、10 个绑定、3 个 notify 索引键,启动与退出过程无控制台 error。容量基准在 1000 个无关 fiber、100 次 provider 重载下记录 400 次 indexed candidate visit,对照全量扫描估算 440400 次;25/25 次失败替换均恢复旧组合。本机单次耗时样本约为 notify/reload 1.9 ms、失败恢复 2.8 ms,仅作后续同机对比,不作为跨机器承诺。
---
## 6. 风险与代价(决策前必须直视)
1. **工程量大且不可半途而废**:阶段 2 之前核心库没有业务价值;一旦功能模块开始迁移,新旧两套启动模型并存会持续产生双重维护成本。当前通过 ContextLoader 默认主路径 + ClassicHost 兼容层控制过渡面,阶段 4 不应再新增第三套生命周期。
2. **静态门面惯性与范式冲突**:全仓库大量 `EventBus.TriggerEvent` 式静态调用点,改显式 ctx 是长尾体力活。缓解:默认上下文门面长期保留(论文 Koishi 也保留了领域词汇层)。
3. **Unity 生命周期摩擦**MonoBehaviour 的 Awake/OnDestroy 与 fiber 生命周期是两套时钟(EventBus 现有 IL 织入恰好横跨两者)。桥接策略需要在阶段 1 原型中先行验证,这是最容易返工的点。
4. **性能**:论文未给定量开销数据(§5.3 自认是存在性证据)。每键每组件的 notify 遍历在高频键上需做索引(`key → inject 它的 fiber 集`),核心库设计时就要按 O(affected) 而非 O(all fibers) 实现。
5. **回报的位置要认清**Cordis 的核心回报是"动态卸载/热替换/依赖响应"。ShrinkSDK 若长期只有"启动时固定组合",改造成纯支出。**先确认产品侧确实需要运行时重配(如模组热插拔、专用服运行中换后端、编辑器内重载),再启动阶段 2。**
6. **论文未覆盖的仍需自建**:版本协商、键的版本兼容(§6.6 明说是 open problem,用键命名空间 + 包版本约束缓解——UPM 依赖恰好能承担 sibling dependency 角色)。
---
## 7. 结论
- ShrinkSDK 与 Cordis 的**分层直觉一致**(核心库 / 编排层 / 领域层),差距集中在**核心库的两个原语缺失**:统一可逆效应追踪(时间)与响应式依赖解析(空间)。
- 改造的本质不是重写功能模块,而是**把 ShrinkApp 的"一次性安装器"升级为 Cordis 的"持续协调的组件加载器"**,并让七个 Integration 桥接包退化为薄适配直至消失。
- Unity 的"程序集不可卸载"不阻塞该范式——可回滚单位是组件实例与效应,而非类型;这与 ModFramework 既有边界声明兼容。
- 阶段 0-4 已完成到 Basic Starter 的默认运行主路径与 ModFramework 事务性 HMR;阶段 5 已完成访问契约、诊断/notify 索引、isolate/intercept、程序集常驻策略、配置资产、调试面与容量基准。后续应以真实项目负载持续采样和收紧领域策略,不再新增第三套生命周期。