---
title: "稽核計畫重做：計畫概要與評估目標、兩頁 Excel、報告稿匯出 — 討論稿 (FR-141)"
brand: "Guidant AI · **FR-141** 稽核計畫重做"
eyebrow: "FR-141 · 討論稿 · 2026-10-15"
h1: "稽核計畫重做"
subtitle: "計畫概要與評估目標兩個頁籤、一份兩頁 Excel 匯出即匯入範本、亞航格式報告稿匯出"
lede: "現在的稽核計畫頁以「行程」為單位、查核方法只能填到控制項，顧問實際工作是逐條評估目標（AO）決定怎麼查、查誰、誰來查；而且計畫沒辦法匯出成報告稿。本案把這一頁整個重做：「計畫概要」頁籤放亞航報告稿的六段表頭，「評估目標」頁籤用左樹右表單逐條填查核方法與對象；一份兩頁的 Excel 匯出就是匯入範本，匯出三選一（報告稿、Excel、打包）。資料照 OSCAL 評估計畫的標準欄位存。D1～D4 已由決策者裁定，請先開 [雛形](prototype/index.html) 點一遍，再回來看規格。"
chips: [
  {text: "D1～D4 已裁（2026-10-15）", kind: ok},
  {text: "雛形第二輪完成：左樹右表單", kind: accent},
  {text: "動兩支套件：jedi-oscal-v2、jedi-compliance-audit", kind: crit},
  {text: "與 FR-140 卡 3 的 AP 複製對齊", kind: warn}
]
footer: "FR-141 · 討論稿 · 2026-10-15 · 輸入：決策者 2026-10-15 需求與 D1～D4 裁示、亞航 CMMC L1 報告稿與附件 · 現況依據：程式碼行號（2026-10-15）· 沿革見 git log"
---

> 雛形審過、拍板後寫 design.md，本稿不再改寫。

**狀態**：討論稿（D1～D4 已裁，雛形第二輪完成待審；P1～P3 待裁）　｜　**日期**：2026-10-15　｜　**母卡**：CM-2799　｜　**雛形卡**：CM-2800　｜　**雛形**：[prototype/index.html](prototype/index.html)

路徑慣例：BE 路徑相對 `compliance-manager-be/`；`pkg/` 代表 `~/Projects/Jedicogy/module/jedi-python-package/<套件>/<套件底線名>/`；FE 路徑相對 `compliance-manager-fe/`。

名詞：**AP**＝稽核計畫（assessment plan）；**AO**＝評估目標（assessment objective），每條控制項底下的檢查項目，全系統用 part_id 當鍵（格式 `<控制項代碼>_obj.N`）；**計畫概要**＝一份 AP 的表頭，原稱「場次」（依據、目的、稽核單位、日期起迄、範圍、方法論、稽核人員）；**activity**＝OSCAL AP 裡「一個查核動作」的物件，本案一條 AO 一個；**稽核次**＝FR-140 新增的概念，一輪內每做一次稽核（內部或外部）一筆，每筆自己一份 AP；**受評對象**＝被查的人或系統元件、資產；**查表頁**＝Excel 裡隱藏的工作表，放下拉選項。

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

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-15 | 初稿：需求、兩份範例檔結構、已裁 D1～D4、大量 AO 策略、現況盤點、OSCAL 落點、Excel 契約、匯出三選一、三棒拆卡草案 | 母卡 CM-2799 |
| 2026-10-16 | 決策者看第二輪雛形後：D1 改判為一格多值（換行或分號）＋填寫說明頁＋可選清單頁；頁籤「場次」改名「計畫概要」；稽核單位改單行只填公司名稱；方法組不限數量 | D1 |
| 2026-10-15 | 決策者看第一版雛形後：AO 頁籤改左樹右表單、匯入預覽併入同畫面、稽核次麵包屑；回答「內外稽核不會混，各自一份 AP」 | 雛形第二輪 |

---

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

整案一句話：**稽核計畫頁換成「計畫概要」＋「評估目標」兩個頁籤，顧問逐條 AO 填怎麼查、查誰、誰查；一份兩頁 Excel 匯出即匯入範本；一鍵匯出亞航格式報告稿。**

| 棒 | 做什麼 | 產出 | 完成怎麼判定（決策者親手做什麼、看到什麼） |
|---|---|---|---|
| **1 雛形** | 純前端假資料刻兩個頁籤、匯入預覽、報告稿預覽，加一份範例 Excel | `prototype/` 四頁＋`sample/*.xlsx` | 開雛形索引頁點一遍：切頁籤、左樹點控制項、右側加方法組、套用到所有 AO、匯入預覽對應未知方法；用 Mac Excel 開範例檔點每個下拉 |
| **2 計畫概要＋報告稿** | 套件加「條款與說明」表；計畫概要頁籤；Excel 讀寫骨架與第一頁；docx／pdf 匯出；FR-043 docx 匯入改填新欄位 | 新表 migration、計畫概要頁籤、匯出按鈕三選一（先通報告稿與 Excel 第一頁） | DEV 開一輪的稽核計畫，填六段文字與日期起迄存檔，重整還在；按匯出選報告稿 docx，用 Word 開看到 1～6 段與自己填的內容；選 Excel 開第一頁看到同樣內容 |
| **3 AO＋Excel 第二頁** | 套件 activity 寫 AO、受評對象、負責人員；評估目標頁籤（左樹右表單、控制項預設套用、篩選）；Excel 第二頁讀寫與查表頁；匯入預覽未知方法對應；覆核輪與 FR-140 第 2 次內稽的 AP 複製改 AO 粒度；AR 讀 AP 改逐 AO | 評估目標頁籤、Excel 兩頁完整來回、clone 改寫 | DEV 在評估目標頁籤填三條 AO 存檔，匯出 Excel 第二頁看到那三列有值、其他列預帶控制項預設；改兩格匯回，預覽頁標出那兩格、確認後畫面更新；啟動覆核輪，新 AP 只剩不合格的 AO |

依賴：1 → 審雛形 → design.md 與開 2、3 的卡 → 2、3 可平行（碰的檔不同）。第 3 棒的 AP 複製要接在 FR-140 卡 3（CM-2788）之後，同一支 `audit_round_app_service.py` 周邊不同時派。AR 側改逐 AO 另開 FR。

```{.mermaid cap="圖 1 — 三棒依賴與 FR-140 接點"}
%%{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
    P[棒 1 雛形<br/>CM-2800] --> R{決策者審}
    R --> D[design.md<br/>開棒 2、3 的卡]
    D --> B2[棒 2 計畫概要＋報告稿<br/>terms 表、docx、Excel 第一頁]
    D --> B3[棒 3 AO＋Excel 第二頁<br/>activity 改 AO 粒度、clone]
    F3[FR-140 卡 3 CM-2788<br/>開新稽核次、AP 複製] -.先做.-> B3
    B3 -.另開 FR.-> AR[AR 逐 AO 判定與匯出]
```

---

## 需求 {#need nav="需求"}

### 決策者原話（2026-10-15）

- 稽核計畫要多一塊「文字表單」，內容是報告稿的 1～6 大點，時間用時間範圍；存了要能匯出類似範例稿的 docx 或 pdf；看有沒有合適的 OSCAL 欄位放。
- 現在的稽核計畫很難填寫，不能 by AO 只能 by 控制項，要改成 by AO 填寫、要提供 Excel 匯出匯入，格式參考亞航附件；要加每一項的查核方法與指引、受評對象、稽核人員，都可能多筆，目前人員跟元件放在一起。
- 場次就是 docx 那份的資料；詳細內容在 Excel 處理，顧問 by AO 填他要怎麼檢查；後續 AR 也會改。
- 畫面不要用舊的，舊的真的很難用；場次與 AO 分兩個 Tab；先出雛形。
- 一份 Excel 兩個 sheet：一頁是 docx 表單、一頁是 AO 規劃，這樣不用分兩種匯入；匯出要可以選文件稿或 Excel 完整資料，打包在一起也可以。
- 看第一版雛形後：AO 部分參考 SSP 那種有導航的模式，預覽也找更好的方法；內外稽核會不會混、有沒有辦法分開。

### 兩份範例檔長什麼樣

::: grid2
::: {.card .ok}
#### 報告稿 docx（`亞航-CMMC L1內部稽核報告20260612(稿).docx`）

只有段落、沒有表格，七段：1 依據、2 目的（三點條列）、3 稽核單位、4 稽核日期（一天＋時段）、5 稽核範圍（指向 SSP 文件編號）、6 稽核方法（三段方法論文字）、7 稽核結果（三點發現＋附件指向 Excel）。第 7 段是稽核**結果**，屬 AR 那一側，本案在報告稿留佔位。
:::

::: {.card .ok}
#### 附件 xlsx（`…附件-稽核紀錄.xlsx`）

一張工作表，表頭 4 列（專案名稱、受驗型態、受稽核人數、稽核日期），正文 59 列＝59 條 AO、15 條控制項。欄：序號、索引號（控制項代碼）、實務要求、執行說明、Objectives 控制目標（AO）、Status（實際內容是「方法：指引」多行）、Evidence 佐證（多行檔名）、稽核結果（全 MET）。同一控制項的實務要求只寫第一列。「方法」前綴七種：文件檢查、文件檢視、訪談、現場觀察、電腦設定比對、門禁系統設定比對、測試。
:::
:::

### 新畫面長什麼樣

一頁兩個頁籤，頁首是稽核次麵包屑「專案 › 第 1 輪 › 第 N 次 內部／外部稽核 › 稽核計畫」。

| 頁籤 | 內容 | 填寫方式 |
|---|---|---|
| **計畫概要** | 依據、目的、稽核單位、稽核日期起迄、稽核範圍、稽核方法論六段＋這場的稽核人員名單；上方唯讀標示「第 N 次 內部／外部」（從稽核次讀） | 多行文字框；稽核單位是單行（只填公司名稱，報告稿印成「由○○協助辦理。」）；日期時間起迄；人員多選（可當場新增） |
| **評估目標** | 左側控制族群→控制項樹（完成狀態點、搜尋、篩選「全部／尚未填／已覆寫稽核人員」）；右側一次一條控制項：實務要求與執行說明（唯讀）、「控制項預設」區塊（方法組＋受評人員＋受評元件／資產＋稽核人員，「套用到底下所有 AO」）、每條 AO 一張小卡（方法組可加減、受評人員、受評元件／資產、稽核人員預帶計畫概要名單可覆寫）；上一條／下一條 | 下拉、多選、文字框；「總覽」子檢視是唯讀大表格 |

匯入預覽與正式頁同一個畫面、進入「預覽模式」：左樹節點標「有變更／有問題」，右側被覆寫的欄位高亮、原值灰字劃掉，未知方法就地出對應下拉，頂部橫幅計數與「確認匯入」。

---

## 已裁決策 {#decisions nav="已裁"}

::: {.callout .decided}
**D1 Excel 多值表示＝一格多值，靠填寫說明與預覽檢查兜底**（2026-10-16 改判）

- **查核方法與指引**：一格，一行一組，寫成「方法：指引」，和亞航原表一樣。只認換行分組，不認逗號或分號，因為指引內文常有逗號、分號。
- **受評人員、受評元件／資產、稽核人員**：各一格，多個值用換行或分號（全形「；」或半形「;」）隔開。稽核人員留空＝沿用計畫概要。
- 不限組數與人數，畫面上方法組也可一直加。
- 檔案附「填寫說明」頁（含格內換行快捷鍵）與看得到的「可選清單」頁（人員、元件、方法的正確寫法，可複製）。
- 匯入時名字或方法詞對不到清單，預覽頁在那一格就地讓使用者對應（同 D4）。

**為什麼**：決策者看雛形後判斷顧問不會用往右延伸的固定格數。業界匯入範本（Salesforce 多選欄位、Jira、ServiceNow）多數也是一格多值加分隔符，配「填寫說明＋可選清單＋匯入檢查」三件套；固定格數表太寬、第 N+1 個不知道放哪。

**被排除**：固定格數每格一般下拉（2026-10-15 原裁，表寬約 22 欄，決策者看過後否決）；xlsm 多選巨集（Mac／LibreOffice 不相容、資安擋）；逗號當方法分隔（與指引內文衝突）。
:::

::: {.callout .decided}
**D2 稽核人員＝計畫概要預設、AO 可覆寫**

計畫概要頁籤填整場名單，AO 每列預帶、可個別改，改過的列標「已覆寫」可還原；Excel 每列印有效值。

**為什麼**：只在計畫概要層看不出「這條誰查」；只在 AO 層每列都要重填、與報告稿「稽核單位」段脫節。
:::

::: {.callout .decided}
**D3 受評對象拆兩欄**

「受評人員」一欄、「受評系統元件／資產」一欄。元件與資產在 OSCAL 都是 subject、只是 type 不同，合一欄選。

**為什麼**：現況一個分組多選把人員、元件、資產混在同一欄，決策者點名不好用；拆三欄則 Excel 與畫面各多一欄，沒有對應的業務需要。
:::

::: {.callout .decided}
**D4 匯入遇到選單沒有的方法詞＝預覽頁提示、使用者對應**

例「文件檢視」「門禁系統設定比對」目前後台選單沒有。匯入預覽標出，使用者逐個對應到既有選項或略過；選單本身由管理員在後台 `system_menus` 維護。

**為什麼**：直接擋下顧問會卡住；自動新增選項會讓打錯字也變一個選項、選單長髒。
:::

::: {.callout .decided}
**大量 AO 的填寫策略**（決策者 2026-10-15 同意）

CMMC L2 約 110 條控制項、320 條 AO。控制項層先填一組方法／人員再「套用到底下所有 AO」只改例外；左樹預設收合、可篩「尚未填」；Excel 匯出時預帶值已填好、使用者只改少數格。「填好」的定義：至少一個方法，且受評人員或元件／資產至少一個。
:::

::: {.callout .decided}
**內外稽核不會混：每一次稽核各自一份 AP**

FR-140 之後每一筆稽核次（第 1 次內稽、第 2 次內稽、外稽）自己一份完整的 AP，各有計畫概要表頭與 AO 表。要分開的是**進入點**，不是 AP 內部：總覽頁選第幾次進來（FR-140 卡 10）、稽核計畫頁首麵包屑帶「第 N 次 內部／外部」、可切換同一輪其他次唯讀看。第 2 次內稽從第 1 次複製來的 AO 標「承襲自第 1 次」，改過就消。AP 內容本身不需要任何「內部／外部」欄位。
:::

::: {.callout .decided}
**AR 另案**

評估結果（AR）之後另開 FR 改成逐 AO 讀取與匯出，Excel 的「佐證資料」「稽核結果」兩欄到那時接上。本案 AP 匯出不印那兩欄、匯入忽略。本案第 3 棒只把 AR 「從 AP 反推方法」的讀取改成逐 AO（不改 AR 畫面）。
:::

### 待裁（雛形審完一併裁）

::: {.callout .pending}
**P1 第 2 次內稽複製 AP 縮範圍的單位**（FR-140 卡 3 接點）

FR-140 卡 3 定「複製上一次內稽的 AP、縮到不合格的控制項」。改成 AO 粒度後有兩種縮法：
- **甲：只留不合格的 AO**——第 2 次只查上次沒過的那幾條目標，最省；但同一控制項其他 AO 在第 2 次的 AP 看不到。
- **乙：不合格 AO 所屬控制項整條保留**——與 FR-140 現有文字一致（縮到控制項），顧問看得到整條控制項的脈絡。

**我的建議**：乙。AR 判定逐 AO，但稽核現場是整條控制項一起看；且 FR-140 卡 3、覆核輪 `launch_reverify` 既有邏輯都是控制項粒度，乙改動最小。甲留給未來「AO 層勾選」需求。
:::

::: {.callout .pending}
**P2 報告稿附件表要不要多印「受評對象」「稽核人員」兩欄**

亞航原表沒有這兩欄（它們在原表是混在 Status 文字裡或根本沒寫）。雛形第一版多印了。**建議**：印，顧問交報告時這兩欄有用；但做成匯出時可勾選。
:::

::: {.callout .pending}
**P3 方法選單對齊**

雛形用六個方法（訪談、文件檢查、現場觀察、電腦設定比對、測試、門禁系統設定比對）；DEV 的 `system_menus` AUDIT_METHOD 群組現有 INTERVIEW／DOCUMENT_REVIEW／EXAMINE／COMPUTER_CONFIG_COMPARE／TEST／ACCESS_CONTROL_CONFIG_COMPARE（`pkg/jedi-compliance-audit/app/service/import_adapter/method_keywords.py:11`），有「檢視」沒有「現場觀察」。**建議**：第 2 棒出一支 `phase=seed` migration 補「現場觀察」，不在程式寫死。
:::

---

## 現況盤點：接入點 × 現況 × 本案動作 {#inventory nav="盤點"}

座標以 2026-10-15 為準，runner 開工先開檔核一次。

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

| 表 | 現況 | 本案動作 |
|---|---|---|
| `assessment_plans`（`oscal_ap.py:26`） | `uuid`、`metadata_id`（1:1 `oscal.metadata`）、`status`、`import_ssp_id`、`local_definitions_remarks` | 不改 |
| `ap_reviewed_controls`（`:22`，與 AP 1:1） | `control_selections` 必填、`control_objective_selections` 選填——**從未寫入**，只在 clone 時原樣搬（主專案 `assessment_plan_app_service.py:401`） | 第 3 棒寫入 AO 的 part_id |
| `ap_assessment_activities`（`:23`） | `uuid`、`title`、`description`、`steps` JSONB（每筆 `{uuid,title,description,props[method]}`）、`related_controls`（只寫 `control-selections`，**到控制項層**）、`responsible_roles` | 第 3 棒：一條 AO 一個 activity；`related_controls` 加 `control-objective-selections`；`responsible_roles` 帶 party |
| `ap_tasks`（`:24`） | `parent_id`、`type`、`title`、`description`、`timing`、`associated_activities`、`subjects`、`responsible_roles` JSONB | 第 2 棒：一份 AP 一個 task 當計畫概要容器，`timing.within-date-range` 放日期起迄；第 3 棒：`associated_activities[].subjects` 放每條 AO 的受評對象 |
| `ap_task_subjects`、`ap_task_participants`（`:25`） | FR-040 的行程層受評對象與參與人員連結表 | 第 3 棒後退役（新模型不再用行程層對象）；migration 不刪表，舊資料留 |
| `ap_assessment_subjects`（`:25`） | AP 層受評對象 | 保留，存檔時由 AO 列聯集回填（沿用 FR-040 做法） |
| `ap_local_objectives`（`:23`） | `control_id`、`parts` JSONB，**主專案與套件都沒有呼叫端** | 不用（AO 以 part_id 引用 catalog，不複製） |
| `oscal.parties`／`oscal.roles` | 掛在 AP metadata 下；稽核人員已是 party、角色放 props `responsible-role`＝assessor（`assessment_plan_app_service.py:70-71`） | 第 2 棒：稽核單位存 organization 型 party |
| **沒有表**：`terms-and-conditions`、`assessment-assets`、`objectives-and-methods`、AP 的 back-matter | FR-037 差距審查時略過 | 第 2 棒新增 `ap_terms_and_conditions`；其餘不做 |

套件服務：`pkg/jedi-oscal-v2/app/service/ap/assessment_plan_service.py:43` 的 `set_reviewed_controls`（:113）、`set_assessment_subjects`（:153）、`set_tasks`（:184）**一律整批先刪後寫，task uuid 每次重產**；`app/service/io/oscal_io_service.py:263` `export_oscal` 不支援 assessment-plan。

### ② 主專案 AP 服務與端點

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 端點（`api/project/routes/audit_round_route.py:144-205`） | `GET /audit-round/<round_uid>/ap`、`PUT /ap/<ap_uid>/reviewed-controls`、`PUT …/assessment-subjects`、`PUT …/tasks`、`GET|POST …/parties`；FE 實際只呼叫 tasks 與 parties | 第 2 棒加 `PUT …/terms`、`GET …/export?format=docx|pdf|xlsx|zip`；第 3 棒加 `PUT …/objectives`（整批 AO 列）、`POST …/xlsx-imports/parse`、`GET|POST …/xlsx-imports/<uid>/confirm` |
| App service `app/flow_control/service/assessment_plan_app_service.py:74`（730 行） | `_METHOD_PROP`（:68）、`_methods_to_props`（:115）、指引放 `activity.steps[]`（:130-165）、`related_controls` 只到控制項（:169-178）、`_ap_detail`（:247）、`clone_ap_for_round`（:330，覆核輪 clone 並收斂範圍）、`set_tasks`（:583）、`resolve_ap_and_check_auditor`（:190，限 auditor／manager，過了規劃階段唯讀） | 檔已近上限，新邏輯一律新檔：`ap_terms_app_service.py`（第 2 棒）、`ap_objective_app_service.py`、`ap_workbook_app_service.py`、`ap_clone_service.py`（第 3 棒），原檔一行呼叫。權限守門沿用 `resolve_ap_and_check_auditor` |
| AR 反推 AP（`pkg/jedi-compliance-audit/app/service/assessment_result_app_service.py:303`） | 把 AP activities 反推成 `{control_id: {methods, guidance, subjects}}` | 第 3 棒改成 `{ao_part_id: …}`，控制項層由 AO 聚合；AR 畫面不動 |
| 覆核輪 clone（`pkg/jedi-compliance-audit/app/service/audit_round_app_service.py:596` `launch_reverify`） | clone AP 縮到 not_met 控制項；FR-140 卡 3 會抽成共用函式供「第 2 次內稽」重用 | 第 3 棒接在 FR-140 卡 3 之後改成 AO 粒度（P1 裁法） |
| FR-043 docx 匯入（`pkg/jedi-compliance-audit/app/service/ap_docx_import_app_service.py:288-336`） | adapter 已解析出 basis／objective／audit_org／methodology／scope／audit_date（`ap_report_parser/base.py:13-31`），但**全部塞進每筆 task 的 description** | 第 2 棒改填 terms 表與 organization party；adapter 不擴充 |
| AO key（`pkg/jedi-compliance-audit/app/service/ao_derivation.py`） | part_id `<control_id>_obj.N`，控制項無 AO 時退回 `control_id`；wecm、AR、SSP Excel 隱藏欄、程序書池全用同一把 key | 第 3 棒沿用，不另造 |

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

| 能力 | 位置 | 本案用法 |
|---|---|---|
| SSP docx／pdf 匯出 | `app/oscal/service/export/ssp_export_app_service.py:27`、`ssp_docx_generator.py:47`（docxtpl 範本＋python-docx 追加）、`ssp_libreoffice_converter.py:57`（`LIBREOFFICE_CMD`，逾時 60 秒）、範本 `app/oscal/templates/ssp/ssp_cmmc_template.docx` 由 `scripts/generate_ssp_docx_template.py` 產 | 第 2 棒照同套路加 `ap_report_template.docx` 與 `ap_docx_generator.py`，pdf 走同一支 converter |
| Excel 範本產生器 | `app/module_frame/excel_template/generator.py:132` `ExcelTemplateGenerator`，同目錄 `data_validation_builder.py`、`lookup_builder.py`、`cell_protection.py`、`styles.py` | 第 2、3 棒：查表頁、list 型資料驗證、鎖唯讀欄 |
| 隱藏 A 欄放 AO part_id | `app/oscal/service/ssp_control_impl_import_service.py` 檔頭與 `export_excel`（:105）、`confirm_import`（:375） | 第 3 棒第二頁同慣例 |
| 公式注入防護 | `jedi_common/utils/export_safety.py` `set_text_cell`、`neutralize_formula` | 所有寫入儲存格 |
| FE 匯入 dialog | `src/components/grc/JobImportDialog.vue`（426 行，AP 範圍的 job 匯入，驗證→逐列→確認） | 第 3 棒上傳步驟參考；預覽改在正式頁的預覽模式 |
| FE 匯出 | `src/composables/useSspDocumentExport.js`（docx／pdf／odt 選格式＋`downloadBlob`） | 第 2 棒擴成三選一 |

### ④ FE 現況

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 填寫頁 `src/views/project/RoundApAuthoringView.vue`（995 行，已超標） | 以行程為單位：`TaskRow`（:34-44）、方法 MultiSelect（:719）＋每方法指引 Textarea（:727）、控制項 MultiSelect 只攤到控制項（:142-145）、受評對象分組 MultiSelect 混人員／元件／資產（:146-160、:743）、參與人員 MultiSelect（:754）；單一存檔 `PUT /ap/:apUid/tasks`（:566） | **整頁換掉**：新開 `RoundApPlanView.vue`（殼）＋`ap-plan/SessionTab.vue`、`ObjectiveTab.vue`（左樹右表單）、`ObjectiveControlForm.vue`、`ApImportPreviewBanner.vue`、composable `useApPlan.js`、`useApObjectiveTree.js`。舊頁留到新頁上線後再刪 |
| 路由 `src/config/router/index.js:1118` `project-ap-authoring-round` | 指向舊頁 | 改指新頁 |
| API 常數 `src/config/api/api.js:212-221`、`:252-253` | AUDIT_ROUND、AP_AUTHORING、AP_DOCX_IMPORT | 加 terms、objectives、export、xlsx-imports |
| 總覽頁 `ProjectAuditorOverview.vue:1150`（2128 行） | 按鈕導到 ap-authoring | FR-140 卡 10 加稽核次切換；本案頁首麵包屑讀同一組資料 |
| i18n `src/config/locales/i18n/zh-tw/ap-authoring.json` | 94 行 | 新開 `ap-plan.json` |

::: {.callout .crit}
**🔴 三個靜默落差**

1. `control_objective_selections` 欄位存在但從未寫入——系統其他部分（規劃任務卡、AR 判定）早已逐 AO，只有 AP 停在控制項層，AR 反推時再用控制項聚合。本案是把 AP 拉齊，不是另起一套。
2. FR-043 匯入把六段文字塞進每筆行程的說明，使用者看到的是重複一遍的長文；沒有欄位可放，所以沒人發現。
3. `set_tasks` 先刪後寫、task uuid 每次重產：任何下游拿 task uuid 當鍵都會斷。第 2 棒改成「一份 AP 一個 task」時要查 wecm 與 AR 有沒有引用 task uuid（探脈時未見，runner 開工 grep 一次）。
:::

---

## 資料模型草案 {#model nav="資料模型"}

### OSCAL 落點

| 畫面欄位 | OSCAL 路徑 | 套件表／欄 | 本案動作 |
|---|---|---|---|
| 依據、目的、稽核範圍、稽核方法論 | `assessment-plan.terms-and-conditions.parts[]`（name＝`basis`／`objective`／`scope`／`methodology`，prose） | **新表** `oscal.ap_terms_and_conditions` | 第 2 棒 |
| 稽核單位 | `metadata.parties[type=organization]`＋`responsible-parties[role=assessor]` | `oscal.parties`（既有） | 第 2 棒 |
| 稽核人員（計畫概要） | `metadata.parties[type=person]`，props `responsible-role=assessor` | `oscal.parties`（既有做法） | 不改 |
| 稽核日期起迄 | `tasks[0].timing.within-date-range{start,end}` | `ap_tasks.timing` JSONB | 第 2 棒：一份 AP 固定一個 task |
| 第 N 次 內部／外部 | 稽核次表（FR-140）；OSCAL 匯出時 props `audit_nature`、`nature_seq`（FR-140 卡 10） | `compliance.audit_round_passes` | 讀，不寫 |
| 每條 AO 的查核方法與指引 | `activity.steps[]`（title＝指引，props `method`） | `ap_assessment_activities.steps`（既有結構） | 第 3 棒：一條 AO 一個 activity |
| AO 本身 | `activity.related-controls.control-objective-selections[].include-objectives[].objective-id`＝part_id | `ap_assessment_activities.related_controls` | 第 3 棒 |
| 每條 AO 的受評人員、元件／資產 | `tasks[0].associated-activities[activity-uuid].subjects[]`（type＝party／component／inventory-item） | `ap_tasks.associated_activities` JSONB | 第 3 棒 |
| 每條 AO 的稽核人員 | `activity.responsible-roles[role=assessor].party-uuids` | `ap_assessment_activities.responsible_roles` | 第 3 棒；空＝沿用計畫概要（D2） |
| 控制項層預設 | 不進 OSCAL；只是畫面與 Excel 的填寫輔助，存檔時展開到每條 AO | — | 第 3 棒 FE 邏輯 |

### 新表 `oscal.ap_terms_and_conditions`

| 欄 | 型別 | 說明 |
|---|---|---|
| `id` | bigserial PK | |
| `assessment_plan_id` | FK → `assessment_plans.id` | 與 AP 1:N（一段一列） |
| `part_name` | varchar | `basis`／`objective`／`scope`／`methodology`；值集合進 `system_menus` group `ap_terms_part` 供畫面標籤與順序 |
| `title` | text | 可空 |
| `prose` | text | 內容 |
| `sort_order` | int | |
| `props`、`links` | jsonb | 共用欄 |
| 審計欄 | | 同其他 oscal 表 |

migration 進套件 `jedi-oscal-v2/migrations/`，主專案不另出。GRANT 給 `cm_app`。

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

**「計畫概要」頁**：直向「欄位／內容」兩欄，列：依據、目的、稽核單位（只填公司名稱）、稽核日期起、稽核日期迄、稽核範圍、稽核方法論、稽核人員（換行或分號隔開，匯入比對人名）。隱藏列放 AP uid 與匯出時間戳當版本比對用。

**「評估目標」頁**：

| 欄 | 內容 | 匯入 |
|---|---|---|
| A（隱藏） | AO part_id | 比對鍵；找不到→預覽標「找不到 AO」略過 |
| B 序號、C 控制項代碼、D 實務要求、E 執行說明、F 評估目標 | 灰底唯讀 | 忽略 |
| G 查核方法與指引 | 一行一組「方法：指引」 | 依換行切組、依第一個全形或半形冒號切方法與指引；沒有冒號的行當指引、方法空白；方法詞不在選單→D4 就地對應 |
| H 受評人員 | 換行或分號隔開 | 名字比對 SSP parties；對不到→預覽就地對應 |
| I 受評元件／資產 | 同上 | 比對 SSP components＋inventory-items |
| J 稽核人員 | 同上；空白＝沿用概要 | 與概要名單相同＝不算覆寫 |

一條 AO 一列，表頭凍結，同控制項的 D／E 只在第一列寫。

**「填寫說明」頁**：每一欄怎麼填、格內換行快捷鍵（Mac：Option＋Enter；Windows：Alt＋Enter）、匯入流程。

**「可選清單」頁（看得到）**：查核方法、受評人員、受評元件／資產、稽核人員四欄，匯出時由 SSP 與 `system_menus` 帶入，讓使用者複製正確寫法。

### 匯出三選一

| 選項 | 內容 | 實作 |
|---|---|---|
| 報告稿 docx／pdf | 1～6 段照亞航排版（標題帶「第 N 次 內部／外部」）、第 7 段「稽核結果」佔位「（評估結果完成後帶入）」、附件 AO 表（方法與指引合回「方法：指引」一行一組；P2 決定是否印受評對象與稽核人員） | docxtpl 範本＋LibreOffice 轉 pdf |
| Excel 完整資料 | 上面兩頁檔，匯出即可改完匯回；空 AP 匯出就是空白範本 | `ExcelTemplateGenerator` |
| 打包 zip | docx＋xlsx | 兩者串起來 |

---

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

```{.mermaid cap="圖 2 — 填寫、存檔、匯出"}
%%{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 AP 服務
    participant PK as jedi-oscal-v2
    U->>FE: 計畫概要頁籤填六段＋日期起迄＋稽核人員
    FE->>BE: PUT /ap/{uid}/terms
    BE->>PK: 寫 ap_terms_and_conditions、organization party、tasks[0].timing
    U->>FE: 評估目標頁籤：控制項預設→套用→改例外
    FE->>BE: PUT /ap/{uid}/objectives（展開後逐 AO）
    BE->>PK: 每 AO 一個 activity（steps、objective-selections、responsible-roles）＋ tasks[0].associated-activities.subjects
    U->>FE: 匯出 → 報告稿 pdf
    FE->>BE: GET /ap/{uid}/export?format=pdf
    BE->>BE: 讀同一份內部模型 → docxtpl → LibreOffice
    BE-->>U: 下載
```

```{.mermaid cap="圖 3 — Excel 匯入與預覽"}
%%{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 AP 工作簿服務
    U->>FE: 上傳兩頁 Excel
    FE->>BE: POST /ap/{uid}/xlsx-imports/parse
    BE->>BE: 串流讀、列數上限、第一頁→計畫概要 diff、第二頁→依 part_id 比對、方法詞比對選單
    BE-->>FE: parse_uid＋diff（變更／找不到 AO／未知方法）
    FE->>U: 左樹標徽章、右側高亮、未知方法就地對應
    U->>FE: 處理完待辦 → 確認
    FE->>BE: POST /ap/{uid}/xlsx-imports/{parse_uid}/confirm（帶對應表）
    BE->>BE: 寫 terms＋objectives（同存檔路徑）
    BE-->>FE: 新狀態
```

---

## 拆卡草案 {#cards nav="拆卡"}

第 1 棒已建卡（CM-2800）。第 2、3 棒在 design.md 定案後一次建完。排序以「決策者何時看得到畫面」為準。

| 棒 | 卡 | 做什麼 | 碰哪裡 | 套件 | 依賴 | 驗收 |
|---|---|---|---|---|---|---|
| 1 | CM-2800 | 雛形兩輪 | `prototype/` | — | — | 決策者點過 |
| 2 | 2a | terms 表＋organization party＋單一 task 日期起迄；`PUT …/terms`；FR-043 匯入改填新欄位 | 套件 model／migration／service；主專案 `ap_terms_app_service.py`、route | jedi-oscal-v2、jedi-compliance-audit | design | DEV 存六段重整還在；匯入亞航 docx 六段進新欄位、行程說明不再重複 |
| 2 | 2b | 計畫概要頁籤（新頁殼＋SessionTab）＋稽核次麵包屑讀 FR-140 卡 10 的 API（卡 10 未完成時先讀輪次） | FE 新檔、路由改指 | — | 2a | 畫面填存；麵包屑顯示第 N 次 |
| 2 | 2c | 報告稿 docx／pdf 匯出＋Excel 第一頁匯出＋zip；FE 匯出三選一 | `ap_docx_generator.py`、範本、`ap_workbook_app_service.py`（第一頁）、route、FE composable | — | 2a | Word 開 docx 看 1～6 段；Excel 第一頁內容相同 |
| 3 | 3a | activity 改 AO 粒度：`PUT …/objectives`、`_ap_detail` 回逐 AO；AR 反推改逐 AO；assessment_subjects 聯集回填 | 套件 service；主專案 `ap_objective_app_service.py`；jedi-compliance-audit AR 讀取 | jedi-oscal-v2、jedi-compliance-audit | 2a | API 存三條 AO 再讀回一致；AR 頁方法顯示不變 |
| 3 | 3b | 評估目標頁籤：左樹右表單、控制項預設套用、篩選、總覽子檢視、承襲標籤 | FE `ObjectiveTab.vue` 等新檔 | — | 3a | 照雛形第二輪操作一遍 |
| 3 | 3c | Excel 第二頁匯出（查表頁、資料驗證、鎖格、隱藏 A 欄）＋匯入 parse／confirm＋預覽模式 FE | `ap_workbook_app_service.py` 第二頁、`ap_xlsx_import_service.py`、FE 預覽模式 | — | 3a、3b | 匯出改兩格匯回、預覽標出、確認後畫面更新；未知方法對應 |
| 3 | 3d | AP 複製改 AO 粒度（覆核輪＋FR-140 第 2 次內稽，P1 裁法） | `ap_clone_service.py`（抽自 `clone_ap_for_round` 與 FR-140 卡 3 的共用函式） | jedi-compliance-audit | 3a、**FR-140 卡 3 CM-2788 完成** | 啟動覆核輪新 AP 只剩不合格項且 AO 列的方法／人員承襲 |
| 3 | 3e | 方法選單 seed（P3）＋`ap_terms_part` 選單 seed＋i18n＋舊頁下線 | `scripts/sql/*-seed-*.sql`、FE i18n、路由 | — | 3b | 後台看到新選項；舊路由不可達 |

---

## 不做清單 {#out nav="不做"}

| 像但不做 | 為什麼 |
|---|---|
| AR 畫面改逐 AO、「佐證資料」「稽核結果」欄 | 另開 FR；本案只改 AR 從 AP 讀方法那一段 |
| AP 的 OSCAL JSON 匯出 | 套件 `export_oscal` 不支援 AP；列為第 3 棒可選，不進驗收 |
| docx 匯入擴充（其他格式 adapter、AO 表匯入） | 新主路徑是 Excel；FR-043 只改落點 |
| 一份 AP 多個行程 | 決策者定「計畫概要就是 docx 那份」，一份 AP 一個；多次稽核由 FR-140 的多稽核次承接 |
| `assessment-assets`、`objectives-and-methods`、AP back-matter | 本案用不到 |
| 刪 `ap_task_subjects`／`ap_task_participants` 表 | 舊資料留；下線舊頁後另開清理卡 |

---

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

| # | 坑 | 影響 | 對策 |
|---|---|---|---|
| 1 | `set_*` 先刪後寫、task uuid 重產 | 下游拿 task uuid 當鍵會斷 | 2a 改單一 task 前 grep wecm、AR、job 有無引用 task uuid |
| 2 | 四槽位讀取者十餘處（FR-140 盤點） | FR-140 改完前，AP 掛在輪次上；改完後掛在稽核次 | 本案所有讀 AP 的新碼一律經 FR-140 卡 1 的 pass domain service 取「目前稽核次」，不直接讀輪次 |
| 3 | 覆核輪 clone 靠「同名」重對應 party（`assessment_plan_app_service.py:348`） | 同名稽核人員會對錯 | 3d 改以 party uuid 對應，同名時才退回名字 |
| 4 | `ProjectAuditorOverview.vue` 2128 行、`RoundApAuthoringView.vue` 995 行皆超標 | 不能往裡加 | 新頁全新檔；總覽頁只改一行導向 |
| 5 | Excel 下拉的查表頁選項是匯出當下快照 | 匯出後 SSP 加了人員，舊檔選不到 | 匯入時人名比對不到→預覽標出讓使用者對應（同 D4 機制） |
| 6 | DEV 方法選單與雛形不一致（P3） | 匯入亞航檔「現場觀察」會變未知 | seed migration 補選項 |
| 7 | 控制項無 AO 時 key 退回 control_id | Excel 第二頁該控制項只有一列、part_id＝control_id | 匯出匯入都要處理這個退路（沿用 `ao_derivation.py`） |
| 8 | 同一支 `audit_round_app_service.py` 周邊，FR-140 卡 3／4／11 與本案 3d 都要改 | 平行派會互撞 | 3d 排在 CM-2788 之後，母卡標序列組 |
