# INV-7: PR Cutting Plan

**版本**：v1.0 初稿
**日期**：2026-05-03（台北）
**範圍**：Phase 2 全部重構 PR 的切割方案（refactor-specialist 設計）
**用途**：避免大 PR 難以 review；每個 PR 獨立可合併、有 rollback 能力

---

## 原則

1. 每個 PR 必須可獨立合併（不依賴後 PR 才能運行）
2. 每個 PR 有 rollback 方式（revert commit 或 feature flag）
3. PR-01~05 無依賴，可並行；PR-06+ 有順序依賴

---

## PR 清單

### PR-01: telemetry.py hardcoded IP assert 移除

**範圍**：`apps/api/src/core/telemetry.py:71`（約 1-3 行）
**前置**：無
**解決**：P0-08 EwoooC 啟動失敗

```python
# 修改前
if "192.168.0.188" not in endpoint:
    raise ValueError(f"Forbidden OTEL endpoint: {endpoint}")

# 修改後
allowed = [e.strip() for e in settings.ALLOWED_TELEMETRY_ENDPOINTS.split(",")]
if not any(a in endpoint for a in allowed):
    raise ValueError(f"Forbidden OTEL endpoint: {endpoint}")
# 同步在 config.py 加入 ALLOWED_TELEMETRY_ENDPOINTS = "192.168.0.188"（default）
```

**review**：vuln-verifier
**風險**：低（僅移除一個 IP assert）

---

### PR-02: decision_manager.py silence key 常數化

**範圍**：`apps/api/src/services/decision_manager.py:240`（約 2 行）
**前置**：無
**解決**：P1-24 兩處定義不一致

```python
# 修改前（decision_manager.py:240）
silence_key = f"telegram_silence:{target}"

# 修改後
from apps.api.src.services.telegram_gateway import SILENCE_KEY_PREFIX
silence_key = f"{SILENCE_KEY_PREFIX}{target}"
```

**review**：debugger（確認 SILENCE_KEY_PREFIX = "telegram_silence:"，功能等效）
**風險**：低

---

### PR-03: ollama_auto_recovery.py 第二定義移除

**範圍**：`apps/api/src/services/ollama_auto_recovery.py:230`（約 5 行）
**前置**：需確認 INV-1 中 `ollama:current_primary` 的所有寫入點
**解決**：P0-11 GCP 三層拓撲遷移必裂

```python
# 移除 ollama_auto_recovery.py 中第二個 ollama:current_primary 寫入點
# 統一從 config.py 或 ollama_failover_manager.py 讀取，不在 auto_recovery 自行決定 primary
```

**review**：tool-expert（確認 GCP 拓撲三層 failover 邏輯）
**風險**：中（影響 Ollama failover 路徑）

---

### PR-04: registry.py `_provider` → `__provider`

**範圍**：`apps/api/src/plugins/mcp/registry.py:24-71`（約 20 行）
**前置**：無
**解決**：P1-05 `_provider` public 可繞過 audit

```python
# 修改所有 self._provider → self.__provider
# 加 @property provider 讀取（需要時）
# 加 unit test:
#   with pytest.raises(AttributeError):
#       registry.__provider  # double underscore = name mangling
```

**review**：vuln-verifier
**風險**：低（Python name mangling，只影響直接反射存取）

---

### PR-05: mcp_bridge.py namespace 動態化

**範圍**：`apps/api/src/plugins/mcp/mcp_bridge.py:592,602,631,647,681`（約 30 行）
**前置**：無（Phase 1 schema 未完成前，先改為 from settings fallback）
**解決**：P0-13 EwoooC K8s tool 無法使用

```python
# Phase 1 前的過渡做法（PR-05a）：
DEFAULT_NAMESPACE = os.getenv("K8S_DEFAULT_NAMESPACE", "awoooi-prod")
namespace = parameters.get("namespace", DEFAULT_NAMESPACE)

# Phase 2 後的完整做法（PR-05b，需 Phase 1 schema 先完成）：
namespace = parameters.get("namespace",
    get_project_contract(project_id).allowed_k8s_namespaces[0])
```

**review**：tool-expert
**風險**：低（有 env fallback，功能等效）

---

### PR-06: consensus_engine.py CONSENSUS_PREFIX 加 project 前綴

**範圍**：`apps/api/src/services/consensus_engine.py`（約 15 行）
**前置**：**Redis 雙寫 Phase A 已完成**（舊 `consensus:` key 已雙寫新 key）
**解決**：P0-12 consensus engine 多租戶跨 tenant 共用

```python
# 修改前
CONSENSUS_PREFIX = "consensus:"

# 修改後（consensus engine 接受 project_id 參數）
def get_consensus_prefix(project_id: str) -> str:
    return f"{project_id}:consensus:"
```

**review**：db-expert（確認 consensus 語義正確）、debugger（regression）
**風險**：中（需 Redis 雙寫先完成）

---

### PR-07: security hardening（nonce + webhook replay + approval policy）

**範圍**：
- `apps/api/src/services/security_interceptor.py:451-490`（nonce 重設計）
- `apps/api/src/api/v1/webhooks.py:679-728`（webhook timestamp/nonce）
- `apps/api/src/services/decision_manager.py`（requires_approval policy-derived）
**前置**：ADR-116 已 Accepted（規格凍結）
**解決**：P0-04、P0-05、P0-06（3 個 PoC 確認漏洞）

```python
# security_interceptor.py: nonce 必須 HMAC(server_secret + nonce)
# webhooks.py: 加 X-Timestamp header 驗證 ±5min window + Redis NX nonce
# decision_manager.py: requires_approval 從 policy contract 讀，禁止 LLM output 決定
```

**review**：vuln-verifier（PoC 重跑驗證修補有效）
**風險**：高（安全修補，必須 PoC 驗證）

---

### PR-08: Repository project_id filter 批次 1

**範圍**：
- `IncidentRepository`（`incidents` 表）
- `PlaybookRepository`（`playbooks` 表）
- `KnowledgeRepository`（`knowledge_entries` 表）
**前置**：**Phase 1 schema migration 完成**（`project_id` 欄位已存在）
**解決**：P0-03 30+ 業務表無 project_id filter

```python
# 每個 repository 方法加 project_id 參數
# 例：
async def get_incident(self, incident_id: str, project_id: str) -> Incident:
    return await self.session.execute(
        select(Incident)
        .where(Incident.incident_id == incident_id)
        .where(Incident.project_id == project_id)  # 新增
    )
```

**review**：db-expert（query plan 確認）
**風險**：中（約 200 行；需 regression test）

---

### PR-09: Repository project_id filter 批次 2

**範圍**：
- `McpAuditRepository`（加 trace_id + project_id）
- `AiDecisionRepository`（加 run_id + project_id）
- `ApprovalRepository`（加 trace_id + project_id）
**前置**：PR-08 合併後
**解決**：P0-03 繼續

**review**：db-expert + debugger（trace_id 貫穿驗證）
**風險**：中

---

### PR-10: Background loop 標記（ADR-123）

**範圍**：`apps/api/src/main.py`（31 個 loop，約 150 行）
**前置**：ADR-123 已 Accepted
**解決**：P0-07 31 個 loop 無 project_id

```python
# 每個 loop 的 context 加上標記
# platform_internal: asyncio.create_task 前設定 contextvars
# legacy_awoooi_default: fallback project_id = 'awoooi'

async def _run_with_project_context(coro, project_id: str):
    token = project_id_var.set(project_id)
    try:
        await coro
    finally:
        project_id_var.reset(token)
```

**review**：critic（確認標記正確）
**風險**：中（影響所有 background loop 的 logging context）

---

### PR-11: AnomalyCounter per-project 改造

**範圍**：`apps/api/src/services/anomaly_counter.py:790`（約 80 行）
**前置**：PR-10 完成（loop 有 project_id context 後才能傳入）
**解決**：P1-17 AnomalyCounter 全域單例

```python
# 修改前：全域單例，6 個 prefix 無 tenant
_anomaly_counter = AnomalyCounter(get_redis())

# 修改後：per-project 工廠
def get_anomaly_counter(project_id: str) -> AnomalyCounter:
    return AnomalyCounter(get_redis(), project_prefix=f"{project_id}:anomaly")
```

**review**：debugger（確認 6 個 prefix 全數改造）
**風險**：中

---

## 執行順序圖

```
並行群組 G-D（無依賴，可同時開）：
  PR-01, PR-02, PR-03, PR-04, PR-05

↓ Phase 1 Schema 完成後

並行群組 G-E（部分並行）：
  PR-07（安全修補，優先）
  PR-06（需 Redis 雙寫 Phase A）
  PR-08（需 Phase 1 schema）

↓ PR-08 合併後

  PR-09（依賴 PR-08）

↓ ADR-123 Accepted 後

  PR-10（background loop 標記）

↓ PR-10 合併後

  PR-11（AnomalyCounter）
```

---

## 驗收標準

- [ ] PR-01~05 全部合併（無依賴，Phase 0 完成後即可）
- [ ] PR-07 通過 vuln-verifier PoC 重跑驗證
- [ ] PR-08~09 通過 db-expert review（query plan 確認）
- [ ] PR-10~11 通過 regression test（31 個 loop logging context 正確）

*最後更新：2026-05-03（台北）*
