diff --git a/Docs/superpowers/plans/2026-07-03-braingraph-phase1-core-runtime.md b/Docs/superpowers/plans/2026-07-03-braingraph-phase1-core-runtime.md
new file mode 100644
index 00000000..eebd874b
--- /dev/null
+++ b/Docs/superpowers/plans/2026-07-03-braingraph-phase1-core-runtime.md
@@ -0,0 +1,1310 @@
+# BrainGraph 第 1 阶段:核心运行时 实现计划
+
+> **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:** 实现 BrainGraph 决策框架的纯 C# 核心运行时(不可变状态图 + fluent builder + 每实例运行时 + 条件/事件转换 + IsControllable 让位门 + trace + 池复用重置),全部用 EditMode 单测覆盖,零 Unity 场景依赖。
+
+**Architecture:** HFSM 骨干。声明层(`AiScript.Build`)用 `BrainBuilder` 构建成不可变共享 `AiGraph`(每敌人类型一份 flyweight);每个敌人实例持有一个轻量 `AiRuntime`,通过 `IAiContext`(暴露 `ISensor/IMover/ICombatant/IActorVitals/Blackboard` 的能力接口)驱动真实子系统。核心层只依赖 `BaseGames.Core` + `UnityEngine`(用 `Vector2` 等值类型),用假的能力接口即可完整单测。
+
+**Tech Stack:** Unity 2022+, C#, Unity Test Framework (NUnit, EditMode), asmdef。
+
+**Spec:** `Docs/superpowers/specs/2026-07-03-enemy-ai-framework-design.md`(本阶段覆盖其 §3.1/§3.2/§3.3 的条件+事件转换、§3.6 IsControllable 门、§3.8 池复用重置;§3.4 能力编排 sugar、§3.7 AiScheduler、§4 完整接口方法集、§6 交付面、§7 组件与适配器留待后续阶段)。
+
+---
+
+## 文件结构(本阶段新建)
+
+新程序集 `BaseGames.AI`,位于 `Assets/_Game/Scripts/AI/`:
+
+- `BaseGames.AI.asmdef` — 新程序集,仅引用 `BaseGames.Core`。
+- `AiSignal.cs` — 推送信号枚举。
+- `IAiContext.cs` — 上下文接口(聚合能力接口 + 黑板)。
+- `ISensor.cs` / `IMover.cs` / `ICombatant.cs` / `IActorVitals.cs` — 能力接口(本阶段只声明被测试消费的最小成员,完整成员集在第 3 阶段接适配器时按需补齐)。
+- `Blackboard.cs` — 具名、可枚举的 AI 临时值容器。
+- `Transition.cs` — 一条转换(条件型 / 事件型 + 可读标签 + 优先级)。
+- `AiState.cs` — 一个状态(OnEnter/Tick/OnExit + 出边转换列表)。
+- `AiGraph.cs` — 不可变状态图(状态字典 + 入口 + 全局/父层转换)。
+- `BrainBuilder.cs` — fluent builder,含 `StateBuilder` / `TransitionBuilder` / `GlobalBuilder`。
+- `AiScript.cs` — 抽象声明基类 + `AiDefinitionAttribute`。
+- `TransitionRecord.cs` — trace 记录(from/to/trigger)。
+- `AiRuntime.cs` — 每实例执行器(Tick、转换评估、门、事件队列、trace、Reset)。
+
+测试(`Assets/Tests/EditMode/AI/`):
+- `BlackboardTests.cs`、`BrainBuilderTests.cs`、`AiRuntimeTests.cs`、`Fakes/FakeAiContext.cs`。
+- 修改 `Assets/Tests/EditMode/BaseGames.Tests.EditMode.asmdef` 增加 `BaseGames.AI` 引用。
+
+> 命名/注释不得出现任何参考游戏名(CLAUDE.md 第 4 条)。
+
+---
+
+## Task 1: 新建 `BaseGames.AI` 程序集与基础类型
+
+**Files:**
+- Create: `Assets/_Game/Scripts/AI/BaseGames.AI.asmdef`
+- Create: `Assets/_Game/Scripts/AI/AiSignal.cs`
+- Create: `Assets/_Game/Scripts/AI/Blackboard.cs`
+- Create: `Assets/Tests/EditMode/AI/BlackboardTests.cs`
+- Modify: `Assets/Tests/EditMode/BaseGames.Tests.EditMode.asmdef`
+
+- [ ] **Step 1: 创建程序集定义**
+
+`Assets/_Game/Scripts/AI/BaseGames.AI.asmdef`:
+
+```json
+{
+ "name": "BaseGames.AI",
+ "rootNamespace": "BaseGames.AI",
+ "references": [
+ "BaseGames.Core"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
+```
+
+- [ ] **Step 2: 创建 `AiSignal` 枚举**
+
+`Assets/_Game/Scripts/AI/AiSignal.cs`:
+
+```csharp
+namespace BaseGames.AI
+{
+ /// 决策层的推送信号,用于事件驱动的状态转换(不靠轮询)。
+ public enum AiSignal
+ {
+ Damaged, // 受到伤害
+ Parried, // 被弹反
+ Staggered, // 进入硬直
+ KnockedUp, // 被击飞
+ Died, // 死亡
+ PlayerSpotted,// 首次侦测到玩家
+ AbilityEnded, // 当前能力协程结束/被打断
+ PhaseChanged // Boss 阶段变更
+ }
+}
+```
+
+- [ ] **Step 3: 在测试程序集里加 `BaseGames.AI` 引用**
+
+修改 `Assets/Tests/EditMode/BaseGames.Tests.EditMode.asmdef` 的 `references` 数组,加入一行 `"BaseGames.AI"`(放在 `"BaseGames.Core"` 之后):
+
+```json
+ "references": [
+ "BaseGames.Combat.StatusEffects",
+ "BaseGames.Combat",
+ "BaseGames.Core",
+ "BaseGames.AI",
+ "BaseGames.Core.Events",
+ "BaseGames.Core.Save",
+ "BaseGames.EventChain",
+ "UnityEngine.TestRunner",
+ "UnityEditor.TestRunner"
+ ],
+```
+
+- [ ] **Step 4: 写失败测试 —— Blackboard 读写与枚举**
+
+`Assets/Tests/EditMode/AI/BlackboardTests.cs`:
+
+```csharp
+using NUnit.Framework;
+using UnityEngine;
+using BaseGames.AI;
+
+namespace BaseGames.Tests.EditMode.AI
+{
+ public class BlackboardTests
+ {
+ [Test]
+ public void SetThenGet_ReturnsValue()
+ {
+ var bb = new Blackboard();
+ bb.Set("lastKnown", new Vector2(3f, 4f));
+ Assert.AreEqual(new Vector2(3f, 4f), bb.Get("lastKnown"));
+ }
+
+ [Test]
+ public void Get_MissingKey_ReturnsDefault()
+ {
+ var bb = new Blackboard();
+ Assert.AreEqual(0f, bb.Get("nope"));
+ }
+
+ [Test]
+ public void Keys_EnumeratesAllSetKeys()
+ {
+ var bb = new Blackboard();
+ bb.Set("a", 1);
+ bb.Set("b", 2);
+ CollectionAssert.AreEquivalent(new[] { "a", "b" }, bb.Keys);
+ }
+
+ [Test]
+ public void Clear_RemovesAll()
+ {
+ var bb = new Blackboard();
+ bb.Set("a", 1);
+ bb.Clear();
+ CollectionAssert.IsEmpty(bb.Keys);
+ }
+
+ [Test]
+ public void Snapshot_ReturnsKeyToStringMap_ForIntrospection()
+ {
+ var bb = new Blackboard();
+ bb.Set("hp", 0.5f);
+ var snap = bb.Snapshot();
+ Assert.AreEqual("0.5", snap["hp"]);
+ }
+ }
+}
+```
+
+- [ ] **Step 5: 运行测试确认失败**
+
+Run(Unity Test Runner,EditMode,或 CLI):`Unity -runTests -testPlatform EditMode -testFilter BlackboardTests`
+Expected: 编译失败 / FAIL —— `Blackboard` 未定义。
+
+- [ ] **Step 6: 实现 `Blackboard`**
+
+`Assets/_Game/Scripts/AI/Blackboard.cs`:
+
+```csharp
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace BaseGames.AI
+{
+ ///
+ /// 具名、可枚举的 AI 临时值容器(每敌人实例一份)。
+ /// 保留"具名"是为了让调试器/MCP 能通用枚举当前值(见 spec §3.5)。
+ ///
+ public sealed class Blackboard
+ {
+ readonly Dictionary _values = new Dictionary();
+
+ public void Set(string key, T value) => _values[key] = value;
+
+ public T Get(string key)
+ => _values.TryGetValue(key, out var v) && v is T t ? t : default;
+
+ public bool Has(string key) => _values.ContainsKey(key);
+
+ public IReadOnlyCollection Keys => _values.Keys;
+
+ public void Clear() => _values.Clear();
+
+ /// 供调试器/MCP 自省:key → 值的字符串表示。
+ public Dictionary Snapshot()
+ {
+ var result = new Dictionary(_values.Count);
+ foreach (var kv in _values)
+ {
+ result[kv.Key] = kv.Value is System.IFormattable f
+ ? f.ToString(null, CultureInfo.InvariantCulture)
+ : kv.Value?.ToString() ?? "null";
+ }
+ return result;
+ }
+ }
+}
+```
+
+- [ ] **Step 7: 运行测试确认通过**
+
+Run: EditMode `BlackboardTests`
+Expected: PASS(5 项全绿)。
+
+- [ ] **Step 8: 提交**
+
+```bash
+git add Assets/_Game/Scripts/AI/BaseGames.AI.asmdef Assets/_Game/Scripts/AI/BaseGames.AI.asmdef.meta \
+ Assets/_Game/Scripts/AI/AiSignal.cs Assets/_Game/Scripts/AI/AiSignal.cs.meta \
+ Assets/_Game/Scripts/AI/Blackboard.cs Assets/_Game/Scripts/AI/Blackboard.cs.meta \
+ Assets/Tests/EditMode/AI Assets/Tests/EditMode/BaseGames.Tests.EditMode.asmdef
+git commit -m "feat(ai): BrainGraph 核心程序集 + AiSignal + Blackboard(可自省)"
+```
+
+---
+
+## Task 2: 能力接口、上下文、AiScript 声明基类
+
+**Files:**
+- Create: `Assets/_Game/Scripts/AI/ISensor.cs`
+- Create: `Assets/_Game/Scripts/AI/IMover.cs`
+- Create: `Assets/_Game/Scripts/AI/ICombatant.cs`
+- Create: `Assets/_Game/Scripts/AI/IActorVitals.cs`
+- Create: `Assets/_Game/Scripts/AI/IAiContext.cs`
+- Create: `Assets/_Game/Scripts/AI/AiScript.cs`
+
+> 本阶段接口只声明**测试消费到的最小成员**(YAGNI)。第 3 阶段接适配器时,按 spec §4 的完整映射表补齐其余方法(`SlotDetects`/`JumpTo`/`UseSkillWeighted`/`SetPoiseLevel` 等)。
+
+- [ ] **Step 1: `ISensor`**
+
+`Assets/_Game/Scripts/AI/ISensor.cs`:
+
+```csharp
+using UnityEngine;
+
+namespace BaseGames.AI
+{
+ /// 感知能力接口(第 3 阶段由 SensorAdapter 适配 PhysicsPerceptionSystem/ThreatAssessor)。
+ public interface ISensor
+ {
+ bool SeesPlayer();
+ bool InRange(float range); // 内部用平方距离比较
+ bool LostFor(float seconds); // 丢失玩家已持续 seconds
+ Vector2 LastKnown { get; }
+ }
+}
+```
+
+- [ ] **Step 2: `IMover`**
+
+`Assets/_Game/Scripts/AI/IMover.cs`:
+
+```csharp
+using UnityEngine;
+
+namespace BaseGames.AI
+{
+ /// 移动能力接口(第 3 阶段由 MoverAdapter 适配 IPathAgent/EnemyMovement)。
+ public interface IMover
+ {
+ void MoveTo(Vector2 target);
+ void FacePlayer();
+ void Stop();
+ void WalkRandom();
+ void LookAround();
+ }
+}
+```
+
+- [ ] **Step 3: `ICombatant`**
+
+`Assets/_Game/Scripts/AI/ICombatant.cs`:
+
+```csharp
+namespace BaseGames.AI
+{
+ /// 战斗/能力调度接口(第 3 阶段由 CombatantAdapter 适配 EnemyAbilityRegistry)。
+ public interface ICombatant
+ {
+ bool UseAbility(string abilityId);
+ bool IsAbilityRunning(string abilityId = null); // null = 任意能力
+ bool NoAbilityRunning { get; }
+ bool CanUseAbility(string abilityId);
+ }
+}
+```
+
+- [ ] **Step 4: `IActorVitals`**
+
+`Assets/_Game/Scripts/AI/IActorVitals.cs`:
+
+```csharp
+namespace BaseGames.AI
+{
+ /// 生命体征/受控状态接口(第 3 阶段由 VitalsAdapter 适配 EnemyStats/EnemyBase)。
+ public interface IActorVitals
+ {
+ bool IsAlive { get; }
+ /// 物理态是否为 Controlled——决策层仅在此为真时推进(IsControllable 让位门, spec §3.6)。
+ bool IsControllable { get; }
+ float HpPercent { get; }
+ bool HpBelow(float ratio);
+ }
+}
+```
+
+- [ ] **Step 5: `IAiContext`**
+
+`Assets/_Game/Scripts/AI/IAiContext.cs`:
+
+```csharp
+namespace BaseGames.AI
+{
+ ///
+ /// 传给所有状态回调 / 转换条件的上下文。声明层的 lambda 只能通过它访问实例,
+ /// 不得闭包捕获具体敌人——这是 lambda 挂在共享 AiGraph 上的前提(spec §3.8)。
+ ///
+ public interface IAiContext
+ {
+ ISensor Sensor { get; }
+ IMover Mover { get; }
+ ICombatant Combat { get; }
+ IActorVitals Vitals { get; }
+ Blackboard Blackboard { get; }
+ }
+}
+```
+
+- [ ] **Step 6: `AiScript` 抽象基类 + 特性**
+
+`Assets/_Game/Scripts/AI/AiScript.cs`:
+
+```csharp
+using System;
+
+namespace BaseGames.AI
+{
+ /// 标注一个 AiScript 对应的敌人类型 id,供 AiDefinitionRegistry 反射收集(第 3 阶段)。
+ [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+ public sealed class AiDefinitionAttribute : Attribute
+ {
+ public string Id { get; }
+ public AiDefinitionAttribute(string id) => Id = id;
+ }
+
+ ///
+ /// 一种敌人 AI 的声明基类(唯一事实来源)。子类实现 Build 用 fluent 描述状态机。
+ /// 构建结果 AiGraph 每类型只建一次、实例共享(flyweight, spec §3.1)。
+ ///
+ public abstract class AiScript
+ {
+ AiGraph _cached;
+
+ /// 惰性构建并缓存共享 AiGraph。
+ public AiGraph GetOrBuildGraph()
+ {
+ if (_cached == null)
+ {
+ var b = new BrainBuilder();
+ Build(b);
+ _cached = b.Build();
+ }
+ return _cached;
+ }
+
+ protected abstract void Build(BrainBuilder b);
+ }
+}
+```
+
+- [ ] **Step 7: 编译校验(无独立测试,类型将由后续任务的测试消费)**
+
+Run: 触发 Unity 编译(MCP `unity_get_compilation_errors` 或切到 Editor)。
+Expected: 无编译错误(`BrainBuilder`/`AiGraph` 尚未定义会报错 —— 因此本步骤仅创建接口文件,`AiScript.cs` 依赖的 `BrainBuilder`/`AiGraph` 在 Task 3、4 定义;**先不提交,与 Task 3、4 连续完成后一并编译**)。
+
+> 说明:`AiScript.cs` 引用了 Task 3/4 才定义的 `BrainBuilder`/`AiGraph`。执行时先完成 Step 1-5(接口,可独立编译),Step 6 的 `AiScript.cs` 可暂缓到 Task 4 之后再落,或与 Task 3、4 合并提交。TDD 顺序见下方 Task 3。
+
+- [ ] **Step 8: 提交接口层**
+
+```bash
+git add Assets/_Game/Scripts/AI/ISensor.cs Assets/_Game/Scripts/AI/IMover.cs \
+ Assets/_Game/Scripts/AI/ICombatant.cs Assets/_Game/Scripts/AI/IActorVitals.cs \
+ Assets/_Game/Scripts/AI/IAiContext.cs \
+ Assets/_Game/Scripts/AI/*.meta
+git commit -m "feat(ai): BrainGraph 能力接口(ISensor/IMover/ICombatant/IActorVitals)+IAiContext"
+```
+
+---
+
+## Task 3: `Transition` 与 `AiState`
+
+**Files:**
+- Create: `Assets/_Game/Scripts/AI/Transition.cs`
+- Create: `Assets/_Game/Scripts/AI/AiState.cs`
+- Create: `Assets/_Game/Scripts/AI/TransitionRecord.cs`
+
+这两个是数据结构(无独立行为测试),其行为由 Task 4(builder)与 Task 5/6(runtime)的测试覆盖。因此本任务只创建类型 + 编译校验,不单独写测试。
+
+- [ ] **Step 1: `Transition`**
+
+`Assets/_Game/Scripts/AI/Transition.cs`:
+
+```csharp
+using System;
+
+namespace BaseGames.AI
+{
+ /// 一条转换:条件型(Condition 非空)或事件型(Event 非空)。二者互斥。
+ public sealed class Transition
+ {
+ public string Target { get; }
+ public Func Condition { get; } // 条件型:pull,按 LOD 频率评估
+ public AiSignal? Event { get; } // 事件型:push,收到信号即触发
+ public string Label { get; } // 可读标签(条件源码文本 / 事件名)
+
+ Transition(string target, Func condition, AiSignal? evt, string label)
+ {
+ Target = target;
+ Condition = condition;
+ Event = evt;
+ Label = label;
+ }
+
+ public static Transition OnCondition(string target, Func condition, string label)
+ => new Transition(target, condition, null, label);
+
+ public static Transition OnEvent(string target, AiSignal evt)
+ => new Transition(target, null, evt, "on " + evt);
+
+ public bool IsEvent => Event.HasValue;
+ }
+}
+```
+
+- [ ] **Step 2: `TransitionRecord`(trace 用)**
+
+`Assets/_Game/Scripts/AI/TransitionRecord.cs`:
+
+```csharp
+namespace BaseGames.AI
+{
+ /// 一次已发生的转换,供调试器/MCP 自省"最近为何转换"。
+ public readonly struct TransitionRecord
+ {
+ public readonly string From;
+ public readonly string To;
+ public readonly string Trigger; // 条件标签或事件名
+
+ public TransitionRecord(string from, string to, string trigger)
+ {
+ From = from;
+ To = to;
+ Trigger = trigger;
+ }
+
+ public override string ToString() => $"{From} -> {To} : {Trigger}";
+ }
+}
+```
+
+- [ ] **Step 3: `AiState`**
+
+`Assets/_Game/Scripts/AI/AiState.cs`:
+
+```csharp
+using System;
+using System.Collections.Generic;
+
+namespace BaseGames.AI
+{
+ /// 一个状态:进入/每帧/退出钩子 + 出边转换列表(按优先级有序,首个命中即转)。
+ public sealed class AiState
+ {
+ public string Name { get; }
+ public Action OnEnter { get; internal set; }
+ public Action OnTick { get; internal set; } // (ctx, dt)
+ public Action OnExit { get; internal set; }
+ public List Transitions { get; } = new List();
+
+ public AiState(string name) => Name = name;
+ }
+}
+```
+
+- [ ] **Step 4: 编译校验**
+
+Run: Unity 编译(`unity_get_compilation_errors`)。
+Expected: 无新错误(这些类型自洽;`AiScript.cs` 仍缺 `BrainBuilder`/`AiGraph` → Task 4 补齐)。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add Assets/_Game/Scripts/AI/Transition.cs Assets/_Game/Scripts/AI/AiState.cs \
+ Assets/_Game/Scripts/AI/TransitionRecord.cs Assets/_Game/Scripts/AI/*.meta
+git commit -m "feat(ai): BrainGraph Transition/AiState/TransitionRecord 数据结构"
+```
+
+---
+
+## Task 4: `AiGraph` 与 `BrainBuilder`(fluent 声明)
+
+**Files:**
+- Create: `Assets/_Game/Scripts/AI/AiGraph.cs`
+- Create: `Assets/_Game/Scripts/AI/BrainBuilder.cs`
+- Create: `Assets/Tests/EditMode/AI/BrainBuilderTests.cs`
+
+- [ ] **Step 1: 写失败测试 —— builder 构建出正确的图结构**
+
+`Assets/Tests/EditMode/AI/BrainBuilderTests.cs`:
+
+```csharp
+using NUnit.Framework;
+using BaseGames.AI;
+
+namespace BaseGames.Tests.EditMode.AI
+{
+ public class BrainBuilderTests
+ {
+ static AiGraph BuildSample()
+ {
+ var b = new BrainBuilder();
+ b.Entry("Patrol");
+ b.Global().To("Dead").OnEvent(AiSignal.Died);
+ b.State("Patrol")
+ .To("Chase").When(c => c.Sensor.SeesPlayer());
+ b.State("Chase")
+ .To("Patrol").When(c => c.Sensor.LostFor(2f));
+ b.State("Dead");
+ return b.Build();
+ }
+
+ [Test]
+ public void Build_SetsEntryState()
+ {
+ Assert.AreEqual("Patrol", BuildSample().EntryState);
+ }
+
+ [Test]
+ public void Build_RegistersAllStates()
+ {
+ var g = BuildSample();
+ CollectionAssert.AreEquivalent(
+ new[] { "Patrol", "Chase", "Dead" },
+ System.Linq.Enumerable.ToList(g.StateNames));
+ }
+
+ [Test]
+ public void Build_CapturesConditionLabelFromExpressionText()
+ {
+ var g = BuildSample();
+ var t = g.GetState("Patrol").Transitions[0];
+ StringAssert.Contains("SeesPlayer", t.Label);
+ }
+
+ [Test]
+ public void Build_GlobalTransitionsAreSeparate()
+ {
+ var g = BuildSample();
+ Assert.AreEqual(1, g.GlobalTransitions.Count);
+ Assert.AreEqual("Dead", g.GlobalTransitions[0].Target);
+ Assert.AreEqual(AiSignal.Died, g.GlobalTransitions[0].Event);
+ }
+
+ [Test]
+ public void Build_UnknownTransitionTarget_Throws()
+ {
+ var b = new BrainBuilder();
+ b.Entry("A");
+ b.State("A").To("Nonexistent").When(c => true);
+ Assert.Throws(() => b.Build());
+ }
+ }
+}
+```
+
+- [ ] **Step 2: 运行测试确认失败**
+
+Run: EditMode `BrainBuilderTests`
+Expected: 编译失败 —— `AiGraph`/`BrainBuilder` 未定义。
+
+- [ ] **Step 3: 实现 `AiGraph`**
+
+`Assets/_Game/Scripts/AI/AiGraph.cs`:
+
+```csharp
+using System.Collections.Generic;
+
+namespace BaseGames.AI
+{
+ ///
+ /// 不可变状态图,每敌人类型构建一次、所有实例共享(flyweight, spec §3.1/§3.8)。
+ /// 既被 AiRuntime 执行,又被 MermaidExporter/调试器只读投影。
+ ///
+ public sealed class AiGraph
+ {
+ readonly Dictionary _states;
+ public string EntryState { get; }
+ public IReadOnlyList GlobalTransitions { get; }
+
+ public AiGraph(string entry, Dictionary states, IReadOnlyList globals)
+ {
+ EntryState = entry;
+ _states = states;
+ GlobalTransitions = globals;
+ }
+
+ public AiState GetState(string name) => _states[name];
+ public bool HasState(string name) => _states.ContainsKey(name);
+ public IEnumerable StateNames => _states.Keys;
+ public IEnumerable States => _states.Values;
+ }
+}
+```
+
+- [ ] **Step 4: 实现 `BrainBuilder`(含 `StateBuilder`/`TransitionBuilder`/`GlobalBuilder`)**
+
+`Assets/_Game/Scripts/AI/BrainBuilder.cs`:
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace BaseGames.AI
+{
+ /// fluent builder:声明层的唯一入口。构建出不可变 AiGraph。
+ public sealed class BrainBuilder
+ {
+ readonly Dictionary _states = new Dictionary();
+ readonly List _globals = new List();
+ string _entry;
+
+ public BrainBuilder Entry(string stateName)
+ {
+ _entry = stateName;
+ return this;
+ }
+
+ public StateBuilder State(string name)
+ {
+ if (!_states.TryGetValue(name, out var s))
+ {
+ s = new AiState(name);
+ _states[name] = s;
+ }
+ return new StateBuilder(this, s);
+ }
+
+ public GlobalBuilder Global() => new GlobalBuilder(this);
+
+ internal void AddGlobal(Transition t) => _globals.Add(t);
+
+ public AiGraph Build()
+ {
+ if (string.IsNullOrEmpty(_entry))
+ throw new InvalidOperationException("BrainBuilder: 未设置 Entry 状态。");
+ if (!_states.ContainsKey(_entry))
+ throw new InvalidOperationException($"BrainBuilder: Entry 状态 '{_entry}' 未声明。");
+
+ // 校验所有转换目标存在(根因暴露,不做下游兜底, CLAUDE.md 第 6 条)
+ foreach (var s in _states.Values)
+ foreach (var t in s.Transitions)
+ if (!_states.ContainsKey(t.Target))
+ throw new InvalidOperationException(
+ $"BrainBuilder: 状态 '{s.Name}' 的转换指向未声明状态 '{t.Target}'。");
+ foreach (var t in _globals)
+ if (!_states.ContainsKey(t.Target))
+ throw new InvalidOperationException(
+ $"BrainBuilder: 全局转换指向未声明状态 '{t.Target}'。");
+
+ return new AiGraph(_entry, _states, _globals);
+ }
+
+ // ---- 嵌套 builder ----
+
+ public sealed class StateBuilder
+ {
+ readonly BrainBuilder _owner;
+ readonly AiState _state;
+ internal StateBuilder(BrainBuilder owner, AiState state) { _owner = owner; _state = state; }
+
+ public StateBuilder OnEnter(Action fn) { _state.OnEnter = fn; return this; }
+ public StateBuilder Tick(Action fn) { _state.OnTick = (c, _) => fn(c); return this; }
+ public StateBuilder Tick(Action fn) { _state.OnTick = fn; return this; }
+ public StateBuilder OnExit(Action fn) { _state.OnExit = fn; return this; }
+
+ public TransitionBuilder To(string target) => new TransitionBuilder(_owner, this, _state, target);
+ }
+
+ public sealed class TransitionBuilder
+ {
+ readonly BrainBuilder _owner;
+ readonly StateBuilder _stateBuilder;
+ readonly AiState _state;
+ readonly string _target;
+ internal TransitionBuilder(BrainBuilder owner, StateBuilder sb, AiState state, string target)
+ { _owner = owner; _stateBuilder = sb; _state = state; _target = target; }
+
+ /// 条件转换。label 默认由 [CallerArgumentExpression] 抓取 cond 的源码文本。
+ public StateBuilder When(Func cond,
+ [CallerArgumentExpression("cond")] string label = null)
+ {
+ _state.Transitions.Add(Transition.OnCondition(_target, cond, label));
+ return _stateBuilder;
+ }
+
+ /// 事件转换。
+ public StateBuilder OnEvent(AiSignal evt)
+ {
+ _state.Transitions.Add(Transition.OnEvent(_target, evt));
+ return _stateBuilder;
+ }
+
+ /// 在本状态停留 seconds 秒后转换(读 runtime 的状态计时器)。
+ public StateBuilder After(float seconds)
+ {
+ _state.Transitions.Add(Transition.OnCondition(
+ _target, c => AiRuntime.TimeInState(c) >= seconds, $"after {seconds}s"));
+ return _stateBuilder;
+ }
+ }
+
+ public sealed class GlobalBuilder
+ {
+ readonly BrainBuilder _owner;
+ string _pendingTarget;
+ internal GlobalBuilder(BrainBuilder owner) { _owner = owner; }
+
+ public GlobalBuilder To(string target) { _pendingTarget = target; return this; }
+
+ public GlobalBuilder OnEvent(AiSignal evt)
+ {
+ _owner.AddGlobal(Transition.OnEvent(_pendingTarget, evt));
+ return this;
+ }
+
+ public GlobalBuilder When(Func cond,
+ [CallerArgumentExpression("cond")] string label = null)
+ {
+ _owner.AddGlobal(Transition.OnCondition(_pendingTarget, cond, label));
+ return this;
+ }
+ }
+ }
+}
+```
+
+> 注:`After()` 引用了 `AiRuntime.TimeInState(c)`(Task 5 定义的静态辅助,通过 context 取当前 runtime 的状态计时器)。若执行到本任务时 `AiRuntime` 尚未定义,先临时把 `After` 的实现体改为 `throw new NotImplementedException()` 以通过编译,Task 5 完成后回填(回填是同一函数体,非占位)。测试 `BrainBuilderTests` 不覆盖 `After`,不受影响。
+>
+> **CallerArgumentExpression 兼容性**:需 C# 10。若项目 Unity 版本的语言级别不支持,添加一个 polyfill:新建 `Assets/_Game/Scripts/AI/CallerArgumentExpressionAttribute.cs`:
+> ```csharp
+> #if !NET5_0_OR_GREATER
+> namespace System.Runtime.CompilerServices {
+> [AttributeUsage(AttributeTargets.Parameter)]
+> internal sealed class CallerArgumentExpressionAttribute : Attribute {
+> public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName;
+> public string ParameterName { get; }
+> }
+> }
+> #endif
+> ```
+> 若 polyfill 后编译器仍不填充(语言版本 < 10),标签会退化为默认值 —— 此时把 `Build_CapturesConditionLabelFromExpressionText` 标记为 `[Ignore("需 C# 10")]` 并在第 6 阶段处理,不阻塞核心。
+
+- [ ] **Step 5: 运行测试确认通过**
+
+Run: EditMode `BrainBuilderTests`
+Expected: PASS(5 项)。若 `After` 临时抛异常,本测试不涉及,仍应全绿。
+
+- [ ] **Step 6: 补落 `AiScript.cs`(Task 2 Step 6 暂缓的文件)并编译**
+
+此时 `BrainBuilder`/`AiGraph` 已存在,创建/确认 `AiScript.cs`(内容见 Task 2 Step 6),编译应通过。
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add Assets/_Game/Scripts/AI/AiGraph.cs Assets/_Game/Scripts/AI/BrainBuilder.cs \
+ Assets/_Game/Scripts/AI/AiScript.cs Assets/Tests/EditMode/AI/BrainBuilderTests.cs \
+ Assets/_Game/Scripts/AI/*.meta Assets/Tests/EditMode/AI/*.meta
+git commit -m "feat(ai): BrainGraph AiGraph + BrainBuilder(fluent, 条件源码标签, 目标校验)"
+```
+
+---
+
+## Task 5: `AiRuntime` —— Tick、条件转换、IsControllable 让位门、状态计时器
+
+**Files:**
+- Create: `Assets/_Game/Scripts/AI/AiRuntime.cs`
+- Create: `Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs`
+- Create: `Assets/Tests/EditMode/AI/AiRuntimeTests.cs`
+
+- [ ] **Step 1: 写测试替身 `FakeAiContext`**
+
+`Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs`:
+
+```csharp
+using System.Collections.Generic;
+using UnityEngine;
+using BaseGames.AI;
+
+namespace BaseGames.Tests.EditMode.AI
+{
+ public sealed class FakeSensor : ISensor
+ {
+ public bool Sees; public float LostForValue; public Vector2 Last;
+ public bool SeesPlayer() => Sees;
+ public bool InRange(float range) => Sees; // 简化:可见即在范围
+ public bool LostFor(float seconds) => LostForValue >= seconds;
+ public Vector2 LastKnown => Last;
+ }
+
+ public sealed class FakeMover : IMover
+ {
+ public List Calls = new List();
+ public void MoveTo(Vector2 t) => Calls.Add("MoveTo");
+ public void FacePlayer() => Calls.Add("FacePlayer");
+ public void Stop() => Calls.Add("Stop");
+ public void WalkRandom() => Calls.Add("WalkRandom");
+ public void LookAround() => Calls.Add("LookAround");
+ }
+
+ public sealed class FakeCombat : ICombatant
+ {
+ public string Running; public List Used = new List();
+ public bool UseAbility(string id) { Used.Add(id); Running = id; return true; }
+ public bool IsAbilityRunning(string id = null) => id == null ? Running != null : Running == id;
+ public bool NoAbilityRunning => Running == null;
+ public bool CanUseAbility(string id) => true;
+ }
+
+ public sealed class FakeVitals : IActorVitals
+ {
+ public bool Alive = true; public bool Controllable = true; public float Hp = 1f;
+ public bool IsAlive => Alive;
+ public bool IsControllable => Controllable;
+ public float HpPercent => Hp;
+ public bool HpBelow(float ratio) => Hp < ratio;
+ }
+
+ public sealed class FakeAiContext : IAiContext
+ {
+ public FakeSensor S = new FakeSensor();
+ public FakeMover M = new FakeMover();
+ public FakeCombat C = new FakeCombat();
+ public FakeVitals V = new FakeVitals();
+ public Blackboard BB = new Blackboard();
+ public ISensor Sensor => S;
+ public IMover Mover => M;
+ public ICombatant Combat => C;
+ public IActorVitals Vitals => V;
+ public Blackboard Blackboard => BB;
+ }
+}
+```
+
+- [ ] **Step 2: 写失败测试 —— Tick、条件转换、门、计时器**
+
+`Assets/Tests/EditMode/AI/AiRuntimeTests.cs`:
+
+```csharp
+using NUnit.Framework;
+using BaseGames.AI;
+
+namespace BaseGames.Tests.EditMode.AI
+{
+ public class AiRuntimeTests
+ {
+ static AiGraph Graph()
+ {
+ var b = new BrainBuilder();
+ b.Entry("Patrol");
+ b.Global().To("Dead").OnEvent(AiSignal.Died);
+ b.State("Patrol")
+ .Tick(c => c.Mover.WalkRandom())
+ .To("Chase").When(c => c.Sensor.SeesPlayer());
+ b.State("Chase")
+ .OnEnter(c => c.Mover.FacePlayer())
+ .Tick(c => c.Mover.MoveTo(c.Sensor.LastKnown))
+ .To("Search").When(c => c.Sensor.LostFor(2f));
+ b.State("Search")
+ .To("Patrol").After(3f);
+ b.State("Dead");
+ return b.Build();
+ }
+
+ [Test]
+ public void StartsAtEntry()
+ {
+ var rt = new AiRuntime(Graph(), new FakeAiContext());
+ Assert.AreEqual("Patrol", rt.CurrentStateName);
+ }
+
+ [Test]
+ public void Tick_RunsCurrentStateTickAction()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ rt.Tick(0.1f);
+ CollectionAssert.Contains(ctx.M.Calls, "WalkRandom");
+ }
+
+ [Test]
+ public void ConditionTransition_FiresAndRunsEnterExit()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ ctx.S.Sees = true;
+ rt.Tick(0.1f);
+ Assert.AreEqual("Chase", rt.CurrentStateName);
+ CollectionAssert.Contains(ctx.M.Calls, "FacePlayer"); // Chase.OnEnter
+ }
+
+ [Test]
+ public void Gate_SuspendsDecisionWhenNotControllable()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ ctx.S.Sees = true;
+ ctx.V.Controllable = false; // 受击中
+ rt.Tick(0.1f);
+ Assert.AreEqual("Patrol", rt.CurrentStateName); // 未转换
+ Assert.IsTrue(rt.IsSuspended);
+ CollectionAssert.DoesNotContain(ctx.M.Calls, "WalkRandom"); // Tick 也不跑
+ }
+
+ [Test]
+ public void Gate_ResumesAndReevaluatesWhenControllable()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ ctx.S.Sees = true;
+ ctx.V.Controllable = false;
+ rt.Tick(0.1f);
+ ctx.V.Controllable = true; // 恢复
+ rt.Tick(0.1f);
+ Assert.AreEqual("Chase", rt.CurrentStateName);
+ }
+
+ [Test]
+ public void After_UsesStateTimer()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ ctx.S.Sees = true; rt.Tick(0.1f); // -> Chase
+ ctx.S.Sees = false; ctx.S.LostForValue = 5f; rt.Tick(0.1f); // -> Search
+ Assert.AreEqual("Search", rt.CurrentStateName);
+ rt.Tick(1f); rt.Tick(1f);
+ Assert.AreEqual("Search", rt.CurrentStateName); // 未满 3s
+ rt.Tick(1.5f);
+ Assert.AreEqual("Patrol", rt.CurrentStateName); // 累计 >3s
+ }
+ }
+}
+```
+
+- [ ] **Step 3: 运行测试确认失败**
+
+Run: EditMode `AiRuntimeTests`
+Expected: 编译失败 —— `AiRuntime` 未定义。
+
+- [ ] **Step 4: 实现 `AiRuntime`(本任务先实现条件转换 + 门 + 计时器;事件转换在 Task 6 追加)**
+
+`Assets/_Game/Scripts/AI/AiRuntime.cs`:
+
+```csharp
+using System.Collections.Generic;
+
+namespace BaseGames.AI
+{
+ ///
+ /// 每敌人实例一份的轻量执行器(spec §3.2)。持有当前状态、状态计时器、trace。
+ /// 共享的 AiGraph 不持有任何实例态。
+ ///
+ public sealed class AiRuntime
+ {
+ readonly AiGraph _graph;
+ readonly IAiContext _ctx;
+ AiState _current;
+ float _timeInState;
+
+ readonly Queue _pending = new Queue();
+ readonly List _trace = new List();
+ const int TraceCap = 16;
+
+ public AiRuntime(AiGraph graph, IAiContext ctx)
+ {
+ _graph = graph;
+ _ctx = ctx;
+ EnterInitial();
+ }
+
+ public string CurrentStateName => _current.Name;
+ public bool IsSuspended { get; private set; }
+ public IReadOnlyList Trace => _trace;
+ public float TimeInStateValue => _timeInState;
+
+ void EnterInitial()
+ {
+ _current = _graph.GetState(_graph.EntryState);
+ _timeInState = 0f;
+ _current.OnEnter?.Invoke(_ctx);
+ }
+
+ /// 供 BrainBuilder.After() 通过 context 读取当前 runtime 的状态计时器。
+ static readonly System.Runtime.CompilerServices.ConditionalWeakTable _byContext
+ = new System.Runtime.CompilerServices.ConditionalWeakTable();
+ internal static float TimeInState(IAiContext ctx)
+ => _byContext.TryGetValue(ctx, out var rt) ? rt._timeInState : 0f;
+
+ public void Tick(float dt)
+ {
+ _byContext.Remove(_ctx);
+ _byContext.Add(_ctx, this);
+
+ // IsControllable 让位门(spec §3.6):受击/硬直/击飞时挂起,不推进决策也不跑 Tick。
+ if (!_ctx.Vitals.IsControllable)
+ {
+ IsSuspended = true;
+ return;
+ }
+ IsSuspended = false;
+
+ _timeInState += dt;
+
+ if (TryTransition())
+ return; // 本帧发生转换,新状态下一帧再 Tick
+
+ _current.OnTick?.Invoke(_ctx, dt);
+ }
+
+ bool TryTransition()
+ {
+ // 全局/父层转换优先(本任务只评估条件型;事件型在 Task 6 处理)
+ foreach (var t in _graph.GlobalTransitions)
+ if (!t.IsEvent && t.Condition(_ctx))
+ return Switch(t);
+
+ foreach (var t in _current.Transitions)
+ if (!t.IsEvent && t.Condition(_ctx))
+ return Switch(t);
+
+ return false;
+ }
+
+ bool Switch(Transition t)
+ {
+ var from = _current.Name;
+ _current.OnExit?.Invoke(_ctx);
+ _current = _graph.GetState(t.Target);
+ _timeInState = 0f;
+ RecordTrace(new TransitionRecord(from, t.Target, t.Label));
+ _current.OnEnter?.Invoke(_ctx);
+ return true;
+ }
+
+ void RecordTrace(TransitionRecord r)
+ {
+ _trace.Add(r);
+ if (_trace.Count > TraceCap) _trace.RemoveAt(0);
+ }
+
+ /// 对象池复用时重置到初始态,清空所有实例态(spec §3.8)。
+ public void Reset()
+ {
+ _pending.Clear();
+ _trace.Clear();
+ _ctx.Blackboard.Clear();
+ IsSuspended = false;
+ EnterInitial();
+ }
+ }
+}
+```
+
+- [ ] **Step 5: 回填 `BrainBuilder.After`(若 Task 4 曾临时抛异常)**
+
+确认 `BrainBuilder.TransitionBuilder.After` 的实现体为(Task 4 Step 4 所示的正式版):
+```csharp
+_state.Transitions.Add(Transition.OnCondition(
+ _target, c => AiRuntime.TimeInState(c) >= seconds, $"after {seconds}s"));
+```
+
+- [ ] **Step 6: 运行测试确认通过**
+
+Run: EditMode `AiRuntimeTests`
+Expected: PASS(6 项:入口、Tick、条件转换、门挂起、门恢复重评估、After 计时器)。
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add Assets/_Game/Scripts/AI/AiRuntime.cs Assets/_Game/Scripts/AI/BrainBuilder.cs \
+ Assets/Tests/EditMode/AI/Fakes Assets/Tests/EditMode/AI/AiRuntimeTests.cs \
+ Assets/_Game/Scripts/AI/*.meta Assets/Tests/EditMode/AI/*.meta Assets/Tests/EditMode/AI/Fakes/*.meta
+git commit -m "feat(ai): AiRuntime 条件转换+IsControllable让位门+状态计时器+trace+Reset"
+```
+
+---
+
+## Task 6: `AiRuntime` 事件转换 + trace 触发原因 + 池复用测试
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/AI/AiRuntime.cs`
+- Modify: `Assets/Tests/EditMode/AI/AiRuntimeTests.cs`
+
+- [ ] **Step 1: 追加失败测试 —— 事件转换(push)、优先级、Reset**
+
+在 `AiRuntimeTests` 类中追加:
+
+```csharp
+ [Test]
+ public void EventTransition_FiresOnSignal_EvenBeforeTick()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ rt.Send(AiSignal.Died);
+ rt.Tick(0.1f);
+ Assert.AreEqual("Dead", rt.CurrentStateName);
+ }
+
+ [Test]
+ public void EventTransition_FiresEvenWhenNotControllable()
+ {
+ // 死亡等事件不受让位门限制(门只挡决策推进/条件评估,不挡事件)
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ ctx.V.Controllable = false;
+ rt.Send(AiSignal.Died);
+ rt.Tick(0.1f);
+ Assert.AreEqual("Dead", rt.CurrentStateName);
+ }
+
+ [Test]
+ public void Trace_RecordsTriggerReason()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ ctx.S.Sees = true;
+ rt.Tick(0.1f);
+ Assert.AreEqual(1, rt.Trace.Count);
+ Assert.AreEqual("Patrol", rt.Trace[0].From);
+ Assert.AreEqual("Chase", rt.Trace[0].To);
+ StringAssert.Contains("SeesPlayer", rt.Trace[0].Trigger);
+ }
+
+ [Test]
+ public void Reset_ReturnsToEntryAndClearsState()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ ctx.S.Sees = true; rt.Tick(0.1f); // -> Chase
+ ctx.BB.Set("scratch", 42);
+ rt.Reset();
+ Assert.AreEqual("Patrol", rt.CurrentStateName);
+ Assert.IsEmpty(rt.Trace);
+ Assert.IsFalse(ctx.BB.Has("scratch"));
+ }
+```
+
+- [ ] **Step 2: 运行确认新测试失败**
+
+Run: EditMode `AiRuntimeTests`
+Expected: `EventTransition_*` FAIL(`Send` 未实现事件处理),`Reset_*`/`Trace_*` 可能已过(视 Task 5 实现);至少事件相关 2 项失败。
+
+- [ ] **Step 3: 实现事件处理**
+
+在 `AiRuntime.cs` 追加 `Send` 并在 `Tick` 开头处理事件队列(事件不受让位门限制)。修改如下:
+
+在字段区已有 `_pending`。新增方法:
+
+```csharp
+ /// 推送一个信号,用于事件驱动转换(push, spec §3.3/§4.1)。
+ public void Send(AiSignal signal) => _pending.Enqueue(signal);
+```
+
+将 `Tick` 改为先排空事件(不受门限制),再走门 + 条件:
+
+```csharp
+ public void Tick(float dt)
+ {
+ _byContext.Remove(_ctx);
+ _byContext.Add(_ctx, this);
+
+ // 1) 事件转换:push,最高优先,且不受 IsControllable 门限制
+ while (_pending.Count > 0)
+ {
+ var sig = _pending.Dequeue();
+ if (TryEventTransition(sig))
+ return; // 事件触发转换后本帧结束
+ }
+
+ // 2) IsControllable 让位门
+ if (!_ctx.Vitals.IsControllable)
+ {
+ IsSuspended = true;
+ return;
+ }
+ IsSuspended = false;
+
+ _timeInState += dt;
+
+ // 3) 条件转换
+ if (TryTransition())
+ return;
+
+ _current.OnTick?.Invoke(_ctx, dt);
+ }
+
+ bool TryEventTransition(AiSignal sig)
+ {
+ foreach (var t in _graph.GlobalTransitions)
+ if (t.IsEvent && t.Event.Value == sig)
+ return Switch(t);
+ foreach (var t in _current.Transitions)
+ if (t.IsEvent && t.Event.Value == sig)
+ return Switch(t);
+ return false;
+ }
+```
+
+> `Reset()` 中已有 `_pending.Clear()`(Task 5),无需改动。
+
+- [ ] **Step 4: 运行测试确认全通过**
+
+Run: EditMode `AiRuntimeTests`
+Expected: PASS(Task 5 的 6 项 + 本任务 4 项 = 10 项全绿)。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add Assets/_Game/Scripts/AI/AiRuntime.cs Assets/Tests/EditMode/AI/AiRuntimeTests.cs
+git commit -m "feat(ai): AiRuntime 事件转换(push,不受门限制)+trace触发原因+池复用重置测试"
+```
+
+---
+
+## Task 7: 阶段自检与文档回链
+
+**Files:**
+- Modify: `Docs/superpowers/specs/2026-07-03-enemy-ai-framework-design.md`(在文末追加"实现进度"回链,可选)
+
+- [ ] **Step 1: 全量跑核心测试**
+
+Run: EditMode 全部 `BaseGames.Tests.EditMode.AI.*`
+Expected: `BlackboardTests`(5) + `BrainBuilderTests`(5) + `AiRuntimeTests`(10) 全 PASS。
+
+- [ ] **Step 2: 编译零错误确认**
+
+Run: `unity_get_compilation_errors`
+Expected: 无错误、无警告(除已知的 CallerArgumentExpression 兼容处理)。
+
+- [ ] **Step 3: 验证 flyweight 共享(手动断言脚本,可选)**
+
+用 MCP `unity_execute_code` 执行:构造一个测试 `AiScript` 子类,调用两次 `GetOrBuildGraph()`,断言返回**同一引用**(`ReferenceEquals == true`)→ 证明每类型只建一份、实例共享。
+
+- [ ] **Step 4: 提交阶段收尾**
+
+```bash
+git add -A
+git commit -m "chore(ai): BrainGraph 第1阶段核心运行时收尾——全测试通过"
+```
+
+---
+
+## 后续阶段路线图(各自展开为独立计划)
+
+本阶段交付"可独立单测、可工作的状态机核心"。后续阶段依赖它,届时各写一份计划:
+
+- **第 2 阶段:能力编排 sugar + AiScheduler**
+ - `AbilitySequence` + `Seq.Select/Sequence` fluent(选招→Execute→while(IsRunning)等待→重评估,spec §3.4)。
+ - 嵌套子状态机(层级 HFSM,供 Boss 阶段),`AiScheduler` LOD 错峰调度(spec §3.7)。
+- **第 3 阶段:能力接口完整成员 + 适配器 + 组件 + 信号接线**
+ - 按 spec §4 补齐 4 接口全部方法;`SensorAdapter/MoverAdapter/CombatantAdapter/VitalsAdapter`(`BaseGames.Enemies.AI` asmdef,去 Opsive 引用、加 `BaseGames.AI`)。
+ - `EnemyAiBrain : MonoBehaviour` + `AiDefinitionRegistry`(反射收集 `[AiDefinition]`,`RuntimeInitializeOnLoadMethod` 重置,遵关闭域重载约束)。
+ - `EnemyBase` raise `AiSignal`(受击/弹反/死亡/发现玩家/能力结束/阶段变更),移除 `#if GRAPH_DESIGNER` BT 集成;`IPoolable` 回收回调调 `AiRuntime.Reset()`。
+- **第 4 阶段:交付面**
+ - `MermaidExporter`(`AiGraph→stateDiagram-v2`,菜单 + MCP 静态入口,spec §6.1)。
+ - `AiDebugService`(ServiceLocator 注册,`ListAgents/GetAgent/ExportMermaid/SetPaused/Step/ForceState`,spec §6.3)。
+ - `Editor/BrainGraphDebuggerWindow`(实时高亮 + trace + 黑板 + 能力 + 门状态,spec §6.2)。
+- **第 5 阶段:脚手架接线 + 试点 + 退役 BD**
+ - 扩展 `SceneObjectPlacerTool`/`CharacterWizardWindow` 自动挂 `EnemyAiBrain` + 绑定定义/参数 SO(消除 `SceneObjectPlacerTool.cs:354/524` 手动 TODO)。
+ - 试点 `E001_CaoZhi`(PlayMode 行为验收)+ `ChaoFengBoss`(多阶段/加权技能/弱点窗口)。
+ - parity 后批量迁移,删除 49 个 `BD_*`、`EnemyBase` BT 集成、`com.opsive.*` 包(spec §8)。
+
+---
+
+## 自检记录(writing-plans self-review)
+
+- **Spec 覆盖**:本阶段对应 spec §3.1(共享图)/§3.2(轻量运行时)/§3.3(条件+事件转换)/§3.6(IsControllable门)/§3.8(池复用重置、无实例捕获约束经 IAiContext 落实)。§3.4/§3.5黑板已建/§3.7/§4完整/§6/§7/§8 明确列入后续阶段路线图,无遗漏。
+- **占位扫描**:无 TBD/TODO 占位;`After` 的临时抛异常 + Task 5 回填是明确的两步实现(非占位),已注明回填内容与顺序。
+- **类型一致性**:`IAiContext.Sensor/Mover/Combat/Vitals/Blackboard`、`AiRuntime.CurrentStateName/IsSuspended/Trace/Send/Reset/Tick`、`Transition.OnCondition/OnEvent/IsEvent/Label`、`BrainBuilder.Entry/State/Global/Build`、`StateBuilder.OnEnter/Tick/OnExit/To`、`TransitionBuilder.When/OnEvent/After` 跨任务命名一致。
+- **已知风险**:CallerArgumentExpression 需 C# 10,已给 polyfill + 降级方案(Task 4 Step 4 注)。