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 目標格式)
PM(project manager)或稽核單位(assessor)在 ap-authoring 頁 (/project/projects/:id/round/:roundUid/ap-authoring)可上傳顧問的稽核報告 docx, 系統解析章節內容預填成行程資料,user 預覽、修改、確認後快速建入稽核計畫。 未來會有多種報告格式版本,本期支援亞航 CMMC L1 這一版,架構須可擴充。
明確不做(見 analysis §2.3/§3):
顧問或 PM 拿到稽核報告 Word 檔 → ap-authoring 頁點「匯入稽核計畫」→ 上傳 docx → 系統解析出:稽核日期、稽核單位、稽核方法+每方法查核指引、範圍敘述 → 預覽頁以「行程表單預填」形式呈現,user 補勾查核控制項與受評對象(docx 文字對不到系統 uuid 的部分)→ 確認 → 行程與稽核人員直接建進 AP,受評控制項/受評對象 tab 自動推導回填。
| 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 寫入路徑天然支援多筆、不需改。
複用既有三段式管線(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)
| 層 | 新增 | 說明 |
|---|---|---|
| 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 抽查結論):
_ap_detail 輸出)≠ _TaskItem 輸入 shape,confirm 內需做 detail→payload 轉換;全量覆寫會重生 task/activity uuid,audit_planning 階段尚未派 job 故無害(implementation plan 展開)。add_ap_party() 對同名 party 會 ConflictError(非 idempotent), 若在 confirm transaction 內爆會整筆 rollback。策略:preview 階段先按名稱比對既有 parties 給配對建議;confirm 時同名一律視為配對既有 party,不重複建立。比照 SSP docx 匯入「app service 層強制、fail-closed」慣例。角色術語區分清楚: 參與者角色(權限檢查用)是 participant.role ∈ {manager, auditor}; assessor 是 OSCAL party 的 role_id(§3 表格用法),兩者不同概念。
AssessmentPlanAppService._resolve_ap_and_check_auditor() (含 assert_round_phase({"audit_planning"})),但它是私有方法——實作時把 gate 提為 public helper 供 ApDocxImportAppService 復用(不跨 service 呼叫私有方法、不複刻條件)set_tasks 自動繼承(雙重保險)ap_uid 不存在 → 404(既有 GRC_AP_NOT_FOUND)common/code/grc_error_code.py 新增(命名 GRC_<HTTP><序號>), 語意 mirror SSP docx 匯入既有行為:
GRC_DOCX_FILE_TOO_LARGE 訊息寫 10MB 與常數 20MB 不符,屬既有 bug 不在本 FR 修GRC_DOCX_PARSE_JOB_EXPIRED / NOT_AWAITING 語意)class ApReportParserAdapter(ABC):
version: str # "airasia-cmmc-l1-v1"
def detect(self, doc) -> bool # 章節結構特徵偵測(編號章節「依據/目的/稽核…」標題)
def parse(self, doc) -> ParsedApReportdetect(),第一個命中者解析;全 miss → 回 400「格式無法辨識」+ 已支援版本清單。preview response 帶 adapter_version 供 FE 顯示。ParsedApReport 是版本無關的中間結構,行程為清單:
tasks: list[ParsedApTask],每筆含 title / date / methods[] / method_guidances{} / scope_textlen(tasks) == 1;對應/組 payload 邏輯只依賴此結構, 新版本格式(多場次行程表)只加 adapter 不動主流程。風格完全沿用專案(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.vueProgressSpinner (比照「先開 dialog 再載入」慣例)Steps 用 :active-step(quirk 註記);draft 存 localStorage (mirror useSspDocxDraft,key by parse_uid,防 TTL 過期丟編輯)ap_docx_parse_jobs 新表