# ADR-046: 跨套件 Incident 型別統一治理 | 項目 | 內容 | |------|------| | **狀態** | ✅ 實作完成 (Option B + #123 全部完成 2026-04-01) | | **日期** | 2026-04-01 | | **決策者** | 首席架構師 + 統帥 | | **觸發** | Phase R-R2.1 架構審查 P2-01 | ## 背景 Phase R-R2 完成後,系統中同時存在兩種 Incident 型別: | 型別 | 來源 | 欄位 | |------|------|------| | `src.models.incident.Incident` | 本地 (apps/api) | decision_chain, outcome, ttl_days, vectorized, persisted_to_pg | | `lewooogo_brain.interfaces.incident_processor.Incident` (BrainIncident) | lewooogo-brain 套件 | 精簡欄位,無 decision_chain 等 | ### 問題根源 `IncidentDbAdapter._record_to_incident()` 返回 `BrainIncident`,但靜態型別宣告為本地 `Incident`。 `IncidentMemoryAdapter` 的 `load_incident()` 通過 `brain.DualIncidentMemory` 返回 `BrainIncident`。 部分 Service 層(如 `proposal_service.py`、`incident_service.py`)期望本地 `Incident` 型別, 導致 `AttributeError` 風險或靜態分析誤報。 ## 三種方案 ### Option A:本地 `Incident` 繼承 `BrainIncident` ```python # src/models/incident.py from lewooogo_brain.interfaces.incident_processor import Incident as BrainIncident class Incident(BrainIncident): """AWOOOI 本地擴展 Incident""" decision_chain: AIDecisionChain | None = None ttl_days: int = 30 vectorized: bool = False persisted_to_pg: bool = False ``` - ✅ 型別統一,靜態分析通過 - ⚠️ 強依賴 lewooogo-brain 套件版本 - ⚠️ BrainIncident 變更會影響本地 ### Option B:建立 `IncidentConverter` 轉換層 ```python # src/utils/incident_converter.py def brain_to_local(brain_incident: BrainIncident) -> Incident: ... def local_to_brain(local_incident: Incident) -> BrainIncident: ... ``` - ✅ 兩者保持獨立,變更不互相影響 - ✅ 轉換點明確,可審計 - ⚠️ 每次跨邊界需顯式轉換,增加樣板代碼 ### Option C:逐步廢棄本地 `Incident`,全面切換至 `BrainIncident` - ✅ 最終架構最簡潔 - 🔴 需要修改所有使用本地 `Incident` 的程式碼(30+ 處) - 🔴 proposal_service、incident_service 等需要大規模重構 ## 建議 **推薦 Option B**(IncidentConverter 轉換層),理由: 1. 套件邊界清晰,符合 leWOOOgo 積木化原則 2. 遷移風險最低,可漸進式採用 3. 若未來棄用 lewooogo-brain,只需修改 Converter 短期緩解(已實施): - `_record_to_incident` 返回型別標注為 `Any`(P0-01 修復) - 不阻擋現有業務邏輯 ## 決策結果 (2026-04-01 首席架構師審查) **採用 Option B:IncidentConverter 轉換層** 首席架構師審查評分 72/100 (條件通過),確認 Option B 為最佳方案。 ### 緊急修復已完成 (P2-01) - `signal_worker.py:402` — `persisted_to_pg` 改為 `getattr(incident, "persisted_to_pg", False)` ### ✅ 已實作 (Phase R-R3 Sprint - 2026-04-01) - [x] 建立 `src/utils/incident_converter.py` - `brain_to_local(BrainIncident) -> Incident` ✅ - `local_to_brain(Incident) -> BrainIncident` ✅ - [x] `IncidentDbAdapter._record_to_incident` 返回型別說明更新(仍返回 BrainIncident,供 brain 內部使用) - [x] `IncidentEngineAdapter` 包裝層建立(`incident_engine.py`) - `process_signal()` → brain_to_local 轉換 - `get_incident()` → brain_to_local 轉換 - `update_status()` → 直接委派 - [x] `get_incident_engine()` 返回 `IncidentEngineAdapter`(輸出為 LocalIncident) - [x] #123 `proposal_service.py` 清理 ✅ commit `44840f5` (2026-04-01 ogt) - `_load_incident()` 委派 `IncidentEngineAdapter.get_incident()` - `_persist_incident()` Redis 委派 `brain.DualIncidentMemory.save_incident()` - 移除: 直接 Redis 存取、錯誤 key prefix `"incident:"`、重複 `_record_to_incident()` - [x] `USE_NEW_ENGINE` config 項 ✅ 已標記失效 (P2-03);回滾路徑更新為 `git revert` ### 四個跨邊界轉換點 | 位置 | 方向 | 狀態 | |------|------|------| | `IncidentDbAdapter._record_to_incident()` | DB→BrainIncident (brain 內部) | ✅ 邊界清晰 | | `IncidentEngineAdapter.process_signal()` | BrainIncident→LocalIncident | ✅ 已實作 | | `IncidentEngineAdapter.get_incident()` | BrainIncident→LocalIncident | ✅ 已實作 | | `proposal_service._persist_incident()` | LocalIncident→BrainIncident (save) | ✅ commit `44840f5` | ## 相關文件 - ADR-024: API 分層架構 (Phase 16 絞殺者模式) - ADR-003: leWOOOgo Module Architecture - `feedback_lewooogo_modular_enforcement.md` - Phase R-R2.1 架構審查報告 (commit `d17b67c`)