# 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`)
