---
title: "稽核結果逐評估目標判定重做 — 設計定案 (FR-142)"
brand: "Guidant AI · **FR-142** 稽核結果重做"
eyebrow: "FR-142 · 設計定案 · 2026-10-16"
h1: "稽核結果重做：設計定案"
subtitle: "左樹三層＋控制項總覽、一次一條評估目標、稽核報告第 7 章、Excel 只關聯檔案、報告 Word／PDF"
lede: "稽核階段判定頁整個換掉。左樹控制族群→控制項→評估目標三層；點控制項看控制項總覽，點評估目標依序填佐證、其他佐證、稽核發現、稽核結果、風險（只在不符合時出現）；控制項結論由評估目標自動推算。「稽核報告」頁籤只寫第 7 章，預覽與匯出才組完整報告。Excel 是稽核計畫那份往後加結果欄，佐證只關聯檔案。結案整份鎖定、佐證凍結版本。資料照 OSCAL 評估結果標準欄位存，並修 jedi-oscal-v2 三處匯出不合規。本文件寫定案規則、為什麼這樣定、踩到會怎樣。"
chips: [
  {text: "D1～D12 全部定案（2026-10-16）", kind: ok},
  {text: "六棒七張卡，棒 1 套件修正先做", kind: accent},
  {text: "動兩支套件：jedi-oscal-v2、jedi-compliance-audit", kind: crit},
  {text: "陷阱：佐證候選要走 workflow_execution_control_mapping 二次 scope", kind: crit},
  {text: "POA&M 另開 FR，本案只保交接點", kind: warn}
]
footer: "FR-142 · 設計文件 · 2026-10-16 · 討論稿：[discussion.md](discussion.md)　雛形：[prototype/index.html](prototype/index.html) · 沿革見討論稿與 git log"
---

> 🔴 **這份是設計決策，不是現況。** 內文停在 2026-10-16 定稿當時，實作改動不回頭改寫這裡。

> 狀態：**設計定案（2026-10-16）**｜母卡 CM-2818｜雛形卡 CM-2819（六輪，Done）｜討論稿：[`discussion.md`](discussion.md)｜雛形：[`prototype/index.html`](prototype/index.html)｜範例 Excel：[`prototype/sample/FR-142-稽核結果範例.xlsx`](prototype/sample/FR-142-稽核結果範例.xlsx)

> 🔴 **實作在 FR-142 worktree**：BE `~/Projects/Billows/Audit-Manager/wt-FR-142/compliance-manager-be`、FE `…/wt-FR-142/compliance-manager-fe`、套件 `~/Projects/Jedicogy/module/wt-FR-142/jedi-python-package`，皆 branch `feature/FR-142`（自 FR-139 `b4048bd5e`）；BE 8003 埠、FE 5181、**專用庫 `guidant_ai_fr142`**（從 DEV 複製，DEV 不碰；套件測試帶 `JEDI_TEST_ENV` 指 worktree `.env`）；合回 FR-139 時 DEV 補套套件 013。文件在主工作樹維護。

> 檔案路徑與行號以 2026-10-16 為準，查不到時以函式名重新定位。路徑慣例：BE 路徑相對 `compliance-manager-be/`；`pkg/<套件>/` 代表 `~/Projects/Jedicogy/module/jedi-python-package/<套件>/<套件底線名>/`；FE 路徑相對 `compliance-manager-fe/`。三個 repo 都在 `feature/FR-139`（FR-141 已併入）。

名詞：**評估目標**＝AO（assessment objective），控制項底下的檢查項目，全系統用 part_id 當鍵（`<控制項代碼>_obj.N`；控制項沒有 AO 時退回控制項代碼）；**AR**＝稽核結果（assessment results）；**finding**＝一條評估目標的判定；**observation**＝佐證紀錄；**risk／remediation**＝風險／改善建議；**attestation**＝稽核人員署名的文字陳述（第 7 章）；**稽核次**＝FR-140 的概念，一輪內每次稽核一筆，各自一份 AP 與一筆 AR result。

## 變更紀錄 {#changelog nav="變更紀錄"}

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-16 | v1：D1～D12 定案；資料模型與 OSCAL 落點、API、畫面、Excel 契約、報告、六棒七張卡、端到端驗收 | 母卡 CM-2818 |

---

## 分工概述（30 秒版） {#overview nav="概述"}

整案一句話：**判定頁換成左樹到評估目標、中間一次一條評估目標（佐證→稽核發現→稽核結果→風險），控制項結論自動推算；稽核報告頁籤只寫第 7 章；Excel 延伸稽核計畫、佐證只關聯檔案；結案整份鎖定。**

| 卡 | 做什麼 | 產出 | 完成怎麼判定（決策者親手做什麼、看到什麼） |
|---|---|---|---|
| **1a** | 套件稽核結果存法改成 OSCAL 標準 | jedi-oscal-v2 migration、finding／risk／remediation 修正、匯出合規 | DEV 判一條不符合、填風險後匯出 OSCAL JSON，用官方 schema 驗證通過 |
| **1b** | 評估目標層判定矩陣、佐證與第 7 章存取、控制項結論規則一支 | 兩支套件 service | 開一個新稽核次，DB 該次 finding 數是 0；判三條後是 3 |
| **2** | 後端讀寫 API、結案守門與鎖定 | 主專案新 app service、route | Swagger 存三條評估目標讀回一致；有未判定時按結案被擋；結案後再存被拒 |
| **3** | 前端新頁 | FE 新檔、舊頁退役 | 對照雛形逐頁操作：左樹、控制項總覽、評估目標頁、稽核報告、預覽 |
| **4** | Excel 匯入匯出 | workbook 結果欄、檔名比對、預覽 | 匯出 Excel 改兩格、加一個檔名匯回，預覽標出、確認後畫面更新 |
| **5** | 稽核報告 Word／PDF | docx 範本、匯出服務 | Word 開檔看到 1～7 章與橫式附件，判定是中文 |
| **6** | FR-140 對齊與選單 | 縮範圍改評估目標層、選單 seed | 第 2 次內稽的新稽核計畫只剩不符合評估目標所屬控制項 |

依賴：1a → 1b →（2、4、5、6 可平行）；3 等 2。

```{.mermaid cap="圖 1 — 卡片依賴"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
    A1[1a 套件 OSCAL 修正] --> B1[1b 評估目標層矩陣<br/>observation／attestation]
    B1 --> C2[2 BE API]
    B1 --> C4[4 Excel]
    B1 --> C5[5 報告]
    B1 --> C6[6 FR-140 對齊]
    C2 --> C3[3 FE 新頁]
    C2 --> C5
```

---

## 決策定案 {#decisions nav="決策"}

::: {.callout .decided}
**D1 判定值域＝符合／不符合／不適用三值，加「未判定」當初始狀態但不算判定**

- 畫面與 Excel 四個選項都看得到；Excel 空白＝未判定；送出結案時擋「還有未判定」。
- OSCAL 存法：符合／不符合 → `finding.target.status.state`＝`satisfied`／`not-satisfied`；不適用 → `finding.target.implementation-status.state`＝`not-applicable`，`status.state` 同時填 `satisfied`、`reason`＝`other` 讓匯出合規；未判定＝**不建 finding**，不落假值。
- 清掉套件自創的 `pending`／`not-applicable` 狀態 token 與 `target_status={"product_state":…}`。

**為什麼**：稽核人員現場一定有「還沒查到」的狀態，不能逼他先選；但 OSCAL 的 `status.state` 是封閉值域（只有 satisfied／not-satisfied），「未判定」「不適用」沒有對應值，硬塞進去匯出就不合規。`implementation-status` 官方語意是「控制項實作到什麼程度」，`not-applicable` 是它的正規值，放這裡最貼。

**被排除**：維持現狀四值全進 `status.state`（匯出不合規）；「不適用」改成不建 finding（報告附件要印 N/A，沒 finding 印不出來）。
:::

::: {.callout .decided}
**D2 風險在評估目標上就地填，只有判不符合時出現；不設風險總覽頁籤**

- 評估目標頁判「不符合」時展開風險區：風險等級（無／低／中／高）、風險說明、改善建議。判符合時收起，留「＋ 新增風險」連結給「符合但有疑慮」的少數情況。
- 存法：每條填了風險的評估目標自動生一筆 `risk`（`status`＝`open`），用 `assessment_finding_risks` 連結表綁到該 finding；改善建議存 `risk.remediations[]` 一筆 `lifecycle`＝`recommendation`。
- 判準「有風險」＝風險等級或風險說明任一有值；有風險的評估目標，所屬控制項判不符合（D5）。

**為什麼**：顧問習慣一列填完；「先建風險再勾評估目標」是現在頁面難用的主因。大多數評估目標是符合，風險區收起畫面短一半。

**被排除**：維持「風險獨立建、再勾」為主路徑（難用）；風險總覽頁籤與合併多筆（雛形第一版有，決策者看過後拿掉：風險已逐評估目標呈現、報告附件也逐列印，合併功能 Excel 無法來回）。
:::

::: {.callout .decided}
**D3 「稽核報告」頁籤只寫第 7 章；1～6 章與附件只在預覽與匯出時組出完整報告**

- 頁籤只有一個大編輯器，像在 Word 打第 7 章一樣寫，想分幾點分幾點。
- 「帶入不符合事項」把判不符合的評估目標依「控制項代碼 標題 [評估目標]：稽核發現／風險（等級）／改善建議」格式接在現有內容後面；無不符合時提示「無不符合事項」。
- 「預覽報告」開全螢幕：第 1～6 章來自稽核計畫（FR-141 的 `ap_terms_and_conditions` 等），第 7 章為本編輯器內容，附件為逐評估目標結果表（D11）。
- 存法：整段存 `ar_results.attestations[]` 一筆，`parts[]` 一個 part（name 固定 `audit-conclusion`），`responsible-parties` 帶稽核人員。

**為什麼**：1～6 章與附件放在頁籤裡頁面太長，使用者會誤以為沒地方寫；第 7 章才是這一頁要做的事。OSCAL 的 `attestation` 官方語意是「評估者寫的文字陳述」，比 `result.description` 或 `remarks` 貼切。

**被排除**：固定三個子節（看不懂）；左右分欄唯讀 1～6 章（難看）；1～6 章與附件全攤在頁籤裡（太長）。
:::

::: {.callout .decided}
**D4 舊資料不遷移，只影響新開的稽核次**

- 稽核次開始時若該次 AR result 已有 finding（舊控制項層）就維持舊讀法；新開的稽核次一律 AO 層。判斷依據：finding 的 `target_id` 是否含 `_obj.`。
- 舊頁 `RoundAuditReviewView.vue` 退役後，既有控制項層稽核次用新頁唯讀顯示（每控制項一條合成卡），不開放編輯。

**為什麼**：舊輪資料只有 DEV 與少數測試專案；把控制項判定拆到每條 AO 要猜，猜錯比不猜糟。FR-140 已有「每次稽核一筆 result」的分界點可借用。
:::

::: {.callout .decided}
**D5 控制項結論不存欄位，由 AO 即時推導；三處各自實作收斂成套件一支**

- 規則：任一 AO 不符合或掛風險 → 不符合；全部 AO 不適用 → 不適用；還有未判定 → 未判定；其餘 → 符合。
- 落點：jedi-compliance-audit 一支純函式（`ar_import/aggregation.py` 的 `aggregate_verdict` 擴充「掛風險」條件），FE 不再自己算、FR-140 縮範圍規則改呼叫它。
- 統計（控制項數／符合／不符合／不適用／未判定／風險數）由 BE 回傳，FE 只顯示。

**為什麼**：現在 FE（`RoundAuditReviewView.vue:411`）、BE 匯入（`aggregation.py:10`）、縮範圍（`pass_scope_rule.py:12`）三套規則各寫一次，「掛風險也算不符合」是新條件，三處改必漏。OSCAL AR 本來就沒有控制項層判定欄，存了反而要維護一致性。
:::

::: {.callout .decided}
**D6 左樹到評估目標三層，點控制項看控制項總覽**

- 左樹：控制族群 → 控制項 → 評估目標，與專案規劃頁同（FR-141 稽核計畫只到控制項，是 FR-141 做淺了）。
- 燈號放最前面，顏色加形狀區分四態，色弱可辨；hover 顯示「稽核結果：X」與全文；樹上方一列圖例。評估目標文字最多兩行。
- 點控制項：中間顯示控制項總覽（結論、依據、計數、逐評估目標結果表）；點表格列進評估目標頁；評估目標頁頁首有返回箭頭。
- 中間頁首「控制項結論」保留文字膠囊。

**為什麼**：右側膠囊把評估目標與控制項說明擠到被截斷；中間一次只放一條評估目標，資訊才不會擠在一起；點控制項要看得到控制項結果。

**被排除**：膠囊放右側（擠）；文字無限換行（樹太長）；點控制項直接跳第一條評估目標（看不到控制項結果）。
:::

::: {.callout .decided}
**D7 評估目標頁照稽核人員實際做事的順序：佐證 → 其他佐證 → 稽核發現 → 稽核結果 → 風險**

- 欄位：佐證（清單）、其他佐證（訪談、現場觀察等文字）、稽核發現、稽核結果＊（四選一）、風險（D2）。
- 「其他佐證」與佐證清單分開；拿掉檔案旁的備註框——要指出檔案哪一頁寫在稽核發現。

**為什麼**：稽核人員是先看證據、寫下看到什麼、最後才判過不過；第一版把判定放最上面等於先下結論。備註框與「其他佐證」太像，使用者分不出來。
:::

::: {.callout .decided}
**D8 佐證＝這條評估目標對到的任務佐證（檔案、連結、問卷），一張平的清單，預設全部採用，取消勾選＝不採用**

- 來源：`public.workflow_execution_control_mapping` 對到該控制項／評估目標的任務，底下 `compliance.job_evidences` 類型 FILE／LINK／SURVEY 的項目，合成一張清單、同筆去重；不分任務顯示，每列後面標上傳者與日期。
- 只有檔案、連結、問卷可採用；任務討論（`job_execution_comments`）與報告類型不列。
- 「加入其他佐證」：從整個專案其他任務的佐證挑；「現場上傳」：稽核人員現場拍的照片，存為本次稽核的佐證，清單上標「現場」。
- **「採用」的語意**：記在稽核結果這邊，表示「本評估目標判定採用這些證據」，不改任務端資料、不標有效無效；同一份證據可被多條評估目標各自採用。
- 判「符合」但沒有任何佐證：提示「未附佐證」，不擋。

**為什麼**：舊頁 `RoundAuditReviewView` 是「上面頁籤瀏覽、下面觀察表單另外多選關聯」兩塊；併成一張清單，看與勾在同一處。稽核人員不需要知道任務分組；討論是溝通紀錄、不是證據，有用的內容應寫進稽核發現。

**被排除**：自編證據池（第一版，未對應系統資料）；依任務分組（看不出需要）；討論可勾選採用（決策者裁：只供參考，後再裁拿掉）；回到舊頁頁籤式（一條評估目標佐證通常三五筆，分頁籤要一直切）。
:::

::: {.callout .decided}
**D9 Excel：與稽核計畫同一份四頁，計畫欄灰底唯讀，後接結果欄；佐證只關聯檔案**

- 結果欄：稽核結果、佐證檔案、其他佐證、稽核發現、風險等級、風險說明、改善建議。
- **佐證檔案**一行一個檔名，只含檔案與現場上傳；刪一行＝不採用、加一行＝採用。連結與問卷只在畫面設定，Excel 不出現也不讀。
- 「可選清單」頁新增「佐證檔案」欄，列出專案內可關聯的檔名供複製。
- 匯入時檔名比對專案檔案；對不到或同名多筆時，預覽頁就地處理：對應到某個檔案／改為其他佐證文字／略過。

**為什麼**：稽核人員不會照「類型：名稱」格式寫；檔名是唯一實際會出現在 Excel 的證據形式，也是亞航附件的寫法。
:::

::: {.callout .decided}
**D10 結案＝本次稽核整份鎖定**

- 結案守門三條：尚有評估目標未判定、不符合項目未填稽核發現（或有風險等級未填風險說明）、第 7 章未填寫。任一成立即擋，列出原因。
- 通過後再確認一次；結案後判定、風險、佐證、第 7 章全部唯讀，畫面顯示「本次稽核已於 YYYY-MM-DD 結案，內容不可修改」。
- 結案時凍結每筆採用佐證當下的版本（檔案記雜湊值與版本；連結記網址；問卷記填答快照識別），之後受稽單位更換檔案不影響稽核紀錄。
- 要改只能在下一次稽核重做（FR-140 流程）。

**為什麼**：決策者原話「過了就過了，那不是結案報告，不然一直改等等會有爭議」。結案後產生 POA&M，若仍可改判定會與 POA&M 對不上。
:::

::: {.callout .decided}
**D11 報告附件橫式 8 欄、判定印中文**

- 正文 1～7 章直式；附件獨立一節橫式（docx 分節換方向）。
- 8 欄：序號、索引號、實務要求與執行說明（合一欄）、控制目標、查核方法：指引、佐證資料、稽核結果、稽核發現與風險（合一欄）。
- 稽核結果印「符合／不符合／不適用／未判定」，不印 MET。不符合列淡紅底。
- 預覽與匯出可取消「含稽核發現與風險」，取消後為亞航原表 7 欄。

**為什麼**：10 欄直式 A4 每欄不到 2 公分不可讀；合併兩組欄後與亞航原表寬度相近。
:::

::: {.callout .decided}
**D12 表單文案用專業用語，不放說明句**

- 欄位、按鈕、提示訊息用表單慣用詞（佐證、稽核發現、稽核結果、風險等級、改善建議、選擇文件、上傳、結案、已儲存、匯入完成）。
- 不放 placeholder 範例句、欄位旁說明句、頁首說明列；驗證訊息寫成「尚有 N 項評估目標未判定」「不符合項目須填寫稽核發現」這類格式。
- 判定類 badge 與燈號全頁同一套樣式；不使用左側粗色邊框卡片（見 CLAUDE.md UI 禁令）。
:::

---

## 現況接入點 {#inventory nav="接入點"}

座標以 2026-10-16 為準，runner 開工先開檔核一次。**FR-141 已完整併入 `feature/FR-139`**（`git merge-base --is-ancestor` 成立），其頁面骨架、六支 `app/flow_control/service/ap_*.py`、docx 範本都在主工作樹。

### ① AR 資料模型（套件 jedi-oscal-v2，schema `oscal`，`pkg/jedi-oscal-v2/infra/model/ar/`）

| 表 | 現況 | 本案動作 |
|---|---|---|
| `assessment_results`（`oscal_ar.py:27`） | `uid`、`metadata_id`、`status`、`import_ap_id`、`local_definitions` | 不改 |
| `ar_results`（`oscal_ar_results.py:25`） | 每稽核次一筆（FR-140）；`reviewed_controls` NN、`attestations`／`assessment_log` JSONB **無任何 service 寫入** | 棒 1：加 attestation 寫入 service；`reviewed_controls.control-objective-selections` 寫入本次 AO 清單 |
| `assessment_findings`（`oscal_ar_finding.py:26`） | `target_type` 永遠 `objective-id` 但新輪 `target_id` 存 control_id；`target_status_state` 存自創 token（`pending`／`not-applicable`）；`target_status` JSONB 存 `{"product_state":…}`；**沒有** `target.status.reason`／`implementation-status`／`target.description` 欄 | 棒 1：每 AO 一筆、`target_id`＝part_id；`target_status` JSONB 改存 OSCAL 形狀 `{state, reason, remarks}`；加 `implementation_status` JSONB；`description` 存判定理由 |
| `assessment_observations`（`oscal_ar_observation.py:26`） | 欄位齊全（`methods` NN、`relevant_evidence`、`subjects`、`collected`）；**無 service**；finding→observation 只有 JSONB 軟參照 | 棒 1：加 observation service；每條有發現或佐證的 AO 一筆，`methods` 從 AP activity 的方法帶、`relevant_evidence[].description` 放佐證文字 |
| `assessment_risks`（`oscal_ar_risk.py:25`） | `characterizations` 被寫成 `{"severity":…}`（`assessment_risk_service.py:99-100`），**不是 OSCAL 陣列形狀** | 棒 1：改 `[{origin:{actors:[稽核人員]}, facets:[{name:'severity', system:'http://csrc.nist.gov/ns/oscal', value:'高'}]}]` |
| `assessment_finding_risks`（`oscal_ar_finding_risk.py:25`） | `link_findings` 只寫這張表；匯出讀 `finding.related_risks` JSONB，**沒同步**，service 建的關聯匯出會消失 | 棒 1：寫連結表時同步回填 JSONB（或匯出改讀連結表，二擇一，建議後者） |
| `assessment_remediations`（`oscal_ar_remediation.py`） | `lifecycle` 可空、註解寫 recommendation/required；官方必填且值域 recommendation／planned／completed | 棒 1：NOT NULL＋CHECK 三值；建議一律 `recommendation` |
| `ar_assessment_subjects` | AR 層受評對象 | 不改 |

### ② 套件服務與主專案端點

| 元件 | 現況 | 本案動作 |
|---|---|---|
| `pkg/jedi-oscal-v2/domain/service/ar/ar_finding_matrix_service.py:40-48` | `init_finding_matrix` 每個 id 一筆 finding、初始 `pending`；`_STATE_TO_TOKEN` 四值對照 | 棒 1：初始化**不建** finding（未判定＝無 finding）；upsert 改寫 OSCAL 形狀 |
| `pkg/jedi-oscal-v2/app/service/io/oscal_io_service.py:1603-1608` | 匯出時 `target_status` 有值就原樣吐進 `target.status` | 棒 1：改組 `{state, reason}`＋`implementation-status` |
| `pkg/jedi-compliance-audit/.../assessment_result_app_service.py`（860 行，超標） | `init_ar_matrix_for_round`（:204）傳 control_ids；`judge_finding`（:536）；observation／risk／remediation CRUD（:572～:802）；`finalize_assessment`（:834）看 pending 數擋 | 棒 1：`init` 改傳 AO part_id 清單；棒 2：**新開檔** `ar_objective_app_service.py` 放整批存、統計、結案守門，不往 860 行裡加 |
| `pkg/jedi-compliance-audit/.../ar_import/aggregation.py:10` | `aggregate_verdict` 四規則，只有 FR-044 匯入在用 | 棒 1：加「掛風險」條件，成為 D5 唯一規則 |
| `pkg/jedi-compliance-audit/.../pass_scope_rule.py:12` | `carried_control_ids`：AO 沒自己的 finding 時退回控制層 | 棒 6：直接讀 AO 層 finding＋D5 規則 |
| 主專案 route `api/project/routes/audit_round_route.py:235-395` | `/audit-round/<round_uid>/ar/findings`（GET）、`finding/<uid>`（PUT 單筆）、observations／risks CRUD、`risk/<uid>/findings`（PUT） | 棒 2：加 `PUT …/ar/objectives`（整批）、`PUT …/ar/attestation`、`GET …/ar/summary`、`POST …/ar/finalize` 守門改版；舊單筆端點留給舊稽核次 |
| `ar_import_route.py`＋`ar_import_app_service.py`（621 行）＋`airasia_cmmc_l1_ar_v1.py` | FR-044 的亞航格式 xlsx 匯入（序號、索引號、實務要求、說明、AO、方法、佐證、稽核結果） | 棒 4：新主路徑是 FR-142 格式（有 part_id）；亞航原格式 parser 留作「沒有 part_id 的舊檔」退路 |

### ③ FE 現況

| 元件 | 現況 | 本案動作 |
|---|---|---|
| `src/views/project/RoundAuditReviewView.vue`（1,083 行，超標） | 路由 `/project/projects/:id/round/:roundUid/audit`；兩頁籤「控制項稽核判定」「風險」；讀 `ao_findings` 聚合成控制項狀態（:409-424）、存檔把同一判定寫到該控制項全部 finding（:551-555）；風險建在「風險」頁籤再勾 finding | 棒 3：**全新檔** `RoundAuditResultView.vue`＋`ar-result/` 子元件（左樹、控制項總覽、評估目標頁、稽核報告、報告預覽），路由改指；舊檔退役（D4 舊稽核次用新頁唯讀） |
| `src/components/grc/ar-import/ArImportDialog.vue`（720 行） | FR-044 匯入對話框 | 棒 4：改成預覽模式（同 FR-141 做法，不另開清單） |
| FR-141 的 `ObjectiveTab.vue` 等左樹右表單元件 | 已在主工作樹 | 棒 3：抽共用（左樹、控制項頁首、上一條下一條）或直接 copy，由 runner 看耦合度決定 |

### ④ 可沿用的匯出匯入能力

| 能力 | 座標 | 本案動作 |
|---|---|---|
| 稽核計畫 Excel 讀寫六支 | `app/flow_control/service/ap_workbook_{layout,writer,reader,diff,export_service,import_service}.py` | 棒 4：layout 加 `COL_VERDICT…COL_REC` 六欄常數；writer／reader 加結果欄；diff 加結果欄比對；新開 `ar_workbook_export_service.py`／`ar_workbook_import_service.py` 組合 |
| 報告稿 docx＋pdf | `app/flow_control/service/ap_docx_generator.py`、`ap_report_export_service.py`、範本 `app/oscal/templates/ap/ap_report_template.docx`、`SspLibreOfficeConverter` | 棒 5：範本複製成 `ar_report_template.docx`（第 7 章佔位改真內容、附件表橫式加欄）；新開 `ar_report_export_service.py` |
| 解析暫存 | `oscal.ap_docx_parse_jobs`，`source_type` 區分 | 棒 4：`source_type='ar_xlsx'` |

---

## 詳細設計 {#design nav="設計"}

### OSCAL 落點（AR，v1.2.2）

| 畫面欄位 | OSCAL | 落表 |
|---|---|---|
| 稽核結果 | `result.findings[]`（每評估目標一筆）：`target.type`＝`objective-id`、`target.target-id`＝part_id、`target.status.state`＝satisfied／not-satisfied、`target.status.reason`＝pass／fail／other、`target.implementation-status.state`＝not-applicable（不適用時）；未判定＝不建 finding | `assessment_findings`：`target_id`、`target_status`（JSONB 改 OSCAL 形狀）、新欄 `implementation_status` |
| 稽核發現 | `finding.description`（必填；符合且空白時系統補「經檢視佐證，符合本評估目標」）、`finding.title`＝評估目標文字 | `assessment_findings.description`／`title` |
| 佐證（採用的檔案、連結、問卷、現場上傳） | `observation` 一筆：`methods`＝`EXAMINE`、`relevant-evidence[]` 每筆 `href` 指向該系統物件（檔案預覽、連結網址、問卷填答）、`description`＝名稱；`collected`＝存檔時間；finding 用 `related-observations` 連 | `assessment_observations`（`relevant_evidence` JSONB） |
| 其他佐證 | `observation` 另一筆：`description`＝文字、`methods` 照稽核計畫該評估目標的查核方法映射（訪談→INTERVIEW、文件檢查／現場觀察→EXAMINE、測試／設定比對→TEST） | `assessment_observations` |
| 風險等級 | `risk.characterizations[]`：`{origin:{actors:[稽核人員]}, facets:[{name:'severity', system:'http://csrc.nist.gov/ns/oscal', value:'低／中／高'}]}` | `assessment_risks.characterizations`（陣列） |
| 風險說明 | `risk.title`（取說明前段）、`risk.description`＝`risk.statement`＝說明全文、`risk.status`＝open | `assessment_risks` |
| 改善建議 | `risk.remediations[]` 一筆：`lifecycle`＝recommendation、`title`／`description`＝建議文 | `assessment_remediations` |
| 評估目標↔風險 | `finding.related-risks[]` | `assessment_finding_risks` 連結表（匯出改讀此表） |
| 第 7 章稽核結果 | `result.attestations[]` 一筆：`parts[{name:'audit-conclusion', prose:全文}]`、`responsible-parties`＝稽核人員 | `ar_results.attestations` JSONB |
| 本次判定範圍 | `result.reviewed-controls.control-objective-selections[]`＝本次評估目標 part_id 清單 | `ar_results.reviewed_controls` |
| 第 1～6 章 | 從 AP 讀（FR-141） | 不重存 |
| 控制項結論 | **無欄位**，D5 即時推導 | — |

**官方必填核對（v1.2.2 JSON schema）**：finding（uuid／title／description／target，target 再需 type／target-id／status）、observation（uuid／description／methods／collected）、risk（uuid／title／description／statement／status）、remediation（uuid／lifecycle／title／description）、result（uuid／title／description／start／reviewed-controls）、attestation（parts）——上表每一項都有來源，畫面不需多填。

```{.mermaid cap="圖 2 — 一條評估目標存成哪些 OSCAL 物件"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
    AO[評估目標頁<br/>佐證／其他佐證／稽核發現<br/>稽核結果／風險] --> F[finding<br/>target-id=part_id<br/>status.state / reason<br/>implementation-status]
    AO --> O1[observation 佐證<br/>EXAMINE<br/>relevant-evidence=檔案／連結／問卷]
    AO --> O2[observation 其他佐證<br/>methods 從稽核計畫帶<br/>description=文字]
    AO -->|判不符合且有填| K[risk<br/>characterizations.facets severity<br/>status=open]
    K --> M[remediation<br/>lifecycle=recommendation]
    F -.related-observations.-> O1
    F -.related-observations.-> O2
    F -.related-risks 連結表.-> K
    T[稽核報告頁籤第 7 章] --> A[result.attestations<br/>part name=audit-conclusion]
```

### 讀寫 API（棒 2）

全部掛在既有 `/audit-round/<round_uid>/ar` 下，讀取時以 FR-140 的「目前稽核次」取 AR result（GET 可帶 `pass` 讀指定稽核次）。新邏輯一律新檔 `app/flow_control/service/ar_objective_app_service.py`，不往套件 824 行的 `assessment_result_app_service.py` 加。

| 方法 | 路徑 | 用途 |
|---|---|---|
| GET | `…/ar/objectives` | 控制項樹（族群→控制項→評估目標）、每條評估目標的稽核結果／稽核發現／其他佐證／風險、候選佐證清單與採用狀態、控制項結論（D5）、統計六格、AP 參考（查核方法、受評對象、稽核人員） |
| PUT | `…/ar/objective/<part_id>` | 存一條評估目標（稽核結果、稽核發現、採用佐證 id、加入其他佐證 id、現場上傳 id、其他佐證文字、風險等級／說明／改善建議）；回傳該控制項最新結論與統計 |
| POST | `…/ar/onsite-evidence` | 現場上傳（multipart），存為本次稽核佐證，回傳 id |
| GET／PUT | `…/ar/conclusion` | 第 7 章文字（attestation） |
| GET | `…/ar/conclusion/draft` | 「帶入不符合事項」草稿文字 |
| POST | `…/ar/close` | 結案：守門三條（412 列原因）→ 凍結佐證 → 呼叫既有 `finalize_assessment`（有不符合生 POA&M）→ 鎖定 |
| GET | `…/ar/export?format=docx\|pdf\|xlsx\|zip&with_findings=` | 匯出三選一（棒 4、5） |
| POST | `…/ar-xlsx/parse`、`/ar-xlsx/<parse_uid>/confirm` | Excel 匯入預覽與確認（棒 4） |

**鎖定**：結案後所有寫入端點回 409（新 error code，`GRC_409xxx`）。**權限**：寫入要專案稽核人員角色，走 `common/authz` 資源域守門（同既有 AR 端點）。

**候選佐證**：`public.workflow_execution_control_mapping`（`workflow_execution_id, version_id, control_id, ao_part_id`）對到該控制項的工作流程 → `compliance.job_evidences` 類型 FILE／LINK／SURVEY；**二次 scope**：只留 `workflow_execution_id` 真的對到該 control_id 的（memory `feedback_evidence_per_control_scope_wf_control_mapping`）；同 uid 去重。`relevant-evidence` 存字串 `file_uid`，不存數字 `file_id`。

**採用狀態存法**：observation（佐證那筆）`relevant_evidence[]` 存「被採用」的完整清單（不是存排除清單），讀取時「候選且在清單中＝勾選」。首次讀取該評估目標時尚無 observation，畫面視為全部勾選；第一次存檔才寫入。

### 畫面（棒 3）

照雛形 [`prototype/ar-authoring.html`](prototype/ar-authoring.html)。FE 新檔 `src/views/project/RoundAuditResultView.vue` 加 `src/components/grc/ar-result/` 子元件：`ArObjectiveTree.vue`（左樹三層＋燈號＋圖例）、`ArControlOverview.vue`、`ArObjectiveForm.vue`（佐證清單、其他佐證、稽核發現、稽核結果、風險）、`ArEvidencePicker.vue`、`ArConclusionTab.vue`、`ArReportPreview.vue`。左樹可沿用 FR-141 `src/components/grc/ap-plan/ApObjectiveTree.vue` 的結構但需擴到評估目標層。路由 `/project/projects/:id/round/:roundUid/audit` 改指新頁；舊 `RoundAuditReviewView.vue`（1,083 行）退役。D4：舊稽核次（finding `target_id` 不含 `_obj.`）用新頁唯讀。

### Excel 契約（填寫說明、計畫概要、評估目標、可選清單四頁，延伸 FR-141）

「評估目標」頁欄序：

| 欄 | 內容 | 規則 |
|---|---|---|
| A（隱藏） | part_id | 比對鍵；不在本次稽核範圍的列略過 |
| B～J | 序號、控制項代碼、實務要求、執行說明、評估目標、查核方法與指引、受評人員、受評元件／資產、稽核人員 | **FR-141 計畫欄，灰底唯讀，匯入忽略** |
| K | 稽核結果 | 下拉：符合／不符合／不適用；空白＝未判定；無效值預覽就地對應 |
| L | 佐證檔案 | 一行一個檔名，只含檔案；匯出列已採用者；匯入比對專案檔案，對不到預覽三選一 |
| M | 其他佐證 | 自由文字 |
| N | 稽核發現 | 不符合必填（匯入不擋，結案才擋） |
| O | 風險等級 | 下拉：低／中／高；空白＝無 |
| P | 風險說明 | 與 O 任一有值＝有風險 |
| Q | 改善建議 | → remediation |

「可選清單」頁：稽核結果、風險等級、佐證檔案三欄（黃底），其餘沿用 FR-141。「計畫概要」頁唯讀、匯入不讀。範例：[`prototype/sample/FR-142-稽核結果範例.xlsx`](prototype/sample/FR-142-稽核結果範例.xlsx)（產生腳本 `make_sample_xlsx.py`）。

匯入預覽（[`prototype/import-preview.html`](prototype/import-preview.html)）：同一畫面預覽模式；異動的評估目標標示、原值劃掉；無效值與找不到的檔名標「待確認」就地處理；對不到 part_id 的列略過；全部處理完才能匯入。

### 報告（棒 5）

### 匯出三選一

`GET /audit-round/<round_uid>/ar/export?format=docx|pdf|xlsx|zip&with_findings=true|false`：

- **稽核報告**（Word／PDF）：標題帶稽核次；第 1～6 章從 AP 帶；第 7 章＝attestation 全文；附件依 D11。
- **稽核結果工作表（Excel）**：上面四頁。
- **全部檔案**（ZIP）：docx＋xlsx。

範本：複製 `app/oscal/templates/ap/ap_report_template.docx` 為 `ar_report_template.docx`（打包只收 `app/oscal/templates/`，不要放 flow_control）；正文 1～7 章直式，附件獨立 section 橫式，8 欄依 D11。生成沿用 FR-141 `app/flow_control/service/ap_docx_generator.py` 的寫法、pdf 沿用 `SspLibreOfficeConverter`。新檔 `ar_report_export_service.py`、`ar_docx_generator.py`。

### 縮範圍規則（棒 6）

`pkg/jedi-compliance-audit/app/service/pass_scope_rule.py`（28 行）`carried_control_ids` 改讀評估目標層 finding，套 D5 的同一支規則：控制項結論為不符合者帶入下一次稽核。舊稽核次（控制項層 finding）維持原邏輯。

---

## 端到端流程 {#flow nav="流程"}

```{.mermaid cap="圖 3 — 稽核次內從開始稽核到結案"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant U as 稽核人員
    participant FE as 稽核結果頁
    participant BE as BE ar_objective_app_service
    participant PKG as jedi-oscal-v2 AR services
    U->>FE: 進入稽核次（status=auditing）
    FE->>BE: GET /ar/objectives
    BE->>PKG: 讀 AP activities（AO 清單＋方法）＋ 既有 findings／observations／risks
    PKG-->>BE: 合成逐 AO 列（未判定＝無 finding）
    BE-->>FE: AO 列＋控制項結論（D5）＋統計
    U->>FE: 逐條判定、填發現／佐證／風險／建議；或匯入 Excel
    FE->>BE: PUT /ar/objectives（整批）
    BE->>PKG: upsert finding（OSCAL 形狀）／observation／risk＋remediation／連結表
    U->>FE: 稽核報告頁籤寫第 7 章（可帶入不符合事項）
    FE->>BE: PUT /ar/attestation
    U->>FE: 送出結案
    FE->>BE: POST /ar/finalize
    BE->>BE: 守門：未判定>0 或 不符合無發現 → 412
    BE->>PKG: finalize_assessment（有不符合→POA&M）
    U->>FE: 匯出報告 docx／pdf
    FE->>BE: GET /ar/export?format=docx
    BE-->>U: 1～6 章（AP）＋7 章（attestation）＋附件（findings）
```

---

## 端到端驗收 {#acceptance nav="驗收"}

1. **新稽核次**：DEV 從 FR-140 流程開一個新的內部稽核次，進稽核結果頁，左樹三層、全部燈號空心，DB 該 result finding 數為 0。
2. **逐條判定**：AC.L1-b.1.iv [a] 取消一份佐證、寫其他佐證與稽核發現、判不符合、風險等級高與改善建議，存檔重整還在；左樹 [a] 紅燈、控制項紅燈、族群紅燈；控制項總覽依據寫「評估目標 [a] 不符合」。
3. **符合有風險**：另一條判符合，按「新增風險」填風險等級中，所屬控制項變不符合。
4. **Excel 來回**：匯出 Excel，改兩格稽核結果、加一個專案內檔名、寫一個不存在的檔名，匯回，預覽標出異動與「佐證檔案找不到」，選「改為其他佐證文字」後匯入，畫面更新。
5. **第 7 章與報告**：稽核報告頁籤按帶入不符合事項、修改、預覽報告：第 1 頁 1～7 章，第 2 頁橫式附件、判定中文、不符合列淡紅；下載 Word 用 Word 開確認。
6. **結案**：還有未判定時按結案被擋並列原因；補完後結案成功，頁面唯讀，再呼叫寫入 API 回 409；有不符合時 POA&M 有產生。
7. **OSCAL**：匯出該 AR 的 OSCAL JSON，用 `~/.claude/skills/oscal-knowledge/references/json-schemas/oscal_assessment-results_schema.json` 驗證通過；finding 的 `target.status.state` 為 satisfied／not-satisfied、risk 的 `characterizations` 為陣列、finding 的 `related-risks` 有值。
8. **第 2 次內稽**：從有不符合的稽核次開下一次內稽，新稽核計畫只剩不符合評估目標所屬控制項。

---

## 拆分表 {#cards nav="拆卡"}

| 卡 | 做什麼 | 碰哪裡 | 套件 | 依賴 | 驗收 |
|---|---|---|---|---|---|
| 1a | finding OSCAL 形狀、risk facets、匯出讀連結表、remediation lifecycle、清 pending token、migration | `pkg/jedi-oscal-v2`：`domain/service/ar/ar_finding_matrix_service.py`（186 行）、`app/service/ar/assessment_risk_service.py`（168）、`app/service/io/oscal_io_service.py`（1,720，超標只准新開檔）、`infra/model/ar/`、`migrations/013-*.sql` | jedi-oscal-v2 | — | 驗收 7 |
| 1b | observation／attestation service、新稽核次不預建 finding、D5 規則收斂、匯出佐證凍結欄位 | `pkg/jedi-oscal-v2` 新 service；`pkg/jedi-compliance-audit/app/service/assessment_result_app_service.py`（824，超標只准新開檔）、`ar_import/aggregation.py` | 兩支 | 1a | 驗收 1 |
| 2 | `ar_objective_app_service.py` 與 API、結案守門＋鎖定＋凍結 | `app/flow_control/service/`、`api/project/routes/`、`common/code/` | — | 1b | 驗收 2、3、6 的 API 部分 |
| 3 | FE 新頁 | FE `src/views/project/`、`src/components/grc/ar-result/` | — | 2 | 驗收 2、3、6 畫面部分 |
| 4 | Excel 結果欄、檔名比對（佐證欄只用換行切、比對過 `ap_workbook_diff._norm`）、預覽；匯出名「稽核結果工作表（Excel）」 | `app/flow_control/service/ap_workbook_*.py` 擴充＋新 `ar_workbook_*`；FE 預覽模式 | — | 1b（FE 部分等 3） | 驗收 4 |
| 5 | 報告 Word／PDF | `app/oscal/templates/ap/ar_report_template.docx`、`app/flow_control/service/ar_*` | — | 2 | 驗收 5 |
| 6 | 縮範圍改評估目標層、選單 seed | `pkg/jedi-compliance-audit/app/service/pass_scope_rule.py`、`scripts/sql/*-seed-*.sql` | jedi-compliance-audit | 1b | 驗收 8 |

---

## 坑與陷阱 {#pitfalls nav="坑"}

| # | 坑 | 影響 | 對策 |
|---|---|---|---|
| 1 | `init_ar_matrix_for_round` 現在預建 finding 且 `target_id` 存 control_id（2026-06-17 拍板） | FR-140 縮範圍、FE 聚合、FR-044 匯入都假設「每控制項一筆」 | 1b 改 AO 層時 grep `target_id` 全部讀取者（pass_scope_rule、aggregation、ar_import、FE）一次改；D4 用 `_obj.` 判新舊 |
| 2 | `target_status` JSONB 現存 `{"product_state"}`，匯出直接吐 | 改形狀後舊資料匯出仍不合規 | 1a migration 把舊值轉成 `{state, reason}`；舊 `pending` 列刪除（＝未判定） |
| 3 | finding↔risk 連結表與 JSONB 不同步 | 匯出遺失關聯 | 1a 匯出改讀連結表，JSONB 欄停用（不刪） |
| 4 | `assessment_result_app_service.py` 860 行、`RoundAuditReviewView.vue` 1,083 行、`ArImportDialog.vue` 720 行皆超標 | 不能往裡加 | 2a／3a／4a 全部新開檔；舊檔只改一行導向或退役 |
| 5 | 「有風險」判準＝風險等級或風險說明任一有值 | 只填改善建議 → 不算風險、控制項不會變不符合 | 填寫說明明寫；匯入預覽標「有改善建議但無風險等級」提醒（不擋） |
| 6 | 「帶入不符合事項」接在現有內容後 | 按兩次內容重複 | 實作同雛形：接在後面，不覆蓋 |
| 7 | OSCAL `status.state` 封閉值域；「不適用」要同時填 `satisfied`＋`reason=other`＋`implementation-status` | 漏任一匯出不合規或報告印錯 | 1a 寫在 upsert 一個地方；驗收用官方 JSON schema 跑 |
| 8 | 報告附件欄多 | 直式放不下 | D11：附件節橫式、欄數 8；取消「含稽核發現與風險」為 7 欄 |
| 9 | 同一支 `audit_round_app_service.py` 周邊 FR-140 卡與本案 6a 都要改 | 平行派互撞 | 6a 排在 FR-140 相關卡之後，母卡標序列組 |
| 10 | finding `description` 官方必填，但符合時稽核人員常不寫發現 | 匯出不合 schema | 存檔時空白自動補「經檢視佐證，符合本評估目標」，畫面不顯示這句 |
| 11 | 佐證候選清單要走 `workflow_execution_control_mapping` 權威 scope | 同名檔案跨控制項被吸進來（FR-044 STG 實踩） | 依 memory `feedback_evidence_per_control_scope_wf_control_mapping`：二次過濾 `workflow_execution_id` 真的對到該控制項；`relevant-evidence` 存字串 `file_uid` 不存數字 `file_id` |
| 12 | 結案凍結佐證版本 | 受稽單位結案後換檔，稽核紀錄指向新檔 | 結案時把每筆採用佐證的 `content_hash`／版本寫進 `relevant-evidence.props` |
| 13 | Excel 檔名比對同名多筆 | 兩個任務交了同名檔案 | 預覽就地讓使用者選，不自動挑 |
