---
title: "稽核計畫重做 — 設計定案 (FR-141)"
brand: "Guidant AI · **FR-141** 稽核計畫重做"
eyebrow: "FR-141 · 設計定案 v2 · 2026-10-16"
h1: "稽核計畫重做：設計定案"
subtitle: "計畫概要與評估目標兩個頁籤、一份 Excel 匯出即匯入範本、亞航格式報告稿匯出"
lede: "稽核計畫頁整個換掉。「計畫概要」頁籤放報告稿前六段與稽核人員；「評估目標」頁籤左樹右表單，逐條評估目標（AO）填查核方法與指引、受評人員、受評元件／資產、稽核人員。一份 Excel（填寫說明、計畫概要、評估目標、可選清單四頁）匯出就是匯入範本，多值一格寫、換行或分號隔開。匯出三選一：報告稿 docx／pdf、Excel、打包 zip。資料照 OSCAL 評估計畫標準欄位存。本文件寫定案規則、為什麼這樣定、踩到會怎樣。"
chips: [
  {text: "已實作完成、決策者驗收（2026-10-16）", kind: ok},
  {text: "D1～D4、P1～P3 全部定案（2026-10-16）", kind: ok},
  {text: "兩棒八張卡，計畫概要頁籤先出畫面", kind: accent},
  {text: "動兩支套件：jedi-oscal-v2、jedi-compliance-audit", kind: crit},
  {text: "陷阱：set_* 先刪後寫、task uuid 每次重產", kind: crit},
  {text: "P1 改共用縮範圍函式，覆核輪一起變", kind: warn}
]
footer: "FR-141 · 設計文件 · 2026-10-16 · 討論稿：[discussion.md](discussion.md)　雛形：[prototype/index.html](prototype/index.html) · 沿革見討論稿與 git log"
---

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

> 狀態：**已實作、決策者驗收（2026-10-16）**；實作與複測期間改判的規則見下方「實作後改判」，**以該節為準**｜母卡 CM-2799｜雛形卡 CM-2800（三輪）｜討論稿：[`discussion.md`](discussion.md)｜雛形：[`prototype/index.html`](prototype/index.html)

> 檔案路徑與行號以 2026-10-16 為準，查不到時以函式名重新定位。路徑慣例：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`；控制項沒有 AO 時退回控制項代碼本身）；**計畫概要**＝一份 AP 的表頭（依據、目的、稽核單位、稽核日期起迄、稽核範圍、稽核方法論、稽核人員）；**activity**＝OSCAL AP 裡「一個查核動作」，本案一條 AO 一個；**稽核次**＝FR-140 的 `compliance.audit_round_passes`，一輪內每做一次稽核一筆、每筆自己一份 AP；**finding**＝AR 裡對一條 AO 的判定紀錄。

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

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-16 | v1：D1～D4、P1～P3 定案；資料模型、Excel 契約、匯出三選一、縮範圍規則、兩棒八張卡、端到端驗收 | 母卡 CM-2799 |
| 2026-10-16 | v2：實作完成後補「實作後改判」節（A5、B5、P3 結果、頁籤改名、拿掉控制項預設、附件表稽核人員、匯入可新增稽核人員）；內文舊規則就地標註 | 子卡 CM-2810、CM-2814、測試棒 |

---

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

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

| 卡 | 做什麼 | 產出 | 完成怎麼判定（決策者親手做什麼、看到什麼） |
|---|---|---|---|
| **A1** | 計畫概要存得起來 | 新表 `oscal.ap_terms_and_conditions`、讀寫 API、稽核單位與日期起迄落點 | DEV 呼叫 API 存六段文字、稽核單位、日期起迄，再讀回一致 |
| **A2** | 新稽核計畫頁殼＋計畫概要頁籤 | FE 新頁、路由改指、稽核次麵包屑 | 對照雛形計畫概要頁籤：填六段、稽核單位單行、日期起迄、稽核人員，按儲存重整還在；麵包屑顯示第 N 次 內部／外部 |
| **A3** | 報告稿 docx／pdf 匯出 | docx 範本、產生器、匯出按鈕 | 匯出 docx 用 Word 開，看到標題帶稽核次、1～6 段是自己填的內容、第 7 段佔位、附件表；選 pdf 一樣內容 |
| **A4** | FR-043 docx 匯入改填計畫概要 | 匯入確認時寫新表 | 匯入亞航報告稿，計畫概要六段有值，不再塞進說明欄 |
| **B1** | AP 改逐 AO 存 | activity 一條 AO 一個、讀寫 API、AR 讀方法改逐 AO | DEV 呼叫 API 存三條 AO 的方法、人員、元件、稽核人員，讀回一致；AR 頁方法顯示不變 |
| **B2** | 評估目標頁籤（後改名「查核項目規劃」） | 左樹右表單、~~控制項預設套用~~（已拿掉，見「實作後改判」）、篩選、總覽 | 對照雛形評估目標頁籤操作一遍，存檔重整還在 |
| **B3** | Excel 匯出匯入 | 四頁 Excel 匯出、解析、預覽模式、確認 | 匯出 Excel 改兩格匯回，預覽黃底標出、未知方法與人名就地對應、確認後畫面更新 |
| **B4** | 第 2 次內稽與覆核輪的範圍規則＋選單 seed | 縮範圍含未判定、AO 承襲、「現場觀察」可選 | 第 1 次內稽判 1 條不合格、留 1 條控制項全沒判，第 2 次計畫出現這兩條控制項；後台方法選單看得到「現場觀察」 |

依賴：A1 → A2、A3、A4；B1 → B2、B3；B4 只依賴 B1。A 線與 B 線可平行（A 碰 terms 表與 docx、B 碰 activity 與 Excel）。最先看得到畫面的是 A2。

```{.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
    subgraph A[A 線：計畫概要與報告稿]
      A1[A1 terms 表＋API] --> A2[A2 新頁殼＋計畫概要頁籤]
      A1 --> A3[A3 報告稿匯出]
      A1 --> A4[A4 docx 匯入改落點]
    end
    subgraph B[B 線：逐 AO 與 Excel]
      B1[B1 AP 逐 AO 存] --> B2[B2 評估目標頁籤]
      B1 --> B3[B3 Excel 匯出匯入]
      B1 --> B4[B4 縮範圍規則＋選單 seed]
    end
    A2 -.頁殼.-> B2
```

B2 掛在 A2 的頁殼上；A2 先完成最順，若 B2 先到可先建頁殼、A2 接上。

---

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

| 決策 | 定案 | 理由 | 被排除方案 |
|---|---|---|---|
| **D1** Excel 多值怎麼寫 | 一格多值。查核方法與指引一格、一行一組「方法：指引」，只認換行分組；人員、元件、稽核人員一格，換行或分號（全形、半形）隔開；稽核人員空白＝沿用計畫概要。附「填寫說明」與看得到的「可選清單」兩頁，匯入時對不到清單的值在預覽就地對應 | 決策者判斷顧問不會用往右延伸的固定格數；業界匯入範本（Salesforce 多選欄位、Jira、ServiceNow）多採一格多值，配填寫說明、可選清單、匯入檢查三件套 | 固定格數每格一般下拉（表寬約 22 欄，決策者看雛形後否決）；xlsm 多選巨集（Mac／LibreOffice 不相容、資安擋）；逗號分方法組（與指引內文衝突） |
| **D2** 稽核人員在哪一層 | 計畫概要填整場名單，每條 AO 預帶、可覆寫；Excel 空白＝沿用 | 只在概要層看不出這條誰查；只在 AO 層要每列重填 | 只在概要層；只在 AO 層 |
| **D3** 受評對象分幾欄 | 受評人員一欄、受評系統元件／資產一欄 | 現況人員與元件混在一個下拉，決策者點名不好用；元件與資產在 OSCAL 同為 subject | 維持一欄；拆三欄 |
| **D4** 方法詞或人名對不到清單 | 匯入預覽在那一格就地對應到既有選項或略過；同詞一次對應全部生效；全部處理完才能確認。選單由管理員在後台維護 | 直接擋會卡住顧問；自動新增會讓錯字變選項 | 直接擋下；匯入自動新增 |
| **P1** 第 2 次內稽的範圍 | 上一次內稽 AR 中判「不合格」的 AO，以及**沒有任何判定**的 AO，其所屬的整條控制項帶入；判「合格」的不帶。覆核輪用同一份函式、同一條規則 | 整條控制項帶入讓顧問看得到前後脈絡，且與 FR-140、覆核輪既有粒度一致；沒判定代表沒人查過，不帶就從頭到尾漏掉，到外稽才爆 | 只留不合格的 AO；不帶未判定（現況） |
| **P2** 報告稿附件表 | 多印「受評對象」「稽核人員」兩欄，匯出時可勾選不印 | 顧問交報告時這兩欄有用；亞航原表沒有，所以給關閉選項 | 不印 |
| **P3** 方法選單 | 經 `system_menus` seed migration：「檢視」改名「檢視／現場觀察」，不新增選項（決策者 2026-10-16 裁甲） | 選項是客戶與顧問的事，進 `system_menus` 不寫死；DEV 詞庫已把「現場實地觀察」對到「檢視」 | FE 或 BE 寫死選項 |
| **頁籤名** | 「計畫概要」 | 「場次」決策者看不懂；「稽核大綱」易誤會成目錄；「計畫說明」太空泛 | 場次、稽核大綱、計畫說明 |
| **稽核單位** | 單行，只填公司名稱；報告稿印成「由○○協助辦理。」 | 決策者：使用者只會填公司名稱 | 多行文字 |
| **內外稽核** | 每一次稽核各自一份 AP；頁首麵包屑帶「第 N 次 內部／外部」可切同輪其他次（已結束唯讀、未開始不可點）；AP 內容不放內外欄位 | FR-140 已把 AP 掛在稽核次；要分的是進入點不是內容 | AP 內加內外欄位 |
| **AR** | 另開 FR 改逐 AO 判定與匯出；本案只把 AR「從 AP 讀方法」改逐 AO（AR 逐 AO 已另開 FR-142） | 範圍控制；Excel 的佐證、稽核結果兩欄等 AR 案接上 | 本案一併改 AR 畫面 |

### 實作後改判 {#revised nav="改判"}

實作與決策者複測期間定下或推翻的規則。**與上表或下方內文衝突時，以本節為準。**

| 項目 | 定案 | 理由 | 取代／被排除 |
|---|---|---|---|
| **A5** docx 匯入與逐 AO 衝突 | docx 匯入只寫計畫概要與稽核人員，**不碰行程與 AO** | A4 與 B1 各自驗收都過，合在一起時 docx 匯入呼叫舊 `set_tasks` 先刪後寫，把逐 AO 資料清掉且無錯誤訊息 | 取代 A4 原本「確認時一併寫行程」；被排除：匯入後重建 AO（會覆蓋顧問已填的內容） |
| **B5** 受評人員當場新增 | 存在這份 AP 自己的 metadata party，角色 `assessed`；同名回既有那個人；不寫回 SSP。稽核人員選項依角色過濾，受評人員不會出現在稽核人員下拉 | AP 讀的是 SSP 凍結快照，且稽核期間資源庫鎖住不能寫 | 被排除：加進 SSP |
| **P3** 方法選單 | 決策者裁甲：「檢視」改名「檢視／現場觀察」，不新增第七項 | 與 NIST 三法對得起來、選項不重複 | 取代驗收第 8 條「看得到『現場觀察』」→ 改為看得到「檢視／現場觀察」 |
| **頁籤改名** | 第二個頁籤「評估目標」改名 **「查核項目規劃」**（en「Assessment Item Planning」）；Excel 第三頁同名，匯入時新舊頁名（「評估目標」）都認 | 這頁是逐條排「用什麼方法查、查誰、誰去查」，「評估目標」易誤會成只列 AO | 畫面裡指單一 AO 的「評估目標」字樣（如「已完成 12／30 項評估目標」、Excel 欄名）不改 |
| **拿掉「控制項預設」** | 查核項目規劃頁點控制項時，**沒有**控制項預設區塊；只有實務要求說明、AO 一覽、上一項／下一項。系統不存控制項層，Excel、總覽、Word 附件表也都只有逐 AO | 「控制項預設」只是不存檔的一鍵複製，畫面上看起來像可設定控制項層，讓決策者以為有存、進而要求四處都要有控制項列 | 取代 B2「控制項預設套用」與驗收第 4 條；曾做的 Excel「整個控制項」列已撤回。若日後要控制項層，要另案設計「存不存、AO 是否沿用、改了是否同步」 |
| **附件表稽核人員** | AO 沒有個別覆寫稽核人員時，Word／PDF 附件表印計畫概要名單（與畫面 `effectiveAuditors` 同規則）；Excel 不變，空白＝沿用 | 原本只印覆寫值，沒覆寫的列整欄空白 | — |
| **Excel 匯入對不到的人** | 受評人員與稽核人員都可「當場新增」；**預設就是新增**，橫幅顯示「將新增 N 位人員」；稽核人員確認時 AP 有同名非受評人員就沿用並設成 assessor，否則新建。元件／資產與查核方法仍要手動對應 | 決策者：「沒有就增加」「沒有對應的都幫我標注新增預設」 | 取代 D4 中「人名對不到就地對應到既有選項或略過」的人員部分；方法詞照 D4 |
| **匯入預覽篩選** | 進出預覽一律停在「全部」 | 自動切「待處理」時，若待對應只在計畫概要頁籤，查核項目規劃左樹整片空白，看起來像匯入失敗 | — |

---

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

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

| 表 | 現況 | 本案動作 |
|---|---|---|
| `assessment_plans`（`oscal_ap.py:26`） | `uuid`、`metadata_id`、`status`、`import_ssp_id` | 不改 |
| `ap_reviewed_controls`（`:22`） | `control_selections` 必填；`control_objective_selections` 從未寫入，只在 clone 時原樣搬 | B1 寫入本 AP 全部 AO 的 part_id |
| `ap_assessment_activities`（`:23`） | `steps` JSONB（`{uuid,title,description,props[method]}`）、`related_controls`（只寫 `control-selections`）、`responsible_roles` | B1 一條 AO 一個 activity |
| `ap_tasks`（`:24`） | `timing`、`associated_activities`、`subjects` 等 JSONB | A1 一份 AP 固定一個 task 放日期起迄；B1 用 `associated_activities[].subjects` 放每條 AO 的受評對象 |
| `ap_task_subjects`、`ap_task_participants`（`:25`） | FR-040 行程層連結表 | 新頁不再寫；表與舊資料保留 |
| `ap_assessment_subjects` | AP 層受評對象 | 保留，存檔時由 AO 聯集回填（沿用 FR-040 做法） |
| `oscal.parties` | 稽核人員是 person 型 party，props `responsible-role=assessor`（`assessment_plan_app_service.py:70-71`） | A1 稽核單位存 organization 型 party |
| 無表：`terms-and-conditions` | FR-037 差距審查時略過 | A1 新增 |

套件服務 `pkg/jedi-oscal-v2/app/service/ap/assessment_plan_service.py`（215 行）的 `set_reviewed_controls`（:113）、`set_assessment_subjects`（:153）、`set_tasks`（:184）一律整批先刪後寫，task uuid 每次重產。

jedi-oscal-v2 在主專案目前是 pin `2.6.0`（`pyproject.toml:85`），path dependency 註解在 `:139`。要改套件的卡先切 path、收口時還原 pin 並發版。jedi-compliance-audit 已是 path（`:124`，pin 在 `:70` 註解）。

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

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 端點 `api/project/routes/audit_round_route.py:144-205` | `GET /audit-round/<round_uid>/ap`、`PUT /ap/<ap_uid>/reviewed-controls`、`…/assessment-subjects`、`…/tasks`、`GET|POST …/parties` | A1 加 `GET|PUT /ap/<ap_uid>/overview`；A3 加 `GET /ap/<ap_uid>/export?format=docx|pdf|xlsx|zip`；B1 加 `GET|PUT /ap/<ap_uid>/objectives`；B3 加 `POST /ap/<ap_uid>/xlsx-imports`、`GET|DELETE /ap-xlsx-import/<parse_uid>`、`POST /ap-xlsx-import/<parse_uid>/confirm` |
| `app/flow_control/service/assessment_plan_app_service.py`（750 行，近上限） | `_METHOD_PROP`（:68）、`_methods_to_props`（:115）、指引放 steps（:130-165）、`related_controls` 只到控制項（:169-178）、`_ap_detail`（:247）、`clone_ap_for_round`（:330）、`set_tasks`（:583）、`resolve_ap_and_check_auditor`（:190） | 新邏輯一律新檔，原檔一行呼叫；權限沿用 `resolve_ap_and_check_auditor`（auditor／manager，過了規劃階段唯讀） |
| AR 反推 AP `pkg/jedi-compliance-audit/app/service/assessment_result_app_service.py:303`（檔 860 行已超標） | 把 activities 反推成 `{control_id: {methods, guidance, subjects}}` | B1 改逐 AO，新邏輯開新檔 |
| 縮範圍 `assessment_result_app_service.py:360` `narrowed_control_ids_from_parent` | 只收 `target_status_state == "not-satisfied"` 的 AO 所屬控制項（:376-381） | B4 改成「不合格或沒有 finding」 |
| 共用 clone `pkg/jedi-compliance-audit/app/service/audit_round_pass_app_service.py:145` `clone_ap_narrowed_to_not_met` | FR-140 抽出，覆核輪與第 N 次內稽共用 | B4 確認 clone 帶 AO 層資料 |
| FR-043 docx 匯入 `pkg/jedi-compliance-audit/app/service/ap_docx_import_app_service.py`（376 行） | adapter 已解析 basis／objective／audit_org／methodology／scope／audit_date（`ap_report_parser/base.py:13-31`），全部塞進每筆 task 的 description（:288-336） | A4 改寫計畫概要 |
| AO key `pkg/jedi-compliance-audit/app/service/ao_derivation.py` | part_id，控制項無 AO 時退回 control_id | 全案沿用 |

### ③ 可沿用

| 能力 | 位置 | 用法 |
|---|---|---|
| SSP docx／pdf 匯出 | `app/oscal/service/export/ssp_export_app_service.py:27`、`ssp_docx_generator.py`（237 行，docxtpl 範本＋python-docx 追加）、`ssp_libreoffice_converter.py:57`（讀 `LIBREOFFICE_CMD`，逾時 60 秒）、範本由 `scripts/generate_ssp_docx_template.py` 產 | A3 照同套路 |
| 公式注入防護 | `jedi_common/utils/export_safety.py` `set_text_cell`、`neutralize_formula` | A3、B3 所有寫入儲存格 |
| SSP 控制項 Excel 隱藏 A 欄 part_id 慣例 | `app/oscal/service/ssp_control_impl_import_service.py`（`export_excel` :105、`confirm_import` :375） | B3 |
| AP docx 匯入的 parse job 暫存 | `pkg/jedi-compliance-audit/infra/model/ap_docx_parse_job.py`（`ap_docx_parse_jobs` 表） | B3 照同模式暫存解析結果 |
| FE 匯出 | `src/composables/useSspDocumentExport.js`（`downloadBlob` :90） | A3 |
| FE 稽核次 | FR-140 卡 10 已出「列稽核次」API 與 AP／AR／POA&M 讀取可指定 pass | A2 麵包屑 |

### ④ FE

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| `src/views/project/RoundApAuthoringView.vue`（1000 行，超標） | 以行程為單位 | 不改；新頁上線後在 B2 收尾時刪 |
| 路由 `src/config/router/index.js:1118` `project-ap-authoring-round` | 指向舊頁 | A2 改指新頁 |
| API 常數 `src/config/api/api.js:212-221` | AUDIT_ROUND、AP_AUTHORING | 各卡補自己的端點 |
| i18n `src/config/locales/i18n/zh-tw/ap-authoring.json` | 舊頁用 | 新開 `ap-plan.json`（zh-tw、en） |

---

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

### 資料模型與 OSCAL 落點

| 畫面欄位 | OSCAL 路徑 | 存哪 |
|---|---|---|
| 依據、目的、稽核範圍、稽核方法論 | `terms-and-conditions.parts[]`（name＝`basis`／`objective`／`scope`／`methodology`） | 新表 `oscal.ap_terms_and_conditions` |
| 稽核單位 | `metadata.parties[type=organization]`＋`responsible-parties[role=assessor]` | `oscal.parties` |
| 稽核日期起迄 | `tasks[0].timing.within-date-range{start,end}` | `ap_tasks.timing` |
| 計畫概要稽核人員 | `metadata.parties[type=person]`，props `responsible-role=assessor` | `oscal.parties`（既有做法） |
| 每條 AO 的方法與指引 | `activity.steps[]`（description＝指引，props `method`） | `ap_assessment_activities.steps` |
| AO 本身 | `activity.related-controls.control-selections[].include-controls[].control-id`＋`control-objective-selections[].include-objectives[].objective-id`＝part_id | `ap_assessment_activities.related_controls` |
| 每條 AO 的受評人員、元件／資產 | `tasks[0].associated-activities[activity-uuid].subjects[]`（type＝`party`／`component`／`inventory-item`） | `ap_tasks.associated_activities` |
| 每條 AO 的稽核人員（覆寫時） | `activity.responsible-roles[role=assessor].party-uuids`；空＝沿用計畫概要 | `ap_assessment_activities.responsible_roles` |
| 第 N 次 內部／外部 | FR-140 稽核次，AP props `audit_nature`、`nature_seq` | 讀，不寫 |
| ~~控制項預設~~ | 已拿掉（見「實作後改判」） | — |

#### 新表 `oscal.ap_terms_and_conditions`

| 欄 | 型別 | 說明 |
|---|---|---|
| `id` | bigserial PK | |
| `assessment_plan_id` | bigint FK → `oscal.assessment_plans.id` ON DELETE CASCADE | |
| `part_name` | varchar(40) NOT NULL | `basis`／`objective`／`scope`／`methodology`；UNIQUE(assessment_plan_id, part_name) |
| `title` | text | 可空 |
| `prose` | text | |
| `sort_order` | int NOT NULL DEFAULT 0 | |
| `props`、`links` | jsonb | |
| `remarks` | text | |
| `created_at`、`updated_at`、`created_user`、`updated_user` | | 同 FR-040 表 |

主線 migration `scripts/sql/2026-10-16-fr141-ap-terms-and-conditions.sql`，比照 `2026-06-21-fr040-ap-task-link-tables.sql`（IF NOT EXISTS、GRANT `cm_app`、sequence 權限、`INSERT schema_migrations`）。新增主線 migration → 出貨基線待重產。

套件 model 新增 `pkg/jedi-oscal-v2/infra/model/ap/` 對應 ORM、entity、repo、service 方法 `set_terms(ap_id, parts)`／`get_terms(ap_id)`（以 part_name upsert，不先刪後寫）。

#### 一份 AP 一個 task

A1 起，新頁寫入時一份 AP 只保留一個 task（type＝`action`，title＝「稽核計畫」），放日期起迄與 associated_activities。舊資料可能有多個 task：讀取時取第一個 task 的 timing 當日期起迄；第一次以新頁存檔時合併成一個 task（其他 task 的 associated_activities 搬過來）。

🔴 動手前 grep task uuid 的下游引用（wecm、AR、job、`ap_task_subjects`、`ap_task_participants`）；`set_tasks` 先刪後寫會重產 uuid，任何拿 task uuid 當鍵的地方都會斷。B1 改 activities 寫法時對 activity uuid 做同一件事——**activity 以 AO part_id 做自然鍵 upsert，不先刪後寫**，uuid 才穩定。

### 讀寫 API

| 端點 | 方法 | 內容 |
|---|---|---|
| `/ap/<ap_uid>/overview` | GET | `{basis, objective, scope, methodology, audit_org, start, end, auditors:[{party_uuid,name}], pass:{pass_no, audit_nature, nature_seq}, readonly}` |
| `/ap/<ap_uid>/overview` | PUT | 同上可寫欄位；auditors 傳 party_uuid 陣列（新增人員沿用既有 `POST /ap/<ap_uid>/parties`） |
| `/ap/<ap_uid>/objectives` | GET | `{controls:[{control_id, title, practice, description, objectives:[{part_id, label, text, methods:[{method, guidance}], people:[party_uuid], assets:[{type, uuid}], auditors:[party_uuid]|null, inherited_from_pass:int|null}]}], options:{methods, people, assets, auditors}}` |
| `/ap/<ap_uid>/objectives` | PUT | `{objectives:[{part_id, methods, people, assets, auditors}]}`，只送有改的 AO 也可（以 part_id upsert） |
| `/ap/<ap_uid>/export?format=docx|pdf|xlsx|zip&print_subjects=1&print_auditors=1` | GET | 檔案下載 |
| `/ap/<ap_uid>/xlsx-imports` | POST multipart | 回 `{parse_uid, diff}` |
| `/ap-xlsx-import/<parse_uid>` | GET／DELETE | 取回或丟棄解析結果 |
| `/ap-xlsx-import/<parse_uid>/confirm` | POST | `{method_mapping:{詞: method_key|"__skip"}, name_mapping:{名: party_uuid|"__skip"}}` |

全部寫入端點在 app service 層呼叫 `resolve_ap_and_check_auditor`，GET 走既有讀取守門。error code 進 `common/code/grc_error_code.py`，前綴 `GRC_`。

### 計畫概要頁籤（A2）

照雛形 `prototype/ap-authoring.html` 計畫概要頁籤：依據、目的、稽核範圍、稽核方法論為多行文字框；稽核單位單行（提示「只填公司名稱」）；稽核日期起迄兩個日期時間；稽核人員多選可當場新增。頁首：麵包屑「專案 › 第 N 輪 › 第 N 次 內部／外部稽核 › 稽核計畫」，稽核次膠囊點開列同輪全部稽核次（目前、已結束唯讀、未開始不可點），讀 FR-140 卡 10 的列稽核次 API；稽核性質為 NULL 時顯示「未標示」。頁籤「計畫概要」「評估目標」，評估目標頁籤在 B2 前先放「建置中」佔位。

新頁檔：`src/views/project/RoundApPlanView.vue`（殼）、`src/components/grc/ap-plan/ApPlanOverviewTab.vue`、`ApPassBreadcrumb.vue`、composable `src/composables/useApPlan.js`。

### 評估目標頁籤（B2）

照雛形評估目標頁籤：左側樹（控制族群→控制項，完成狀態點、搜尋、篩選「全部／尚未填／已覆寫人員」、完成計數）；右側一次一條控制項：實務要求與執行說明唯讀、~~控制項預設區塊~~（已拿掉，見「實作後改判」）；每條 AO 一張卡（方法組不限數量、受評人員、受評元件／資產、稽核人員預帶概要名單可覆寫可還原、「承襲自第 N 次」標籤改過即消）；上一條／下一條只在篩選後可見的控制項間跳；「總覽（唯讀表格）」子檢視。

「填好」＝至少一個方法，且受評人員或元件／資產至少一個。

新檔：`ApPlanObjectivesTab.vue`、`ApObjectiveTree.vue`、`ApObjectiveControlForm.vue`、`ApObjectiveCard.vue`、`ApMethodGroups.vue`、`ApObjectivesOverview.vue`、composable `useApObjectives.js`。每支 Vue 檔 800 行內、script 函式 40 行內。

### Excel 契約（B3，D1）

四頁，匯出的檔就是匯入範本；空 AP 匯出就是空白範本。

| 頁 | 內容 |
|---|---|
| 填寫說明 | 每欄怎麼填、格內換行快捷鍵（Mac：Option＋Enter；Windows：Alt＋Enter）、匯入流程 |
| 計畫概要 | 直向「欄位／內容」：依據、目的、稽核單位、稽核日期起、稽核日期迄（`YYYY-MM-DD HH:MM`）、稽核範圍、稽核方法論、稽核人員（換行或分號） |
| 評估目標 | 見下表 |
| 可選清單 | 查核方法、受評人員、受評元件／資產、稽核人員四欄，匯出時由 SSP 與 `system_menus` 帶入 |

「評估目標」頁：

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

解析一律串流讀、列數上限（沿用 SSP 匯入的上限常數，先 grep）、檔案大小上限、只收 .xlsx；儲存格寫出一律經 `export_safety`。解析結果暫存（比照 `ap_docx_parse_jobs`，新表或沿用同表加 kind 欄由卡上先 grep 決定），確認時以 part_id upsert，只寫有變更的 AO。

預覽在正式頁的「預覽模式」呈現（雛形 `prototype/import-preview.html`）：頂部橫幅計數＋確認匯入；左樹「待處理」「有變更」徽章；被改欄位黃底、原值灰字；未知方法詞與人名就地對應；全部對應完才能確認；「略過的列」對話框。

### 報告稿（A3）

docx 範本 `app/flow_control/templates/ap/ap_report_template.docx`（由腳本產，比照 SSP），標題「{框架} 第 N 次{內部／外部}稽核計畫」（稽核性質 NULL 時省略「第 N 次…」），段落：一 依據、二 目的、三 稽核單位（「由{稽核單位}協助辦理。」＋稽核人員一句）、四 稽核日期（起迄同日印「YYYY年MM月DD日上午HH:MM-HH:MM」，跨日印「…至…」）、五 稽核範圍、六 稽核方法、七 稽核結果（佔位「（評估結果完成後帶入）」）；附件「稽核檢查紀錄表」：序號、索引號、實務要求、執行說明、評估目標、查核方法：指引（一行一組）、受評對象（P2 可關）、稽核人員（P2 可關）。B1 之前 AP 尚無逐 AO 資料，附件表以控制項列出、方法欄空白；B1 完成後自動帶入。

pdf 經 `SspLibreOfficeConverter`。zip＝docx＋xlsx（xlsx 要 B3 完成；B3 之前 zip 選項隱藏）。

### 縮範圍規則（B4，P1）

`narrowed_control_ids_from_parent`（`assessment_result_app_service.py:360`）改為：

```
control_ids   = 上一次 AP 的 in-scope 控制項
ao_to_control = 上一次 AP 的 AO→控制項對照
judged        = { finding.target_id: finding.target_status_state }  for 上一次 AR 的 findings
帶入控制項 = { ao_to_control[ao] for ao in ao_to_control if judged.get(ao) != "satisfied" }
```

即不合格、待判（`pending`）或沒有 finding 的 AO 所屬控制項帶入。DEV 2026-10-16 實查 `oscal.assessment_findings.target_status_state` 三種值：`pending` 113、`satisfied` 58、`not-satisfied` 20——**只有明確 `satisfied` 才排除**，`pending` 等同未判定，要帶入。這支同時供覆核輪（`launch_reverify`）與第 N 次內稽使用，覆核輪一起變。新邏輯開新檔（`assessment_result_app_service.py` 已 860 行），原處一行委派。

clone 時 AO 層資料（activities、associated_activities.subjects、responsible_roles、terms）一併複製，被縮掉的控制項的 AO activity 不複製；複製來的 AO 在 activity props 記 `inherited-from-pass`＝來源 pass_no，FE 據此顯示「承襲自第 N 次」，使用者改過該 AO 即清掉此 prop。

### 方法選單 seed（B4，P3）

DEV 選單（2026-10-16 實查 `system_menus` group `AUDIT_METHOD`）：檢視 `EXAMINE`、訪談 `INTERVIEW`、測試 `TEST`、文件檢查 `DOCUMENT_REVIEW`、電腦設定比對 `COMPUTER_CONFIG_COMPARE`、門禁系統設定比對 `ACCESS_CONTROL_CONFIG_COMPARE`。

🔴 **開工前先回寫問決策者**：docx 匯入詞庫 `pkg/jedi-compliance-audit/app/service/import_adapter/method_keywords.py:11` 已把「現場實地觀察」對到 `EXAMINE`（檢視）。NIST 800-171A 的三法是 Interview／Examine／Test，「現場觀察」就是 Examine 的一種。兩條路：
- **甲（建議）**：不新增選項，把 `EXAMINE` 的顯示名改成「檢視／現場觀察」，詞庫補「現場觀察」。選項不重複、與 NIST 三法對得起來。
- **乙**：新增第七項「現場觀察」`ONSITE_OBSERVATION`，`phase=seed` migration（`ON CONFLICT DO NOTHING`，先例 `2026-05-22-seed-oscal-ssp-roles.sql`），詞庫「現場實地觀察」改對新項。

兩案都同步 B3 匯入比對與 FE i18n；改顯示名也走 seed migration（UPDATE 該列 key），不寫死。`phase=seed` 不需重產出貨基線。

---

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

1. **計畫概要**：DEV 開一輪走到稽核計畫填寫站，進新稽核計畫頁，麵包屑顯示第 N 次 內部／外部。計畫概要頁籤填六段、稽核單位「鈊安資安顧問」、日期起迄、兩位稽核人員，儲存、重整還在。DB：`oscal.ap_terms_and_conditions` 該 AP 四列；`oscal.parties` 多一筆 organization。
2. **報告稿**：匯出 docx，Word 開看到標題帶稽核次、三段寫「由鈊安資安顧問協助辦理。」、四段日期格式正確、第 7 段佔位、附件表；匯出 pdf 內容相同；關掉 P2 兩欄再匯出，附件表少兩欄。
3. **docx 匯入**：匯入亞航報告稿，計畫概要六段有值、行程說明不再重複貼長文。
4. **逐 AO 填寫**：查核項目規劃頁籤逐條 AO 填方法與人員（控制項預設已拿掉），改其中一條 AO 的稽核人員，存檔重整還在；左樹完成數正確；總覽表格內容一致。DB：每條 AO 一個 activity，`related_controls` 帶 `control-objective-selections`。
5. **AR 不壞**：同一輪進稽核執行，AR 頁的方法顯示與改動前一致（逐 AO）。
6. **Excel**：匯出 Excel，四頁齊全、A 欄隱藏；在「評估目標」頁改兩格、把一個方法寫成「文件檢視」、一個人名打錯，匯入：預覽黃底標出兩格、未知方法與人名就地對應，對應完確認，畫面更新。
7. **第 2 次內稽範圍**：第 1 次內稽判 1 條 AO 不合格、另一條控制項的 AO 全部不判，走完改善回規劃再到稽核計畫填寫站：第 2 次 AP 包含這兩條控制項、其餘不在；帶進來的 AO 顯示「承襲自第 1 次」，方法與人員跟第 1 次一樣。覆核輪同規則。
8. **選單**：後台方法選單看得到「檢視／現場觀察」（P3 裁甲，見「實作後改判」）；匯入含「現場觀察」的 Excel 對到該項、不再被列為未知。

---

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

| 卡 | 做什麼 | 碰哪裡 | 套件 | 依賴 | 驗收 |
|---|---|---|---|---|---|
| A1 | 計畫概要存得起來：新表＋套件 model／repo／service＋主專案 app service、route、schema；稽核單位 organization party；單一 task 日期起迄 | migration、`pkg/jedi-oscal-v2/infra/model/ap/`、`app/service/ap/`、主專案新檔 `app/flow_control/service/ap_overview_app_service.py`、route | jedi-oscal-v2（切 path） | — | 驗收 1 的 API 部分 |
| A2 | 新稽核計畫頁殼＋計畫概要頁籤＋稽核次麵包屑；路由改指 | FE 新檔、路由、api 常數、i18n | — | A1 | 驗收 1 |
| A3 | 報告稿 docx／pdf 匯出（含 P2 勾選）＋FE 匯出選單 | 主專案 `app/flow_control/service/ap_report_export_service.py`、`ap_docx_generator.py`、範本與產範本腳本、route；FE 匯出 composable | — | A1 | 驗收 2 |
| A4 | FR-043 docx 匯入確認時寫計畫概要 | `pkg/jedi-compliance-audit/app/service/ap_docx_import_app_service.py` | jedi-compliance-audit | A1 | 驗收 3 |
| B1 | AP 逐 AO 存：activity 以 part_id upsert、objectives 讀寫 API、assessment_subjects 聯集回填、AR 讀方法改逐 AO | `pkg/jedi-oscal-v2/app/service/ap/`、主專案新檔 `ap_objectives_app_service.py`、route；`pkg/jedi-compliance-audit` AR 讀取新檔 | jedi-oscal-v2、jedi-compliance-audit | — | 驗收 4 API 部分、驗收 5 |
| B2 | 評估目標頁籤（左樹右表單、預設套用、篩選、總覽、承襲標籤）；舊頁刪除 | FE 新檔；刪 `RoundApAuthoringView.vue` | — | B1、A2 | 驗收 4 |
| B3 | Excel 四頁匯出＋解析＋暫存＋預覽模式＋確認 | 主專案 `ap_workbook_export_service.py`、`ap_workbook_import_service.py`、route；FE 預覽模式 | — | B1（預覽模式畫面掛 B2 元件） | 驗收 6 |
| B4 | 縮範圍含未判定（P1）＋clone 帶 AO 層與承襲 prop＋「現場觀察」seed（P3） | `pkg/jedi-compliance-audit` 縮範圍新檔、`assessment_plan_app_service.clone_ap_for_round`、seed migration、method_keywords、FE i18n | jedi-compliance-audit | B1 | 驗收 7、8 |

互斥：A1 與 B1 都改 jedi-oscal-v2 AP service，但檔案不同（A1 新 terms 方法、B1 改 activities）；同時派時各自只 add 自己的檔。B3 與 B2 都碰 FE 稽核計畫頁，B3 的預覽模式等 B2 元件到位再做畫面部分。B4 改 `clone_ap_for_round` 時 B1 必須已完成。

收口（不開卡，母卡收尾時做）：jedi-oscal-v2 與 jedi-compliance-audit 發版、還原 pin（jedi-compliance-audit 與 FR-140 同批發）；出貨基線重產（A1 的主線 migration）；`docs/spec-site/current/audit-execution/ap-authoring.md` 改寫；e2e 另開一棒。

---

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

| # | 坑 | 踩到會怎樣 | 對策 |
|---|---|---|---|
| 1 | `set_*` 先刪後寫、uuid 重產 | 下游拿 task／activity uuid 當鍵的全斷，且無錯誤訊息 | A1、B1 動手前 grep 引用；terms 以 part_name upsert、activity 以 part_id upsert |
| 2 | AP 掛在稽核次（FR-140） | 讀輪次四槽位會讀到別一次的 AP | 新碼一律經 FR-140 的 pass domain service 取目前稽核次 |
| 3 | 縮範圍函式是覆核輪與第 N 次內稽共用 | 改 B4 時覆核輪範圍一起變 | 這是預期行為（P1 決策適用兩者）；B4 回寫要寫明，複測覆核輪 |
| 4 | `target_status_state` 有第三個值 `pending`（DEV 113 筆，比判定過的還多） | 照現況只收 not-satisfied，待判的整批漏掉 | 規則寫成「只有明確 satisfied 才排除」 |
| 5 | 控制項沒有 AO 時 key＝control_id | Excel 只有一列、activity 的 objective-selections 為空 | 沿用 `ao_derivation.py`；匯出匯入都處理此退路 |
| 6 | 指引內文含冒號 | 「文件檢查：核對 A：B 表」被切錯 | 只以每行**第一個**冒號切 |
| 7 | 超標檔：`assessment_result_app_service.py` 860、`RoundApAuthoringView.vue` 1000、`ProjectAuditorOverview.vue` 2128 | 往裡加會被 reviewer 標阻斷 | 新邏輯全開新檔，原檔一行委派 |
| 8 | jedi-oscal-v2 目前是 pin | 改了套件但主專案吃 Nexus 版，改動不生效 | A1、B1 開工先切 path dependency（`pyproject.toml:139`），path 改動不 commit |
| 9 | Excel 可選清單是匯出當下快照 | 匯出後 SSP 新增人員，舊檔寫新名字會被當未知 | 預覽就地對應兜底（D4） |
