1171 lines
36 KiB
Markdown
1171 lines
36 KiB
Markdown
# ArchitectureScope Container Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 按 `docs/specs/architecture-scope-container-implementation-spec.md` 实现统一的 `ArchitectureScope` / FlowScope Container / Scope Tree,并替换当前 `RootContext + FeatureScope` 试验入口。
|
||
|
||
**Architecture:** 第一阶段只做核心 Scope 与容器边界:`FlowScope.Create()` 创建 Root,Root / Feature / Child 都通过同一个 `IArchitectureScope` 表达。保留现有 `FlowScope.Container.Container` 中已经成立的父子查找、局部覆盖、`PerScope()`、Dispose 子树等能力,在其上新增 Core 接口与 `ArchitectureScope` 包装/约束层;旧 `FlowScope.Composition` 不继续扩展,最后删除或降级。
|
||
|
||
**Tech Stack:** Unity C#,NUnit EditMode tests,现有 `FlowScope.Runtime.asmdef` / `FlowScope.Tests.EditMode.asmdef`,验证优先使用 `dotnet build --no-restore`。
|
||
|
||
---
|
||
|
||
## 执行边界
|
||
|
||
本计划只实现以下内容:
|
||
|
||
- `FlowScope.Create()` 根入口。
|
||
- `IArchitectureScope` / `IScopeInfo` / `IFeature` / `IRegistrationBuilder`。
|
||
- `ArchitectureScope` 统一 Scope 节点。
|
||
- Register / Resolve / CreateScope / CreateFeature / Dispose 规则。
|
||
- 旧 Composition 测试中仍有价值的 Root 服务、Feature 隔离、命名注册用例迁移到 Core 测试。
|
||
- 移除或降级旧 `RootContext + FeatureScope + Command/Query` 试验入口。
|
||
|
||
本计划不实现:
|
||
|
||
- Command / Query / Event。
|
||
- 完整 MVC / MVVM。
|
||
- UI Panel 生命周期统一接管。
|
||
- 自动扫描 Feature。
|
||
- 资源、存档、音频、UI 模块改造。
|
||
|
||
执行前必须先确认当前工作树已有改动,不能覆盖用户已有修改:
|
||
|
||
```powershell
|
||
git status --short
|
||
git diff -- "My project/Assets/FlowScope/Runtime/Container/Container.cs"
|
||
git diff -- "My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs"
|
||
git diff -- "My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs"
|
||
```
|
||
|
||
如果这些文件已有未合并改动,执行者必须在当前改动基础上续写,不得 `git restore` 或重写文件。
|
||
|
||
## 文件结构
|
||
|
||
### 新增 Runtime Core 文件
|
||
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/FlowScope.cs`
|
||
- 静态入口,创建 Root Scope。
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs`
|
||
- 统一 Scope 主接口。
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs`
|
||
- 只读 Scope 元信息接口。
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/IFeature.cs`
|
||
- Feature 安装接口。
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs`
|
||
- Core 层注册生命周期 builder 接口。
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs`
|
||
- 统一 Scope 节点实现。
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/Container/ScopeExceptions.cs`
|
||
- 核心 Scope 异常类型。
|
||
|
||
### 修改现有 Container 文件
|
||
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Container/Container.cs`
|
||
- 暴露 `ParentInfo` / `Path` / `IsDisposed` 所需的最小只读能力。
|
||
- 增加同级重名 Scope 检查。
|
||
- 确保 `CreateScope(string name)` 空名失败。
|
||
- 保持注册只写当前 Container,不改变父级。
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs`
|
||
- 将 `ServiceLifetime.Scoped` 命名调整为 `PerScope`,或在 Core 层适配为 `PerScope`。
|
||
- 保持 `PerScope` 缓存到 Resolve 发起 Scope 的语义。
|
||
|
||
### 新增 Core EditMode 测试
|
||
|
||
- Create: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs`
|
||
- Create: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs`
|
||
- Create: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs`
|
||
- Create: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs`
|
||
|
||
### 删除或降级旧 Composition
|
||
|
||
- Delete or quarantine: `My project/Assets/FlowScope/Runtime/Composition/CompositionContracts.cs`
|
||
- Delete or quarantine: `My project/Assets/FlowScope/Runtime/Composition/FlowScopeApp.cs`
|
||
- Delete or migrate: `My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs`
|
||
|
||
如果执行时选择“降级”而不是删除,必须放到 `FlowScope.LegacyComposition` 命名空间,并且不得被新 Core 测试依赖。
|
||
|
||
## Task 1: 新增 Core 接口与根入口
|
||
|
||
**Files:**
|
||
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/IFeature.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/FlowScope.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs`
|
||
|
||
- [ ] **Step 1: 创建目录和 Unity meta**
|
||
|
||
执行者在 Unity 项目中创建目录:
|
||
|
||
```text
|
||
My project/Assets/FlowScope/Runtime/Core/
|
||
My project/Assets/FlowScope/Runtime/Core/Container/
|
||
My project/Assets/FlowScope/Tests/EditMode/Core/
|
||
```
|
||
|
||
如果通过文件系统创建,执行后打开 Unity 或保持后续生成 `.meta` 被纳入同一提交。不要手写 `.meta`。
|
||
|
||
- [ ] **Step 2: 先写 Root 入口测试**
|
||
|
||
在 `ArchitectureScopeRegistrationTests.cs` 写入:
|
||
|
||
```csharp
|
||
using NUnit.Framework;
|
||
|
||
namespace FlowScope.Tests.EditMode.Core
|
||
{
|
||
public sealed class ArchitectureScopeRegistrationTests
|
||
{
|
||
[Test]
|
||
public void Create_ReturnsRootArchitectureScope()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
|
||
Assert.That(root.Name, Is.EqualTo("Root"));
|
||
Assert.That(root.Parent, Is.Null);
|
||
Assert.That(root.Path, Is.EqualTo("Root"));
|
||
Assert.That(root.IsDisposed, Is.False);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 运行测试确认失败**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: FAIL,缺少 `FlowScope.FlowScope` / `IArchitectureScope`。
|
||
|
||
- [ ] **Step 4: 新增接口代码**
|
||
|
||
`IScopeInfo.cs`:
|
||
|
||
```csharp
|
||
namespace FlowScope
|
||
{
|
||
public interface IScopeInfo
|
||
{
|
||
string Name { get; }
|
||
IScopeInfo Parent { get; }
|
||
string Path { get; }
|
||
bool IsDisposed { get; }
|
||
}
|
||
}
|
||
```
|
||
|
||
`IRegistrationBuilder.cs`:
|
||
|
||
```csharp
|
||
namespace FlowScope
|
||
{
|
||
public interface IRegistrationBuilder
|
||
{
|
||
void AsSingleton();
|
||
void PerScope();
|
||
void AsTransient();
|
||
}
|
||
}
|
||
```
|
||
|
||
`IFeature.cs`:
|
||
|
||
```csharp
|
||
namespace FlowScope
|
||
{
|
||
public interface IFeature
|
||
{
|
||
void Install(IArchitectureScope scope);
|
||
}
|
||
}
|
||
```
|
||
|
||
`IArchitectureScope.cs`:
|
||
|
||
```csharp
|
||
using System;
|
||
|
||
namespace FlowScope
|
||
{
|
||
public interface IArchitectureScope : IScopeInfo, IDisposable
|
||
{
|
||
IArchitectureScope CreateScope(string name);
|
||
IArchitectureScope CreateFeature<TFeature>() where TFeature : IFeature, new();
|
||
IArchitectureScope CreateFeature<TFeature>(string name) where TFeature : IFeature, new();
|
||
|
||
IRegistrationBuilder Register<TImplementation>();
|
||
IRegistrationBuilder Register<TImplementation>(string name);
|
||
IRegistrationBuilder Register<TService, TImplementation>() where TImplementation : TService;
|
||
IRegistrationBuilder Register<TService, TImplementation>(string name) where TImplementation : TService;
|
||
|
||
void RegisterInstance<TService>(TService instance);
|
||
void RegisterInstance<TService>(TService instance, string name);
|
||
|
||
TService Resolve<TService>();
|
||
TService Resolve<TService>(string name);
|
||
bool TryResolve<TService>(out TService value);
|
||
bool TryResolve<TService>(out TService value, string name);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: 新增临时可编译入口**
|
||
|
||
`FlowScope.cs`:
|
||
|
||
```csharp
|
||
using FlowScope.Core.Container;
|
||
|
||
namespace FlowScope
|
||
{
|
||
public static class FlowScope
|
||
{
|
||
public static IArchitectureScope Create()
|
||
{
|
||
return Create("Root");
|
||
}
|
||
|
||
public static IArchitectureScope Create(string rootName)
|
||
{
|
||
return new ArchitectureScope(rootName);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: 暂存后续实现需要的空壳**
|
||
|
||
`ArchitectureScope.cs` 先写可编译空壳,但未实现行为:
|
||
|
||
```csharp
|
||
using System;
|
||
|
||
namespace FlowScope.Core.Container
|
||
{
|
||
internal sealed class ArchitectureScope : IArchitectureScope
|
||
{
|
||
public ArchitectureScope(string name)
|
||
{
|
||
Name = name;
|
||
Path = name;
|
||
}
|
||
|
||
public string Name { get; }
|
||
public IScopeInfo Parent => null;
|
||
public string Path { get; }
|
||
public bool IsDisposed { get; private set; }
|
||
|
||
public IArchitectureScope CreateScope(string name) => throw new NotImplementedException();
|
||
public IArchitectureScope CreateFeature<TFeature>() where TFeature : IFeature, new() => throw new NotImplementedException();
|
||
public IArchitectureScope CreateFeature<TFeature>(string name) where TFeature : IFeature, new() => throw new NotImplementedException();
|
||
public IRegistrationBuilder Register<TImplementation>() => throw new NotImplementedException();
|
||
public IRegistrationBuilder Register<TImplementation>(string name) => throw new NotImplementedException();
|
||
public IRegistrationBuilder Register<TService, TImplementation>() where TImplementation : TService => throw new NotImplementedException();
|
||
public IRegistrationBuilder Register<TService, TImplementation>(string name) where TImplementation : TService => throw new NotImplementedException();
|
||
public void RegisterInstance<TService>(TService instance) => throw new NotImplementedException();
|
||
public void RegisterInstance<TService>(TService instance, string name) => throw new NotImplementedException();
|
||
public TService Resolve<TService>() => throw new NotImplementedException();
|
||
public TService Resolve<TService>(string name) => throw new NotImplementedException();
|
||
public bool TryResolve<TService>(out TService value, string name) => throw new NotImplementedException();
|
||
public bool TryResolve<TService>(out TService value) => throw new NotImplementedException();
|
||
public void Dispose() => IsDisposed = true;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 7: 运行测试确认入口测试通过**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Runtime.csproj" --no-restore
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: PASS build。此时只有 Root 入口行为成立。
|
||
|
||
**建议提交点:** `feat: 新增 ArchitectureScope 核心接口入口`
|
||
|
||
## Task 2: 实现 Register 只写当前 Scope
|
||
|
||
**Files:**
|
||
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Core/Container/RegistrationBuilderAdapter.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs`
|
||
|
||
- [ ] **Step 1: 增加 Register 测试**
|
||
|
||
向 `ArchitectureScopeRegistrationTests.cs` 增加:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void Register_WritesOnlyCurrentScope()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
using var child = root.CreateScope("Child");
|
||
|
||
root.Register<IService, RootService>().AsSingleton();
|
||
child.Register<IService, ChildService>().AsSingleton();
|
||
|
||
Assert.That(root.Resolve<IService>(), Is.TypeOf<RootService>());
|
||
Assert.That(child.Resolve<IService>(), Is.TypeOf<ChildService>());
|
||
}
|
||
|
||
[Test]
|
||
public void Register_DuplicateKeyInSameScopeThrows()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
root.Register<IService, RootService>().AsSingleton();
|
||
|
||
Assert.Throws<InvalidOperationException>(() =>
|
||
root.Register<IService, ChildService>().AsSingleton());
|
||
}
|
||
|
||
private interface IService { }
|
||
private sealed class RootService : IService { }
|
||
private sealed class ChildService : IService { }
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: FAIL,`CreateScope` / `Register` / `Resolve` 未实现。
|
||
|
||
- [ ] **Step 3: 用现有 Container 实现 Register 代理**
|
||
|
||
`RegistrationBuilderAdapter.cs`:
|
||
|
||
```csharp
|
||
using FlowScope.Container;
|
||
|
||
namespace FlowScope.Core.Container
|
||
{
|
||
internal sealed class RegistrationBuilderAdapter : IRegistrationBuilder
|
||
{
|
||
private readonly ContainerRegistrationBuilder _inner;
|
||
|
||
public RegistrationBuilderAdapter(ContainerRegistrationBuilder inner)
|
||
{
|
||
_inner = inner;
|
||
}
|
||
|
||
public void AsSingleton() => _inner.AsSingleton();
|
||
public void PerScope() => _inner.PerScope();
|
||
public void AsTransient() => _inner.AsTransient();
|
||
}
|
||
}
|
||
```
|
||
|
||
在 `ArchitectureScope` 中持有现有 Container:
|
||
|
||
```csharp
|
||
private readonly FlowScope.Container.Container _container;
|
||
|
||
public IRegistrationBuilder Register<TImplementation>()
|
||
{
|
||
ThrowIfDisposed();
|
||
return new RegistrationBuilderAdapter(_container.Register<TImplementation>());
|
||
}
|
||
|
||
public IRegistrationBuilder Register<TImplementation>(string name)
|
||
{
|
||
ThrowIfDisposed();
|
||
return new RegistrationBuilderAdapter(_container.Register<TImplementation>(name));
|
||
}
|
||
|
||
public IRegistrationBuilder Register<TService, TImplementation>()
|
||
where TImplementation : TService
|
||
{
|
||
ThrowIfDisposed();
|
||
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>());
|
||
}
|
||
|
||
public IRegistrationBuilder Register<TService, TImplementation>(string name)
|
||
where TImplementation : TService
|
||
{
|
||
ThrowIfDisposed();
|
||
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>(name));
|
||
}
|
||
|
||
public void RegisterInstance<TService>(TService instance)
|
||
{
|
||
RegisterInstance(instance, null);
|
||
}
|
||
|
||
public void RegisterInstance<TService>(TService instance, string name)
|
||
{
|
||
ThrowIfDisposed();
|
||
_container.RegisterInstance(instance, name);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 实现 Resolve 代理**
|
||
|
||
```csharp
|
||
public TService Resolve<TService>()
|
||
{
|
||
ThrowIfDisposed();
|
||
return _container.Resolve<TService>();
|
||
}
|
||
|
||
public TService Resolve<TService>(string name)
|
||
{
|
||
ThrowIfDisposed();
|
||
return _container.Resolve<TService>(name);
|
||
}
|
||
|
||
public bool TryResolve<TService>(out TService value)
|
||
{
|
||
ThrowIfDisposed();
|
||
return _container.TryResolve(out value);
|
||
}
|
||
|
||
public bool TryResolve<TService>(out TService value, string name)
|
||
{
|
||
ThrowIfDisposed();
|
||
return _container.TryResolve(out value, name);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: 运行测试确认通过**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: PASS build,新增 Register 测试通过。
|
||
|
||
**建议提交点:** `feat: 实现 ArchitectureScope 注册解析入口`
|
||
|
||
## Task 3: 实现 Scope Tree 与 Path
|
||
|
||
**Files:**
|
||
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs`
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Container/Container.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs`
|
||
|
||
- [ ] **Step 1: 写 Scope Tree 测试**
|
||
|
||
`ArchitectureScopeResolveTests.cs`:
|
||
|
||
```csharp
|
||
using NUnit.Framework;
|
||
|
||
namespace FlowScope.Tests.EditMode.Core
|
||
{
|
||
public sealed class ArchitectureScopeResolveTests
|
||
{
|
||
[Test]
|
||
public void CreateScope_CreatesChildWithParentAndPath()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
using var feature = root.CreateScope("BossFightFeature");
|
||
using var child = feature.CreateScope("RevivePanel");
|
||
|
||
Assert.That(feature.Parent, Is.SameAs(root));
|
||
Assert.That(child.Parent, Is.SameAs(feature));
|
||
Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature"));
|
||
Assert.That(child.Path, Is.EqualTo("Root/BossFightFeature/RevivePanel"));
|
||
}
|
||
|
||
[Test]
|
||
public void CreateScope_DuplicateSiblingNameThrows()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
using var first = root.CreateScope("BossFightFeature");
|
||
|
||
Assert.Throws<InvalidOperationException>(() => root.CreateScope("BossFightFeature"));
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试确认失败**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: FAIL,`CreateScope` 未实现或未检查重名。
|
||
|
||
- [ ] **Step 3: 在 ArchitectureScope 内维护子 Scope**
|
||
|
||
实现要点:
|
||
|
||
```csharp
|
||
private readonly ArchitectureScope _parent;
|
||
private readonly List<ArchitectureScope> _children = new List<ArchitectureScope>();
|
||
|
||
public IScopeInfo Parent => _parent;
|
||
|
||
public IArchitectureScope CreateScope(string name)
|
||
{
|
||
ThrowIfDisposed();
|
||
if (string.IsNullOrWhiteSpace(name))
|
||
{
|
||
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
|
||
}
|
||
|
||
if (_children.Exists(child => !child.IsDisposed && child.Name == name))
|
||
{
|
||
throw new InvalidOperationException($"Scope '{name}' already exists under '{Path}'.");
|
||
}
|
||
|
||
var childContainer = _container.CreateScope(name);
|
||
var child = new ArchitectureScope(name, this, childContainer);
|
||
_children.Add(child);
|
||
return child;
|
||
}
|
||
```
|
||
|
||
构造函数需要变成:
|
||
|
||
```csharp
|
||
public ArchitectureScope(string name)
|
||
: this(name, null, new FlowScope.Container.Container())
|
||
{
|
||
}
|
||
|
||
private ArchitectureScope(string name, ArchitectureScope parent, FlowScope.Container.Container container)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(name))
|
||
{
|
||
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
|
||
}
|
||
|
||
Name = name;
|
||
_parent = parent;
|
||
_container = container;
|
||
Path = parent == null ? name : $"{parent.Path}/{name}";
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 确保 Dispose 从父节点移除**
|
||
|
||
在 `Dispose` 中释放子 Scope 后:
|
||
|
||
```csharp
|
||
_parent?._children.Remove(this);
|
||
```
|
||
|
||
- [ ] **Step 5: 运行测试确认通过**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: PASS build,Scope Tree / Path 测试通过。
|
||
|
||
**建议提交点:** `feat: 实现 ArchitectureScope 树结构`
|
||
|
||
## Task 4: 固化 Resolve 父级查找与子级覆盖
|
||
|
||
**Files:**
|
||
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs`
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Container/Container.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs`
|
||
|
||
- [ ] **Step 1: 增加父级查找与覆盖测试**
|
||
|
||
```csharp
|
||
[Test]
|
||
public void Resolve_FallsBackToParentWhenCurrentMissing()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
using var feature = root.CreateScope("BossFightFeature");
|
||
|
||
var clock = new RootClock(42);
|
||
root.RegisterInstance<IRootClock>(clock);
|
||
|
||
Assert.That(feature.Resolve<IRootClock>(), Is.SameAs(clock));
|
||
}
|
||
|
||
[Test]
|
||
public void Resolve_ChildRegistrationOverridesParent()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
using var feature = root.CreateScope("BossFightFeature");
|
||
|
||
root.Register<IRewardService, GlobalRewardService>().AsSingleton();
|
||
feature.Register<IRewardService, BossRewardService>().AsSingleton();
|
||
|
||
Assert.That(root.Resolve<IRewardService>(), Is.TypeOf<GlobalRewardService>());
|
||
Assert.That(feature.Resolve<IRewardService>(), Is.TypeOf<BossRewardService>());
|
||
}
|
||
|
||
private interface IRootClock { int Value { get; } }
|
||
private sealed class RootClock : IRootClock
|
||
{
|
||
public RootClock(int value) => Value = value;
|
||
public int Value { get; }
|
||
}
|
||
|
||
private interface IRewardService { }
|
||
private sealed class GlobalRewardService : IRewardService { }
|
||
private sealed class BossRewardService : IRewardService { }
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: PASS。现有 `Container.TryFindRegistration` 已支持向父查找;如果失败,修复 Container 父子链传递。
|
||
|
||
- [ ] **Step 3: 增加 TryResolve 语义测试**
|
||
|
||
```csharp
|
||
[Test]
|
||
public void TryResolve_MissingRegistrationReturnsFalse()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
|
||
Assert.That(root.TryResolve<IRewardService>(out var value), Is.False);
|
||
Assert.That(value, Is.Null);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 确保构造异常不被 TryResolve 吞掉**
|
||
|
||
```csharp
|
||
[Test]
|
||
public void TryResolve_ConstructorFailureThrows()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
root.Register<BrokenService>().AsTransient();
|
||
|
||
Assert.Throws<InvalidOperationException>(() =>
|
||
root.TryResolve<BrokenService>(out _));
|
||
}
|
||
|
||
private sealed class BrokenService
|
||
{
|
||
public BrokenService()
|
||
{
|
||
throw new InvalidOperationException("broken");
|
||
}
|
||
}
|
||
```
|
||
|
||
Expected: PASS。若现有反射调用把异常包成 `TargetInvocationException`,按当前实际行为断言外层异常,但不要返回 false。
|
||
|
||
**建议提交点:** `test: 固化 Scope 解析查找语义`
|
||
|
||
## Task 5: 固化生命周期语义
|
||
|
||
**Files:**
|
||
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs`
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Container/Container.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs`
|
||
|
||
- [ ] **Step 1: 写生命周期测试**
|
||
|
||
`ArchitectureScopeDisposeTests.cs`:
|
||
|
||
```csharp
|
||
using NUnit.Framework;
|
||
|
||
namespace FlowScope.Tests.EditMode.Core
|
||
{
|
||
public sealed class ArchitectureScopeDisposeTests
|
||
{
|
||
[Test]
|
||
public void Singleton_FromParentIsSharedByChildrenAndDisposedByParent()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
var first = root.CreateScope("First");
|
||
var second = root.CreateScope("Second");
|
||
|
||
root.Register<IDisposableService, DisposableService>().AsSingleton();
|
||
|
||
var rootInstance = root.Resolve<IDisposableService>();
|
||
Assert.That(first.Resolve<IDisposableService>(), Is.SameAs(rootInstance));
|
||
Assert.That(second.Resolve<IDisposableService>(), Is.SameAs(rootInstance));
|
||
|
||
first.Dispose();
|
||
Assert.That(rootInstance.IsDisposed, Is.False);
|
||
|
||
root.Dispose();
|
||
Assert.That(rootInstance.IsDisposed, Is.True);
|
||
}
|
||
|
||
[Test]
|
||
public void PerScope_FromParentCreatesDifferentInstancesPerChildScope()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
var first = root.CreateScope("First");
|
||
var second = root.CreateScope("Second");
|
||
|
||
root.Register<IDisposableService, DisposableService>().PerScope();
|
||
|
||
var firstInstance = first.Resolve<IDisposableService>();
|
||
var secondInstance = second.Resolve<IDisposableService>();
|
||
|
||
Assert.That(first.Resolve<IDisposableService>(), Is.SameAs(firstInstance));
|
||
Assert.That(second.Resolve<IDisposableService>(), Is.SameAs(secondInstance));
|
||
Assert.That(firstInstance, Is.Not.SameAs(secondInstance));
|
||
|
||
first.Dispose();
|
||
Assert.That(firstInstance.IsDisposed, Is.True);
|
||
Assert.That(secondInstance.IsDisposed, Is.False);
|
||
}
|
||
|
||
private interface IDisposableService
|
||
{
|
||
bool IsDisposed { get; }
|
||
}
|
||
|
||
private sealed class DisposableService : IDisposableService, System.IDisposable
|
||
{
|
||
public bool IsDisposed { get; private set; }
|
||
public void Dispose() => IsDisposed = true;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: PASS。现有 Container 大概率已满足;如果失败,修复 `ContainerRegistration.ResolveScoped(requester)` 的缓存归属。
|
||
|
||
- [ ] **Step 3: 写 Dispose 顺序与幂等测试**
|
||
|
||
```csharp
|
||
[Test]
|
||
public void Dispose_ParentDisposesChildrenBeforeOwnInstancesAndIsIdempotent()
|
||
{
|
||
DisposeRecorder.Order.Clear();
|
||
var root = FlowScope.FlowScope.Create();
|
||
var child = root.CreateScope("Child");
|
||
|
||
root.Register<ITracked, RootTracked>().AsSingleton();
|
||
child.Register<IChildTracked, ChildTracked>().AsSingleton();
|
||
|
||
root.Resolve<ITracked>();
|
||
child.Resolve<IChildTracked>();
|
||
|
||
root.Dispose();
|
||
root.Dispose();
|
||
|
||
CollectionAssert.AreEqual(new[] { "child", "root" }, DisposeRecorder.Order);
|
||
}
|
||
|
||
private interface ITracked { }
|
||
private interface IChildTracked { }
|
||
|
||
private static class DisposeRecorder
|
||
{
|
||
public static readonly System.Collections.Generic.List<string> Order =
|
||
new System.Collections.Generic.List<string>();
|
||
}
|
||
|
||
private sealed class RootTracked : ITracked, System.IDisposable
|
||
{
|
||
public void Dispose() => DisposeRecorder.Order.Add("root");
|
||
}
|
||
|
||
private sealed class ChildTracked : IChildTracked, System.IDisposable
|
||
{
|
||
public void Dispose() => DisposeRecorder.Order.Add("child");
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 明确 Instance 不释放外部实例**
|
||
|
||
保留或迁移现有 `ContainerTests.RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance`,在 Core 测试中也覆盖:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void RegisterInstance_DefaultDoesNotDisposeExternalInstance()
|
||
{
|
||
var root = FlowScope.FlowScope.Create();
|
||
var service = new DisposableService();
|
||
|
||
root.RegisterInstance<IDisposableService>(service);
|
||
root.Dispose();
|
||
|
||
Assert.That(service.IsDisposed, Is.False);
|
||
}
|
||
```
|
||
|
||
**建议提交点:** `test: 固化 ArchitectureScope 生命周期语义`
|
||
|
||
## Task 6: 实现 CreateFeature 自动创建与安装
|
||
|
||
**Files:**
|
||
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs`
|
||
|
||
- [ ] **Step 1: 写 CreateFeature 测试**
|
||
|
||
`ArchitectureScopeFeatureTests.cs`:
|
||
|
||
```csharp
|
||
using System;
|
||
using NUnit.Framework;
|
||
|
||
namespace FlowScope.Tests.EditMode.Core
|
||
{
|
||
public sealed class ArchitectureScopeFeatureTests
|
||
{
|
||
[Test]
|
||
public void CreateFeature_UsesFeatureTypeNameAndCallsInstall()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
|
||
using var feature = root.CreateFeature<BossFightFeature>();
|
||
|
||
Assert.That(feature.Name, Is.EqualTo(nameof(BossFightFeature)));
|
||
Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature"));
|
||
Assert.That(feature.Resolve<BossFightModel>(), Is.Not.Null);
|
||
}
|
||
|
||
[Test]
|
||
public void CreateFeature_WithNameUsesExplicitName()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
|
||
using var feature = root.CreateFeature<BossFightFeature>("BossFight");
|
||
|
||
Assert.That(feature.Name, Is.EqualTo("BossFight"));
|
||
Assert.That(feature.Resolve<BossFightModel>(), Is.Not.Null);
|
||
}
|
||
|
||
private sealed class BossFightFeature : IFeature
|
||
{
|
||
public void Install(IArchitectureScope scope)
|
||
{
|
||
scope.Register<BossFightModel>().PerScope();
|
||
}
|
||
}
|
||
|
||
private sealed class BossFightModel { }
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 实现 CreateFeature**
|
||
|
||
在 `ArchitectureScope` 中:
|
||
|
||
```csharp
|
||
public IArchitectureScope CreateFeature<TFeature>()
|
||
where TFeature : IFeature, new()
|
||
{
|
||
return CreateFeature<TFeature>(typeof(TFeature).Name);
|
||
}
|
||
|
||
public IArchitectureScope CreateFeature<TFeature>(string name)
|
||
where TFeature : IFeature, new()
|
||
{
|
||
ThrowIfDisposed();
|
||
var featureScope = (ArchitectureScope)CreateScope(name);
|
||
try
|
||
{
|
||
var feature = new TFeature();
|
||
feature.Install(featureScope);
|
||
return featureScope;
|
||
}
|
||
catch
|
||
{
|
||
featureScope.Dispose();
|
||
throw;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 写 Install 失败清理测试**
|
||
|
||
```csharp
|
||
[Test]
|
||
public void CreateFeature_WhenInstallFailsDisposesCreatedScope()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
|
||
Assert.Throws<InvalidOperationException>(() => root.CreateFeature<BrokenFeature>());
|
||
|
||
using var feature = root.CreateFeature<BossFightFeature>(nameof(BrokenFeature));
|
||
Assert.That(feature.Name, Is.EqualTo(nameof(BrokenFeature)));
|
||
}
|
||
|
||
private sealed class BrokenFeature : IFeature
|
||
{
|
||
public void Install(IArchitectureScope scope)
|
||
{
|
||
throw new InvalidOperationException("install failed");
|
||
}
|
||
}
|
||
```
|
||
|
||
这个测试证明失败 Scope 已从父级移除,否则同名创建会失败。
|
||
|
||
- [ ] **Step 4: 运行测试确认通过**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: PASS build,CreateFeature 默认名、显式名、Install、失败清理全部通过。
|
||
|
||
**建议提交点:** `feat: 实现 CreateFeature 自动安装`
|
||
|
||
## Task 7: 迁移有价值的旧 Composition 用例
|
||
|
||
**Files:**
|
||
|
||
- Modify: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs`
|
||
- Modify: `My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs`
|
||
- Delete or quarantine: `My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs`
|
||
|
||
- [ ] **Step 1: 迁移 Root 服务可被 Feature Resolve**
|
||
|
||
新增测试:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void FeatureScope_CanResolveRootServices()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
var clock = new RootClock(10);
|
||
root.RegisterInstance<IRootClock>(clock);
|
||
|
||
using var feature = root.CreateFeature<ClockFeature>();
|
||
|
||
Assert.That(feature.Resolve<IRootClock>(), Is.SameAs(clock));
|
||
Assert.That(feature.Resolve<ClockModel>().Value, Is.EqualTo(10));
|
||
}
|
||
|
||
private interface IRootClock { int Value { get; } }
|
||
private sealed class RootClock : IRootClock
|
||
{
|
||
public RootClock(int value) => Value = value;
|
||
public int Value { get; }
|
||
}
|
||
|
||
private sealed class ClockFeature : IFeature
|
||
{
|
||
public void Install(IArchitectureScope scope)
|
||
{
|
||
scope.Register<ClockModel>().PerScope();
|
||
}
|
||
}
|
||
|
||
private sealed class ClockModel
|
||
{
|
||
public ClockModel(IRootClock clock) => Value = clock.Value;
|
||
public int Value { get; }
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 迁移 Feature 级服务隔离**
|
||
|
||
新增测试:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void TwoFeatureScopes_DoNotSharePerScopeFeatureServices()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
root.Register<IFeatureService, FeatureService>().PerScope();
|
||
|
||
using var first = root.CreateFeature<EmptyFeature>("FirstActivity");
|
||
using var second = root.CreateFeature<EmptyFeature>("SecondActivity");
|
||
|
||
Assert.That(first.Resolve<IFeatureService>(), Is.SameAs(first.Resolve<IFeatureService>()));
|
||
Assert.That(first.Resolve<IFeatureService>(), Is.Not.SameAs(second.Resolve<IFeatureService>()));
|
||
}
|
||
|
||
private interface IFeatureService { }
|
||
private sealed class FeatureService : IFeatureService { }
|
||
private sealed class EmptyFeature : IFeature
|
||
{
|
||
public void Install(IArchitectureScope scope) { }
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 迁移命名注册**
|
||
|
||
新增测试:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void NamedRegistration_ResolvesByTypeAndNameFromFeature()
|
||
{
|
||
using var root = FlowScope.FlowScope.Create();
|
||
root.Register<IRewardService, BossRewardService>("boss").PerScope();
|
||
root.Register<IRewardService, ScratchRewardService>("scratch").PerScope();
|
||
|
||
using var boss = root.CreateFeature<EmptyFeature>("BossFight");
|
||
using var scratch = root.CreateFeature<EmptyFeature>("ScratchTicket");
|
||
|
||
Assert.That(boss.Resolve<IRewardService>("boss"), Is.TypeOf<BossRewardService>());
|
||
Assert.That(scratch.Resolve<IRewardService>("scratch"), Is.TypeOf<ScratchRewardService>());
|
||
Assert.That(boss.Resolve<IRewardService>("boss"), Is.SameAs(boss.Resolve<IRewardService>("boss")));
|
||
Assert.That(boss.Resolve<IRewardService>("boss"), Is.Not.SameAs(scratch.Resolve<IRewardService>("boss")));
|
||
}
|
||
|
||
private interface IRewardService { }
|
||
private sealed class BossRewardService : IRewardService { }
|
||
private sealed class ScratchRewardService : IRewardService { }
|
||
```
|
||
|
||
- [ ] **Step 4: 不迁移 Command / Query 测试**
|
||
|
||
旧测试 `FeatureScope_CommandAndQueryOperateOnScopedArchitecture` 不迁移。原因:spec 明确第一阶段不进入 Command / Query / Event。
|
||
|
||
- [ ] **Step 5: 运行测试**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: PASS build。
|
||
|
||
**建议提交点:** `test: 迁移 Composition 核心用例到 ArchitectureScope`
|
||
|
||
## Task 8: 删除或隔离旧 Composition 试验线
|
||
|
||
**Files:**
|
||
|
||
- Delete or quarantine: `My project/Assets/FlowScope/Runtime/Composition/CompositionContracts.cs`
|
||
- Delete or quarantine: `My project/Assets/FlowScope/Runtime/Composition/FlowScopeApp.cs`
|
||
- Delete or quarantine: `My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs`
|
||
|
||
- [ ] **Step 1: 查找旧 Composition 引用**
|
||
|
||
```powershell
|
||
rg -n "FlowScope\.Composition|IRootContext|IFeatureScope|ICommandContext|IQueryContext|CreateFeatureScope|FlowScopeApp" "My project/Assets/FlowScope"
|
||
```
|
||
|
||
Expected: 只剩 Runtime/Composition 与 Tests/EditMode/Composition 自身引用。
|
||
|
||
- [ ] **Step 2: 删除旧 Runtime/Composition 文件**
|
||
|
||
如果没有外部引用,执行:
|
||
|
||
```powershell
|
||
git rm "My project/Assets/FlowScope/Runtime/Composition/CompositionContracts.cs" "My project/Assets/FlowScope/Runtime/Composition/CompositionContracts.cs.meta" "My project/Assets/FlowScope/Runtime/Composition/FlowScopeApp.cs" "My project/Assets/FlowScope/Runtime/Composition/FlowScopeApp.cs.meta"
|
||
```
|
||
|
||
如果 Unity `.meta` 尚未生成或未跟踪,按实际文件列表只删除存在的路径。
|
||
|
||
- [ ] **Step 3: 删除旧 Composition 测试**
|
||
|
||
```powershell
|
||
git rm "My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs" "My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs.meta"
|
||
```
|
||
|
||
- [ ] **Step 4: 删除空目录 meta**
|
||
|
||
如果目录为空,删除对应目录和 `.meta`:
|
||
|
||
```powershell
|
||
git rm "My project/Assets/FlowScope/Runtime/Composition.meta" "My project/Assets/FlowScope/Tests/EditMode/Composition.meta"
|
||
```
|
||
|
||
如果目录里仍有迁移期 adapter,不删除目录。
|
||
|
||
- [ ] **Step 5: 再次确认旧入口不存在**
|
||
|
||
```powershell
|
||
rg -n "FlowScope\.Composition|IRootContext|IFeatureScope|ICommandContext|IQueryContext|CreateFeatureScope|FlowScopeApp" "My project/Assets/FlowScope"
|
||
```
|
||
|
||
Expected: exit code 1,表示没有匹配;这里 exit code 1 是通过条件。
|
||
|
||
**建议提交点:** `refactor: 移除旧 Composition 试验入口`
|
||
|
||
## Task 9: 总体验证与验收
|
||
|
||
**Files:**
|
||
|
||
- Verify: `My project/FlowScope.Runtime.csproj`
|
||
- Verify: `My project/FlowScope.Tests.EditMode.csproj`
|
||
- Verify: `docs/specs/architecture-scope-container-implementation-spec.md`
|
||
|
||
- [ ] **Step 1: 编译 Runtime**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Runtime.csproj" --no-restore
|
||
```
|
||
|
||
Expected: Build succeeded。
|
||
|
||
- [ ] **Step 2: 编译 EditMode Tests**
|
||
|
||
```powershell
|
||
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: Build succeeded。
|
||
|
||
- [ ] **Step 3: 验证负向边界**
|
||
|
||
```powershell
|
||
rg -n "RootContext|FeatureScope|IRootContext|IFeatureScope|ICommandContext|IQueryContext|CreateFeatureScope|FlowScopeApp" "My project/Assets/FlowScope/Runtime"
|
||
```
|
||
|
||
Expected: exit code 1,表示 Runtime 中不再存在旧试验入口。
|
||
|
||
- [ ] **Step 4: 验证第一阶段没有引入后续模块**
|
||
|
||
```powershell
|
||
rg -n "CommandContext|QueryContext|EventContext|UseMvvm|UseMvc|UIPanelLifecycle" "My project/Assets/FlowScope/Runtime/Core"
|
||
```
|
||
|
||
Expected: exit code 1,表示 Core 没有提前引入 Command / Query / Event / MVC / MVVM / UI Panel 生命周期接管。
|
||
|
||
- [ ] **Step 5: 格式与空白检查**
|
||
|
||
```powershell
|
||
git diff --check
|
||
```
|
||
|
||
Expected: 无输出。
|
||
|
||
- [ ] **Step 6: 检查 staged 范围**
|
||
|
||
如果用户要求提交,执行者只应 stage 本计划相关文件:
|
||
|
||
```powershell
|
||
git add "My project/Assets/FlowScope/Runtime/Core" "My project/Assets/FlowScope/Tests/EditMode/Core" "My project/Assets/FlowScope/Runtime/Container/Container.cs" "My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs"
|
||
```
|
||
|
||
如果旧 Composition 文件被删除,再额外 stage 精确删除路径。不要 broad add 整个项目。
|
||
|
||
**建议最终提交信息:** `feat: 实现统一 ArchitectureScope 容器树`
|
||
|
||
## 自检清单
|
||
|
||
- [ ] `FlowScope.Create()` 返回 Root `IArchitectureScope`。
|
||
- [ ] Root / Feature / Child 都是同一种 `IArchitectureScope`。
|
||
- [ ] `CreateFeature<TFeature>()` 默认名为 `typeof(TFeature).Name`。
|
||
- [ ] `CreateFeature<TFeature>(name)` 自动创建 Scope 并调用 `Install`。
|
||
- [ ] 任意 Scope 可 Register,但只写自己。
|
||
- [ ] Resolve 从当前 Scope 开始,找不到向 Parent 查找。
|
||
- [ ] 子 Scope 覆盖父 Scope 时不修改父 Scope。
|
||
- [ ] Singleton 由注册所在 Scope 持有。
|
||
- [ ] PerScope 由 Resolve 发起 Scope 持有。
|
||
- [ ] Transient 每次创建新实例,并按最终规则释放。
|
||
- [ ] RegisterInstance 默认不释放外部实例。
|
||
- [ ] 子 Scope Dispose 不释放父 Scope 对象。
|
||
- [ ] 父 Scope Dispose 释放所有子 Scope。
|
||
- [ ] Dispose 幂等。
|
||
- [ ] 旧 `RootContext + FeatureScope + Command/Query` 不再是新核心入口。
|
||
- [ ] 第一阶段没有实现 Command / Query / Event、完整 MVC/MVVM、UI Panel 生命周期统一接管。
|