# 自動證據分類功能 — 白話需求 (Phase 0 草稿)

> 規劃日期：2026-05-27
> 撰寫人：raymond + Claude
> 狀態：Phase 0 草稿，待 brainstorm 後搬至 `docs/features/<feature-name>/`

## 一句話描述

在專案頁加一個「自動分類證據」按鈕，按下後系統會把這輪 AP 上傳到 Drive 的所有證據檔，透過 AI 自動分到對應的 CMMC 控制項 / AO 資料夾，user 在審閱頁微調確認後完成歸檔。

## 為什麼要做

目前 user 跑完一輪 AP 後，要把幾十到幾百份證據手動分類到 17 個控制項 × 59 個 AO 的對應資料夾，耗時且容易漏分。POC 證明 Claude AI 可以做到 ~98% 自動分類準確度，可大幅降低人工。

## 整體流程（六步）

```
① 上傳證據到 Drive 暫存
       ↓
② Web 按按鈕觸發分類 (BE 收 request)
       ↓
③ BE 開 Docker container 跑分類 (~10 分鐘 / 130 檔)
       ↓
④ Container 把檔自動 copy 到對應 AO 資料夾 (in Drive)
       ↓
⑤ User 進審閱 UI 微調 / 確認
       ↓
⑥ User 儲存後 BE 把調整落實寫回 Drive，完成歸檔
```

---

## Step ① — 上傳證據到 Drive 暫存

**現況沿用，不需新功能**

- User 把這輪 AP 的證據檔放到自己專案的 Drive Evidences 資料夾
  - 可從 Drive Web / Desktop / mobile 拖檔
  - 或從 compliance-manager 上傳 UI
- 既有 drive_sync 自動同步檔案 metadata 進 BE

## Step ② — Web 按按鈕觸發分類

### 介面變化（compliance-manager-fe 專案總覽頁）

新增 **「🪄 自動分類證據」** 按鈕

按鈕狀態邏輯：

| 條件 | 按鈕狀態 |
|---|---|
| 專案沒連 Drive | 隱藏 |
| Evidences 資料夾沒檔案 | 灰 disabled，提示「請先上傳證據檔」 |
| 上次分類 job 還在跑 | 灰 disabled，顯示「分類中... 已處理 23/130 (預估剩 7 分鐘)」 |
| 上次分類完成 | 正常啟用，hover 提示「上次執行 2026-05-27 14:30 · 點此重新分類」 |

點按鈕：
1. 跳確認 dialog：「將分析 Evidences 資料夾內 N 個證據檔，預估 X 分鐘，是否開始？」
2. 確認後送 `POST /api/projects/{uid}/auto-classify-evidence` 給 BE
3. BE 立刻回 `202 Accepted` + `job_uid`
4. Web 切換按鈕顯示進度狀態，每 10 秒 polling job 狀態

### 權限

- 預設只有專案 manager 可觸發
- Auditor 可以**審閱**結果但不能觸發新 job
- 一個專案同時只能有一個分類 job 在跑（重複觸發擋掉）

## Step ③ — BE 啟動 Docker container 跑分類

### BE 內部動作

1. 驗證：權限 OK、Drive 連線 OK、無 active job、Evidences 有檔
2. 建 job 紀錄：`(job_uid, project_uid, framework_id, status=queued, started_by, started_at)`
3. 從 OSCAL catalog table 撈該專案使用的框架（CMMC L1）AO 清單，dump 成 JSON 寫 shared volume
4. 起 container：
   ```
   docker run --rm \
     -v /var/lib/cm/jobs/<job_uid>:/job \
     -e DB_HOST=... -e DB_SECRET=... \
     -e DRIVE_TOKEN_ENCRYPTION_KEY=... \
     -e ANTHROPIC_API_KEY=... \
     cmmc-classifier:latest \
     classify --project-uid X --tenant-id Y \
              --catalog /job/catalog.json \
              --output /job/report.json
   ```
5. Subprocess block 等 container 退出
6. 讀 `/job/report.json` + 更新 job status = `completed` 或 `failed`
7. Push 完成通知給 user（in-app notification）

### Container 內部動作

1. 讀 BE 給的 catalog JSON
2. 從 DB `compliance.tenant_drive_integrations` 撈 tenant 的 OAuth refresh token，解密 + 換 access token
3. List Drive Evidences 資料夾根層的檔案
4. 對每個檔：下載 → 抽文字（docx / csv / log / txt）→ 呼叫 Claude API 分類（with prompt caching）→ 寫 report 條目
5. 全部跑完寫 `/job/report.json` 退出（不直接寫 Drive，留給 BE）

### 為什麼用 Docker？

- 隔離（classifier 與 BE 跑不同 Python 環境，分類期間 BE 不被影響）
- 可控制資源（CPU / memory 上限）
- 之後容易換掉（換 model、改 prompt 不用動 BE）
- 部署簡單（image 一次 build，多次跑）

### 時間估算

POC 實測 134 檔 + Claude Sonnet 4.6 + 5 workers 並行 = **~10 分鐘**。

## Step ④ — 自動分類到各 AO 資料夾

### Drive 結構

**每次分類都建一個獨立 run 資料夾**放結果，互不覆蓋：

```
Project Drive/
  AP-Q1-2026/
    Evidences/
      ├── A.docx                            ← 原始檔（保留不動）
      ├── B.csv
      ├── ...
      │
      ├── 自動分類_2026-05-27_14-30/        ← 第一次跑 (run folder)
      │   ├── [AC] Access Control/
      │   │   ├── [AC.L1-3.1.1] Authorized Access Control/
      │   │   │   ├── [a] authorized users are identified/
      │   │   │   │   └── A.docx            ← copy of original
      │   │   │   ├── [b] .../
      │   │   │   └── ...
      │   │   └── [AC.L1-3.1.2] .../
      │   ├── [IA] .../
      │   ├── ...
      │   ├── _report.json                  ← Claude reasoning 完整報告
      │   └── _summary.md                   ← 人可讀統計摘要
      │
      └── 自動分類_2026-05-27_15-00/        ← 第二次跑（re-classify）
          └── ... (同上結構)
```

### Run 資料夾命名

格式：`自動分類_YYYY-MM-DD_HH-MM`（依 BE timezone）

如果同分鐘內重跑：加 `_2`、`_3` 後綴避免衝突。

### BE 行為

1. 解析 report，計算 run folder 名稱
2. 在 Evidences 下建立 run folder
3. 對每筆 file × matched_AO（高信心 ≥ 0.80）：
   - 在 run folder 內建立 AO 資料夾結構（domain → control → AO）
   - 用 Drive API `files.copy` 把原檔複製到對應 AO 資料夾
4. 把 `_report.json` 和 `_summary.md` 寫到 run folder 根（給未來 audit / 機器讀）
5. **Evidences 根目錄的原檔完全不動**
6. 既有 drive_sync 偵測 run folder + 內容，同步進 BE
7. job 紀錄 run folder 名稱 + Drive folder ID + status = `completed`

### 為什麼用 run folder 而不直接展開在 Evidences/

| 優點 | 說明 |
|---|---|
| 多次重跑不衝突 | 第二次跑不會覆蓋第一次的結果，user 可比對哪次更準 |
| 完整 audit trail | 過去每次 AI 分類的結果 + reasoning 都保留 |
| Drive 結構乾淨 | Evidences 根只有原始檔 + N 個 run 資料夾，不會跟 AO 結構混在一起 |
| 可獨立刪 / 歸檔 | 舊的分類 run 可整資料夾砍掉或搬到 archive |
| Job 對應清楚 | 一個 job = 一個 run folder，1:1 mapping |

### 重跑邏輯（user 觸發 re-classify）

- **預設**：跑全部 Evidences 根目錄的檔，產出新 run folder（舊的保留）
- **進階選項**（v1.x 再加）：「只跑跟上次有差異的檔」 / 「只重跑未分類檔」

## Step ⑤ — User 審閱 UI 微調 / 確認

### 入口

- 收到分類完成通知 → 點通知
- 或專案頁的「審閱分類結果」按鈕（job 完成後出現）

### 進入剛剛雛形設計的審閱 UI

功能（已雛形實作）：
- 顯示 stats：134 檔 / 128 已分類 / 2 未分類 / 共 484 個 AO 配對
- **檔案清單**（左）：依 filter / search 列檔，顯示信心 + 配對數
- **詳細審閱**（右）：
  - 已分類檔：列出每個 AO placement + Claude reasoning，可逐項 ✓/✗ 移除
  - 未分類檔：列出 Claude 評估過的所有候選 AO（含 reasoning + 信心），可一鍵採納
  - 加入其他 AO：開 modal，搜尋 + 從全 59 AO 挑選
- **每檔額外動作**：
  - 標為非證據（從審閱隊列移除）
  - 從證據池刪除（連 Drive 原檔一起 trash）
- **快捷鍵**：↑↓ 或 j/k 跳上下一筆，加速逐檔審閱
- **Inbox mode**：「未分類審閱」按鈕快速切換到只看未分類的 review queue

### Pending edits 模型

審閱期間 user 的每個動作是 pending 狀態，沒寫回 Drive。Save bar 顯示「N 個變更待儲存」。
User 可隨時 discard 或 save。

## Step ⑥ — 儲存後歸檔

User 點「儲存變更」：

**所有編輯都套用在「目前審閱中的 run folder」內**（不影響其他 run）。

1. BE 把 pending edits 落實到 Drive：

| 編輯類型 | Drive 動作（在 run folder 內） |
|---|---|
| 新增 AO 配對 | `files.copy` 把原檔複製到 `<run-folder>/[XX]/[YY]/[z]/` |
| 移除 AO 配對 | 從 `<run-folder>/[XX]/[YY]/[z]/` trash 副本 |
| 標為非證據 | 不動 Drive，只更新 BE flag（該 run 的未分類隊列隱藏） |
| 從證據池刪除 | trash 原檔（Evidences 根） + 所有 run folder 內的副本 |

2. 寫 audit log：`(user_id, job_uid, run_folder, file_id, action, before_aos, after_aos, timestamp)`
3. 既有 drive_sync 自動同步最新狀態進 BE
4. UI 顯示「儲存完成，共 N 個變更已套用至 <run folder 名稱>」

---

## 資料儲存方案（實驗階段：JSON-in-Drive）

實驗階段不動 DB，全部 state 都用 JSON 存在 Drive 的 run folder 內，BE 當 thin proxy。

### Run folder 內檔案

```
Evidences/
  自動分類_2026-05-27_14-30/
    ├── _state.json              ← UI 讀寫的 single source of truth
    ├── _report-original.json    ← Claude 原始輸出（immutable 留底）
    ├── [AC] Access Control/...   ← 實體檔副本（依 _state.json 內容生成）
    └── [IA] ...
```

### `_state.json` 結構

> ⚠️ 以下為 FR-030 **初版** schema。實際欄位已隨 FR-030.2/030.3 擴充（token_usage / archive_files / archived_at / error 等），且 `run_uid` / `edit_log` 實際未實作。**完整最新 schema 以 [`docs/api/evidence-classification/state-json-schema.md`](../../api/evidence-classification/state-json-schema.md) 為準。**

```json
{
  "metadata": {
    "run_uid": "abc-123",
    "drive_run_folder_id": "1xyzABC...",
    "drive_run_folder_name": "自動分類_2026-05-27_14-30",
    "framework_id": "cmmc-l1",
    "confidence_threshold": 0.80,
    "model": "claude-sonnet-4-6",
    "triggered_at": "2026-05-27T14:30:15Z",
    "completed_at": "2026-05-27T14:40:22Z",
    "last_edited_at": "2026-05-27T15:05:33Z",
    "last_edited_by": "user@example.com"
  },
  "files": [
    {
      "file_drive_id": "1yktzl...",
      "file_name": "authorized-device-registry.csv",
      "is_na": false,
      "is_deleted": false,
      "matches": [
        {"ao_id": "AC.L1-3.1.1[c]", "confidence": 0.97, "reasoning": "..."},
        {"ao_id": "AC.L1-3.1.1[f]", "confidence": 0.95, "reasoning": "..."}
      ],
      "placements": [
        {"ao_id": "AC.L1-3.1.1[c]", "placement_drive_id": "1b36g1...", "source": "ai", "added_at": "2026-05-27T14:35:01Z"}
      ]
    }
  ],
  "edit_log": [
    {"at": "2026-05-27T15:02:11Z", "by": "user@...", "file_drive_id": "1yktzl...", "action": "remove_placement", "ao_id": "AC.L1-3.1.1[f]"}
  ]
}
```

兩個 key 概念分開：
- **`matches`**：Claude 原始評估，**immutable**。降閾值重看都靠這個算
- **`placements`**：當前真實狀態，**mutable**。UI edit 就是改這個

### BE 最小工作量（3 個 endpoint，不動 DB）

| Endpoint | 行為 |
|---|---|
| `POST /api/projects/<uid>/classify-evidence` | 起 container，回 `{ run_folder_id, status: "queued" }` |
| `GET /api/classification-runs/<folder-id>/state` | 從 Drive 拉 `_state.json` 回傳 |
| `PUT /api/classification-runs/<folder-id>/state` | 收新 state → diff 舊 placements vs 新 → Drive copy/trash → 覆寫 `_state.json` |

### 編輯流程

```
① Container 跑完
   ↓ 寫 _state.json + _report-original.json 到 run folder

② User 進審閱 UI
   ↓ GET _state.json → 載入瀏覽器 in-memory

③ User 編輯
   ↓ 所有 pending changes 累積在瀏覽器 state.files[].placements / is_na 等

④ User 按儲存變更
   ↓ PUT 整份新 state
   BE:
     a. diff 舊 vs 新 placements
     b. 對每筆 diff 跑 Drive copy / trash
     c. 把新版 _state.json 覆寫回 Drive
     d. Drive 自動保留舊版 revision（內建 audit log）
```

### 好處 vs 取捨

| 好處 | 說明 |
|---|---|
| 零 schema migration | 完全不動 DB，純 Drive 操作 |
| 開發快 | BE 3 個 endpoint + UI 接 GET/PUT |
| Audit 內建 | Drive file revisions = 免費 audit log |
| 跨 run 自然 isolated | 不同 run folder 各自有 `_state.json` |

| 取捨 | 應對 |
|---|---|
| 不能 cross-run 跨查 | 實驗期沒需求，正式版再轉 DB |
| 並發編輯衝突 | 加 ETag / `If-Match` 鎖；後改的拒絕 |
| 大 JSON 全寫 | 134 檔 ~400KB OK，過萬檔再考慮 |
| Drive API rate limit | 編輯 batch save，不要每次 edit 都打 |

### 演進路徑

```
[現在 — 實驗階段]                  純 JSON in Drive，BE 當 thin proxy
       ↓
[v1.0 正式版]                      DB schema (3 tables)，Drive 結構照舊
                                   _state.json 仍寫但變 derived export
       ↓
[v2.0 性能版]                      DB 為主，可跨 run 查 + filter
                                   _state.json 純 export 不再 source
```

### v1.0 之後的 DB schema（後階段才做）

> 此段為未來規劃，**v1 實驗階段不實作**

3 個新表：
- `evidence_classification_runs` — run metadata
- `evidence_classification_results` — 每檔每 AO match（含被切掉的低信心）
- `evidence_classification_edits` — user 編輯 audit log

具體 schema 詳見後續 Phase 3 implementation plan。

---

## Job 狀態機

```
queued → running → completed
                ↘ failed (可重試)
                ↘ cancelled (user 主動取消)
```

| 狀態 | 說明 |
|---|---|
| `queued` | 剛建，尚未起 container（通常很短） |
| `running` | container 跑中，BE 可以顯示「已處理 X/Y」進度 |
| `completed` | 分類完成，user 可進入審閱 |
| `failed` | container 異常退出，user 可重試 |
| `cancelled` | user 主動取消（少見） |

## Edge cases

| 情境 | 處理 |
|---|---|
| 檔案太多（>500） | BE 分批起 container（每批 100 檔），UI 顯示批次進度 |
| 單檔太大（>50MB） | 跳過 + log 警示，列在「未處理」清單供 user 知悉 |
| 不支援檔型（.zip / .mp4 / .pdf 純圖等） | 跳過 + 顯示在「未處理」清單 |
| 同名檔案多次上傳 | Drive 允許重名，UI 用 Drive 內部 fileId 區分顯示 |
| Container 中途失敗 | 保留已處理的 report 部分，user 可選「採用部分結果」或「重跑全部」 |
| Review 期間 user 又上傳新檔 | 新檔不在本 job 範圍，需要再跑一次自動分類 |
| 同專案重複觸發 | 第二次點按鈕擋下，顯示「已有分類 job 在跑」|
| Drive OAuth 失效 | Container 偵測到 401 → fail job + 提示 user 重新連線 Drive |
| AO 資料夾已被 user 手動建過 | 重用既有資料夾，不重建 |
| AO 資料夾下已有同名檔 | 跳過 copy（避免重複），report 標記「已存在」|

## v1 範圍

✅ 包含：
- CMMC L1 框架支援
- 單專案單 framework
- 一次完整 batch 分類 + 完整審閱
- Manager 觸發 / Auditor 審閱
- 自動 AO 資料夾結構建立
- Audit log

❌ v1 不包含（後續再說）：
- 多框架（NIST 800-171 / ISO 27001 / CMMC L2-3）
- 真即時分類（檔案一上傳就分類）
- Mobile UI
- 自訂 prompt / 自訂閾值 per AO
- 跨專案批次操作
- 多人協作審閱（同時編輯同一份 job 結果）
- 分類結果版本比對（這次 vs 上次）

## 待釐清（給 brainstorm phase 解）

1. **權限細節**：除了 manager，PM / auditor / 其他角色各有什麼權限？
2. **通知機制**：完成時要 in-app notification、email、Drive 通知？哪些必要？
3. **失敗重試策略**：是「全部重跑」還是「只跑失敗的」？是 user 手動還是自動？
4. **歷史 jobs**：要保留歷次分類結果做比對 / audit 嗎？儲存多久？
5. **成本控管**：Claude API call 成本，要設 per-project quota？月度上限？超量怎麼處理？
6. **N/A 檔的處理**：標為「非證據」後是純 flag、隱藏？還是搬到 `_archived` 資料夾？
7. **若 AO 資料夾已有手動分的檔**：自動分類會 merge（不刪舊只加新）還是 skip？
8. **Container 失敗的觀測**：BE 如何拿到 container 內的 error log？mount volume 寫日誌？
9. **批次規模 sweet spot**：BE 起 1 個 container 跑 130 檔，還是起 5 個 container 各跑 26 檔（平行）？
10. **信心閾值要不要可調**：UI 上要不要讓 user 在按按鈕時調 confidence（預設 0.80）？
11. **Run folder 保留策略**：多次跑後 Drive 會累積 N 個 run folder，要不要設「保留最近 X 次 / 自動歸檔到 archive 子資料夾 / 超過 Y 天自動 trash」？
12. **多 run 並存的 UI**：審閱頁要不要支援切換不同 run 看？預設看最新 run，可下拉切到歷史 run？
13. **第二次跑的 base**：第二次跑分類時，要拿「目前 Evidences 根目錄」當輸入，還是「上次 run folder 內的檔」？影響到 user 新增 / 刪除原檔後的行為。

## 建議下一步

1. 跟 stakeholder 過一遍上述「待釐清」清單
2. 進 Phase 1 brainstorm（用 `superpowers:brainstorming` skill）細究 UX 流程
3. 進 Phase 2 歸檔到 `docs/features/FR-030-2605-auto-evidence-classification/`
4. Phase 3 寫 implementation plan（含 BE schema 變更 / API 設計 / FE 元件 / Docker image build 流程）

## 相關既有資產

- POC script: `scripts/evidence/classify/classify_evidence_drive.py`
- CMMC L1 AO catalog: `docs/reference/CMMC-Level 1-Evidences/cmmc_l1_aos.json`
- 雛形 UI: `scripts/evidence/classify/prototype-ui/index.html`
- 分類結果範例: `scripts/evidence/classify/classification_report_drive.json`
- 證據 sample data: `docs/reference/CMMC-Level 1-Evidences/CMMC-Level 1-Evidences-sample/`
- AO 分類分歧分析: `docs/analysis/2026-05-27-cmmc-ao-classification-disputes.md`
