# FR-030.3 — 證據分類支援 .xlsx / .pptx（本地抽文字）

> 規劃日期：2026-05-29
> 撰寫人：raymond + Claude
> 狀態：設計草稿，待 user 確認抽取預設 → spec review → implementation plan
> arc 關聯：↳ FR-030 子（延續「補上分類器目前跳過的常見檔型」，sibling of FR-030.2）

## 一句話描述

讓 FR-030 證據分類器支援 Excel（.xlsx）與 PowerPoint（.pptx）證據 —— 兩者 Claude API 無原生 block，沿用 FR-030.2 已建好的「本地抽文字 → text block」路線（同 docx 機制），把 cell / 投影片文字抽出來送分類，分類與歸檔流程完全不變。

## 為什麼要做

合規證據裡 Excel 清單（資產清冊、帳號表、設定矩陣）與簡報（教育訓練、政策說明）很常見。FR-030.2 完工後，分類器已能吃文字檔 + 圖片 + PDF，但 .xlsx / .pptx 仍落在 `build_content_blocks` 的「unsupported → skip」分支，被列未處理要人工歸檔。本期補上這兩種。

## 為什麼是「抽文字」不是 Vision

| 檔型 | Claude API 原生 block | 處理路線 |
|------|----------------------|----------|
| 圖片 / PDF | 有（image / document）| base64 直送（FR-030.2 已做）|
| docx / **xlsx** / **pptx** | **無** | **本地抽純文字 → text block** |

xlsx / pptx 跟 docx 同類，沒有原生 block，必須自己在本地解析成文字。所以本功能**不碰 vision 路徑**，只擴充 FR-030.2 的 text 分支。

## 核心原則（同 FR-030.2）

> 原分類 / 歸檔流程不變。`_state.json` 結構、Drive 歸檔、review UI 全沿用。xlsx / pptx 只是多兩種能抽成文字的檔型，出分類器後與其它檔走同一條歸檔路。

---

## 架構：擴充 FR-030.2 的 text 分支

FR-030.2 後，`build_content_blocks` 的文字分支為：
```python
if ext in {".docx", ".csv", ".log", ".txt", ".md"}:
    text = _extract_text_legacy(path)
    return [{"type": "text", "text": f"Filename: {filename}\n..."}], None
```

本期改動：
1. ext 集合加 `.xlsx`、`.pptx`
2. `_extract_text_legacy` 加兩個分支抽取（mirror 既有 `.docx` 用 python-docx 的 pattern）

### 抽取邏輯（待 user 確認的預設）

**`.xlsx`（openpyxl）**：
```python
elif ext == ".xlsx":
    from openpyxl import load_workbook
    wb = load_workbook(str(path), read_only=True, data_only=True)  # data_only=取值不取公式
    parts = []
    for ws in wb.worksheets:
        parts.append(f"# Sheet: {ws.title}")
        for row in ws.iter_rows(values_only=True):
            cells = [str(c) for c in row if c is not None]
            if cells:
                parts.append(" | ".join(cells))
    text_out = "\n".join(parts)
```
- **所有工作表**、逐列 cell 用 `|` 串接（比照 docx 表格）
- `data_only=True`：取**計算後的值**而非公式字串
- `read_only=True`：大檔記憶體友善

**`.pptx`（python-pptx）**：
```python
elif ext == ".pptx":
    from pptx import Presentation
    prs = Presentation(str(path))
    parts = []
    for i, slide in enumerate(prs.slides, 1):
        parts.append(f"# Slide {i}")
        for shape in slide.shapes:
            if shape.has_text_frame and shape.text_frame.text.strip():
                parts.append(shape.text_frame.text)
            if shape.has_table:
                for r in shape.table.rows:
                    parts.append(" | ".join(c.text for c in r.cells))
    text_out = "\n".join(parts)
```
- **所有投影片**的文字框 + 表格
- **預設不含**講者備忘稿（speaker notes）— 通常非證據內容（← 待 user 確認，要含 notes 可加 `slide.notes_slide`）

兩者最後沿用既有 `MAX_TEXT_CHARS=8000` 截斷（在 `_extract_text_legacy` 內）。

### 待 user 確認的設計決策

1. **xlsx 取值不取公式**（`data_only=True`）— OK？
2. **pptx 不含講者備忘稿** — OK？要含的話加一行。
3. **超大 xlsx / 超多投影片**：是否要設上限（如 xlsx >N 列、pptx >M 頁就截斷或 skip）？或單純靠 `MAX_TEXT_CHARS` 截斷即可（傾向後者，簡單）。

---

## 範圍邊界

✅ 本次做：
- `.xlsx` / `.pptx` 走本地抽文字 → text block → 分類
- 新增依賴 `openpyxl` / `python-pptx`（container requirements）
- 擴充 `_extract_text_legacy` + `build_content_blocks` ext 集合 + 單元測試

❌ 本次不做：
- `.xls` / `.ppt`（舊二進位格式，需 LibreOffice 轉換，另議）
- xlsx 內嵌圖表 / 圖片的視覺辨識（只抽文字）
- 成本控管、`_state.json` schema 變更、review UI 改版、vision 路徑任何改動

## 測試

- 單元測試 `build_content_blocks` 對 `.xlsx` / `.pptx`：用 openpyxl / python-pptx 動態建小檔，斷言抽出的 text block 含 cell / slide 文字
- 回歸：既有 docx/csv/image/pdf 分支不受影響

## 整合點 / 既有資產

- FR-030.2 已建管道：`scripts/evidence/classify/docker/container_entrypoint.py`（`_extract_text_legacy` / `build_content_blocks`）
- FR-030.2 spec：`docs/features/FR-030.2-2605-evidence-classify-image-vision/design.md`
- container 依賴：`scripts/evidence/classify/docker/requirements.txt`
- 測試：`scripts/evidence/classify/docker/test_container_entrypoint.py`（沿用，加 case）

---

## §11 實作後對齊（Reconciliation，2026-05-30 收尾補）

本 FR 原本只規劃 xlsx/pptx 抽文字，但 2026-05-30 的 task arc 一路長成「證據分類器全面強化」。實際交付遠超原 design，以下逐條對齊：

| # | 原設計 | 實際做了什麼 | 原因 |
|---|--------|-------------|------|
| 1 | xlsx/pptx 抽文字 | ✅ 照做 | — |
| 2 | 「不做」內嵌圖視覺辨識 | ❌ 推翻 → **做了**（office 內嵌圖 → LibreOffice→PDF→vision，cost-aware：有圖才轉） | user 提出「貼圖的文件分不到」是真痛點；BE 既有 LibreOffice converter 可借鏡 |
| 3 | （無）trigger 選項 | 新增：選模型（sonnet/opus，UI flag 隱藏預設 sonnet）+ analyze-only 歸檔模式 | user 要先驗分析效果不污染 Drive + 想試 Opus |
| 4 | （無）一次歸檔 | 新增 `POST /classification-run/<id>/archive` + 審閱頁「立即歸檔」按鈕/提醒 | analyze-only 跑完需事後正式歸檔 |
| 5 | （無）可觀測性 | 新增 container log 上傳 Drive（憑證遮蔽）+ token/成本統計（UI flag 隱藏預設關） | 查圖片 Fail 免 SSH + 想知道成本 |
| 6 | （無）prompt 調校 | 用真實證據集三方對照 → 加證據提示 + AO-letter 完整性 + 過配收斂 | 真實 run 發現的分類落差 |

### 關鍵決策軌跡（詳見 analysis）

- **分類落差三分法**：辨識缺口（可 prompt 修）/ 字母粒度（B 規則）/ User 標籤爭議（改模型無解，需修 ground truth）。詳見 `docs/analysis/2026-05-30-cmmc-evidence-classification-accuracy-gap.md`
- **不降 confidence 門檻**：用 prompt 讓對的 AO 信心升高，而非降門檻（會惡化過配）
- **prompt 收斂到此為止**：過配 22→8 後，剩餘多為單一頑固檔，再追會過擬合此資料集
- **內嵌圖只在「有圖」才轉 PDF**：避免 text-only 文件白付 vision 成本

### 對話紀錄

`docs/conversation-history/2026-05-30/evidence-classify-arc/`（逐輪 dump，2 parts）
