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 樣本文件:亞航-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.methodstask.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 helperApDocxImportAppService 復用(不跨 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

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 的正式歸屬)