# FR-043 稽核計畫 docx 匯入（ap-authoring import）— 設計（SD）

> Date: 2026-07-02 · Branch: `feature/FR-043`
> 前置研究（docx/xlsx ↔ OSCAL 欄位對應、被排除選項）見
> [`docs/analysis/2026-07-02-ap-docx-import-oscal-mapping.md`](../../analysis/2026-07-02-ap-docx-import-oscal-mapping.md)
> 樣本文件：`亞航-CMMC L1內部稽核報告20260612(稿).docx`（顧問實際交付格式，v1 目標格式）

## 1. 需求

PM（project manager）或稽核單位（assessor）在 ap-authoring 頁
（`/project/projects/:id/round/:roundUid/ap-authoring`）可**上傳顧問的稽核報告 docx**，
系統解析章節內容預填成行程資料，user **預覽、修改、確認**後快速建入稽核計畫。
未來會有多種報告格式版本，本期支援亞航 CMMC L1 這一版，架構須可擴充。

**明確不做**（見 analysis §2.3/§3）：
- docx §1 依據／§2 目的不落 AP 欄位（歸屬 SSP），預覽時作為「參考資訊」顯示，可勾選附進行程備註
- 附件檢查紀錄 xlsx 不在本 FR（屬 AR 匯入，另立 FR）
- 不新增任何 AP schema 欄位（前置研究結論：現有欄位全部可承載）

## 2. 使用情境

顧問或 PM 拿到稽核報告 Word 檔 → ap-authoring 頁點「匯入稽核計畫」→ 上傳 docx →
系統解析出：稽核日期、稽核單位、稽核方法＋每方法查核指引、範圍敘述 →
預覽頁以「行程表單預填」形式呈現，user 補勾查核控制項與受評對象（docx 文字對不到系統
uuid 的部分）→ 確認 → 行程與稽核人員直接建進 AP，受評控制項/受評對象 tab 自動推導回填。

## 3. 欄位對應（v1 亞航格式 → AP）

| docx 章節 | 解析目標 | 寫入落點 | 預覽可改 |
|---|---|---|---|
| 4 稽核日期 | 日期＋**開始時間** | `task.timing` = 日期＋從時段文字抽出的開始時間（「上午10:00」→ 10:00；上午/下午 12 小時制轉 24 小時；抽不出時間才 fallback 日期無時刻）；時段全文照舊進備註。**timing payload 與手動建行程完全同形**，避免 date-only 被當 UTC 午夜、前端 +8 顯示成 08:00 | ✅ |
| 3 稽核單位 | 單位名稱 | `POST /ap/:uid/parties`（party_type=organization, role=assessor）→ task participants | ✅（可配對既有 party 或建新） |
| 6 稽核方法 | 方法清單（對照 `system_menu` AUDIT_METHOD）＋ §6 逐子點分流（規則見下）| `task.methods` ＋ `task.steps[]` ＋ 參考資訊 | ✅ |
| 5 稽核範圍 | 範圍敘述全文 | task 備註（`description`）；受評對象留給 user 預覽時勾選（無可靠 uuid 配對依據，寧空勿錯配） | ✅ |
| —（查核控制項） | 文件未逐條列出控制項時（v1 亞航即此況，語意=全範圍查核）**預設全選**：suggested task 的 controls 帶入該 AP profile 展開的全部 control_id；未來格式若逐場列控制項則照解析結果帶入 | `task.controls`，preview 可刪減 | ✅ |
| 1 依據＋2 目的 | 全文 | 預設勾選「附進備註」；提示正式歸屬為 SSP | ✅（可取消） |
| 報告標題 | 首行標題 | `task.title` 預設值＝**行程化標題**：去掉文件性尾綴（「報告」「紀錄」「(稿)」及尾端日期戳），例「CMMC 2.0 Level 1內部稽核報告」→「CMMC 2.0 Level 1內部稽核」；去綴後為空才 fallback 原標題 | ✅ |

**§6 稽核方法的逐子點分流規則**（每一子點都必須爬到、不得遺漏，但各歸各位；
指引框只放「這個方法要看什麼」，跨方法論述上移參考資訊）：

| §6 子點 | 內容 | 去處 |
|---|---|---|
| 6.3 前半 | 「本次稽核採用訪談、文件與政策紀錄檢查、現場實地觀察、實體電腦組態設定比對」 | methods 清單（比對 AUDIT_METHOD 選項）|
| 6.3 逐方法句 | 「訪談能提供…」→訪談；「文件能提供…」→文件檢查；「現場實地觀察與電腦組態設定比對能據以判斷…」→檢視＋電腦設定比對（同句共用）| 各方法 `steps[].description`，**一方法一句、不重複塞總述** |
| 6.1＋6.2 | 評鑑指南依據＋方法選擇彈性原則（跨方法論述）| 參考資訊區「稽核方法論」section（與依據/目的並列，checkbox 附進備註）|

產出物 = **1..N 筆行程（task）的 `SetTasksRequest._TaskItem` 形狀**＋
**0~N 筆待建 party**。confirm 時 append 到既有行程清單（不覆蓋）。

> 行程數是**格式決定的**：v1 亞航格式整份報告描述單一場次 → 固定解析出 1 筆；
> 未來格式（多天期／各部門分場次的稽核行程表，ISO 27001 常見）一場次一筆。
> 因此中間結構與 preview UI **從 day one 就是清單**，confirm payload 本來就是
> tasks[]（`SetTasksRequest`），BE 寫入路徑天然支援多筆、不需改。

## 4. 架構 — 全面 mirror SSP docx 匯入管線

複用既有三段式管線（`ssp_docx_import_app_service.py` pattern），不發明新流程：

```
POST   /ap/<ap_uid>/docx-imports/parse      上傳+解析 → {parse_uid, status}
GET    /ap-docx-import/<parse_uid>          預覽（解析結果 + 建議 payload）
POST   /ap-docx-import/<parse_uid>/confirm  確認寫入（帶 user 修改後的 tasks/parties）
DELETE /ap-docx-import/<parse_uid>          廢棄（soft delete）
```

### 4.1 BE 分層

| 層 | 新增 | 說明 |
|---|---|---|
| route | `api/project/routes/ap_docx_import_route.py` | 掛在 project blueprint（與 ap-authoring 其他端點同族），multipart 上傳 |
| serializer | `api/project/serializers/ap_docx_import.py` | Parse/Preview/Confirm schema；confirm 的 tasks 直接復用 `SetTasksRequest._TaskItem` 形狀 |
| app service | `app/grc/service/ap_docx_import_app_service.py` | 與被複用的 `assessment_plan_app_service.py` 同住 `app/grc/`（DI 掛 grc container）。檔案驗證（.docx、20MB，比照 `_DOCX_MAX_SIZE`）→ parse job → parser → 持久化 `parsed_result` |
| parser | `app/grc/service/ap_report_parser/`（adapter registry） | 見 §5 多版本設計 |
| domain/infra | `ap_docx_parse_jobs` 表 + entity + repo | 照 `ssp_docx_parse_job` 同形（uid, status, source_uid=ap_uid, parsed_result JSONB, **TTL 24 小時**（mirror `_PARSE_JOB_TTL_HOURS`）, tenant-scoped），**放 `oscal` schema**（與 sibling parse job 表同處）；SQL migration 走鐵則（GRANT cm_app + schema_migrations） |

**confirm 寫入不另開 write path**（禁止重複造輪子）：
`confirm` 組好 payload 後呼叫既有 `AssessmentPlanAppService.set_tasks()`（append 語意：
先讀既有 tasks 串接再全量覆寫）與 party 建立。既有的
audit_planning 階段檢查、Tab2/Tab3 聯集回填、activity/steps 落地全部自動繼承。

實作注意（spec review 抽查結論）：
- **append 的 round-trip 轉換**：既有 tasks 的讀取 shape（`_ap_detail` 輸出）≠
  `_TaskItem` 輸入 shape，confirm 內需做 detail→payload 轉換；全量覆寫會重生
  task/activity uuid，audit_planning 階段尚未派 job 故無害（implementation plan 展開）。
- **party 同名策略**：`add_ap_party()` 對同名 party 會 `ConflictError`（非 idempotent），
  若在 confirm transaction 內爆會整筆 rollback。策略：preview 階段先按名稱比對既有
  parties 給配對建議；confirm 時同名一律視為配對既有 party，不重複建立。

### 4.2 權限

比照 SSP docx 匯入「app service 層強制、fail-closed」慣例。角色術語區分清楚：
**參與者角色**（權限檢查用）是 `participant.role ∈ {manager, auditor}`；
`assessor` 是 OSCAL party 的 `role_id`（§3 表格用法），兩者不同概念。

- parse / confirm / discard：project participant role ∈ {manager, auditor}。
  既有 authoring 守門是 `AssessmentPlanAppService._resolve_ap_and_check_auditor()`
  （含 `assert_round_phase({"audit_planning"})`），但它是私有方法——實作時**把 gate 提為
  public helper** 供 `ApDocxImportAppService` 復用（不跨 service 呼叫私有方法、不複刻條件）
- preview：project participant（任一角色）
- confirm 的階段/角色守門走 `set_tasks` 自動繼承（雙重保險）
- 前提：AP 已存在（generate-draft 後）；`ap_uid` 不存在 → 404（既有 GRC_AP_NOT_FOUND）

### 4.3 錯誤碼

`common/code/grc_error_code.py` 新增（命名 `GRC_<HTTP><序號>`），
語意 mirror SSP docx 匯入既有行為：
- docx 格式無法辨識（400，附已支援版本清單）
- 檔案過大（400）— **新增 AP 專用 code、訊息寫 20MB**；既有 `GRC_DOCX_FILE_TOO_LARGE`
  訊息寫 10MB 與常數 20MB 不符，屬既有 bug 不在本 FR 修
- parse job 不存在（404）
- parse job **過期 → 412**、狀態非 awaiting_review（防重複 confirm）→ 412
  （mirror `GRC_DOCX_PARSE_JOB_EXPIRED` / `NOT_AWAITING` 語意）
- 非 audit_planning 階段（412，既有）、無權限（403，既有）

## 5. 多版本格式：parser adapter registry

```python
class ApReportParserAdapter(ABC):
    version: str                       # "airasia-cmmc-l1-v1"
    def detect(self, doc) -> bool      # 章節結構特徵偵測（編號章節「依據/目的/稽核…」標題）
    def parse(self, doc) -> ParsedApReport
```

- registry 依序 `detect()`，第一個命中者解析；全 miss → 回 400「格式無法辨識」＋
  已支援版本清單。preview response 帶 `adapter_version` 供 FE 顯示。
- `ParsedApReport` 是版本無關的中間結構，**行程為清單**：
  - report 層（跨場次共用）：audit_org / basis_text / objective_text / raw_sections
  - `tasks: list[ParsedApTask]`，每筆含 title / date / methods[] /
    method_guidances{} / scope_text
  - v1 亞航 adapter 固定產出 `len(tasks) == 1`；對應/組 payload 邏輯只依賴此結構，
    新版本格式（多場次行程表）只加 adapter 不動主流程。
- **方案取捨**：規則式 adapter（採用）vs AI 抽取（排除，本期）。規則式對已知格式
  確定性高、零 token 成本、可離線測試；AI 抽取留作未來「未知格式 fallback adapter」
  的擴充位（registry 尾端掛一個 LLM adapter 即可，不影響架構）。

## 6. FE 設計

風格完全沿用專案（PrimeVue 3.53 ＋ 既有 design tokens），結構 mirror
`ssp-docx-import-v2/` 三步 wizard，但以 **Dialog wizard** 掛在 ap-authoring 頁
（範圍小、不需獨立 route）：

- 入口：`RoundApAuthoringView.vue` header「匯入稽核計畫」按鈕（僅 audit_planning 階段
  ＋participant role ∈ {manager, auditor} ＋ **AP 已存在**時顯示），開 `ApDocxImportDialog.vue`
- 重複匯入同一份檔**允許**（append 語意，行程可手動刪）；Step 2 頂部提示
  「將新增 N 筆行程到現有 M 筆之後」讓 user 自行判斷
- **Step 1 上傳**：FileUpload（.docx、20MB）→ 呼叫 parse → `ProgressSpinner`
  （比照「先開 dialog 再載入」慣例）
- **Step 2 預覽與修改**：
  - 上：解析摘要（adapter 版本、辨識出的章節 chips）
  - 中：**行程預填清單**（支援多筆，v1 只會一筆）——每筆一張可展開卡，欄位與
    ap-authoring 行程卡相同（標題/日期/方法 MultiSelect/每方法查核指引/備註），
    全部可改、可整筆勾除不匯入；查核控制項與受評對象兩欄為空、就地補勾
    （選項母體同 authoring 頁）
  - 下：稽核人員配對（解析出的「鈊安資安顧問」→ 下拉選既有 party 或「建立新的」）＋
    參考資訊區（依據/目的全文，checkbox「附進備註」預設勾選）
  - PrimeVue `Steps` 用 `:active-step`（quirk 註記）；draft 存 localStorage
    （mirror `useSspDocxDraft`，key by parse_uid，防 TTL 過期丟編輯）
- **Step 3 完成**：confirm → toast ＋ 匯入摘要（建了幾筆行程/幾位人員）→
  關閉 dialog、refresh tasks/parties/兩個推導 tab
- UX 準則（ui-ux-pro-max）：步驟指示（Step x of 3）、submit loading→success/error、
  錯誤訊息含恢復路徑（格式不符→列支援版本）、dialog 關閉前有未確認編輯需二次確認
  （sheet-dismiss-confirm）

## 7. 測試重點（Phase 4 由 feature-test-planner 展開）

- parser：亞航樣本 docx 全章節解析 snapshot（樣本檔入測試 fixture，期望值表由
  test planner 依 §3 對應表展開）；缺章節/亂序/非目標格式 → 格式錯誤
- app service：權限矩陣（manager/auditor/一般 participant/無關者 × parse/preview/confirm）、
  階段守門（非 audit_planning 412）、TTL 過期 412、**double-confirm 同一 parse_uid 被
  NOT_AWAITING 擋下**、confirm append 不覆蓋既有行程、party 同名走配對不建新
- FE e2e（compliance-manager-test repo）：上傳→預覽改欄位→確認→行程出現在列表

## 8. 不動的東西

- jedi-* 套件：零異動（parse 在主專案、寫入走既有 app service）
- AP schema：零新欄位；唯一 DB 變更 = `ap_docx_parse_jobs` 新表
- ap-authoring 既有編輯流程：不變，匯入只是「預填來源」

## 9. 未來擴充（本期不做）

- AR 匯入（附件檢查紀錄 xlsx → observations/findings）— 另立 FR
- 報告匯出（系統資料反向產 Word；屆時補 terms-and-conditions/location/timing 時段）
- LLM fallback adapter（未知格式）
- SSP 側「依據/目的」編輯入口引導（docx §1/§2 的正式歸屬）
