# AI 證據自動分類 — 功能規格

> **版本**:v1.2.1(2026-05-28)— 實驗階段(experimental)
> **狀態**:dev demo-ready,正式環境不建議啟用
> **相關文件**:
> - `design.md` — 設計決策與技術 trade-off
> - `implementation-plan.md` — 階段性實作計畫
> - `../../api/evidence-classification/api-spec.md` — API 端點規格
> - `../../changelog/2026-05-28-*` — 變更紀錄
>
> **本文目的**:作為此功能的「行為規格 source of truth」,涵蓋功能範圍、使用流程、資料模型、權限、限制。供開發 / QA / 文件 / 未來維護參考。

---

## 1. 功能概述

### 1.1 解決的問題

合規稽核準備階段,user 需把幾百份證據檔(政策文件、稽核 log、權限清單等)對照到合規框架的每個評估項目(Assessment Objective, AO)資料夾。以 CMMC Level 1 為例:6 個 domain、17 個 control、**59 個 AO**,134 份證據檔人工整理需 1-3 天。

### 1.2 解決方式

使用 LLM(Claude Sonnet 4.6)分析每份檔案的內容與檔名,自動對應到一個或多個 AO,並把檔案 copy 到 Drive 上對應的三層分類資料夾。User 透過審閱介面逐檔複核與調整。

### 1.3 範圍(v1)

**In scope**:
- CMMC Level 1(NIST SP 800-171A 對照)框架
- 證據檔以 Google Drive 為唯一儲存
- 單一專案、單一 AP 為觸發單位
- 單一活動 job 限制(同 project 不能並行)
- 預覽 docx / xlsx / pptx / pdf / 文字 / 圖片

**Out of scope(列為 follow-up)**:
- 其他合規框架(ISO 27001、NIST SP 800-53 等)
- 跨 tenant / super admin 場景
- 多 AP / 多 project 並行
- 大檔(> 50MB)預覽
- DB-backed job 紀錄(目前 in-memory + Drive scan 復原)

---

## 2. 使用者角色與權限

| 角色 | 觸發分類 | 編輯結果 | 儲存變更 | 查看結果 |
|------|:--:|:--:|:--:|:--:|
| Project Manager | ✓ | ✓ | ✓ | ✓ |
| Project Auditor / 一般成員 | ✗ | ✗ | ✗ | ✓(read-only,v1 未實作)|
| Tenant Admin | 視是否同時為 PM | 視是否同時為 PM | 視是否同時為 PM | ✓ |
| 跨 tenant Super Admin | 未支援(v1) | — | — | — |

**權限檢查點**:
- 觸發(`POST .../classify-evidence`):service 層查 `project_participant`,`role == "manager"` 才放行,否則 `EC_403001 Forbidden`
- 編輯 / 儲存(`PUT .../state`):v1 簡化,僅檢 JWT 不檢 project 角色(實驗階段可接受;正式版要補)
- 預覽(`GET .../file/.../preview`):v1 同上

---

## 3. 前置條件

1. **Tenant 已連 Google Drive**(`tenant_integrations.status = 'CONNECTED'`)
2. **專案已 Drive 初始化**(`drive_folder_mappings` 有對應 project / AP / Evidences mapping)
   - v0.4 起 init folders 流程會自動建 `Evidences/` 子資料夾並寫 EVIDENCES scope mapping
   - v0.4 之前 init 的專案需**重跑 init** 才有 Evidences mapping(handler 為 idempotent,安全)
3. **Evidences 資料夾有檔案**(至少 1 份)
4. **環境變數**:BE 主機 / process env 含有效 `ANTHROPIC_API_KEY`
5. **基礎建設**:
   - Docker daemon 運行中,BE 跑的 user 具 `docker` group
   - 已 build `cmmc-classifier:latest` image(`scripts/evidence/classify/docker/`)
   - LibreOffice 安裝於 BE 主機(預覽 docx → pdf 用,與 SSP 匯出共用)

---

## 4. 功能流程

### 4.1 觸發分類

1. PM 進入「專案總覽」(`/project/projects/<uid>/ap/<ap_uid>/`)
2. 系統檢查 `drive_ready`(透過 `GET .../classify-evidence/jobs?ap_uid=<...>`)
   - **不 ready** → 不顯示「自動分類證據」按鈕
   - **ready** → 顯示按鈕(已有 active job 時切換成「分類中...」+ spinner)
3. PM 點按鈕 → 開 trigger Dialog,顯示信心閾值 slider(預設 0.80)+ 歷史執行清單
4. PM 確認觸發 → 跳 confirm dialog 顯示「N 個檔案,預估 M 分鐘」
5. PM 確認 → BE 收 `POST .../classify-evidence`,回傳 `{ job_uid, status: queued, input_file_count, estimated_minutes }`
6. UI 顯示 toast「{N} 個檔案,預估 {M} 分鐘。可關閉視窗」
7. **PM 可立即關閉 Dialog 繼續其他工作**

**⚠ STG demo 期間例外**:從 commit `c6e9de1` 起 `drive_ready` gate 暫時拿掉(`ProjectAuditorOverview.vue` v-if 沒檢查 driveReady),按鈕一律顯示。原因:STG 部署期間既有專案的 EVIDENCES mapping 尚未 backfill 完成。STG 部署到位後要加回(見 §9 限制 / §12 roadmap)。

### 4.2 後台執行

1. BE worker thread 起 docker container `cmmc-classifier:latest`,傳入 tenant OAuth 憑證 + AP Evidences folder ID + framework catalog
2. Container:
   - 從 Drive 下載每個檔案的內容
   - 對每檔呼叫 Claude API(prompt caching,catalog block ~2300 tokens cached)
   - 產出 per-file:`matches` 陣列(候選 AOs + confidence + reasoning)+ `placements` 陣列(`confidence >= threshold` 的 AOs)
   - 寫 `_state.json` + `_report-original.json` 到 host `/tmp/cm-jobs/<job_uid>/`
3. BE worker:
   - 在 Drive 建 `自動分類_YYYY-MM-DD_HH-MM/` run folder(`Evidences/` 內)
   - 為每個 placement copy 原檔到 `<run_folder>/[domain]/[control]/[ao]/` 三層資料夾
   - 上傳 `_state.json` + `_report-original.json` 到 run folder
   - 更新 `JobRegistry` status = `completed`
4. FE polling(5s 一次,page-level)偵測 job 完成 → 跳 toast「{N}/{M} 已分類,可進入審閱頁」

### 4.3 審閱與編輯

1. PM 進審閱頁(`/evidence-classification-review/...`)
2. 載入 `_state.json` 顯示:
   - 統計列:處理檔案 / 已分類 / 未分類 / AO 配對總數 / AO 涵蓋率
   - Filter bar:
     - **未分類審閱 toggle**(進入時自動把 minConf 拉到 0,離開回 0.80,避免 user 進 inbox 還看不到目標檔的矛盾)
     - 搜尋 / 領域 / 狀態
     - **最高信心 slider 範圍 `[0, 1]`,預設 0.80**(原本 `[0.5, 1]`,改成 0 起以利顯示 AI 沒抓到的 0 分檔)
   - 左欄檔案清單(占 1/3 寬)
   - 右欄 Detail panel(占 2/3 寬)
3. **左欄檔案清單**每列顯示:
   - 檔名(換行,不截斷)+ 副檔名 + AO 配對數
   - 最高信心 ProgressBar + 數值
   - 預覽 icon button
   - 是否「未分類」/「已標為非證據」tag
4. **右欄 Detail panel**:
   - 上方:檔名 + Drive ID + AO 評估數 + 上下篇導覽
   - **已配對 section**(實心卡 + primary border):每張卡顯示分類路徑 / AO ID / 信心 / reasoning / 移除按鈕
     - 新加入未儲存:綠色左邊 + 「新加入」chip + 綠色提示
     - 標記移除未儲存:虛線 + 透明 + 「已標記移除」紅字提示 + 復原按鈕
   - **AI 推薦 section**(虛線卡):AI 評估但尚未配對的 AO(自動過濾 < minConf)
     - 一鍵「加入此 評估項目」直接配對
   - Save bar:標為非證據 / 從證據池刪除 / 加入其他 評估項目 配對 / 捨棄變更 / 儲存變更(N)
5. PM 編輯動作:
   - 從候選加入 / 從已配對移除
   - 從 AOPicker dialog 樹狀選 / 取消(diff 後 apply)
   - 標為非證據(可還原)
   - 從證據池刪除(Drive 上原檔 + 副本全 trash,不可復原)
6. PM 點「儲存變更」→ `PUT .../state` → BE diff 舊新 state → 跑 Drive copy / trash → 覆寫 `_state.json`

### 4.4 預覽證據檔

1. PM 點檔案列右側 👁 → 開 Preview Dialog(95vw × 95vh)
2. BE `GET .../file/<file_drive_id>/preview`:
   - 用 tenant OAuth 從 Drive 抓 bytes
   - **Office 格式(docx/doc/odt/rtf/xlsx/xls/ods/pptx/ppt/odp)**:LibreOffice headless 轉成 PDF
   - 其他格式:原樣串流
   - 回 binary,含正確 mime-type
3. FE 依 response Content-Type 決定渲染器:
   - `application/pdf` → iframe(browser 原生 PDF viewer)
   - `image/*` → `<img>`
   - `text/* / json / xml / yaml` → `<pre>` 純文字
   - 其他 → fallback「不支援內嵌預覽」+「在 Drive 開啟」+「下載」

### 4.5 歷史復原(BE 重啟後)

1. BE in-memory `JobRegistry` 重啟後清空
2. 下次 `GET .../classify-evidence/jobs?ap_uid=<...>`:
   - 取空 registry
   - 用 `_derive_evidence_folder_id(tenant_id, ap_uid)` 找該 AP 的 Evidences folder
   - 列 children 過濾 `自動分類_*` 命名規則的 subfolder
   - 為每個 reconstructed entry 設 `status: completed`、`run_folder_id`、`started_at` / `completed_at`(folder modifiedTime)
   - Dedup 用 `run_folder_id`,registry 紀錄優先
3. PM 仍可從歷史清單點「查看分類結果」進舊的審閱頁

---

## 5. 資料模型

### 5.1 In-memory(BE process)

**JobRegistry**(`app/evidence_classification/service/job_registry.py`):
```python
{
    "<job_uid>": {
        "job_uid": str (UUID4),
        "project_uid": str,
        "project_id": int,
        "tenant_id": int,
        "evidence_folder_id": str,   # Drive folder ID
        "framework_id": str,         # "cmmc-l1"
        "confidence_threshold": float,
        "status": "queued" | "running" | "completed" | "failed",
        "started_at": ISO datetime,
        "completed_at": ISO datetime | None,
        "run_folder_id": str | None,
        "run_folder_name": str | None,    # "自動分類_2026-05-28_09-26"
        "input_file_count": int | None,
        "classified_count": int | None,
        "started_by_user_id": int,
        "error": str | None,
    }
}
```

### 5.2 Persistent(DB)

**`compliance.drive_folder_mappings`** — 新增 `EVIDENCES` scope(本期 migration):
```
scope_type    scope_uid       parent_drive_folder_id    drive_folder_id    說明
EVIDENCES     AP.uid          AP folder.id              Drive folder.id    AI 證據池
ARCHIVE       AP.uid          AP folder.id              Drive folder.id    (既有)
...           ...             ...                       ...                (其他 scope)
```

無其他 DB schema 變動。

### 5.3 Drive(`_state.json` schema)

```json
{
  "metadata": {
    "framework_id": "cmmc-l1",
    "confidence_threshold": 0.80,
    "model": "claude-sonnet-4-6",
    "triggered_at": "ISO datetime",
    "completed_at": "ISO datetime",
    "drive_run_folder_id": "...",
    "drive_run_folder_name": "自動分類_..."
  },
  "files": [
    {
      "file_drive_id": "...",
      "file_name": "...",
      "is_na": false,
      "is_deleted": false,
      "matches": [
        { "ao_id": "AC.L1-3.1.1[c]", "confidence": 0.97, "reasoning": "..." }
      ],
      "placements": [
        { "ao_id": "AC.L1-3.1.1[c]", "placement_drive_id": "...",
          "source": "ai" | "manual", "added_at": "ISO datetime" }
      ]
    }
  ]
}
```

### 5.4 Drive 資料夾結構(以一個 AP 為例)

```
GuidantAI/
└── <Project>/
    └── <AP>/
        ├── Evidences/                        ← 證據池(user 丟檔處)
        │   ├── policy-doc.docx
        │   ├── access-log.csv
        │   ├── ...
        │   ├── 自動分類_2026-05-28_09-26/    ← run folder 1
        │   │   ├── _state.json
        │   │   ├── _report-original.json
        │   │   ├── [AC] Access Control/
        │   │   │   ├── [AC.L1-3.1.1] Authorized Access Control/
        │   │   │   │   ├── [a] authorized users are identified/
        │   │   │   │   │   └── policy-doc.docx          ← copy from Evidences
        │   │   │   │   ├── [b] processes acting .../
        │   │   │   │   └── ...
        │   │   │   └── ...
        │   │   ├── [IA] Identification and Authentication/
        │   │   └── ...
        │   └── 自動分類_2026-05-28_14-30/    ← run folder 2 (re-run)
        ├── _Archive/                         ← (既有,軟刪除堆積區)
        └── [AC] / [IA] / ...                 ← (既有,控制項分類資料夾,給人工配對用)
```

---

## 6. API 規格摘要

完整定義見 `docs/api/evidence-classification/api-spec.md`。

| Method | Path | 用途 |
|--------|------|------|
| POST | `/api/1.0/project/<project_uid>/ap/<ap_uid>/classify-evidence` | 觸發分類 |
| GET | `/api/1.0/project/<project_uid>/classify-evidence/jobs?ap_uid=<...>` | 列 jobs + drive_ready flag |
| GET | `/api/1.0/project/<project_uid>/classify-evidence/jobs/<job_uid>` | 單 job 狀態(polling 用)|
| GET | `/api/1.0/classification-run/<run_folder_id>/state` | 讀 _state.json |
| PUT | `/api/1.0/classification-run/<run_folder_id>/state` | 儲存編輯後 state |
| GET | `/api/1.0/classification-run/<run_folder_id>/file/<file_drive_id>/preview` | 預覽證據檔(office→pdf)|

---

## 7. Error code 對照

完整見 `common/code/evidence_classification_error_code.py`。

| Code | HTTP | 說明 |
|------|------|------|
| EC_403001 | 403 | 僅專案管理者可觸發 |
| EC_404001 | 404 | 專案不存在 |
| EC_404002 | 404 | Run folder 不存在 |
| EC_404003 | 404 | _state.json 不存在 |
| EC_404004 | 404 | Job 不存在 |
| EC_404005 | 404 | Evidences 資料夾不存在 / 無法存取 |
| EC_404006 | 404 | AP 尚未與 Drive 連結 |
| EC_404007 | 404 | AP 資料夾下找不到 Evidences 子資料夾 |
| EC_409001 | 409 | Job 已在執行(同 project 限一個 active)|
| EC_412001 | 412 | Tenant 未連 Drive |
| EC_412002 | 412 | Evidences 資料夾無檔案 |
| EC_412003 | 412 | Catalog 建立失敗 |
| EC_500001 | 500 | Container 執行失敗 |
| EC_500002 | 500 | Container 逾時 |
| EC_500003 | 500 | Drive ops 失敗 |
| EC_500004 | 500 | _state.json 解析失敗 |

---

## 8. 非功能性需求(NFR)

### 8.1 效能

| 項目 | 目標 | 實測(134 檔)|
|------|------|------|
| AI 分類 latency | < 15 分鐘 | ~10 分鐘 |
| BE Drive 收尾 | < 20 分鐘 | ~17 分鐘(270+ copy 操作)|
| 預覽 — text / pdf / image | < 3 秒 | 通常 < 1 秒 |
| 預覽 — docx 轉 PDF | < 10 秒 | 1-3 秒(LibreOffice 啟動成本) |
| Polling 間隔 | 5 秒 | 5 秒 |

### 8.2 並發 & 限制

- 同 project 同時間最多 1 個 active job(EC_409001)
- 不同 project / tenant 可並行(各自獨立 container)
- 全機資源無上限(v0 限制,正式版要加 max concurrent + queue)
- 預覽單檔大小 cap 50MB
- 同 run folder 兩個 user 同時 PUT 採 last-write-wins(無 ETag,v1 未實作)

### 8.3 可靠性

- Container 失敗 → JobRegistry 標 `failed` + 紀錄 error,user 可重觸發
- BE worker thread 失敗 → 同上(但 BE process crash 會丟失 in-memory 紀錄)
- Drive copy 個別失敗 → 紀錄到 `placement.copy_error` 欄位,其他 placement 繼續處理
- BE 重啟 → 跑中 job 丟失;完成的歷史可從 Drive scan 復原

### 8.4 安全

- API 全要求 JWT(`@jwt_required`)
- 觸發限定 project manager
- Tenant OAuth token 加密儲存(jedi-common FernetCrypto)
- 預覽 endpoint 為 BE proxy,user 不直接拿 Drive token
- RLS:`drive_folder_mappings` / `tenant_integrations` 都吃 RLS,跨 tenant 隔離由 PG 保證
- v0 caveat:state get/put/preview 僅檢 JWT 不檢 project 角色

---

## 9. 限制與已知問題

| 項目 | 影響 | 解法 / Workaround |
|------|------|-----|
| `JobRegistry` 是 in-memory | BE 重啟後跑中 job 丟失 | 完成的可 Drive scan 復原(`list_jobs` 帶 `ap_uid` 時 BE 掃 Evidences folder 自動 reconstruct);跑中的需重觸發 |
| `drive_ready` gate 暫時拿掉 | 未連 Drive 的專案也顯示「自動分類證據」按鈕,點下去才 toast | STG 部署 + 既有 project 重跑 init folders 補 EVIDENCES mapping 後,把 v-if 加回 `&& ecJobs.driveReady.value`(commit `c6e9de1` 的反向) |
| run folder 命名時區 | UTC 跟 FE 本地時間差 8h | v1.2.1 b7fa0e9f 起硬綁 `Asia/Taipei`;舊紀錄留原樣 |
| 同 active job 顯示兩條歷史 | (已修)BE 建好 run folder 才把 run_folder_id 寫 registry → 中間期 Drive scan recovery dedup 比不到 | v1.2.1 543b2e1d 改為 create_folder 後立刻 update registry |
| Framework 硬綁 cmmc-l1 | 其他框架不支援 | 等 v1 把 catalog 搬 DB |
| 無 cross-tenant 支援 | super admin 跨 tenant 不能用 | 詳見 `docs/analysis/2026-05-28-worker-thread-session-scope.md` |
| 預覽大檔(>50MB)失敗 | 超大 spreadsheet / 掃描 PDF | FE 自動顯示「在 Drive 開啟」fallback |
| 同 run 並發儲存 last-write-wins | 兩 user 編輯衝突 | v1 加 ETag |
| 無 audit log | 只能從 Drive `_state.json` revisions 反查 | v1 加 audit table |

---

## 10. 依賴

### 10.1 內部
- jedi-common(FernetCrypto / session_scope / auth_context)
- jedi-project(ProjectEntity / ProjectDomainService)
- jedi-oscal(間接,review page 顯示用)
- `app/oscal/service/export/ssp_libreoffice_converter.py`(預覽 docx→pdf 共用)
- `app/cloud_integration/`(Drive OAuth + folder mappings)

### 10.2 外部
- Anthropic API(`claude-sonnet-4-6` + prompt caching)
- Google Drive API v3
- Docker daemon(host 端,跑 classifier container)
- LibreOffice headless(host 端,預覽 docx 轉 PDF)

### 10.3 設定
- 環境變數 `ANTHROPIC_API_KEY`(放 BE process env,不入版控)
- `cmmc-classifier:latest` Docker image(host 端 build)

---

## 11. 接受條件(Acceptance Criteria)

以 Gherkin 風格陳述,供測試 repo 對應 Cucumber feature file:

```gherkin
Feature: AI Evidence Auto-Classification

Background:
  Given my tenant is connected to Google Drive
  And the project's AP has been Drive-initialized with an Evidences folder
  And the Evidences folder contains evidence files

Scenario: Manager can trigger classification
  Given I am a project manager on the ProjectAuditorOverview
  When I click "自動分類證據"
  And I confirm with threshold 0.80
  Then a job_uid is returned and a toast confirms queueing
  And the main button switches to "分類中..." with a spinner

Scenario: Non-manager cannot see trigger button
  Given I am a project auditor (not manager)
  Then the "自動分類證據" button is not visible

Scenario: Drive not ready hides the trigger button
  Given the AP has no Drive folder mapping
  Then the "自動分類證據" button is not visible

Scenario: Classification completes and triggers toast notification
  Given an active classification job
  When the BE worker finishes drive operations
  Then a success toast appears with "{classified}/{total} 已分類"
  And the main button reverts to "自動分類證據"

Scenario: Reviewer adjusts placements then saves
  Given a completed classification with run_folder_id "..."
  When I open the review page
  And I remove a placement
  Then it reappears in "AI 推薦" section with original confidence
  When I click "套用變更" in AOPicker after toggling AOs
  Then placements are updated locally with proper "新加入" / "已標記移除" indicators
  When I click "儲存變更"
  Then a success toast confirms the operation count
  And Drive folder contents reflect the changes (copies created / trashed)

Scenario: Docx preview inline
  Given the file list shows a .docx evidence file
  When I click the 👁 icon
  Then a preview dialog opens
  And the document is rendered as PDF inline (BE converted via LibreOffice)

Scenario: Unsupported preview falls back to Drive link
  Given the file is an obscure format with no inline renderer
  When I click the 👁 icon
  Then a fallback message appears with "在 Drive 開啟" and "下載" buttons

Scenario: History survives BE restart
  Given a completed classification exists in Drive
  And the BE process has been restarted
  When I open the trigger dialog
  Then the history list shows the past run reconstructed from Drive scan
  And clicking "查看分類結果" loads the review page successfully
```

---

## 12. 後續演進(roadmap)

詳見 `docs/release_notes/v1.2.1.md` §8。重點:

| 優先級 | 項目 | 觸發時機 |
|--------|------|---------|
| **高** | **drive_ready gate 加回**:STG 部署完整 + 既有 project 重跑 init folders 補 EVIDENCES mapping 後,把 `&& ecJobs.driveReady.value` 加回 `ProjectAuditorOverview.vue` v-if | STG 跑完 backfill 之後立刻 |
| 高 | DB 化 JobRegistry + run folder reverse-lookup | 上 staging 之前 |
| 高 | Cross-tenant super admin 支援 | 當第一個多 tenant 客戶需求出現 |
| 中 | Framework catalog 搬 DB(支援多框架)| 確認第二個目標框架時 |
| 中 | 並發鎖(ETag)+ audit log | 多 user 場景 |
| 低 | AOPicker 虛擬列表 | 評估項目 > 200 的框架時 |
| 低 | 預覽快取 + 大檔串流 | 預覽使用頻繁時 |

---

## 13. 變更歷史

| 日期 | 版本 | 主要變更 |
|------|------|---------|
| 2026-05-29 | v1.2.1 (未 push) | minConf slider 範圍 0.5→0 / 「未分類審閱」toggle 自動 minConf=0 / `drive_ready` gate 暫時拿掉(STG demo workaround,要加回)/ MF template edit jump-to-control tab 切換修正 |
| 2026-05-28 | v1.2.1 | Drive scan 歷史復原 / 預覽(含 LibreOffice docx→pdf)/ EVIDENCES scope mapping(init folders 自動建)/ worker thread session_scope 修正 / run folder 時區改 Asia/Taipei / dedup 修(run folder 建好立即 update registry)/ verify-and-repair user 屬性 typo 修 |
| 2026-05-27 | v1.2.0 ~ v1.2.1 過渡 | docker container 收口(S1)/ BE module(S2)/ FE review page(S3)/ trigger dialog(S4)|
| 2026-05-26 | POC | 純 script,以 docs/reference/CMMC-Level 1-Evidences 為測試集
