Files
Workspace/CORDIS_MIGRATION.md
T

37 KiB
Raw Blame History

ShrinkSDK × Cordis 组织形式改造方案

生成日期: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);
  • 复合效应的逆由组合自动推导——组件的拆卸从其加载过程推导出来,而非并列手写
  • 实现形态就是一个 APIctx.effect(callback) 返回 dispose 闭包。

响应式余效应(reactive coeffect)——空间维度的原语:

  • 依赖是键值表 Σ : K ⇀ Vctx.set(key, value) 提供依赖、ctx.get(key) 消费依赖;
  • 组件声明规范 d ⊆ Kinject),满足谓词 σ ⊨ 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-inverseShrinkModContextHost 已把外部 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(一等上下文) ShrinkAppContextHost+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.CodeGenCecil 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.CodeGeninject/provide 织入、访问器生成
│
├── ② 加载器配置层
│   └── (ShrinkApp.Core 演进为加载器壳 + 兼容适配;Starter 演进为配置树生成器)
│
├── ③ 应用框架层(现有功能模块,改造为组件)
│   ├── ShrinkEventBus/                # 保留;另提供 ShrinkContext 适配(订阅=效应)
│   ├── ShrinkDataSaver/ ShrinkNetwork/ ShrinkCommand/ ShrinkTutorial/
│   │                                  # 各自成为组件:声明 inject/provideapply=原初始化逻辑
│   └── 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>> 支持分步效应);
  • IShrinkEventSubscriptionIDisposable 天然是逆操作,写一行适配即可纳入;
  • 供给键建议 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)、ShrinkFiberuid 目标、提交视图、Retired)、IShrinkComponent / IShrinkIterativeComponentShrinkContextDefaultsDomain Reload 重置)。
  • 测试对照论文元理论:定理 7/16/20 行为(恢复初始、选择性撤销、LIFO)、算法 6 介导纪律(未声明/未激活拒绝)、定义 46 uid 语义(同值替换仍重载)、L-Unload 守卫(依赖者先排空)、4.3.3 惯性(装载中途退役永不 Active、链式卸载)、定理 64(步进边界部分回滚)、L-Raise(apply 失败部分回滚)、循环依赖静止、供给冲突、父子级联、隔离域丢弃恢复、Domain Reset。
  • 实现教训(已记入模块 READMEMono 的 IAsyncEnumerable/ValueTask 迭代器消费端恢复经同步上下文调度、不内联,破坏确定性语义——分步效应改用自定义 IShrinkStepEffectEnumerator(UniTask 步进协议)后恢复全链路内联。
  • 验证:ShrinkContext.Core.Tests 21/21 通过;全仓 EditMode 138 个测试中 8 个失败全部位于既有 ShrinkDataSaver.Tests(缺 LogAssert.ExpectShrinkSettings.SaveAsync 空引用等测试侧问题),与本次纯新增改动无关(git 确认无既有文件修改)。

下一步决策点:原型语义已验证,是否进入阶段 1/2(加载器 + ShrinkApp 迁移)取决于产品侧对运行时重配(热插拔/热替换)的需求确认。

阶段 1:核心库落地

  • ShrinkCtxget/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/):ShrinkLoaderEntryid/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.hostingModeClassicHost/ContextLoader)双模式并存,经典路径零行为变化;ShrinkAppLoaderHost + ShrinkAppLoaderBootstrapper 自动引导并接回 ShrinkApp.ServicesSetModuleDisabledAsync 运行中按模块开关。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.ShrinkAppSettingshostingMode = 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 第二片(同日)

  • ShrinkCommandAppComponentshrink.service.command 键)与 ShrinkDataSaverAppComponentshrink.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 ModeAssets/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 默认走 ShrinkModContextHostuseContextHost=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=truemodLoaded=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/193PlayMode 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、程序集常驻策略、配置资产、调试面与容量基准。后续应以真实项目负载持续采样和收紧领域策略,不再新增第三套生命周期。