- 将 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 迁移与旧代码地图
331 lines
37 KiB
Markdown
331 lines
37 KiB
Markdown
# ShrinkSDK × Cordis 组织形式改造方案(已归档)
|
||
|
||
> 归档说明:阶段 0 至 5 已完成并合入当前架构。本文只保存迁移论证、过程决策和历史验收数据;当前状态以仓库根 `DESIGN.md` 为准。
|
||
|
||
> 生成日期: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 本次独立重读后的校准
|
||
|
||
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 | 无 | 多租户测试、按组件降权等能力缺失 |
|
||
| 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 模块拓扑(改造后)
|
||
|
||
```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.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 草案)
|
||
|
||
```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`(算法 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.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`)、B(inject `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 调试台场景 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.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;全局静态桥在多根上下文并行时仍需引用计数或实例化。这些不阻塞单默认根主路径,但不能误写成完全隔离保证。
|
||
|
||
### 阶段 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.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 集合。
|
||
|
||
#### 阶段 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. 风险与代价(决策前必须直视)
|
||
|
||
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、程序集常驻策略、配置资产、调试面与容量基准。后续应以真实项目负载持续采样和收紧领域策略,不再新增第三套生命周期。
|