# INV-2: Repository project_id Retrofit Map

**版本**：v1.0 初稿
**日期**：2026-05-03（台北）
**範圍**：`apps/api/src/db/models.py` + `apps/api/migrations/` 全部
**用途**：Phase 2 在 30+ 業務表加 project_id 欄位、backfill 歷史資料、repository filter 改造

---

## 1. 現有資料庫表清單（從 db/models.py 實際讀取）

| 表名 | SQLAlchemy 類名 | 有無 project_id | 是否需要加 | Phase |
|------|----------------|----------------|------------|-------|
| `approval_records` | ApprovalRecord | ❌ | ✅ 是（approval 需 tenant 隔離）| Phase 2 |
| `timeline_events` | TimelineEvent | ❌ | ✅ 是（關聯 incident）| Phase 2 |
| `audit_logs` | AuditLog | ❌ | ✅ 是（審計必須 project-scoped）| Phase 2 |
| `mcp_audit_log` | McpAuditLog | ❌ | ✅ 是（MCP audit 必 project-scoped）+ 加 trace_id / run_id | Phase 2 |
| `mcp_daily_stats` | McpDailyStats | ❌ | ✅ 是（統計分 project）| Phase 2 |
| `k8s_state_snapshots` | K8sStateSnapshot | ❌ | ✅ 是（K8s 狀態屬於 tenant）| Phase 2 |
| `prometheus_snapshots` | PrometheusSnapshot | ❌ | ✅ 是（metrics 屬於 tenant）| Phase 2 |
| `auto_repair_executions` | AutoRepairExecution | ❌ | ✅ 是（修復操作屬於 tenant）| Phase 2 |
| `alert_operation_log` | AlertOperationLog | ❌ | ✅ 是（告警操作屬於 tenant）| Phase 2 |
| `incidents` | Incident | ❌ | ✅ 是（P0-03 核心問題）+ 加 trace_id / awooop_run_id | Phase 2 |
| `knowledge_entries` | KnowledgeEntry | ❌ | ✅ 是（KM 需要 per-project namespace）| Phase 2 |
| `incident_evidence` | IncidentEvidence | ❌ | ✅ 是（關聯 incident）| Phase 2 |
| `playbooks` | Playbook | ❌ | ✅ 是（playbook 屬於 tenant）| Phase 2 |
| `dynamic_baselines` | DynamicBaseline | ❌ | ✅ 是（基線屬於 tenant）| Phase 2 |
| `log_clusters` | LogCluster | ❌ | ✅ 是（日誌分析屬於 tenant）| Phase 2 |
| `agent_sessions` | AgentSession | ❌ | ✅ 是（session 必 tenant-scoped）| Phase 2 |
| `ai_governance_events` | AiGovernanceEvent | ❌ | ✅ 是（治理事件屬於 tenant）| Phase 2 |
| `governance_remediation_dispatch` | GovernanceRemediationDispatch | ❌ | ✅ 是 | Phase 2 |
| `trust_records` | TrustRecord | ❌ | ✅ 是（trust 屬於 tenant）| Phase 2 |
| `ai_provider_version_history` | AiProviderVersionHistory | ❌ | `platform_internal`（追蹤全平台 provider 版本，project_id = __platform__）| Phase 2 |

> 注：migrations 目錄還有其他表（adr*.sql 建立的），需逐一確認。

---

## 2. Migration SQL 表清單（從 migrations/*.sql 掃描）

| Migration 檔案 | 建立的表 | 是否需要 project_id |
|----------------|---------|-------------------|
| `adr071_notification_lifecycle.sql` | notification 相關表 | ✅ 需確認 |
| `adr088_trust_score_persistence.sql` | trust 相關表 | ✅ 需確認 |
| `adr090_asset_inventory_foundation.sql` | asset inventory 表 | ✅ 是（asset 屬於 tenant）|
| `adr091_aider_events_schema.sql` | aider_events | ✅ 是 |
| `adr092_p1_learning_chain_fix.sql` | learning chain 相關 | ✅ 是 |
| `adr093_notification_routing.sql` | notification routing | ✅ 需確認 |
| `adr094_hermes_dispatch_log.sql` | hermes_dispatch_log | ✅ 是 |
| `adr104_playbook_versioning.sql` | playbook 版本相關 | ✅ 是 |
| `adr105_mcp_audit_snapshots.sql` | ⚠️ 注意：表名是 `mcp_audit_snapshots` 但 models.py 用的是 `mcp_audit_log` → 需確認是否同一張表或已廢棄 | 確認後決定 |

---

## 3. project_id 加入策略

### 3.1 Migration 範本

```sql
-- Phase 2：在既有表加 project_id（以 incidents 為例）
ALTER TABLE incidents
    ADD COLUMN IF NOT EXISTS project_id VARCHAR(64)
    NOT NULL DEFAULT 'awoooi';  -- backfill default

-- backfill 驗證後移除 DEFAULT（不能永遠有 DEFAULT）
-- 此步驟在 backfill 確認完成後再執行：
-- ALTER TABLE incidents ALTER COLUMN project_id DROP DEFAULT;

-- Index
CREATE INDEX CONCURRENTLY IF NOT EXISTS
    idx_incidents_project_id ON incidents(project_id);

-- Foreign key（可選，建議使用）
ALTER TABLE incidents
    ADD CONSTRAINT fk_incidents_project
    FOREIGN KEY (project_id) REFERENCES awooop_projects(project_id);
```

### 3.2 Backfill 策略

所有歷史資料：
- `project_id = 'awoooi'`（AWOOOI first tenant，Phase 1 已建立）
- 不需要轉換或映射
- 在 migration SQL 中以 `DEFAULT 'awoooi'` 完成 backfill

### 3.3 Repository 方法改造（批次計畫）

Phase 2 PR-08（批次 1）：
- `IncidentRepository.get_by_id(incident_id)` → `get_by_id(incident_id, project_id)`
- `IncidentRepository.list_active()` → `list_active(project_id)`
- `PlaybookRepository.find_by_alert_type(alert_type)` → `find_by_alert_type(alert_type, project_id)`
- `KnowledgeRepository.search(query)` → `search(query, project_id)`

Phase 2 PR-09（批次 2）：
- `McpAuditRepository.*` 全部加 project_id + trace_id
- `AiDecisionRepository.*` 全部加 project_id + run_id
- `ApprovalRepository.*` 全部加 project_id + trace_id

---

## 4. RLS 設計（ADR-118，補充）

```sql
-- 每張表的 RLS policy（以 incidents 為例）
ALTER TABLE incidents ENABLE ROW LEVEL SECURITY;

CREATE POLICY incidents_tenant_isolation ON incidents
    USING (
        project_id = current_setting('app.project_id', TRUE)
        OR current_user = 'awooop_platform'
    );

-- 應用程式在每次 connection/transaction 開始設定：
-- SET LOCAL app.project_id = '{project_id}';
```

---

## 5. 特別注意項目

| 問題 | 影響 |
|------|------|
| `adr105_mcp_audit_snapshots.sql` 建的表名 `mcp_audit_snapshots` vs models.py 的 `mcp_audit_log` | P0-02 的一部分：表名不一致，migration 可能廢棄或重建 |
| `knowledge_entries` 需要 pgvector namespace 隔離 | KM 向量搜索的 namespace 需同步加 project_id scope |
| `agent_sessions` 已有 session_id，改造時要確保 session 不跨 tenant | 改造前先確認 session lookup 路徑 |

---

## 6. 驗收標準

- [ ] 所有 20 張表（db/models.py）完成 project_id migration
- [ ] backfill：所有舊資料 project_id = 'awoooi'
- [ ] 所有 repository 方法加 project_id filter
- [ ] cross-project SELECT 全部被 RLS 拒絕（pytest 覆蓋）
- [ ] `mcp_audit_snapshots` vs `mcp_audit_log` 衝突已解決

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