diff --git a/docs/superpowers/plans/2026-07-24-buildflow-architecture-and-sop.md b/docs/superpowers/plans/2026-07-24-buildflow-architecture-and-sop.md
index b24fc4a..3ca7927 100644
--- a/docs/superpowers/plans/2026-07-24-buildflow-architecture-and-sop.md
+++ b/docs/superpowers/plans/2026-07-24-buildflow-architecture-and-sop.md
@@ -400,11 +400,11 @@ git commit -m "refactor: classify backend business errors"
- Create `tests/BuildFlow.Workflow.Tests/Form/FormTestFixtureClassFixture.cs` from `FormTestFixture.cs`.
- Create `tests/BuildFlow.Workflow.Tests/Notification/NoOpPlatformNotificationPublisher.cs` from `NotificationServiceTests.cs`.
-- [ ] **Step 1: Move declarations without changing behavior**
+- [x] **Step 1: Move declarations without changing behavior**
Preserve namespaces, modifiers, XML comments, JSON attributes, extension methods, and member order. Each original file retains its namesake type; each moved declaration gets a same-named file.
-- [ ] **Step 2: Run the file organization guard**
+- [x] **Step 2: Run the file organization guard**
```bash
dotnet test tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj --no-restore --filter FullyQualifiedName~SourceFilesShouldContainAtMostOneTopLevelClassRecordOrEnum
@@ -412,7 +412,7 @@ dotnet test tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.cspr
Expected: PASS with zero violations under `src` and `tests`.
-- [ ] **Step 3: Run affected projects**
+- [x] **Step 3: Run affected projects**
```bash
dotnet test tests/BuildFlow.Platform.Tests/BuildFlow.Platform.Tests.csproj --no-restore
@@ -423,7 +423,7 @@ dotnet build src/BuildFlow.Host/BuildFlow.Host.csproj --no-restore
Expected: PASS, 0 warning, 0 error.
-- [ ] **Step 4: Commit**
+- [x] **Step 4: Commit**
```bash
git add src/BuildFlow.Host/Configuration src/modules/workflow/Domain/Engine tests
diff --git a/src/BuildFlow.Host/Configuration/ConsulConfigurationProvider.cs b/src/BuildFlow.Host/Configuration/ConsulConfigurationProvider.cs
index b2e5a53..fbfd50b 100644
--- a/src/BuildFlow.Host/Configuration/ConsulConfigurationProvider.cs
+++ b/src/BuildFlow.Host/Configuration/ConsulConfigurationProvider.cs
@@ -2,36 +2,6 @@ using System.Net.Http.Headers;
namespace BuildFlow.Host.Configuration;
-///
-/// 轻量 Consul KV 配置 Provider(自实现,替代 Winton.Extensions.Configuration.Consul)。
-/// 启动时用 Consul HTTP API 拉取 prefix 下所有 KV,扁平化注入 IConfiguration。
-/// 支持 ReloadOnChange(后台 long-poll watch)。
-///
-/// 与 Winton 的区别:完全可控、有日志、不黑盒;用 HttpClient 直接调 /v1/kv/?recurse=true。
-///
-/// KV key 映射:Consul key 去掉 root 前缀后,剩余部分直接作为 IConfiguration key。
-/// 如 Consul key "buildflow/Jwt:SigningKey",root="buildflow/",剩余 "Jwt:SigningKey" → IConfiguration key "Jwt:SigningKey"。
-///
-internal sealed class ConsulConfigurationSource : IConfigurationSource
-{
- private readonly ConsulOptions _options;
-
- public ConsulConfigurationSource(ConsulOptions options) => _options = options;
-
- public IConfigurationProvider Build(IConfigurationBuilder builder) =>
- new ConsulConfigurationProvider(_options);
-}
-
-internal sealed class ConsulOptions
-{
- public required string Host { get; init; } // 如 http://43.134.235.58:8520
- public required string Token { get; init; } // ACL token
- public required string KeyPrefix { get; init; } // 如 buildflow/
- public bool Optional { get; init; } = true;
- public bool ReloadOnChange { get; init; } = true;
- public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(30);
-}
-
internal sealed class ConsulConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly ConsulOptions _options;
@@ -160,14 +130,3 @@ internal sealed class ConsulConfigurationProvider : ConfigurationProvider, IDisp
_loggerFactory?.Dispose();
}
}
-
-// Consul KV API 响应项(recurse 接口)
-internal sealed class ConsulKvItem
-{
- public int CreateIndex { get; set; }
- public int ModifyIndex { get; set; }
- public string Key { get; set; } = "";
- public int LockIndex { get; set; }
- public string? Value { get; set; }
- public int Flags { get; set; }
-}
diff --git a/src/BuildFlow.Host/Configuration/ConsulConfigurationSource.cs b/src/BuildFlow.Host/Configuration/ConsulConfigurationSource.cs
new file mode 100644
index 0000000..3475aa3
--- /dev/null
+++ b/src/BuildFlow.Host/Configuration/ConsulConfigurationSource.cs
@@ -0,0 +1,21 @@
+namespace BuildFlow.Host.Configuration;
+
+///
+/// 轻量 Consul KV 配置 Provider(自实现,替代 Winton.Extensions.Configuration.Consul)。
+/// 启动时用 Consul HTTP API 拉取 prefix 下所有 KV,扁平化注入 IConfiguration。
+/// 支持 ReloadOnChange(后台 long-poll watch)。
+///
+/// 与 Winton 的区别:完全可控、有日志、不黑盒;用 HttpClient 直接调 /v1/kv/?recurse=true。
+///
+/// KV key 映射:Consul key 去掉 root 前缀后,剩余部分直接作为 IConfiguration key。
+/// 如 Consul key "buildflow/Jwt:SigningKey",root="buildflow/",剩余 "Jwt:SigningKey" → IConfiguration key "Jwt:SigningKey"。
+///
+internal sealed class ConsulConfigurationSource : IConfigurationSource
+{
+ private readonly ConsulOptions _options;
+
+ public ConsulConfigurationSource(ConsulOptions options) => _options = options;
+
+ public IConfigurationProvider Build(IConfigurationBuilder builder) =>
+ new ConsulConfigurationProvider(_options);
+}
diff --git a/src/BuildFlow.Host/Configuration/ConsulKvItem.cs b/src/BuildFlow.Host/Configuration/ConsulKvItem.cs
new file mode 100644
index 0000000..84038f5
--- /dev/null
+++ b/src/BuildFlow.Host/Configuration/ConsulKvItem.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Configuration;
+
+// Consul KV API 响应项(recurse 接口)
+internal sealed class ConsulKvItem
+{
+ public int CreateIndex { get; set; }
+ public int ModifyIndex { get; set; }
+ public string Key { get; set; } = "";
+ public int LockIndex { get; set; }
+ public string? Value { get; set; }
+ public int Flags { get; set; }
+}
diff --git a/src/BuildFlow.Host/Configuration/ConsulOptions.cs b/src/BuildFlow.Host/Configuration/ConsulOptions.cs
new file mode 100644
index 0000000..438dd18
--- /dev/null
+++ b/src/BuildFlow.Host/Configuration/ConsulOptions.cs
@@ -0,0 +1,11 @@
+namespace BuildFlow.Host.Configuration;
+
+internal sealed class ConsulOptions
+{
+ public required string Host { get; init; } // 如 http://43.134.235.58:8520
+ public required string Token { get; init; } // ACL token
+ public required string KeyPrefix { get; init; } // 如 buildflow/
+ public bool Optional { get; init; } = true;
+ public bool ReloadOnChange { get; init; } = true;
+ public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(30);
+}
diff --git a/src/modules/workflow/Domain/Engine/GraphEdge.cs b/src/modules/workflow/Domain/Engine/GraphEdge.cs
new file mode 100644
index 0000000..1ea90e3
--- /dev/null
+++ b/src/modules/workflow/Domain/Engine/GraphEdge.cs
@@ -0,0 +1,49 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Workflow.Domain.Enums;
+
+namespace Workflow.Domain.Engine;
+
+///
+/// X6 边。source/target 用 { cell, port } 结构;几何信息(vertices/router/connector)放顶层;
+/// 业务字段(edgeType/condition/order)放 data。
+///
+public sealed class GraphEdge
+{
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("shape")]
+ public string? Shape { get; set; }
+
+ [JsonPropertyName("source")]
+ public GraphEndpoint Source { get; set; } = new();
+
+ [JsonPropertyName("target")]
+ public GraphEndpoint Target { get; set; } = new();
+
+ /// 业务数据:edgeType / condition / order。
+ [JsonPropertyName("data")]
+ public JsonElement? Data { get; set; }
+
+ [JsonPropertyName("label")]
+ public JsonElement? Label { get; set; }
+
+ [JsonPropertyName("vertices")]
+ public List? Vertices { get; set; }
+
+ [JsonPropertyName("router")]
+ public JsonElement? Router { get; set; }
+
+ [JsonPropertyName("connector")]
+ public JsonElement? Connector { get; set; }
+
+ /// 边类型(Normal=0/Approved=1/Rejected=2)。缺省为 Normal。
+ public EdgeType GetEdgeType() => Data.TryGetInt32("edgeType") is { } v ? (EdgeType)v : EdgeType.Normal;
+
+ /// 条件表达式(Condition/ExclusiveGateway 出边)。
+ public string? GetCondition() => Data.TryGetString("condition");
+
+ /// 分支评估顺序(值小优先)。
+ public int GetOrder() => Data.TryGetInt32("order") ?? 0;
+}
diff --git a/src/modules/workflow/Domain/Engine/GraphEndpoint.cs b/src/modules/workflow/Domain/Engine/GraphEndpoint.cs
new file mode 100644
index 0000000..3a1a1ca
--- /dev/null
+++ b/src/modules/workflow/Domain/Engine/GraphEndpoint.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace Workflow.Domain.Engine;
+
+public sealed class GraphEndpoint
+{
+ /// 节点 id(对应 GraphNode.Id)。
+ [JsonPropertyName("cell")]
+ public string? Cell { get; set; }
+
+ /// 端口 id(top/right/bottom/left)。
+ [JsonPropertyName("port")]
+ public string? Port { get; set; }
+}
diff --git a/src/modules/workflow/Domain/Engine/GraphJsonExtensions.cs b/src/modules/workflow/Domain/Engine/GraphJsonExtensions.cs
new file mode 100644
index 0000000..a58eb12
--- /dev/null
+++ b/src/modules/workflow/Domain/Engine/GraphJsonExtensions.cs
@@ -0,0 +1,25 @@
+using System.Text.Json;
+
+namespace Workflow.Domain.Engine;
+
+///
+/// JsonElement? 安全取值扩展:统一处理 Nullable 解构与 ValueKind 校验,
+/// 避免 WorkflowGraph 各 getter 重复 null 检查。
+///
+internal static class GraphJsonExtensions
+{
+ public static string? TryGetString(this JsonElement? element, string name) =>
+ element is { ValueKind: JsonValueKind.Object } el
+ && el.TryGetProperty(name, out var p)
+ && p.ValueKind == JsonValueKind.String
+ ? p.GetString()
+ : null;
+
+ public static int? TryGetInt32(this JsonElement? element, string name) =>
+ element is { ValueKind: JsonValueKind.Object } el
+ && el.TryGetProperty(name, out var p)
+ && (p.ValueKind == JsonValueKind.Number || p.ValueKind == JsonValueKind.String)
+ && p.TryGetInt32(out var n)
+ ? n
+ : null;
+}
diff --git a/src/modules/workflow/Domain/Engine/GraphNode.cs b/src/modules/workflow/Domain/Engine/GraphNode.cs
new file mode 100644
index 0000000..cee918f
--- /dev/null
+++ b/src/modules/workflow/Domain/Engine/GraphNode.cs
@@ -0,0 +1,45 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Workflow.Domain.Engine;
+
+///
+/// X6 节点。shape 决定渲染器并反推 NodeType(见 );
+/// 业务配置统一进 data;x/y/width/height/ports 为几何与渲染信息。
+///
+public sealed class GraphNode
+{
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("shape")]
+ public string Shape { get; set; } = string.Empty;
+
+ [JsonPropertyName("x")]
+ public double X { get; set; }
+
+ [JsonPropertyName("y")]
+ public double Y { get; set; }
+
+ [JsonPropertyName("width")]
+ public double? Width { get; set; }
+
+ [JsonPropertyName("height")]
+ public double? Height { get; set; }
+
+ [JsonPropertyName("angle")]
+ public double? Angle { get; set; }
+
+ [JsonPropertyName("visible")]
+ public bool? Visible { get; set; }
+
+ /// 业务数据(节点配置)。替代旧 WorkflowNode.Config 字符串字段。
+ [JsonPropertyName("data")]
+ public JsonElement? Data { get; set; }
+
+ /// 显示名(对应旧 WorkflowNode.Name)。
+ public string? GetTitle() => Data.TryGetString("title");
+
+ [JsonPropertyName("ports")]
+ public JsonElement? Ports { get; set; }
+}
diff --git a/src/modules/workflow/Domain/Engine/GraphPoint.cs b/src/modules/workflow/Domain/Engine/GraphPoint.cs
new file mode 100644
index 0000000..8a827ab
--- /dev/null
+++ b/src/modules/workflow/Domain/Engine/GraphPoint.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Workflow.Domain.Engine;
+
+public sealed class GraphPoint
+{
+ [JsonPropertyName("x")]
+ public double X { get; set; }
+
+ [JsonPropertyName("y")]
+ public double Y { get; set; }
+}
diff --git a/src/modules/workflow/Domain/Engine/TreeBranch.cs b/src/modules/workflow/Domain/Engine/TreeBranch.cs
new file mode 100644
index 0000000..e569091
--- /dev/null
+++ b/src/modules/workflow/Domain/Engine/TreeBranch.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+
+namespace Workflow.Domain.Engine;
+
+/// 条件/并行分支容器中的一条分支。
+public sealed class TreeBranch
+{
+ /// 分支 id,纯 Guid 字符串。
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ /// 分支显示名(如"条件 1"/"默认分支"/"分支 A")。
+ [JsonPropertyName("label")]
+ public string? Label { get; set; }
+
+ ///
+ /// 条件表达式(仅 ConditionBranch 使用;复用 ConditionEvaluator 语法,如 金额 > 5000)。
+ /// 默认分支(else)置空,编译时 order 最大,引擎兜底命中。
+ ///
+ [JsonPropertyName("condition")]
+ public string? Condition { get; set; }
+
+ /// 该分支内的首节点(分支内又是顺序链;null 表示空分支,编译时直通收口)。
+ [JsonPropertyName("child")]
+ public TreeNode? Child { get; set; }
+}
diff --git a/src/modules/workflow/Domain/Engine/TreeNode.cs b/src/modules/workflow/Domain/Engine/TreeNode.cs
new file mode 100644
index 0000000..3f0910e
--- /dev/null
+++ b/src/modules/workflow/Domain/Engine/TreeNode.cs
@@ -0,0 +1,41 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Workflow.Domain.Enums;
+
+namespace Workflow.Domain.Engine;
+
+///
+/// 树节点。一条顺序链上的节点通过 链接;
+/// 容器型节点(条件/并行)通过 挂多条分支。
+///
+public sealed class TreeNode
+{
+ /// 节点 id,必须是纯 Guid 字符串(引擎 ParseGuid 硬约束)。
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ /// 节点类型。决定渲染卡片与配置面板。
+ [JsonPropertyName("type")]
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public TreeNodeTypeEnum Type { get; set; }
+
+ ///
+ /// 节点业务配置(自由对象)。沿用引擎 node.data 的字段约定:
+ /// 审批节点:approvalMode/assigneeRules/timeoutMinutes/fieldPermissions 等;
+ /// 抄送节点:recipients;分支内首节点的 condition 在 上。
+ ///
+ [JsonPropertyName("config")]
+ public JsonElement? Config { get; set; }
+
+ /// 显示标题(冗余于 config.title,便于编辑器直接读取)。
+ [JsonPropertyName("title")]
+ public string? Title { get; set; }
+
+ /// 顺序链上的下一个节点(null 表示链尾,编译器自动接 End)。
+ [JsonPropertyName("child")]
+ public TreeNode? Child { get; set; }
+
+ /// 分支集合(仅 ConditionBranch / ParallelBranch 容器节点使用)。
+ [JsonPropertyName("branches")]
+ public List? Branches { get; set; }
+}
diff --git a/src/modules/workflow/Domain/Engine/WorkflowGraph.cs b/src/modules/workflow/Domain/Engine/WorkflowGraph.cs
index 2993c65..1bd8df2 100644
--- a/src/modules/workflow/Domain/Engine/WorkflowGraph.cs
+++ b/src/modules/workflow/Domain/Engine/WorkflowGraph.cs
@@ -20,130 +20,3 @@ public sealed class WorkflowGraph
/// 空图工厂(容错用)。
public static WorkflowGraph Empty() => new();
}
-
-///
-/// X6 节点。shape 决定渲染器并反推 NodeType(见 );
-/// 业务配置统一进 data;x/y/width/height/ports 为几何与渲染信息。
-///
-public sealed class GraphNode
-{
- [JsonPropertyName("id")]
- public string Id { get; set; } = string.Empty;
-
- [JsonPropertyName("shape")]
- public string Shape { get; set; } = string.Empty;
-
- [JsonPropertyName("x")]
- public double X { get; set; }
-
- [JsonPropertyName("y")]
- public double Y { get; set; }
-
- [JsonPropertyName("width")]
- public double? Width { get; set; }
-
- [JsonPropertyName("height")]
- public double? Height { get; set; }
-
- [JsonPropertyName("angle")]
- public double? Angle { get; set; }
-
- [JsonPropertyName("visible")]
- public bool? Visible { get; set; }
-
- /// 业务数据(节点配置)。替代旧 WorkflowNode.Config 字符串字段。
- [JsonPropertyName("data")]
- public JsonElement? Data { get; set; }
-
- /// 显示名(对应旧 WorkflowNode.Name)。
- public string? GetTitle() => Data.TryGetString("title");
-
- [JsonPropertyName("ports")]
- public JsonElement? Ports { get; set; }
-}
-
-///
-/// X6 边。source/target 用 { cell, port } 结构;几何信息(vertices/router/connector)放顶层;
-/// 业务字段(edgeType/condition/order)放 data。
-///
-public sealed class GraphEdge
-{
- [JsonPropertyName("id")]
- public string Id { get; set; } = string.Empty;
-
- [JsonPropertyName("shape")]
- public string? Shape { get; set; }
-
- [JsonPropertyName("source")]
- public GraphEndpoint Source { get; set; } = new();
-
- [JsonPropertyName("target")]
- public GraphEndpoint Target { get; set; } = new();
-
- /// 业务数据:edgeType / condition / order。
- [JsonPropertyName("data")]
- public JsonElement? Data { get; set; }
-
- [JsonPropertyName("label")]
- public JsonElement? Label { get; set; }
-
- [JsonPropertyName("vertices")]
- public List? Vertices { get; set; }
-
- [JsonPropertyName("router")]
- public JsonElement? Router { get; set; }
-
- [JsonPropertyName("connector")]
- public JsonElement? Connector { get; set; }
-
- /// 边类型(Normal=0/Approved=1/Rejected=2)。缺省为 Normal。
- public EdgeType GetEdgeType() => Data.TryGetInt32("edgeType") is { } v ? (EdgeType)v : EdgeType.Normal;
-
- /// 条件表达式(Condition/ExclusiveGateway 出边)。
- public string? GetCondition() => Data.TryGetString("condition");
-
- /// 分支评估顺序(值小优先)。
- public int GetOrder() => Data.TryGetInt32("order") ?? 0;
-}
-
-public sealed class GraphEndpoint
-{
- /// 节点 id(对应 GraphNode.Id)。
- [JsonPropertyName("cell")]
- public string? Cell { get; set; }
-
- /// 端口 id(top/right/bottom/left)。
- [JsonPropertyName("port")]
- public string? Port { get; set; }
-}
-
-public sealed class GraphPoint
-{
- [JsonPropertyName("x")]
- public double X { get; set; }
-
- [JsonPropertyName("y")]
- public double Y { get; set; }
-}
-
-///
-/// JsonElement? 安全取值扩展:统一处理 Nullable 解构与 ValueKind 校验,
-/// 避免 WorkflowGraph 各 getter 重复 null 检查。
-///
-internal static class GraphJsonExtensions
-{
- public static string? TryGetString(this JsonElement? element, string name) =>
- element is { ValueKind: JsonValueKind.Object } el
- && el.TryGetProperty(name, out var p)
- && p.ValueKind == JsonValueKind.String
- ? p.GetString()
- : null;
-
- public static int? TryGetInt32(this JsonElement? element, string name) =>
- element is { ValueKind: JsonValueKind.Object } el
- && el.TryGetProperty(name, out var p)
- && (p.ValueKind == JsonValueKind.Number || p.ValueKind == JsonValueKind.String)
- && p.TryGetInt32(out var n)
- ? n
- : null;
-}
diff --git a/src/modules/workflow/Domain/Engine/WorkflowTree.cs b/src/modules/workflow/Domain/Engine/WorkflowTree.cs
index 31a5cdf..dc74b57 100644
--- a/src/modules/workflow/Domain/Engine/WorkflowTree.cs
+++ b/src/modules/workflow/Domain/Engine/WorkflowTree.cs
@@ -27,62 +27,3 @@ public sealed class WorkflowTree
/// 空树工厂:用于缺省/容错。
public static WorkflowTree Empty() => new();
}
-
-///
-/// 树节点。一条顺序链上的节点通过 链接;
-/// 容器型节点(条件/并行)通过 挂多条分支。
-///
-public sealed class TreeNode
-{
- /// 节点 id,必须是纯 Guid 字符串(引擎 ParseGuid 硬约束)。
- [JsonPropertyName("id")]
- public string Id { get; set; } = string.Empty;
-
- /// 节点类型。决定渲染卡片与配置面板。
- [JsonPropertyName("type")]
- [JsonConverter(typeof(JsonStringEnumConverter))]
- public TreeNodeTypeEnum Type { get; set; }
-
- ///
- /// 节点业务配置(自由对象)。沿用引擎 node.data 的字段约定:
- /// 审批节点:approvalMode/assigneeRules/timeoutMinutes/fieldPermissions 等;
- /// 抄送节点:recipients;分支内首节点的 condition 在 上。
- ///
- [JsonPropertyName("config")]
- public JsonElement? Config { get; set; }
-
- /// 显示标题(冗余于 config.title,便于编辑器直接读取)。
- [JsonPropertyName("title")]
- public string? Title { get; set; }
-
- /// 顺序链上的下一个节点(null 表示链尾,编译器自动接 End)。
- [JsonPropertyName("child")]
- public TreeNode? Child { get; set; }
-
- /// 分支集合(仅 ConditionBranch / ParallelBranch 容器节点使用)。
- [JsonPropertyName("branches")]
- public List? Branches { get; set; }
-}
-
-/// 条件/并行分支容器中的一条分支。
-public sealed class TreeBranch
-{
- /// 分支 id,纯 Guid 字符串。
- [JsonPropertyName("id")]
- public string Id { get; set; } = string.Empty;
-
- /// 分支显示名(如"条件 1"/"默认分支"/"分支 A")。
- [JsonPropertyName("label")]
- public string? Label { get; set; }
-
- ///
- /// 条件表达式(仅 ConditionBranch 使用;复用 ConditionEvaluator 语法,如 金额 > 5000)。
- /// 默认分支(else)置空,编译时 order 最大,引擎兜底命中。
- ///
- [JsonPropertyName("condition")]
- public string? Condition { get; set; }
-
- /// 该分支内的首节点(分支内又是顺序链;null 表示空分支,编译时直通收口)。
- [JsonPropertyName("child")]
- public TreeNode? Child { get; set; }
-}
diff --git a/tests/BuildFlow.Platform.Tests/DictionaryConfiguration.cs b/tests/BuildFlow.Platform.Tests/DictionaryConfiguration.cs
new file mode 100644
index 0000000..4cfb66b
--- /dev/null
+++ b/tests/BuildFlow.Platform.Tests/DictionaryConfiguration.cs
@@ -0,0 +1,64 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Primitives;
+
+namespace BuildFlow.Platform.Tests;
+
+/// 极简字典实现的 IConfiguration,仅供测试使用。
+/// 完整实现 GetSection/GetChildren,让 GetValue<T> 扩展方法(走 ConfigurationBinder)能正确工作。
+/// 注意:ConfigurationBinder.GetValue 内部调用 GetSection(key).Value,故必须返回有效 section。
+internal sealed class DictionaryConfiguration : IConfiguration
+{
+ private readonly Dictionary _values = [];
+
+ public string? this[string key]
+ {
+ get => _values.TryGetValue(key, out var v) ? v : null;
+ set => _values[key] = value;
+ }
+
+ // 支持集合初始化器语法:new DictionaryConfiguration { ["k"] = "v" }
+ public void Add(string key, string? value) => _values[key] = value;
+
+ IEnumerable IConfiguration.GetChildren() =>
+ _values.Select(kv => (IConfigurationSection)new LeafSection(kv.Key, kv.Value)).ToList();
+
+ IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance;
+
+ IConfigurationSection IConfiguration.GetSection(string key) =>
+ new LeafSection(key, _values.TryGetValue(key, out var v) ? v : null);
+
+ private sealed class LeafSection(string key, string? value) : IConfigurationSection
+ {
+ public string Key => key;
+ public string Path => key;
+ public string? Value { get; set; } = value;
+
+ string? IConfiguration.this[string childKey]
+ {
+ get => null;
+ set { }
+ }
+
+ IConfigurationSection IConfiguration.GetSection(string childKey) =>
+ new LeafSection($"{key}:{childKey}", null);
+
+ IEnumerable IConfiguration.GetChildren() => [];
+
+ IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance;
+ }
+
+ /// 永不变更的占位 IChangeToken,避免引入额外包依赖。
+ private sealed class EmptyChangeToken : IChangeToken
+ {
+ public static readonly EmptyChangeToken Instance = new();
+ public bool ActiveChangeCallbacks => false;
+ public bool HasChanged => false;
+ public IDisposable RegisterChangeCallback(Action