FR-044 稽核紀錄 xlsx 匯入(AR import)— 設計(SD)

Date: 2026-07-03 · Branch: feature/FR-044 前置研究(xlsx 實測、系統 AR 結構、跨框架判定狀態、決策與被排除選項)見 docs/analysis/2026-07-03-ar-import-verdict-mapping.md 姊妹作:FR-043(AP docx 匯入)— 管線與 registry 同族,本 FR 將共用基建泛化 樣本文件:亞航-CMMC L1內部稽核報告附件-稽核紀錄.xlsx(v1 目標樣板)

1. 需求

稽核人員(auditor)或 PM 在稽核執行頁 (/project/projects/:id/round/:roundUid/auditRoundAuditReviewView.vue上傳顧問的逐條檢查紀錄 xlsx,系統解析成 AO 判定+觀察+佐證連結, user 預覽、修改、確認後批次寫入本輪 AR。

User 拍板決策(2026-07-03,詳 analysis §5)

  • 判定狀態用 CMMC 三態:MET / NOT MET / NOT APPLICABLE — FindingState enum 加 NOT_APPLICABLE(方案 A,jedi_oscal_v2 套件異動已核准)
  • observation 粒度:一 AO 列一筆
  • NOT MET:自動建風險、嚴重度預設「中」,影響/矯正建議稽核員後補

明確不做

  • 不自動上傳/建立佐證檔案(只連結既有 job_evidences/surveys;配不到列給人工)
  • 不從 xlsx 推風險嚴重度(文件無此資訊,一律預設中)
  • ISO 等其他框架樣板本期不實作(架構留位,見 §5)
  • xlsx 表頭 meta(專案名稱/受驗證型態/人數)不落欄位,preview 顯示供核對

2. 欄位對應(v1 亞航檢查紀錄 → AR)

xlsx 欄 系統落點 對應方式

粒度前提(spec review 校正):live 系統的判定粒度是控制項層 — 一控制一筆 finding(target_id = control_id),AO 細節放 observation (2026-06-17 user 拍板,assessment_result_app_service.py 註記)。 xlsx 是 AO 粒度(59 列)→ 匯入需做「AO 判定 → 控制層判定」聚合

xlsx 欄 系統落點 對應方式
索引號(AC.L1-b.1.i finding 所屬控制項(一控制一筆,target_id=control_id 系統 catalog 有兩代 id 格式(舊:NIST 式 AC.L1-3.1.1×17 條;新:CMMC 2.13 AG 式 AC.L1-b.1.i×15 條,FR-045 之後匯入的框架)→ 對照採 identity-first candidate resolution:每個 AG id 的候選 = [原樣] + AG_TO_NIST[原樣],取「存在於本輪 findings」者;同輪 identity 命中即不做 NIST 映射(不混用)。2026-07-04 STG 實測:v2.13 catalog 輪次被無條件 AG→NIST 轉換打成空交集、誤報框架不符(GRC_400095),此規則即該次修正。兩代格式皆為正式支援對象(user 拍板 2026-07-04),非過渡相容:同一份顧問 xlsx 對舊 NIST 式輪次走對照表、對新 AG 式輪次走 identity,測試兩代並列常備
Objectives [a]~[h] observation(AO 明細載體,一列一筆):description 以 [a] <AO 原文> 開頭 順序對齊:[a]=該控制項第 1 個 AO;分母與順序復用 canonical derive_ao_pairsao_derivation.py,勿另寫讀法);「xlsx 列數 = 系統 AO 數」不合者整控制項降級 preview 人工核對;part 順序穩定性列 pre-flight
稽核結果(逐 AO) 控制層 finding target_status_state聚合 逐列先映射 MET→met、NOT MET→not_met、N/A→not_applicable、空值→pending,再按控制項聚合:任一 not_met → not_met;全 not_applicable → not_applicable;含 pending → pending;其餘(全 met 或 met+N/A 混)→ met(CMMC 計分 N/A 視同 MET)。逐 AO 原始判定同時記在該列 observation(description 尾註「判定:MET」),不丟失明細
Status 實作狀態 同列 observation:description=AO+全文+判定尾註、methods[]=前綴關鍵詞比對 復用 FR-043 _METHOD_KEYWORDS(上收共用層,見 §5);比不出方法時 methods 留空、preview 可補
Evidence 佐證資料 observation relevant_evidence[](結構同稽核頁佐證下拉:{uid, file_uid, file_name, evidence_type…} 分級配對(見 §4)
表頭稽核日期 observation collected 2026年6月2日 → timestamptz;缺值 fallback 匯入當下。需擴充主專案 create_observation 加 optional collected 參數(現硬寫 now,屬「差一點就加參數 extend」)
—(聚合後 not_met 的控制項) risk:severity=、status=open、title={control_id} 未符合、description=彙整該控制 NOT MET 各 AO 的判定說明;必以 link_risk_findings 連回 finding(finalize 前置 GRC_RISK_NO_FINDING_LINKED 要求每 risk 至少連一 finding) preview 顯示「將建立中風險」徽章

寫入語意(全部走既有路徑):每控制項 = judge_finding(聚合後判定)+逐 AO 列 create_observation(軟連結 finding);not_met 控制項加 add_risklink_risk_findings。 批次在 app service 內迴圈呼叫上述既有方法,單一 @transaction(全成全敗,已驗證 @transaction 可重入);不繞過任何守門。

重複匯入語意:finding 判定=覆寫;observation=累加;risk 防重:同 control、 匯入來源(props 標記)、status=open 的既有風險 → 跳過再建。preview 頂部顯示 「本輪已有 N 個控制項判定將被覆寫」(控制層計數)。

3. 架構 — mirror FR-043 三段式管線

POST   /audit-round/<round_uid>/ar-imports/parse   上傳+解析 → {parse_uid, status}
GET    /ar-import/<parse_uid>                       預覽
POST   /ar-import/<parse_uid>/confirm               確認批次寫入
DELETE /ar-import/<parse_uid>                       廢棄
新增 說明
route api/project/routes/ar_import_route.py project blueprint,multipart
serializer api/project/serializers/ar_import.py Parse/Preview/Confirm schema
app service app/grc/service/ar_import_app_service.py 檔案驗證(.xlsx、10MB 比照 _EXCEL_MAX_SIZE)→ parse job → adapter → AO 對齊+佐證配對 → 持久化 parsed_result
adapter 共用 import 基建(§5)+ airasia_cmmc_l1_ar_v1 openpyxl/pandas 讀表
domain/infra ar_xlsx_parse_jobs 表(oscal schema、24h TTL、tenant-scoped,照 ssp_excel_parse_job 同形)+ entity/repo/DI SQL migration 走鐵則;tenant-scoped 表必含 tenant_idorg_unit_id 兩欄(曾有漏 org_unit_id 導致 INSERT 500 前例)
套件 jedi_oscal_v2 單一套件、單次異動,範圍三件事(spec review 校正):① FindingStateNOT_APPLICABLE = "not_applicable";② ArFindingMatrixService_STATE_TO_TOKEN / _TOKEN_TO_STATE 加對應,token 定為 'not-applicable'(OSCAL objective-status 無此值,比照 pending 前例存 product token);③ list_findings stats 加 na 桶(stats 產生在套件內,非主專案) dev 走 path dependency,發版等 user 明示

not_applicable 的漣漪(主專案側,一次盤齊;POA&M 段依 spec review 校正)

  • POA&M 產生點是 finalize_assessment(auditing→remediation 的 finalize, assessment_result_app_service.py;close_round 只檢查 POA&M 全 closed),掃描條件 token 'not-satisfied'N/A 用新 token 天然不會產 POA&M,此處不需改
  • 要確認的兩處:finalize_assessmentstats["pending"] > 0 前置(套件 na 桶加好即 自動正確);FE judgedCount = met + not_met 是否計入 na(應計入=已判定)
  • FE verdictOptions 加第四選項+i18n(verdict_not_applicable)+統計卡顯示 na 桶

權限與階段

  • parse / confirm / discard:participant role ∈ {auditor, manager}。 _check_auditorAssessmentResultAppService 私有方法 — 比照 FR-043 的做法 把 gate 提為 public helperArImportAppService 復用,不複製第二份
  • 僅 round stage=auditing 可 parse/confirm(assert_round_phase({"auditing"}), 匯入走既有寫入路徑二次繼承)
  • 前提:本輪已 start-auditing(findings 框架存在);未啟動時回 412(import 專屬新檢查, error code 依 GRC_412<序號> 命名;區別於既有 _require_round_ar_result 的 404)

錯誤碼

grc_error_code.py 新增,語意 mirror FR-043:格式無法辨識(400,附支援樣板清單)、 框架不符(400,「檔案框架 X ≠ 本輪框架 Y」)、AO 數不一致(不擋整檔,preview 降級標示)、檔案過大(400)、parse job 不存在(404)/過期(412)/非 awaiting(412)。

4. 佐證分級配對

配對母體=該控制項各 AO 的 jobs → job_evidencesdescriptionupload_files.file_name)+ surveys,去重邏輯同 FE obsEvidenceOptions(同源同語意)。

正規化:一格多名先拆(換行/逗號/頓號)→ 去括號註記 → 去副檔名 → 去空白標點 → lower-case;「(同N)」交叉引用解析為第 N 列的佐證集合。

級別 規則 preview 行為
1 精確 正規化後全等 ✅ 自動勾選
2 模糊 token 重疊/編輯距離達門檻 ☑️ 自動勾選+「模糊配對」徽章(一鍵取消)
3 無配對 未命中 灰列原文名;user 可從下拉手動勾;原文名一律保留在 observation description 內不丟失

原則:寧漏配(人工補)勿錯配;門檻與案例以樣本 88 個名稱做 fixture 測試調校。

5. 多框架 × 多樣板:三層架構(含 FR-043 基建泛化)

「框架知識」與「樣板長相」是兩個獨立變動軸(同框架多顧問樣板/同顧問多框架),拆三層:

Template adapter(每顧問樣板一支;本期 airasia_cmmc_l1_ar_v1)
  detect(file) → 表頭/欄位特徵;parse(file) → ParsedArRecord(framework 無關:
  rows[{control_ref原文, objective_seq, verdict原字串, method_text, evidence_names[]}]+meta)
  宣告所屬 framework
        ↓
Framework profile(每框架一份設定;本期 CMMC)
  control-id 對照表(AG b.1.i ↔ catalog id)
  verdict 詞彙 → 核心四態 mapping(MET→met…;原始 verdict 字串存 finding props)
  AO 對齊策略(CMMC=逐 AO;ISO=控制項層判定,走 ao_derivation 控制層 fallback,架構已有位)
        ↓
共用 import core(parse job/AO 對齊/佐證配對/preview/confirm 批次寫入)
  • 框架一致性檢查:adapter 宣告的 framework ≠ 本輪 AP 綁定框架 → 400(防 ISO 檔匯進 CMMC 輪次)
  • FR-043 基建泛化(本 FR 內完成,屬既有 code 的針對性整併非 unrelated refactor): adapter registry base 與 _METHOD_KEYWORDSap_report_parser/ 上收到共用模組 (如 app/grc/service/import_adapter/),AP docx registry 與 AR xlsx registry 為同 base 兩個 instance;FR-043 行為不變(其測試為 regression 保護)
  • 框架 verdict set 的 system_menu 化(FE 判定選項依框架驅動)留待第二個框架上線時做, 本期 FE 只加 not_applicable 第四選項

6. FE 設計

沿用專案風格與 FR-043 wizard 模式:

  • 入口:RoundAuditReviewView.vue header「匯入稽核紀錄」按鈕(stage=auditing+ auditor/manager+findings 已建立時顯示;注意現有 canEdit 只看 stage, participant role 需另取),開 ArImportDialog.vue 三步 Dialog wizard
  • Step 1 上傳(.xlsx、10MB)→ parse → spinner(先開 dialog 再載入慣例)
  • Step 2 預覽與修改:按控制項分組(可摺疊),每 AO 列=判定 SelectButton(四態,可改) +觀察文字(可編)+佐證 chips(分級徽章+下拉補勾)+ NOT MET 列「將建立中風險」 徽章;頂部:解析摘要(樣板版本/框架/覆寫警示/AO 不一致降級清單); draft 存 localStorage(key by parse_uid)
  • Step 3 完成:confirm → toast+摘要(判定 N 筆/觀察 N 筆/風險 N 筆/佐證連結 N 筆) → refresh findings/stats
  • 關閉前有未確認編輯二次確認;重複匯入允許(覆寫判定警示同 Step 2 頂部)

7. 測試重點(Phase 4 由 feature-test-planner 展開)

  • adapter:樣本 59 列 snapshot(fixture 入測試專案);AO 數不一致降級;「(同N)」解析; 一格多名拆分
  • 佐證配對:88 名稱 fixture 的分級判定(精確/模糊/無配對各取代表案例,含 l/I 打字錯)
  • 聚合規則:全 met/任一 not_met/全 na/met+na 混(→met)/含 pending 五種組合
  • app service:權限矩陣×parse/preview/confirm、stage 守門(非 auditing 412)、 double-confirm 412、TTL 412、confirm 全成全敗(transaction rollback)、 聚合 not_met 建風險 severity=中+link_risk_findings 已連(finalize 不被 GRC_RISK_NO_FINDING_LINKED 擋)、重複匯入 risk 防重不重複建not_applicable token 不產 POA&M(finalize_assessment 掃描驗證)、stats na 桶
  • FR-043 regression:registry 泛化後 AP docx 匯入測試全綠
  • FE e2e(compliance-manager-test repo):上傳→改判定→確認→稽核頁狀態更新

8. 未來擴充(本期不做)

  • ISO 27001 檢查紀錄樣板(框架 profile+控制項層判定;verdict set system_menu 化同期)
  • PCI「Not Tested」獨立統計桶評估(analysis §7 反悔條件)
  • AR 匯出(系統資料反向產檢查紀錄表)

9. 實作校正與 STG 修正(收尾 2026-07-04,✓ shipped)

本節記錄「設計 → 實作 → STG 實測」過程中與上文的差異與補強。上文 §1–§8 是設計初稿, 下列為實作實況與五個 STG 實測 root cause 修正。決策軌跡見 docs/analysis/2026-07-04-fr044-stg-bugfix-decisions.md

已建交付:Phase A–F 全數落地(純函式 → adapter → framework profile → app service → route/serializer/DI → FE 三步 wizard + 佐證分級配對 UI)。套件 jedi-oscal-v2 2.2.0 已推 Nexus(含 FindingState.NOT_APPLICABLE + matrix na 桶;該 release 同時含 FR-045 v2.13 PDF parser,user 拍板一起出)。DEV + STG DB 已套 ar_xlsx_parse_jobs migration。

五個 STG 實測修正(皆與上文設計有出入,已修正並補測)

# 症狀(STG) root cause 修法(採用) 被排除選項 commit
1 v2.13 catalog 輪次匯入誤報框架不符 GRC_400095 系統 catalog 有兩代 control_id 格式共存(NIST AC.L1-3.1.1×17 / AG AC.L1-b.1.i×15);原無條件 AG_TO_NIST 轉換對 v2.13 交集為空 identity-first candidate resolution(見 §2 已更新);兩代皆正式支援;v2.13 下 PE.L1-b.1.ix 不再一對三拆分(AO 數同 xlsx) 「只支援一代 / 強制轉一代」(會逼既有輪次洗資料) 72d377fb
2 preview「解析出 0 項」,GET /ar-import/<uid>data:null common/util/response_util.py:return_response 對 payload 頂層含 meta key 有歷史魔法分支,拆成 {data, meta} 丟棄 controls 等欄位(AP docx 無 meta key 故沒事) app service 回傳 metareport_meta(+ serializer + FE 對齊);陷阱登記 docs/claude/domain-capabilities.md 「改 return_response 本體」(全專案多 route 依賴該分頁 meta 分支,全域 breaking) 43c3c4d8 / FE 16b7931
3 佐證勾選了但稽核頁顯示不出已連結 佐證池跨控制汙染:同檔名證據被上傳到多個控制,走 control-tree 的池把別控制同名版本吸進來、matcher 照名字誤配 → 存了不屬於該控制的 evidence uid 用權威 public.workflow_execution_control_mapping 二次 scope(只留 wf 真正 map 到該控制的證據);file_id/drive_url/file_size shape 補齊 「改 control-tree 產生邏輯」(會牽動稽核頁 obsEvidenceOptions,風險大) 766d8a61
4 點佐證預覽「查無檔案」 匯入把數字 file_id 誤存進 file_uid,但預覽端點 PDF_FILE_PREVIEW/{uid} 需 upload_files 字串 uid file_uidev.file.uid(字串)、file_id 另存數字、file_size.size 85b9a1e9
5 重匯一直疊重複觀察 observation 設計為累加(給稽核員長期加),但**匯入重傳應「最新覆蓋」 parse_job import_summary 追蹤本次 observation_uids,confirm 前 best-effort 清掉上一批匯入來源**觀察(手動觀察不動) 「掃描 findings 全清」(會誤刪手動觀察);「加 observation source 欄位」(schema 異動,過重) f58701ad

§2 line 52「重複匯入語意 observation=累加」已被 #5 取代 → 正確語意為「匯入觀察=最新覆蓋、手動觀察=累加」。

佐證分級配對(§4 補強):池組裝=build_control_tree_by_ssp_id → 控制 AO → jobs → get_job_evidences_by_job_execution_uid + surveys,再經 #3 的 wf 權威 scope 去汙染;matcher (ar_import/evidence_matcher.py)分級 exact/fuzzy/none + (同N) marker;FE ArImportDialog 渲染分級徽章 + 自動勾選 + confirm 送 relevant_evidence「無配對從完整池手動下拉補勾」本期未做(列 follow-up)。

已知 follow-up:POC DB migration 未套;所有 git commit 未 push(等 user);佐證 manual-from-full-pool 未做。