Pārlūkot izejas kodu

docs(house-fault): 拆完舱室故障隔离+双端提示实现计划(7任务+4阶段提纲)+进度回写

第一阶段 Task1-7:HouseFault/StartupFaultPolicy 纯TDD → SerialBin 6处登记结构化坏舱
→ InitTL 零可跑才中止 → InitHouse 逐舱 try-catch → AppData 快照透出+reportCloudAlarm 上报 → 真机拔插验收。
后续4阶段提纲(运行期升级/operate监控页/front告警/整体验收)。基于 codegraph 逐符号核实改面。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 2 dienas atpakaļ
vecāks
revīzija
9648ecded5

+ 784 - 0
项目文档/开发计划/2026-06-23-舱室故障隔离与双端故障提示-实现计划.md

@@ -0,0 +1,784 @@
+# 舱室故障隔离与双端故障提示 实现计划
+
+> **给执行者(子代理/续接会话):** 必读子技能 —— 用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐 Task 执行。每步用 `- [ ]` 复选框跟踪。
+> 设计依据:`需求文档/specs/2026-06-23-舱室故障隔离与双端故障提示-design.md`(逐行源码核实的现状 + 目标 + 验收)。
+> **源码已用 codegraph 读透**(StartMain.InitTL/InitHouse、control 版 SerialBin 四个 errorlist 来源、MonitorSnapshot、告警通道 `/api/tl/control/alarm/reportCloudAlarm`),本计划所有 file:line 与签名均照此对齐。
+
+**目标(一句话):** 开机时部分舱坏 → 只排除坏舱、好舱照常培养、整机绝不因单舱失败而中止;并把"哪个舱/什么故障/何时/是否已隔离"经现有告警通道送到 operate + front 双端明确展示。
+
+**架构(怎么改):** control 侧把"一刀切 errorlist 非空即整体中止"改成"结构化坏舱清单 → 剔除坏舱 → 仅在零可跑舱时才中止";坏舱清单贯穿 `/status` 快照(operate 监控页轮询)+ 复用 `reportCloudAlarm` 告警(front 展示)。纯决策逻辑(坏舱剔除/致命判定)抽成无 I/O 静态类做 TDD;改启动核心(SerialBin/InitTL/InitHouse)的部分单测覆盖决策、行为靠真机拔插验收。
+
+**技术栈:** C# net6.0-windows;xUnit(复用现有 `IvfTl.ControlHost.Tests` 工程,不新建);现有 `HttpService.reportCloudAlarm` 告警通道 + `MonitorSnapshot` 快照;无新增第三方依赖。
+
+**分支:** `feature/house-fault-isolation`(独立于 D2-02;改 control 启动核心,风险较高,单独分支)。
+
+---
+
+## 文件结构(改面地图,决策先锁这里)
+
+**新增(纯数据/纯逻辑,可单测):**
+- `ivf_tl_operate_2.0/control/IvfTl.Control.Entity/InitEntitys/HouseFault.cs` — 坏舱故障数据载体 + `HouseFaultType` 枚举。放 Entity 工程,**ivf_tl_Com(SerialBin)与 ivf_tl_Control(StartMain/AppData/MonitorSnapshot)都引用得到**(二者均已 `using IvfTl.Control.Entity.*`)。
+- `ivf_tl_operate_2.0/control/ivf_tl_Control/StartupFaultPolicy.cs` — 无 I/O 静态决策类(坏舱集合/可跑舱/致命判定)。放 ivf_tl_Control,测试工程引用得到。
+
+**修改(改启动核心,真机门控):**
+- `ivf_tl_operate_2.0/control/ivf_tl_Com/SerialBin.cs` — 加 `public List<HouseFault> Faults`,在现有 4 处 `errorlist.Add` 旁同步登记结构化故障(保留 errorlist 供日志兼容)。
+- `ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs` — `InitTL` 用 `StartupFaultPolicy` 算可跑舱、仅零可跑舱才中止;`InitHouse` 逐舱构造/StartTask 各自 try-catch。
+- `ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.cs` — 加 `List<HouseFault> StartupFaults`,`GetMonitorSnapshot` 透出,启动排除时经 `HttpService` 上报告警。
+- `ivf_tl_operate_2.0/control/ivf_tl_Control/MonitorSnapshot.cs` — 加 `List<HouseFaultRow> Faults` 字段。
+
+**新增测试(并入现有工程):**
+- `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/HouseFaultTests.cs`
+- `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs`
+- `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs`
+
+---
+
+## 阶段划分(对齐 spec §59:①②control 侧纯单测+真机可自主,③④双端 UI 真机门控)
+
+- **第一阶段(本计划详细 Task1-7)** = control 坏舱剔除 + 故障登记 + 启动排除上报。Task1/2/6a 纯 TDD 自主;Task3/4/5/6b/7 改启动核心 + 真机拔插,Claude 可自主跑(UAC 提权、当前无活体培养)。
+- **第二/三/四/五阶段(本计划末尾提纲)** = 运行期故障升级去抖 / operate 监控页"舱故障"区 / front 告警展示 / 真机拔插整体验收。UI 两项真机门控,待第一阶段落地后单独拆细。
+
+---
+
+# 第一阶段:control 坏舱剔除 + 故障登记 + 上报
+
+## Task 1: HouseFault 数据载体 + 枚举
+
+**Files:**
+- Create: `ivf_tl_operate_2.0/control/IvfTl.Control.Entity/InitEntitys/HouseFault.cs`
+- Test: `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/HouseFaultTests.cs`
+
+- [ ] **Step 1: 写失败测试**
+
+`IvfTl.ControlHost.Tests/HouseFaultTests.cs`:
+```csharp
+using System;
+using IvfTl.Control.Entity.InitEntitys;
+using Xunit;
+
+namespace IvfTl.ControlHost.Tests
+{
+    public class HouseFaultTests
+    {
+        [Fact]
+        public void HouseFault_Holds_All_Fields()
+        {
+            var at = new DateTime(2026, 6, 23, 12, 0, 0, DateTimeKind.Utc);
+            var f = new HouseFault
+            {
+                HouseSn = 6,
+                Port = "COM7",
+                Type = HouseFaultType.CcdSnMissing,
+                Reason = "相机列表中不存在仓室的CCDSN12345",
+                Stage = "扫口握手",
+                At = at,
+                Isolated = true
+            };
+
+            Assert.Equal(6, f.HouseSn);
+            Assert.Equal("COM7", f.Port);
+            Assert.Equal(HouseFaultType.CcdSnMissing, f.Type);
+            Assert.Equal("相机列表中不存在仓室的CCDSN12345", f.Reason);
+            Assert.Equal("扫口握手", f.Stage);
+            Assert.Equal(at, f.At);
+            Assert.True(f.Isolated);
+        }
+
+        [Fact]
+        public void HouseFault_Defaults_HouseSn_Unknown_As_Minus1()
+        {
+            var f = new HouseFault { Type = HouseFaultType.CameraDuplicateSn };
+            Assert.Equal(-1, f.HouseSn);   // -1 = 舱号未知(相机/串口级故障)
+            Assert.False(f.Isolated);
+        }
+    }
+}
+```
+
+- [ ] **Step 2: 跑测试确认失败**
+
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter HouseFaultTests`
+Expected: FAIL —— 编译错误 `HouseFault`/`HouseFaultType` 未定义。
+
+- [ ] **Step 3: 写最小实现**
+
+`IvfTl.Control.Entity/InitEntitys/HouseFault.cs`:
+```csharp
+using System;
+
+namespace IvfTl.Control.Entity.InitEntitys
+{
+    /// <summary>
+    /// 舱级故障类型。启动期 SerialBin 登记 + 运行期升级共用。
+    /// </summary>
+    public enum HouseFaultType
+    {
+        CameraReadFailed = 0,    // 相机 SN 读取异常(舱号未知)
+        CameraDuplicateSn = 1,   // 两台相机报同一 SN,配对不明(舱号未知)
+        HouseSnDuplicate = 2,    // 两个串口握手返回同一舱号
+        CcdSnMissing = 3,        // 相机列表中不存在该舱 CCDSN(该舱相机半坏)
+        CcdSnDuplicate = 4,      // 两个舱报同一 CCDSN
+        SerialReadException = 5, // 扫口过程串口读异常(舱号可能未知)
+        InitException = 6,       // 舱构造/StartTask 抛异常
+        RuntimeFault = 7         // 运行期突发(第二阶段用)
+    }
+
+    /// <summary>
+    /// 单条舱故障。HouseSn>0=已知具体舱(可被剔除);HouseSn=-1=相机/串口级故障(仅提示,不剔除某舱)。
+    /// 纯数据载体,无任何行为。
+    /// </summary>
+    public class HouseFault
+    {
+        /// <summary>舱号;-1 表示舱号未知(相机/串口级)。</summary>
+        public int HouseSn { get; set; } = -1;
+        /// <summary>串口名(已知时)。</summary>
+        public string Port { get; set; }
+        public HouseFaultType Type { get; set; }
+        /// <summary>人类可读原因(直接取自原 errorlist 文案)。</summary>
+        public string Reason { get; set; }
+        /// <summary>发生阶段:相机枚举/扫口握手/舱初始化/运行期。</summary>
+        public string Stage { get; set; }
+        /// <summary>发生时刻(UTC)。</summary>
+        public DateTime At { get; set; }
+        /// <summary>是否已被隔离(该舱已从可跑清单剔除/已停)。</summary>
+        public bool Isolated { get; set; }
+    }
+}
+```
+
+- [ ] **Step 4: 跑测试确认通过**
+
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter HouseFaultTests`
+Expected: PASS(2 绿)。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add ivf_tl_operate_2.0/control/IvfTl.Control.Entity/InitEntitys/HouseFault.cs \
+        ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/HouseFaultTests.cs
+git commit -m "feat(house-fault): HouseFault 数据载体 + HouseFaultType 枚举(TDD)"
+```
+
+---
+
+## Task 2: StartupFaultPolicy 纯决策(坏舱剔除 / 致命判定)
+
+**Files:**
+- Create: `ivf_tl_operate_2.0/control/ivf_tl_Control/StartupFaultPolicy.cs`
+- Test: `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs`
+
+**决策规则(锁定):** ① 坏舱 = 故障清单里 `HouseSn>0` 的舱号集合(舱号未知的相机/串口级故障**不剔除任何舱**,只作提示);② 可跑舱 = 发现的舱 − 坏舱;③ 致命 = **一个可跑舱都没有**(零可跑)才整机中止。单舱/部分舱坏一律不中止 —— 直接落实 spec §34「仅全部舱失败才中止」。
+
+- [ ] **Step 1: 写失败测试**(完整真值表)
+
+`IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs`:
+```csharp
+using System.Collections.Generic;
+using System.Linq;
+using IvfTl.Control.Entity.InitEntitys;
+using ivf_tl_Control;
+using Xunit;
+
+namespace IvfTl.ControlHost.Tests
+{
+    public class StartupFaultPolicyTests
+    {
+        private static HouseFault Fault(int sn, HouseFaultType t = HouseFaultType.CcdSnMissing)
+            => new HouseFault { HouseSn = sn, Type = t, Reason = "x", Stage = "扫口握手" };
+
+        [Fact]
+        public void BadHouseSns_Only_Counts_Known_Houses()
+        {
+            var faults = new List<HouseFault>
+            {
+                Fault(6),
+                Fault(-1, HouseFaultType.CameraDuplicateSn), // 舱号未知 → 不算坏舱
+                Fault(8),
+            };
+            var bad = StartupFaultPolicy.BadHouseSns(faults);
+            Assert.Equal(new[] { 6, 8 }, bad.OrderBy(x => x).ToArray());
+        }
+
+        [Fact]
+        public void RunnableHouses_Excludes_Bad_And_Sorts()
+        {
+            var discovered = new[] { 2, 4, 6, 7, 8, 9, 11 };
+            var faults = new List<HouseFault> { Fault(6), Fault(8) };
+            var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
+            Assert.Equal(new[] { 2, 4, 7, 9, 11 }, run.ToArray());
+        }
+
+        [Fact]
+        public void RunnableHouses_With_No_Faults_Returns_All_Discovered()
+        {
+            var discovered = new[] { 2, 4, 6 };
+            var run = StartupFaultPolicy.RunnableHouses(discovered, new List<HouseFault>());
+            Assert.Equal(new[] { 2, 4, 6 }, run.ToArray());
+        }
+
+        [Fact]
+        public void RunnableHouses_UnknownHouseFault_Does_Not_Exclude_Anyone()
+        {
+            var discovered = new[] { 2, 4 };
+            var faults = new List<HouseFault> { Fault(-1, HouseFaultType.CameraReadFailed) };
+            var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
+            Assert.Equal(new[] { 2, 4 }, run.ToArray());
+        }
+
+        [Fact]
+        public void IsFatal_True_Only_When_No_Runnable_House()
+        {
+            Assert.True(StartupFaultPolicy.IsFatal(new List<int>()));
+            Assert.False(StartupFaultPolicy.IsFatal(new List<int> { 11 }));
+            Assert.False(StartupFaultPolicy.IsFatal(new List<int> { 2, 4 }));
+        }
+
+        [Fact]
+        public void Partial_Failure_Is_Never_Fatal()
+        {
+            // 7 个舱里坏 2 个 → 仍 5 个可跑 → 不致命(核心需求)
+            var discovered = new[] { 2, 4, 6, 7, 8, 9, 11 };
+            var faults = new List<HouseFault> { Fault(6), Fault(8) };
+            var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
+            Assert.False(StartupFaultPolicy.IsFatal(run));
+        }
+    }
+}
+```
+
+- [ ] **Step 2: 跑测试确认失败**
+
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter StartupFaultPolicyTests`
+Expected: FAIL —— `StartupFaultPolicy` 未定义。
+
+- [ ] **Step 3: 写最小实现**
+
+`ivf_tl_Control/StartupFaultPolicy.cs`:
+```csharp
+using System.Collections.Generic;
+using System.Linq;
+using IvfTl.Control.Entity.InitEntitys;
+
+namespace ivf_tl_Control
+{
+    /// <summary>
+    /// 启动期坏舱决策(无 I/O,纯函数,可单测)。
+    /// 规则:坏舱=故障清单中已知舱号(HouseSn>0);可跑舱=发现的舱−坏舱;致命=零可跑舱。
+    /// 单舱/部分舱坏绝不致命 —— 落实 spec §34。
+    /// </summary>
+    public static class StartupFaultPolicy
+    {
+        public static HashSet<int> BadHouseSns(IEnumerable<HouseFault> faults)
+            => faults.Where(f => f.HouseSn > 0).Select(f => f.HouseSn).ToHashSet();
+
+        public static List<int> RunnableHouses(IEnumerable<int> discovered, IEnumerable<HouseFault> faults)
+        {
+            var bad = BadHouseSns(faults);
+            return discovered.Where(sn => !bad.Contains(sn)).Distinct().OrderBy(x => x).ToList();
+        }
+
+        public static bool IsFatal(IReadOnlyList<int> runnableHouses)
+            => runnableHouses == null || runnableHouses.Count == 0;
+    }
+}
+```
+
+- [ ] **Step 4: 跑测试确认通过**
+
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter StartupFaultPolicyTests`
+Expected: PASS(6 绿)。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add ivf_tl_operate_2.0/control/ivf_tl_Control/StartupFaultPolicy.cs \
+        ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs
+git commit -m "feat(house-fault): StartupFaultPolicy 坏舱剔除/致命判定(TDD 真值表 6 绿)"
+```
+
+---
+
+## Task 3: SerialBin 登记结构化坏舱清单(改启动核心,真机门控)
+
+**Files:**
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Com/SerialBin.cs`
+
+**说明:** control 版 SerialBin 现有 4 处 `errorlist.Add`(camera 重复 SN `:121`、camera 读异常 catch `:135`、舱号重复 `:212`、CCDSN 缺失 `:252`、CCDSN 重复 `:260`、扫口 catch `:318`)。在每处旁同步登记一条 `HouseFault`(**保留 errorlist 不动**,日志兼容)。纯增量,不改任何控制流。无法纯单测(真硬件 I/O),靠 Task 7 真机拔插验证。
+
+- [ ] **Step 1: 加 Faults 字段 + using**
+
+文件顶部已有 `using IvfTl.Control.Entity.InitEntitys;`(`HouseInitData`/`HouseEEPROInfo` 在此命名空间),`HouseFault` 同命名空间,无需加 using。在 `private List<string> errorlist = new List<string>();`(`SerialBin.cs:32`)下方加:
+```csharp
+        /// <summary>结构化坏舱清单(与 errorlist 并存,errorlist 仅日志兼容)。</summary>
+        public List<HouseFault> Faults { get; } = new List<HouseFault>();
+```
+
+- [ ] **Step 2: camera 重复 SN(`GetCameraSn` 现 `:121`)**
+
+在 `errorlist.Add($"ccdid:{i};ccdsn:{SnNumber}");` 这一行**之后**追加(同一 `lock` 块内):
+```csharp
+                            Faults.Add(new HouseFault
+                            {
+                                HouseSn = -1, Type = HouseFaultType.CameraDuplicateSn,
+                                Reason = $"相机SN重复 ccdid:{i};ccdsn:{SnNumber}",
+                                Stage = "相机枚举", At = DateTime.UtcNow
+                            });
+```
+
+- [ ] **Step 3: camera 读异常(`GetCameraSn` catch 现 `:135`)**
+
+在 `errorlist.Add($"获取{i}号相机ccdSn异常");` 之后追加(同 lock 块内):
+```csharp
+                    Faults.Add(new HouseFault
+                    {
+                        HouseSn = -1, Type = HouseFaultType.CameraReadFailed,
+                        Reason = $"获取{i}号相机ccdSn异常", Stage = "相机枚举", At = DateTime.UtcNow
+                    });
+```
+
+- [ ] **Step 4: 舱号重复(`GetHouseInfo` 现 `:212`)**
+
+在 `errorlist.Add($"从{serialModel.housePort}串口...模块号{itemModel.houseSn}重复");` 之后追加(同 lock 块内,`goto CC;` 之前):
+```csharp
+                        Faults.Add(new HouseFault
+                        {
+                            HouseSn = serialModel.houseSn, Port = serialModel.housePort,
+                            Type = HouseFaultType.HouseSnDuplicate,
+                            Reason = $"模块号{serialModel.houseSn}在{serialModel.housePort}与{itemModel.housePort}重复",
+                            Stage = "扫口握手", At = DateTime.UtcNow
+                        });
+```
+
+- [ ] **Step 5: CCDSN 缺失(`GetHouseInfo` 现 `:252`)**
+
+在 `errorlist.Add($"相机列表中不存在仓室的CCDSN{currentCCDSN}");` 之后追加(同 lock 块内):
+```csharp
+                                Faults.Add(new HouseFault
+                                {
+                                    HouseSn = serialModel.houseSn, Port = serialModel.housePort,
+                                    Type = HouseFaultType.CcdSnMissing,
+                                    Reason = $"相机列表中不存在仓室的CCDSN{currentCCDSN}",
+                                    Stage = "扫口握手", At = DateTime.UtcNow
+                                });
+```
+
+- [ ] **Step 6: CCDSN 重复(`GetHouseInfo` 现 `:260`)**
+
+在 `errorlist.Add($"从{serialModel.houseSn}号模块...{itemModel.ccdSn}重复");` 之后追加(同 lock 块内):
+```csharp
+                            Faults.Add(new HouseFault
+                            {
+                                HouseSn = serialModel.houseSn, Port = serialModel.housePort,
+                                Type = HouseFaultType.CcdSnDuplicate,
+                                Reason = $"舱{serialModel.houseSn}的CCDSN{currentCCDSN}与{itemModel.housePort}重复",
+                                Stage = "扫口握手", At = DateTime.UtcNow
+                            });
+```
+
+- [ ] **Step 7: 扫口异常 catch(`GetHouseInfo` catch 现 `:318`)**
+
+在 `errorlist.Add($"获取端口信息异常{portName}:{ex.Message}");` 之后追加(同 lock 块内):
+```csharp
+                    Faults.Add(new HouseFault
+                    {
+                        HouseSn = -1, Port = portName, Type = HouseFaultType.SerialReadException,
+                        Reason = $"获取端口信息异常{portName}:{ex.Message}", Stage = "扫口握手", At = DateTime.UtcNow
+                    });
+```
+
+- [ ] **Step 8: 编译确认 0 错**
+
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug`
+Expected: 0 错(纯增量,Faults 只被写、还没被读)。
+
+- [ ] **Step 9: 提交**
+
+```bash
+git add ivf_tl_operate_2.0/control/ivf_tl_Com/SerialBin.cs
+git commit -m "feat(house-fault): SerialBin 在 4 处 errorlist 旁登记结构化 HouseFault(保留 errorlist)"
+```
+
+---
+
+## Task 4: StartMain.InitTL 改坏舱剔除(改启动核心,真机门控)
+
+**Files:**
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs`
+
+**说明:** 把"errorList 非空即整体 return 空"改为"算可跑舱、仅零可跑才中止、坏舱存 AppData"。InitTL 自身有真硬件 I/O 不纯单测;决策已被 Task 2 单测覆盖,这里只做接线,行为靠 Task 7 真机验证。
+
+- [ ] **Step 1: 加 using**
+
+`StartMain.cs` 顶部 `using IvfTl.Control.Entity.InitEntitys;` 已存在(用于 `TLInitData` 等),`HouseFault` 同命名空间可直接用,无需加 using。
+
+- [ ] **Step 2: 相机 errorlist 不再中止(现 `:78-83`)**
+
+把:
+```csharp
+                var errorList = serialBin.UpdataCamera();
+                AppData.LogService.TLLog($"相机信息:{JsonConvert.SerializeObject(serialBin.CCDidSn)}", LogEnum.RunRecord);
+                if (errorList.Any())
+                {
+                    errora = $"获取相机Id和CCDSN错误{JsonConvert.SerializeObject(errorList)}";
+                    AppData.LogService.TLLog(errora, LogEnum.RunRecord);
+                    return (new TLInitControllerResult(), new List<int>());
+                }
+```
+改为(相机重复/读失败只记日志,不中止 —— 受影响舱稍后在 CCDSN 配对时自然落坏舱被剔除):
+```csharp
+                var errorList = serialBin.UpdataCamera();
+                AppData.LogService.TLLog($"相机信息:{JsonConvert.SerializeObject(serialBin.CCDidSn)}", LogEnum.RunRecord);
+                if (errorList.Any())
+                {
+                    AppData.LogService.TLLog($"相机枚举有异常(不中止,受影响舱将按坏舱剔除):{JsonConvert.SerializeObject(errorList)}", LogEnum.RunRecord);
+                }
+```
+
+- [ ] **Step 3: 串口 errorlist 改坏舱剔除(现 `:89-94`)**
+
+把:
+```csharp
+                if (errorList.Any())
+                {
+                    errora = $"获取串口信息错误{JsonConvert.SerializeObject(errorList)}";
+                    AppData.LogService.TLLog(errora, LogEnum.RunRecord);
+                    return (new TLInitControllerResult(), new List<int>());
+                }
+```
+改为:
+```csharp
+                if (errorList.Any())
+                {
+                    AppData.LogService.TLLog($"扫口有异常(将剔除坏舱后继续):{JsonConvert.SerializeObject(errorList)}", LogEnum.RunRecord);
+                }
+```
+
+- [ ] **Step 4: 用 Policy 算可跑舱、零可跑才中止、坏舱存 AppData(现 `:120`)**
+
+把:
+```csharp
+                List<int> listIntRunHoues = serialBin.SerialModels.Select(x => x.houseSn).ToList();
+```
+改为:
+```csharp
+                var discovered = serialBin.SerialModels.Select(x => x.houseSn).ToList();
+                List<int> listIntRunHoues = StartupFaultPolicy.RunnableHouses(discovered, serialBin.Faults);
+
+                // 坏舱清单存 AppData(供 /status 快照透出 + 上报告警);已知舱标记已隔离。
+                foreach (var f in serialBin.Faults) if (f.HouseSn > 0) f.Isolated = true;
+                AppData.StartupFaults = serialBin.Faults;
+                if (serialBin.Faults.Any())
+                    AppData.LogService.TLLog($"启动坏舱清单:{JsonConvert.SerializeObject(serialBin.Faults)};可跑舱:{string.Join(",", listIntRunHoues)}", LogEnum.RunRecord);
+
+                // 仅零可跑舱才整机中止(单舱/部分舱坏绝不中止 —— spec §34)。
+                if (StartupFaultPolicy.IsFatal(listIntRunHoues))
+                {
+                    errora = "所有舱室初始化失败,无可运行舱室";
+                    AppData.LogService.TLLog(errora, LogEnum.RunRecord);
+                    return (new TLInitControllerResult(), new List<int>());
+                }
+
+                // 上报启动排除告警(每坏舱一条;失败不影响启动)。
+                try { AppData.ReportStartupFaults(); } catch { }
+```
+
+> 注:`AppData.StartupFaults` 与 `AppData.ReportStartupFaults()` 在 Task 6 实现;本 Task 先写调用,Task 6 补定义后整体编译通过。**执行顺序:Task 4 与 Task 6 同一提交前一起编译**(或先做 Task 6 再做 Task 4)。建议:**先做 Task 6 再回填 Task 4 的两处调用**,避免中途不编译。
+
+- [ ] **Step 5: 编译确认 0 错**(在 Task 6 完成后)
+
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug`
+Expected: 0 错。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs
+git commit -m "feat(house-fault): InitTL 改坏舱剔除——零可跑才中止,坏舱存AppData+上报告警"
+```
+
+---
+
+## Task 5: InitHouse 逐舱 try-catch(单舱构造异常只跳过该舱,真机门控)
+
+**Files:**
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs`(`InitHouse` 现 `:160-259`)
+
+**说明:** 现 `InitHouse` 是一个大 try-catch —— 任一 `HouseBinN` 构造抛异常 → 整个 InitHouse return false → **所有舱都起不来**。改为每舱构造 + StartTask 各自 try-catch,单舱异常只记 `HouseFault{InitException}` 并跳过(spec §35)。10 个舱重复,用本地 helper DRY。
+
+- [ ] **Step 1: 在 InitHouse 内加本地 helper(方法体开头,`TLSetting tLSetting = ...` 之前)**
+
+```csharp
+                // 单舱构造/启动各自兜底:抛异常只登记坏舱+跳过,不拖垮其余舱。
+                void TryHouse(int sn, Action build, bool run)
+                {
+                    try
+                    {
+                        build();
+                        if (run) { /* StartTask 在下方按 runHouses 统一触发,这里只构造 */ }
+                    }
+                    catch (Exception ex)
+                    {
+                        AppData.LogService.ExceptionLog(ex, $"舱{sn}构造失败(已隔离,其余舱继续)", null, LogEnum.RunException);
+                        AppData.StartupFaults?.Add(new HouseFault
+                        {
+                            HouseSn = sn, Type = HouseFaultType.InitException,
+                            Reason = $"舱{sn}构造异常:{ex.Message}", Stage = "舱初始化",
+                            At = DateTime.UtcNow, Isolated = true
+                        });
+                    }
+                }
+```
+
+> 注:`using System;`(Action/DateTime)在 StartMain 顶部已隐式可用(已 `using System.Diagnostics;` 等;若 `Action` 报未定义,加 `using System;`)。
+
+- [ ] **Step 2: 每舱构造包进 TryHouse**
+
+把现有每舱两行(以舱 2 为例)
+```csharp
+                housesn = 2;
+                AppData.HouseBin2 = new HouseBin(tLSetting, tLInitControllerResult.HouseList.FirstOrDefault(x => x.houseSn == housesn), allList.Dishes.FirstOrDefault(x => x.houseSn == housesn), allList.BalanceList.FirstOrDefault(x => x.houseSn == housesn), tLInitControllerResult.HouseWellList.Where(x => x.houseSn == housesn).ToList());
+                AppData.InitHouseBinEvent(AppData.HouseBin2);
+```
+改为:
+```csharp
+                housesn = 2;
+                TryHouse(2, () => {
+                    AppData.HouseBin2 = new HouseBin(tLSetting, tLInitControllerResult.HouseList.FirstOrDefault(x => x.houseSn == 2), allList.Dishes.FirstOrDefault(x => x.houseSn == 2), allList.BalanceList.FirstOrDefault(x => x.houseSn == 2), tLInitControllerResult.HouseWellList.Where(x => x.houseSn == 2).ToList());
+                    AppData.InitHouseBinEvent(AppData.HouseBin2);
+                }, false);
+```
+对舱 1~10 同样处理(缓冲瓶 BufferBottleBin/舱 11 沿用原写法,11 失败由 InitTL 的可跑舱判定兜底,不在此包裹)。`CamNum` 赋值段(现 `:215-224`)保持不变,但需对可能为 null 的 HouseBinN 加判空:`AppData.HouseBin2?.CamNum = CamNum;` 不合法 —— 改为 `if (AppData.HouseBin2 != null) AppData.HouseBin2.CamNum = CamNum;`(10 舱逐个)。
+
+- [ ] **Step 3: StartTask 段对 null 兜底(现 `:226-250`)**
+
+把每行 `if (runHouses.Contains(2)) AppData.HouseBin2.StartTask();` 改为:
+```csharp
+                    if (runHouses.Contains(2) && AppData.HouseBin2 != null)
+                        try { AppData.HouseBin2.StartTask(); }
+                        catch (Exception ex) { AppData.LogService.ExceptionLog(ex, "舱2 StartTask 失败(已隔离)", null, LogEnum.RunException); }
+```
+对舱 1/3/4/5/6/7/8/9/10 与 BufferBottleBin(11)同样加 `!= null` 判空 + try-catch。
+
+- [ ] **Step 4: 编译确认 0 错**
+
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug`
+Expected: 0 错。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs
+git commit -m "feat(house-fault): InitHouse 逐舱 try-catch——单舱构造/启动异常只隔离该舱"
+```
+
+---
+
+## Task 6: AppData 坏舱清单字段 + 快照透出 + 告警上报
+
+**Files:**
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.cs`
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Control/MonitorSnapshot.cs`
+- Test: `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs`
+
+### 6a. MonitorSnapshot 加 Faults 字段(纯数据,TDD)
+
+- [ ] **Step 1: 写失败测试**
+
+`IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs`:
+```csharp
+using System;
+using ivf_tl_Control;
+using Xunit;
+
+namespace IvfTl.ControlHost.Tests
+{
+    public class MonitorSnapshotFaultTests
+    {
+        [Fact]
+        public void Snapshot_Faults_Defaults_Empty_Not_Null()
+        {
+            var s = new MonitorSnapshot();
+            Assert.NotNull(s.Faults);
+            Assert.Empty(s.Faults);
+        }
+
+        [Fact]
+        public void HouseFaultRow_Holds_Fields()
+        {
+            var row = new HouseFaultRow
+            {
+                HouseSn = 6, FaultType = "CcdSnMissing",
+                Reason = "相机列表中不存在仓室的CCDSN12345",
+                Stage = "扫口握手", At = new DateTime(2026, 6, 23), Isolated = true
+            };
+            Assert.Equal(6, row.HouseSn);
+            Assert.Equal("CcdSnMissing", row.FaultType);
+            Assert.True(row.Isolated);
+        }
+    }
+}
+```
+
+- [ ] **Step 2: 跑测试确认失败**
+
+Run: `dotnet test ...IvfTl.ControlHost.Tests.csproj --filter MonitorSnapshotFaultTests`
+Expected: FAIL —— `MonitorSnapshot.Faults`/`HouseFaultRow` 未定义。
+
+- [ ] **Step 3: 实现 —— MonitorSnapshot.cs 末尾(`MonitorSnapshot` 类内加字段 + 文件内加 `HouseFaultRow` 类)**
+
+在 `MonitorSnapshot` 类内 `Houses` 字段后加:
+```csharp
+        /// <summary>舱故障列表(启动排除 + 运行期突发),供 operate 监控页红色高亮展示。</summary>
+        public List<HouseFaultRow> Faults { get; set; } = new List<HouseFaultRow>();
+```
+在文件内 `HouseMonitorRow` 类后(命名空间内)加:
+```csharp
+    /// <summary>单条舱故障只读行(operate/front 展示用,字符串化枚举便于跨端)。</summary>
+    public class HouseFaultRow
+    {
+        public int HouseSn { get; set; }
+        public string FaultType { get; set; }
+        public string Reason { get; set; }
+        public string Stage { get; set; }
+        public DateTime At { get; set; }
+        public bool Isolated { get; set; }
+    }
+```
+
+- [ ] **Step 4: 跑测试确认通过**
+
+Run: `dotnet test ...IvfTl.ControlHost.Tests.csproj --filter MonitorSnapshotFaultTests`
+Expected: PASS(2 绿)。
+
+### 6b. AppData.StartupFaults + GetMonitorSnapshot 透出 + ReportStartupFaults
+
+- [ ] **Step 5: AppData 加 StartupFaults 字段**
+
+`AppData.cs` 在 `MqttService` 等属性附近(如 `CurrentUser` 属性后)加:
+```csharp
+        /// <summary>启动期坏舱清单(InitTL 写入,GetMonitorSnapshot 透出,ReportStartupFaults 上报)。</summary>
+        public List<HouseFault> StartupFaults { get; set; } = new List<HouseFault>();
+```
+确认文件顶部已 `using IvfTl.Control.Entity.InitEntitys;`(AppData 大量用 InitEntitys);若无则加。
+
+- [ ] **Step 6: GetMonitorSnapshot 透出 Faults(`AppData.cs:222` 附近,构造 `MonitorSnapshot` 处)**
+
+在 `GetMonitorSnapshot` 内、`new MonitorSnapshot{...}` 赋值各字段后(或返回前),把 StartupFaults 映射进快照:
+```csharp
+            // 舱故障透出(StartupFaults → HouseFaultRow,字符串化枚举跨端)。
+            try
+            {
+                snapshot.Faults = (StartupFaults ?? new List<HouseFault>())
+                    .Select(f => new HouseFaultRow
+                    {
+                        HouseSn = f.HouseSn, FaultType = f.Type.ToString(),
+                        Reason = f.Reason, Stage = f.Stage, At = f.At, Isolated = f.Isolated
+                    }).ToList();
+            }
+            catch { }
+```
+> `snapshot` 为该方法内构造的 `MonitorSnapshot` 局部变量名;若现有代码用对象初始化器一次性 `return new MonitorSnapshot{...}`,改为先赋给局部 `var snapshot = new MonitorSnapshot{...};` 再补上面映射,最后 `return snapshot;`。需要 `using System.Linq;`(AppData 已用 Linq)。
+
+- [ ] **Step 7: AppData 加 ReportStartupFaults(复用现有告警通道)**
+
+参照现有 `MqttAlarm`(`AppData.cs:1350`,走 `HttpService.AlarmApi1`)与 `AlarmApi`(`HttpService.cs:54`,POST `/api/tl/control/alarm/reportCloudAlarm`,带 houseSn/wellSn)。新增:
+```csharp
+        /// <summary>
+        /// 上报启动期坏舱告警(每坏舱一条)。复用 HttpService.AlarmApi(reportCloudAlarm)。
+        /// alarmTypeKey 按故障类型映射;paramList 携带原因/阶段。全 try 兜底,失败不影响启动。
+        /// 注:新 alarmTypeKey 需在 Java 告警字典登记(否则落库被拒)—— 见计划「跨端依赖」。
+        /// </summary>
+        public void ReportStartupFaults()
+        {
+            if (StartupFaults == null || StartupFaults.Count == 0) return;
+            string tlSn = "";
+            try { tlSn = TLSetting?.tlSn ?? ""; } catch { }
+            foreach (var f in StartupFaults)
+            {
+                try
+                {
+                    string key = f.Type == HouseFaultType.CcdSnMissing || f.Type == HouseFaultType.CameraDuplicateSn || f.Type == HouseFaultType.CameraReadFailed
+                        ? "HOUSE_CAMERA_FAULT"
+                        : (f.Type == HouseFaultType.SerialReadException ? "HOUSE_SERIAL_FAULT" : "HOUSE_INIT_EXCLUDED");
+                    HttpService.AlarmApi(tlSn, f.HouseSn > 0 ? f.HouseSn : 0, 0, key,
+                        new List<string> { f.Type.ToString(), f.Reason, f.Stage });
+                }
+                catch (Exception ex) { ExLog(ex, "ReportStartupFaults"); }
+            }
+        }
+```
+
+- [ ] **Step 8: 编译 + 跑全量单测**
+
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug` → 0 错
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj`
+Expected: 既有 27 + 新增(HouseFault 2 + Policy 6 + Snapshot 2)= **37 绿 0 失败**。
+
+- [ ] **Step 9: 提交**
+
+```bash
+git add ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.cs \
+        ivf_tl_operate_2.0/control/ivf_tl_Control/MonitorSnapshot.cs \
+        ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs
+git commit -m "feat(house-fault): AppData.StartupFaults+快照透出+ReportStartupFaults;MonitorSnapshot.Faults(TDD 37绿)"
+```
+
+---
+
+## Task 7: 真机拔插验收(Claude 自主,UAC 提权)
+
+**前置:** 当前无活体培养(拔插不伤生物);UAC 可静默提权起停 control(见记忆 [[control-realmachine-smoke-startup]]);control 须从 operate 输出根的 `control\` 子目录跑(`..\tl-shared.config` 才解析得到,见交接卡 2026-06-23 真机段第 4 点)。
+
+- [ ] **Step 1: 编译 Release + 部署**
+
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Release`(真机/连内网必须 Release);确认 `control\` 子目录布局 + `tl-shared.config` 在 operate 根。
+
+- [ ] **Step 2: 基线(全好舱)** —— 提权起 control,`/status` 看 `snapshot.Faults` 为空、`Houses` 含全部扫到的舱、`started:true`。记录可跑舱清单。
+
+- [ ] **Step 3: 制造一个"半坏"舱** —— 物理拔掉某有效舱的相机 USB(该舱串口仍活)→ 重启 control。预期:
+  - control **不中止**、`started:true`;
+  - 该舱出现在 `snapshot.Faults`(`FaultType=HOUSE_CAMERA_FAULT/CcdSnMissing`、`Isolated:true`),且**不在**可跑舱清单;
+  - **其余舱全部正常 StartAsync**(读温≈36℃、握手正常)。
+
+- [ ] **Step 4: 告警落库核实** —— 用 JDBC(`临时文件/DbSql.java` 模式)连 108 查 `aivfo-tl-control` 告警表,确认该坏舱告警入库(alarmTypeKey + houseSn + 原因)。**若被拒**,记录:需 Java 告警字典登记新 alarmTypeKey(见「跨端依赖」),作为 Phase 4 门控项。
+
+- [ ] **Step 5: 恢复 + 复测** —— 插回相机重启 control,确认该舱回到可跑、`Faults` 不再含它。
+
+- [ ] **Step 6: 回写文档**(见回写协议)+ 提交。
+
+```bash
+git add 项目文档/进度/进度状态.yaml 项目文档/进度/交接卡.md 项目文档/进度/工作计划表.md 项目文档/进度/待验证清单.md 项目文档/进度/进度数据.js
+git commit -m "docs(house-fault): 第一阶段 control 坏舱剔除真机拔插验收回写"
+```
+
+---
+
+# 后续阶段(提纲,第一阶段落地后单独拆细)
+
+## 第二阶段:运行期故障升级 + 去抖(control,可自主)
+- `HouseBin/BufferBottleBin.MainThread`、`ComBin.StartSendCommandThread` 的 `catch` 里:同舱连续失败计数达阈值(去抖,避免每圈刷屏)→ 升级一条 `HouseFault{RuntimeFault}` 进 `AppData.StartupFaults`(改名/或新增 `RuntimeFaults`)+ `ReportStartupFaults` 同款上报 `HOUSE_RUNTIME_FAULT`;失败计数清零条件 = 该舱恢复一次成功通讯。
+- 纯逻辑(计数/阈值/去抖)可 TDD;升级动作真机验证(运行中拔串口/相机)。
+
+## 第三阶段:operate 监控页"舱故障"区(真机门控)
+- `ivf_tl_Operate` 监控页(读 `ControlClient` → `/status` 的 `snapshot.Faults`)新增"舱故障"区:红色高亮列出每故障舱(舱号/类型/时间/已隔离),与现有监控三块并列。
+- 复用现有轮询;UI 真机门控(需 operate 外壳跑起来,受僵尸 operate Mutex 影响,需先清)。
+
+## 第四阶段:front 告警展示(真机门控)+ 跨端依赖
+- **跨端依赖(必须先确认):** 新 alarmTypeKey(`HOUSE_INIT_EXCLUDED`/`HOUSE_CAMERA_FAULT`/`HOUSE_SERIAL_FAULT`/`HOUSE_RUNTIME_FAULT`)需在 Java 告警字典/告警类型配置登记,否则 `reportCloudAlarm` 落库被拒、front 也无从展示。Task 7 Step 4 已先探;此阶段落地登记。
+- front 舱位图/告警列表:故障舱标红 + 统一文案("X 号舱相机故障,已隔离,其余舱正常");接现有 alarm_data + MQTT 流。
+
+## 第五阶段:真机拔插整体验收(spec §48-51 三条)
+- ① 启动期半坏舱排除、其余正常;② 运行期拔串口/相机隔离、其余采集不中断;③ 双端(operate 监控页 + front)都明确显示故障舱(舱号/类型/时间)。
+- 逐条回写 `待验证清单.md`。
+
+---
+
+## 自查(写完对照 spec)
+
+- **spec §27 启动期按舱容错** → Task 2(Policy 剔除)+ Task 3(SerialBin 登记)+ Task 4(InitTL 不再一刀切)+ Task 5(InitHouse 逐舱兜底)。✓
+- **spec §28 双端明确提示** → Task 6(快照 Faults + ReportStartupFaults 上报)覆盖 control 侧出口;operate/front 展示在第三/四阶段。✓(第一阶段做到"故障可被双端拿到",展示分阶段)
+- **spec §33-35 设计要点(区分致命/单舱、剔除坏舱、逐舱 try-catch)** → Task 2/3/4/5 一一对应。✓
+- **spec §38-41 统一上报(复用 alarm_data+MQTT、/status 加舱故障字段、运行期去抖)** → Task 6(复用 reportCloudAlarm + 快照字段)+ 第二阶段(去抖)。✓
+- **spec §48-51 真机验收** → Task 7(启动期)+ 第五阶段(运行期+双端)。✓
+- **类型一致性自查:** `HouseFault`(Entity)字段贯穿 SerialBin/InitTL/AppData;`HouseFaultRow`(MonitorSnapshot)与 AppData 映射字段名一致(HouseSn/FaultType/Reason/Stage/At/Isolated);`StartupFaultPolicy.RunnableHouses/IsFatal/BadHouseSns` 签名在 Task2 定义、Task4 调用一致;`AppData.StartupFaults`/`ReportStartupFaults` Task4 调用、Task6 定义一致(执行注:先做 Task6 再回填 Task4 调用,保证中途可编译)。✓
+- **占位扫描:** 无 TBD/TODO;每代码步均有完整代码块。真机行为类步骤(Task3/4/5/7)给出确切编辑点 file:line 与验证预期,非"自行处理"。✓

+ 19 - 0
项目文档/进度/交接卡.md

@@ -438,3 +438,22 @@
   - **产出 spec** `需求文档/specs/2026-06-23-舱室故障隔离与双端故障提示-design.md`:启动期按舱容错 + **双端故障提示硬约束**(operate监控页+front 都明确显示哪个舱/什么故障/时间,复用现有 alarm_data+MQTT 管道)。
 - **遗留环境**:control 15840 仍在跑(提权,停需提权 taskkill);僵尸 operate 20268 仍需真重启清;admin/123456 用户已留库(用户要的,可逆);`临时文件/` 下留 JDBC 工具 + 冒烟 ps1。
 - **下一步(用户已定 a+c)**:① 收尾提交 D2-02 第一阶段(本回写);② 拆"舱室故障隔离+双端提示"实现计划。D2-02 第二阶段 MJPEG 按需。本分支待并 main。
+
+---
+
+## 2026-06-23 · 新加固专项"舱室故障隔离+双端故障提示"实现计划已拆完(brainstorm/spec 已在前,本轮=writing-plans)
+
+- **背景**:D2-02 第一阶段(control 后端)代码+真机已全过、文档已回写提交(commit 100e5f8,工作区干净)= ①收尾完成。本轮做用户已定 a+c 的 ②:把加固专项 spec 拆成可执行开发计划。
+- **流程(writing-plans skill)**:先 codegraph 逐符号读透 control 改面 → 写 bite-sized 计划 → 自查对照 spec。**代码一行未写**,本轮只到计划。
+- **codegraph 核实的改面(计划所有 file:line/签名据此)**:
+  - control 用的是 `control/ivf_tl_Com/SerialBin.cs`(StartMain `using ivf_tl_Com`),**非** operate 死栈 `ivf_tl_Entity/ComEntitys/SerialBin.cs`(那套 D3-04 待删)。control 版 errorlist 仅 6 处来源:相机重复SN(:121)/相机读异常catch(:135)/舱号重复(:212)/CCDSN缺失(:252)/CCDSN重复(:260)/扫口catch(:318)。注:control 版"串口打开失败/握手失败"是静默 goto CC**不进 errorlist**(与 operate 版不同)。
+  - 缺口坐实:`StartMain.InitTL`(StartMain.cs:78-94)两处"errorList.Any() 即 return 空"=一刀切中止;`InitHouse`(:160-259)是一个大 try-catch,任一 HouseBinN 构造抛异常→return false→所有舱起不来。
+  - 复用点:告警通道=`HttpService.AlarmApi`(HttpService.cs:54)/`AlarmApi1`(:85)→POST `/api/tl/control/alarm/reportCloudAlarm`(现有 MqttAlarm 已走 AlarmApi1);快照=`AppData.GetMonitorSnapshot`(AppData.cs:222)→`MonitorSnapshot`(operate ControlClient 轮询读);运行期告警出口=`HouseBin_HouseStateEvent`→`ReportAlarmController`。
+- **计划产出** `开发计划/2026-06-23-舱室故障隔离与双端故障提示-实现计划.md`(分支 feature/house-fault-isolation):
+  - **第一阶段 Task1-7(可自主)**:① HouseFault 数据+HouseFaultType 枚举(Entity,纯TDD)② StartupFaultPolicy 坏舱剔除/致命判定(ivf_tl_Control,纯TDD 真值表6测)③ SerialBin 6处 errorlist 旁登记结构化 Faults(保留 errorlist,真机门控)④ InitTL 改"零可跑才中止"+坏舱存 AppData+上报(真机门控)⑤ InitHouse 逐舱 try-catch(真机门控)⑥ AppData.StartupFaults+快照透出 Faults+ReportStartupFaults / MonitorSnapshot.Faults(部分TDD)⑦ 真机拔插验收(拔相机制造半坏舱→该舱排除/其余正常/告警落库)。
+  - **决策规则(锁定)**:坏舱=故障清单 HouseSn>0 的舱(舱号未知的相机/串口级故障不剔除任何舱,只提示);可跑舱=发现−坏舱;**致命=零可跑舱才整机中止**(单舱/部分舱坏绝不中止,落实 spec §34)。
+  - **后续 4 阶段提纲**:运行期故障升级去抖(control 可自主)/ operate 监控页"舱故障"区(真机门控)/ front 告警展示(真机门控)/ 真机拔插整体验收。
+  - **执行注(避免中途不编译)**:Task4 调用 `AppData.StartupFaults`/`ReportStartupFaults`,二者 Task6 定义——**先做 Task6 再回填 Task4 两处调用**。
+- **跨端依赖(计划已标红)**:新 alarmTypeKey(HOUSE_INIT_EXCLUDED/HOUSE_CAMERA_FAULT/HOUSE_SERIAL_FAULT/HOUSE_RUNTIME_FAULT)需在 **Java 告警字典登记**,否则 reportCloudAlarm 落库被拒、front 无从展示。Task7 Step4 先探,第四阶段落地登记。
+- **核实**:计划自查三项过(spec §27/§28/§33-35/§38-41/§48-51 逐节有对应 Task;类型一致性 HouseFault/HouseFaultRow/Policy 签名/AppData 调用贯穿;无 TBD 占位)。**用户已定 a+c 的 ① 已于上一会话完成提交、② 本轮完成**。
+- **下一步**:开工第一阶段——建分支 feature/house-fault-isolation,按计划 Task1 起(建议子代理驱动逐 Task,真机拔插 Claude 自主跑)。D2-02 第一阶段分支 + 本专项分支均待并 main。

+ 4 - 3
项目文档/进度/工作计划表.md

@@ -63,8 +63,9 @@
 - 真机踩坑均已解(僵尸不挡 control / 登录真因=测试目录缺 tl-shared.config 致 BaseUrl 坏 / auth 库本零用户已插 admin/123456 可逆)。
 - 第二阶段(MJPEG 出图)、第三阶段(operate 接入 + V-012 电机真机走位)待拆。
 
-### 新加固专项 · 舱室故障隔离 + 双端故障提示 = ☐ 设计完成待拆实现计划
-- spec `需求文档/specs/2026-06-23-舱室故障隔离与双端故障提示-design.md`。
+### 新加固专项 · 舱室故障隔离 + 双端故障提示 = ☑ 实现计划已拆 · ☐ 待实现
+- spec `需求文档/specs/2026-06-23-舱室故障隔离与双端故障提示-design.md`;**实现计划已落盘** `开发计划/2026-06-23-舱室故障隔离与双端故障提示-实现计划.md`(7 任务 bite-sized + 4 后续阶段提纲,分支 `feature/house-fault-isolation`)
 - **结论**:运行期单舱坏已隔离 ✓;**启动期有缺口**——舱"半坏"(串口活相机坏/编号冲突)致 InitTL 整体中止、好舱起不来 ✗。
 - **目标**:① 启动期按舱容错(单舱坏只排除该舱、好舱继续培养)② **用户硬约束:任何舱异常 operate+front 双端都明确提示哪个舱/什么故障/时间**。
-- 改 control 启动核心,独立分支 bite-sized TDD + 真机拔插验收。待拆实现计划。
+- **第一阶段(计划 Task1-7,可自主)**:HouseFault 数据/枚举 + StartupFaultPolicy 坏舱剔除(纯 TDD)→ SerialBin 登记结构化坏舱 → InitTL 改"零可跑才中止" → InitHouse 逐舱 try-catch → AppData 快照透出 Faults + 复用 reportCloudAlarm 上报 → 真机拔插验收。
+- **后续阶段(提纲)**:运行期故障升级去抖(control)/ operate 监控页"舱故障"区(真机门控)/ front 告警展示 + Java 告警字典登记新 alarmTypeKey(真机门控)/ 真机拔插整体验收。

+ 4 - 4
项目文档/进度/进度数据.js

@@ -1,10 +1,10 @@
 // 实时面板数据源(监控面板.html 读 window.PROGRESS_DATA)。每推进一步更新本文件。
 window.PROGRESS_DATA = {
   project: "operate/control 双进程拆分",
-  generatedAt: "2026-06-23 22:30",
-  phase: "三阶段主体完成;M区全闭合;配置收敛真机验证;D2-02 调试页命令代理 第一阶段 control 后端【代码+真机全过】(27单测绿+真机完整冒烟);新增加固专项 舱室故障隔离+双端提示(spec)",
-  currentTask: "D2-02 第一阶段(control后端)= 代码+真机全部通过:批A/B/C + C-1红线修复,27单测绿,11 commit 在 feature/d2-02-debug-command-proxy。【真机完整冒烟过】借真实舱6→ReadTemp 36.46℃/ShakeHands=6→垂直越界130000/水平越界300000 HTTP400实拒(不下发)→心跳/归还/超时14s自动回收。逐舱2/4/6/7/8/9读温均≈36℃健康。下一步(用户定 a+c):①收尾提交本阶段 ②拆'舱室故障隔离+双端提示'实现计划。第二阶段MJPEG/第三阶段operate接入V-012待拆。",
-  note: "真机踩坑全解:①僵尸 operate 20268 杀不掉(需真重启,但不占串口/Mutex不挡control)②control登录卡点真因=从原始构建目录跑、..\\tl-shared.config缺失致BaseUrl坏(非凭据),修=copy tl-shared.config到bin/Release ③auth库本零用户,按用户要求JDBC插admin/123456(md5(盐+pwd+盐)=582de128...,deleted='2017-01-01'哨兵,可逆),直连登录接口验success+token。新加固专项结论:运行期单舱坏已隔离(每舱独立线程+循环try-catch);启动期有缺口(舱半坏→errorlist→InitTL整体中止,StartMain.cs:89/SerialBin.cs:252),需按舱容错+双端故障明确提示(用户硬约束,spec 2026-06-23-舱室故障隔离与双端故障提示-design.md)。11 commit:批A be88acf等+批B 5922797等+C-1 91afb03+批C 7c43933/b342374。",
+  generatedAt: "2026-06-23 23:30",
+  phase: "三阶段主体完成;M区全闭合;配置收敛真机验证;D2-02 第一阶段 control 后端【代码+真机全过】(27单测绿);新加固专项 舱室故障隔离+双端提示【实现计划已拆完】",
+  currentTask: "新加固专项 舱室故障隔离+双端提示 = 实现计划已拆完落盘(开发计划/2026-06-23-...实现计划.md):第一阶段 Task1-7(HouseFault/StartupFaultPolicy 纯TDD → SerialBin 6处登记结构化坏舱 → InitTL 改'零可跑才中止' → InitHouse 逐舱 try-catch → AppData 快照透出 Faults+复用 reportCloudAlarm 上报 → 真机拔插验收)+ 后续4阶段提纲(运行期升级/operate监控页/front告警/整体验收)。代码一行未写,本轮只到计划。下一步:建分支 feature/house-fault-isolation 开工 Task1。用户定 a+c 的 ①(D2-02收尾提交)②(拆计划)均已完成。",
+  note: "计划基于 codegraph 逐符号核实:control 用 ivf_tl_Com/SerialBin(非operate死栈),errorlist 仅6处来源;InitTL'errorlist非空即整体return'=启动期缺口;InitHouse 大try-catch 单舱构造异常拖垮全体;告警通道=HttpService.AlarmApi→/api/tl/control/alarm/reportCloudAlarm;快照=GetMonitorSnapshot/MonitorSnapshot。决策:坏舱=故障清单HouseSn>0的舱,可跑舱=发现−坏舱,致命=零可跑才中止。跨端依赖(已标红):新alarmTypeKey需Java告警字典登记否则落库被拒,第四阶段落地。环境:僵尸operate 20268需真重启清(不挡control);admin/123456留库可逆。",
   milestones: [
     { name: "阶段1 · control 独立进程骨架(完成)", tasks: [
       { id: "Task1-7", name: "全过+D1-08死锁修复+operate真外壳E2E+数据入库DB铁证", status: "☑" }

+ 11 - 12
项目文档/进度/进度状态.yaml

@@ -1,17 +1,16 @@
 # 续接断点状态(机器可解析)。换会话/换电脑后首先读它定位。
 # 状态取值: 未开始 / 进行中 / 完成 / 代码完成待验证
 # 纪律:本字段只存【当前断点】,历史细节进 交接卡.md(见 CLAUDE.md 第三节)。
-更新时间: 2026-06-23 D2-02 第一阶段(control 后端)代码+【真机】全部通过:27 单测绿 + 真机完整冒烟过(借真实舱6读温36.46℃/握手/电机越界HTTP400实拒/超时14s自动回收),11 commit 在 feature/d2-02-debug-command-proxy。新增加固专项 spec(舱室故障隔离+双端提示)。
+更新时间: 2026-06-23 D2-02 第一阶段已收尾(代码+真机全过,11 commit 在 feature/d2-02-debug-command-proxy 待并 main);新加固专项"舱室故障隔离+双端提示"实现计划已拆完落盘(7任务+4阶段提纲)。
 当前任务: >
-  【D2-02 第一阶段 control 后端 = 代码+真机全过,待收尾提交文档】(分支 feature/d2-02-debug-command-proxy)
-  · D2-02 第一阶段:批A/B/C + C-1红线修复全落地,27单测绿,Debug/Release 0错;**真机完整闭环已验**(见交接卡 2026-06-23 真机段)。
-  · 真机踩坑已解:control 登录卡点=测试目录缺 ..\tl-shared.config 致 BaseUrl 坏(非凭据);auth 库本零用户,已插 admin/123456(md5(盐+pwd+盐)哈希,可逆),登录通。
-  · 下一步(按用户已定 a+c):① 收尾 D2-02 第一阶段(本批文档回写+提交);② 新加固专项"舱室故障隔离+双端故障提示"(spec 已出,待拆实现计划)——用户硬约束:任何舱异常 operate+front 都要明确提示哪个舱/什么故障。
-  · D2-02 第二阶段(MJPEG)/第三阶段(operate接入V-012)仍待拆。本分支待并 main。
+  【新加固专项 舱室故障隔离+双端提示 = 实现计划已拆完,待开工第一阶段(control 坏舱剔除)】(目标分支 feature/house-fault-isolation,尚未建)
+  · D2-02 第一阶段(control 后端)= 代码+真机全过,11 commit 在 feature/d2-02-debug-command-proxy,本分支待并 main。
+  · 本轮产出:`开发计划/2026-06-23-舱室故障隔离与双端故障提示-实现计划.md` —— 第一阶段 Task1-7(HouseFault/StartupFaultPolicy 纯 TDD → SerialBin 登记 → InitTL 零可跑才中止 → InitHouse 逐舱 try-catch → AppData 快照透出+reportCloudAlarm 上报 → 真机拔插验收);后续 4 阶段提纲(运行期升级/operate监控页/front告警/整体验收)。
+  · 下一步:开工第一阶段 —— 建分支 feature/house-fault-isolation,按计划 Task1 起(建议子代理驱动逐 Task,真机拔插由 Claude 自主跑)。
 说明: >
-  D2-02 安全核心真机验证齐全:会话借用/心跳/归还/超时自动回收/红线钳位(越界不下发)全过,逐舱(2/4/6/7/8/9)读温均≈36℃健康
-  新加固专项结论:运行期单舱故障已隔离(每舱独立线程+循环try-catch);**启动期有缺口**——舱"半坏"(串口活但相机坏/编号冲突)会进 errorlist 致 InitTL 整体中止、好舱也起不来(StartMain.cs:89/SerialBin.cs:252);需改按舱容错。两个坏舱:非破坏性手段(握手/温度/相机init/配置)查不出,需电机走位(V-012)或MJPEG出图才暴露
-  环境:僵尸 operate 20268 仍杀不掉(需真重启,但不占串口不挡 control);后端 108+网关10010 全在线。
+  实现计划基于 codegraph 逐符号核实:control 版 SerialBin(ivf_tl_Com)有 6 处坏舱来源、StartMain.InitTL"errorlist 非空即整体 return"是启动期缺口、InitHouse 大 try-catch 单舱构造异常会拖垮全体、现有告警通道=HttpService.AlarmApi→/api/tl/control/alarm/reportCloudAlarm、快照=MonitorSnapshot/GetMonitorSnapshot。决策规则:坏舱=故障清单 HouseSn>0 的舱;可跑舱=发现−坏舱;致命=零可跑才中止(单舱/部分舱坏绝不中止)
+  跨端依赖(计划已标红):新 alarmTypeKey(HOUSE_INIT_EXCLUDED/HOUSE_CAMERA_FAULT/HOUSE_SERIAL_FAULT/HOUSE_RUNTIME_FAULT)需在 Java 告警字典登记否则落库被拒——Task7 Step4 先探、第四阶段落地
+  环境:僵尸 operate 20268 仍需真重启清(不占串口不挡 control);admin/123456 已留库可逆;后端 108+网关10010 在线。
 阶段概览:
   - id: 阶段1
     名称: control 独立进程骨架
@@ -31,6 +30,6 @@
     备注: "Task0-9 全落地,27单测绿(含C-1非零起点红线回归)。真机完整冒烟过(借真实舱/读温/越界实拒/超时回收)。第二(MJPEG)/三(operate接入V-012)阶段待拆。"
   - id: 加固-舱室故障隔离
     名称: 舱室故障隔离 + 双端故障提示(新专项)
-    状态: 设计完成待拆计划
-    备注: "spec 2026-06-23-舱室故障隔离与双端故障提示-design.md。启动期按舱容错(单舱坏不拖垮全体)+ operate/front 双端明确提示哪个舱什么故障(用户硬约束)。改 control 启动核心,单独分支 TDD+真机。"
-下一步: 先收尾提交 D2-02 第一阶段(文档回写)。再拆"舱室故障隔离+双端提示"实现计划。D2-02 第二阶段 MJPEG 按需推进。本分支待并 main
+    状态: 实现计划已拆·待开工第一阶段
+    备注: "spec + 实现计划(开发计划/2026-06-23-...实现计划.md,7任务+4阶段提纲)已落盘。第一阶段=control 坏舱剔除(HouseFault/StartupFaultPolicy 纯TDD + SerialBin登记 + InitTL零可跑才中止 + InitHouse逐舱兜底 + 快照透出/reportCloudAlarm上报 + 真机拔插)。分支 feature/house-fault-isolation 未建。"
+下一步: 开工舱室故障隔离第一阶段(建 feature/house-fault-isolation,按计划 Task1 起,建议子代理驱动)。D2-02 第一阶段(feature/d2-02-debug-command-proxy)+ 本专项分支均待并 main。D2-02 第二阶段 MJPEG 按需推进