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 目標樣板)
稽核人員(auditor)或 PM 在稽核執行頁 (/project/projects/:id/round/:roundUid/audit,RoundAuditReviewView.vue) 上傳顧問的逐條檢查紀錄 xlsx,系統解析成 AO 判定+觀察+佐證連結, user 預覽、修改、確認後批次寫入本輪 AR。
User 拍板決策(2026-07-03,詳 analysis §5):
FindingState enum 加 NOT_APPLICABLE(方案 A,jedi_oscal_v2 套件異動已核准)明確不做:
job_evidences/surveys;配不到列給人工)| 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_pairs(ao_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_risk+link_risk_findings。 批次在 app service 內迴圈呼叫上述既有方法,單一 @transaction(全成全敗,已驗證 @transaction 可重入);不繞過任何守門。
重複匯入語意:finding 判定=覆寫;observation=累加;risk 防重:同 control、 匯入來源(props 標記)、status=open 的既有風險 → 跳過再建。preview 頂部顯示 「本輪已有 N 個控制項判定將被覆寫」(控制層計數)。
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_id+org_unit_id 兩欄(曾有漏 org_unit_id 導致 INSERT 500 前例) |
| 套件 | jedi_oscal_v2 單一套件、單次異動,範圍三件事(spec review 校正):① FindingState 加 NOT_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 校正):
finalize_assessment(auditing→remediation 的 finalize, assessment_result_app_service.py;close_round 只檢查 POA&M 全 closed),掃描條件 token 'not-satisfied' → N/A 用新 token 天然不會產 POA&M,此處不需改finalize_assessment 的 stats["pending"] > 0 前置(套件 na 桶加好即 自動正確);FE judgedCount = met + not_met 是否計入 na(應計入=已判定)verdictOptions 加第四選項+i18n(verdict_not_applicable)+統計卡顯示 na 桶_check_auditor 是 AssessmentResultAppService 私有方法 — 比照 FR-043 的做法 把 gate 提為 public helper 供 ArImportAppService 復用,不複製第二份auditing 可 parse/confirm(assert_round_phase({"auditing"}), 匯入走既有寫入路徑二次繼承)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)。
配對母體=該控制項各 AO 的 jobs → job_evidences(description+ upload_files.file_name)+ surveys,去重邏輯同 FE obsEvidenceOptions(同源同語意)。
正規化:一格多名先拆(換行/逗號/頓號)→ 去括號註記 → 去副檔名 → 去空白標點 → lower-case;「(同N)」交叉引用解析為第 N 列的佐證集合。
| 級別 | 規則 | preview 行為 |
|---|---|---|
| 1 精確 | 正規化後全等 | ✅ 自動勾選 |
| 2 模糊 | token 重疊/編輯距離達門檻 | ☑️ 自動勾選+「模糊配對」徽章(一鍵取消) |
| 3 無配對 | 未命中 | 灰列原文名;user 可從下拉手動勾;原文名一律保留在 observation description 內不丟失 |
原則:寧漏配(人工補)勿錯配;門檻與案例以樣本 88 個名稱做 fixture 測試調校。
「框架知識」與「樣板長相」是兩個獨立變動軸(同框架多顧問樣板/同顧問多框架),拆三層:
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 批次寫入)
_METHOD_KEYWORDS 自 ap_report_parser/ 上收到共用模組 (如 app/grc/service/import_adapter/),AP docx registry 與 AR xlsx registry 為同 base 兩個 instance;FR-043 行為不變(其測試為 regression 保護)not_applicable 第四選項沿用專案風格與 FR-043 wizard 模式:
RoundAuditReviewView.vue header「匯入稽核紀錄」按鈕(stage=auditing+ auditor/manager+findings 已建立時顯示;注意現有 canEdit 只看 stage, participant role 需另取),開 ArImportDialog.vue 三步 Dialog wizardlink_risk_findings 已連(finalize 不被 GRC_RISK_NO_FINDING_LINKED 擋)、重複匯入 risk 防重不重複建、 not_applicable token 不產 POA&M(finalize_assessment 掃描驗證)、stats na 桶本節記錄「設計 → 實作 → 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 回傳 meta→report_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_uid 存 ev.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 未做。