37 KiB
ShrinkSDK × Cordis 组织形式改造方案
生成日期:2026-08-16;独立重读与阶段 5 路线更新:2026-08-17 参考文献:《Cordis: A Programming Paradigm for Spatiotemporal Composability》(北京大学 / DeepSeek-AI,88 页,
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 本次独立重读后的校准
- Cordis 不是“DI + Dispose”。核心纪律是组件对环境的读写都归属于自己的上下文:写入才能被归因并自动求逆,读取才能由余效应介导。静态服务定位器只能作为系统边界或兼容门面;经它发生的隐式访问不在主保证内。
- 时间可组合性不等于有 cleanup 钩子。逆操作必须和前向效应局部绑定、按应用顺序记录并 LIFO 回放。若两个组件会以任意顺序卸载,它们的效应要么在观察等价下独立/可交换,要么把顺序敏感性显式变成余效应依赖。
- 空间可组合性的关键是两步 Leave/Unload:提供者先停止对外供给并排空所有消费者;消费者卸载期间仍通过 committed view 读取旧提供者;最后才执行提供者逆操作。仅在每次
Inject变化后重新检查是否满足,并不足以保证安全卸载。 - 提供者身份不是值相等。fiber uid 必须进入目标视图;即便新旧服务值相同,只要提供者变了,消费者也要重载。异步转换遵循惯性:已经开始的一步要落地,再根据新 target 转入后续 unload,不能在任意 await 点宣称“取消即回滚”。
- 失败是纤程局部事务。apply 失败必须回滚已累积的部分效应,进入失败/非活动状态,不能自动无限重试;显式配置或依赖变化再触发下一次尝试。
- 加载器条目 id 是双向绑定的稳定主键。配置变更应优先交给组件做最小 diff,而不是永久依赖统一重建。HMR 还必须是事务:模块分类、陈旧条目计算、缓存备份/失效、替换;新模块导入失败时完整恢复缓存与旧条目。只有
dispose + use不能称为论文语义的 HMR。 - 系统边界决定“可逆”的上限。获取 socket、注册 handler、订阅事件可以回滚;已经发出的网络字节、写入的文件/存档不能被真正撤回,只能在提交前扣留输出,或执行领域补偿。Network/DataSaver 的组件说明和阶段 4 验收必须区分这两类效应。
- 跨领域集成组件不是过渡瑕疵,而是论文 §6.5 的正式答案。Command-Network、模块-EventBus 这类双向/循环关系应由一个注入双方服务键的薄组件承载,核心包保持互不依赖。真正应淘汰的是隐式反射接桥和安装器包装,不是所有 Integration 包。
- 字符串键仍有碰撞与接口漂移风险。当前先使用命名空间化键;后续应把包版本约束、结构兼容检查或强类型生成键纳入加载器边界。
1.6 对 ShrinkSDK 的工程判断
综合论文第 3~6 节与当前实现,我对 Cordis 在 ShrinkSDK 中的定位是:
- 它应当是生命周期与组合语义的唯一地基,不是新的业务框架。 EventBus、DataSaver、Network、Command、Tutorial 仍然保留自己的领域 API;Cordis 只负责回答“谁拥有这些副作用、依赖变化时谁该运行、组件退出时如何恢复”。后续不能再新增第三套启动/卸载模型。
- 阶段 0~4 已经覆盖论文最关键的正确性路径。 当前
ShrinkContextRuntime已具备 provider uid、committed view、惯性转换、部分回滚和 drain-before-inverse;ShrinkModContextHost已把外部 DLL 的组件源、revision 缓存与旧组合恢复纳入同一事务。接下来主要是把这些保证变得可约束、可诊断、可运营,而不是重新实现 fiber。 - 当前最大语义缺口是“声明没有被完全强制”。
IShrinkComponent.Provide目前仍主要依赖作者纪律,运行时没有拒绝组件写入未声明键;字符串键也只提供名义连接,没有类型和版本兼容保证。若不先固定访问契约,intercept 只能成为一层不可靠的包装。 - 当前最大工程缺口是“发生问题时看不见”。 fiber 已有状态和
LastError,但缺少稳定的运行时快照、事务阶段、依赖等待链和 revision 常驻统计。动态组合系统一旦发生等待、回滚或程序集常驻增长,必须能从诊断面直接回答原因。 - intercept 是访问介导,不是安全沙箱。 它适合表达“社区模组只能只读 DataSaver”“某组件不能访问某类网络能力”等宿主可控策略;但外部 DLL 仍可绕开 ctx 直接调用 CLR/Unity API。不可信代码必须另用进程、WebAssembly 或其他执行隔离,不能把语言层访问控制写成安全边界。
- 程序集不可卸载必须转化为容量策略。 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 | 无 | 多租户测试、按组件降权等能力缺失 |
| HMR(dispose + re-use) | ModFramework 外部 DLL 只增不减热载 | 有"加载"半程,无替换/回滚半程 |
| 组件加载器层 | ShrinkApp.Bootstrap(BeforeSceneLoad 一次性跑完) | 无持续协调循环 |
| 应用框架层(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 模块拓扑(改造后)
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.CodeGen:inject/provide 织入、访问器生成
│
├── ② 加载器配置层
│ └── (ShrinkApp.Core 演进为加载器壳 + 兼容适配;Starter 演进为配置树生成器)
│
├── ③ 应用框架层(现有功能模块,改造为组件)
│ ├── ShrinkEventBus/ # 保留;另提供 ShrinkContext 适配(订阅=效应)
│ ├── ShrinkDataSaver/ ShrinkNetwork/ ShrinkCommand/ ShrinkTutorial/
│ │ # 各自成为组件:声明 inject/provide,apply=原初始化逻辑
│ └── ShrinkModFramework/ # 与 Fiber 模型合流:外部 DLL=延迟组件源;热载=dispose+re-use
│
└── (*.Integration.* 七个桥接包 → 大部分废弃,逻辑并入对应组件或核心键适配)
4.2 组件形态(C# 化的目标 API 草案)
// 组件 = (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(算法 1:armed 单次执行、LIFO、dispose 等待进行中前向)、ShrinkContextRuntime(算法 2-5:set 可逆绑定、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.Tests21/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)、B(injectk)——加载 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 调试台场景 ShrinkAppEntry,ContextLoader 模式):加载器宿主激活全部 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.Appinstaller 标记[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;全局静态桥在多根上下文并行时仍需引用计数或实例化。这些不阻塞单默认根主路径,但不能误写成完全隔离保证。
阶段 4:ModFramework 合流 + 热替换 ✅ 完成(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.Tests9/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 集合。
阶段 5B:isolate 隔离验证与 intercept ✅ 首批完成(2026-08-17)
- loader 支持隔离域条目;同一组件在多个 realm 中独立运行,替换其中一个 realm 的提供者不扰动另一个 realm。
- 补
ctx.intercept(key, metadata)的派生上下文与元数据合并;策略改变依赖的使用方式,不改变满足关系,也不因策略变化触发 fiber 重载。 - 访问介导使用明确的服务策略/包装接口,不把 Harmony 或通用反射 AOP 当成默认实现。
- 以“社区模组只读 DataSaver”为真实验收:读取允许,写入拒绝,核心组件仍保留完整能力。
验收: 多 realm 激活/替换/卸载互不串扰;intercept 更新不改变 provider uid 和 fiber generation;未声明访问、未激活访问与策略拒绝三类错误可以区分;文档明确其不是不可信代码沙箱。
已落地: loader 条目可携带 isolate 与 intercept;intercept 元数据按上下文链合并并在条目原位更新,不触发 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. 风险与代价(决策前必须直视)
- 工程量大且不可半途而废:阶段 2 之前核心库没有业务价值;一旦功能模块开始迁移,新旧两套启动模型并存会持续产生双重维护成本。当前通过 ContextLoader 默认主路径 + ClassicHost 兼容层控制过渡面,阶段 4 不应再新增第三套生命周期。
- 静态门面惯性与范式冲突:全仓库大量
EventBus.TriggerEvent式静态调用点,改显式 ctx 是长尾体力活。缓解:默认上下文门面长期保留(论文 Koishi 也保留了领域词汇层)。 - Unity 生命周期摩擦:MonoBehaviour 的 Awake/OnDestroy 与 fiber 生命周期是两套时钟(EventBus 现有 IL 织入恰好横跨两者)。桥接策略需要在阶段 1 原型中先行验证,这是最容易返工的点。
- 性能:论文未给定量开销数据(§5.3 自认是存在性证据)。每键每组件的 notify 遍历在高频键上需做索引(
key → inject 它的 fiber 集),核心库设计时就要按 O(affected) 而非 O(all fibers) 实现。 - 回报的位置要认清:Cordis 的核心回报是"动态卸载/热替换/依赖响应"。ShrinkSDK 若长期只有"启动时固定组合",改造成纯支出。先确认产品侧确实需要运行时重配(如模组热插拔、专用服运行中换后端、编辑器内重载),再启动阶段 2。
- 论文未覆盖的仍需自建:版本协商、键的版本兼容(§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、程序集常驻策略、配置资产、调试面与容量基准。后续应以真实项目负载持续采样和收紧领域策略,不再新增第三套生命周期。