---
title: "事前準備資料 — 設計文件 (FR-161)"
brand: "Guidant AI · **FR-161** 事前準備資料"
eyebrow: "FR-161 · Pre-engagement Materials — 設計文件 · 2026-10-23"
h1: "事前準備資料"
subtitle: "客戶有一頁可以上傳；大類在系統設定、跟著公司；顧問在資源庫程序書池挑選引用；稽核開始才鎖一份副本"
lede: "顧問訪談前請客戶準備的資料，在系統裡有一頁**事前準備資料**：客戶把檔案丟進大類格子、可以上傳新版、點名稱就能預覽。檔案屬於公司，不屬於任何框架。顧問在合規資源庫的**程序書池** tab 按「從事前準備資料選用」**挑選引用**，選了的才進這個資源庫，跟著客戶的新版更新；不要的「移出」。程序書池既有的存版、還原、複製資源庫、送審凍結四條複製路徑全部照舊生效。本文件寫定案規則、為什麼這樣定、踩到會怎樣。"
chips: [
  {text: "設計定案（D1～D10）；D9 動 jedi-compliance-audit，決策者已同意", kind: ok},
  {text: "同名比對去版次與日期尾巴（D8）", kind: accent},
  {text: "雛形：prototype/index.html（三頁）", kind: accent},
  {text: "不做審閱、留言、通知", kind: plain}
]
footer: "FR-161 · 事前準備資料 — 設計文件 · 2026-10-23 · 討論稿 discussion.md · 雛形 prototype/ · 前作：FR-139 資源庫版本與送審、FR-160 權限分層"
---

| 項目 | 內容 |
|---|---|
| 狀態 | 設計定案；實作卡 CM-3041～3049 已驗收，CM-3050（挑選引用、同名比對放寬）待派 |
| 日期 | 2026-10-23 |
| 討論稿 | [discussion.md](discussion.md) |
| 雛形 | [prototype/index.html](prototype/index.html)（事前準備資料頁、程序書池 tab 重新設計、系統設定大類） |
| 前作 | FR-139（資源庫版本、送審存版、複製資源庫、一對一）、FR-147（資源庫頁）、FR-160（權限分層判準） |

## 需求背景與端到端流程 {#why nav="背景"}

企業導入合規框架時，顧問會先請客戶準備手上有的資料：網路架構、組織架構、資產盤點、政策文件與程序書。顧問訪談前先看過，訪談完回來才整理 SSP。這些檔案現在走信件與雲端硬碟，系統裡沒有落點，顧問整理 SSP 時還要自己再傳一次進程序書池。

本案只做「一個能上傳的地方」，加上顧問在資源庫挑選要引用哪幾份。看過沒、留言、通知都是顧問的作業習慣，系統先不介入。

```{.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'}}}%%
sequenceDiagram
  participant A as 管理員／顧問
  participant S as 系統設定 事前準備資料大類
  participant K as 客戶
  participant P as 事前準備資料頁（公司層）
  participant L as 資源庫程序書池
  participant R as 稽核輪
  A->>S: 第一次開：帶入四個起始大類；增刪改名排序
  K->>P: 丟檔案進大類格子（不會自動進任何資源庫）
  A->>L: 從事前準備資料選用：勾選的才在這個資源庫加一列
  K->>P: 同一份資料上傳新版
  P->>L: 已引用的那列換成新版檔案
  A->>L: 移出：刪掉這個資源庫的那一列
  A->>R: 送審
  L->>R: 既有 clone 把程序書池整批複製（含已引用的列，指當時的檔案）
  K->>P: 之後再傳的不影響這次稽核
```

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

整案一句話：**客戶有一頁可以把檔案丟進大類格子；大類在系統設定、跟著公司；顧問在資源庫程序書池挑選引用，選了的照大類分段出現、跟著新版更新；稽核開始時既有的程序書池複製把它們一起鎖進稽核輪。**

| 階段 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| **一、大類與事前準備資料頁** | 大類、資料、版本三張表；系統設定「事前準備資料大類」表格頁；事前準備資料頁（上傳、上傳新版、同名詢問、預覽、看舊版、刪除、標過時、下載全部）；側選單入口與資源庫列表入口；兩顆權限 | migration、BE 模組、兩個 FE 頁 | 到系統設定看到四個起始大類，新增「機房」、把「網路架構」改名；用受稽方帳號登入只看到「事前準備資料」一頁、格子名稱已變；丟一個檔，再丟 `<同名>_v2_2026-10` 被問「當成新版」選是，看到 v2 與可展開的舊版；點名稱開預覽 |
| **二、程序書池挑選引用** | 程序書池列加「大類」「來源」「客戶標過時」三欄；資源庫程序書池「從事前準備資料選用」對話框、「移出」；已引用的列跟著新版、搬大類、刪除、標過時同步；程序書池 tab 照大類分段、顧問上傳選大類；既有資料歸「政策文件與程序書」 | 套件欄位、同步服務、選用與移出端點、FE 程序書池改版 | 客戶上傳後開資源庫程序書池**看不到**；按「從事前準備資料選用」勾 3 份，照大類分段出現；另一個資源庫沒有；客戶上傳新版，引用那份跟著換；「移出」後消失；專案規劃頁唯讀、看到資源庫選的那幾份 |
| **三、稽核鎖定與網路架構圖** | 確認送審、存版、還原、複製資源庫四條路徑都帶上新欄位；被稽核副本引用的事前準備資料不能刪；「網路架構」段的檔可勾「當網路架構圖匯出」寫 `ssp_diagrams` | 套件 clone 補欄位、刪除守門、diagram 勾選 | 送審後客戶上傳新版，稽核計畫頁的程序書仍是舊檔；再開資源庫看到新版；被引用的檔刪除鈕變灰只能標過時 |

依賴：階段二吃階段一的表；階段三吃階段二的欄位。

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

| 編號 | 定案 | 理由 | 被排除的方案 |
|---|---|---|---|
| **D1** 介面 | 另開一頁「事前準備資料」，客戶與顧問看完全同一頁；不放進 SSP 畫面、不放進專案 | 客戶不進 SSP 畫面；專案在這個時間點還不存在 | 放進 SSP 畫面（客戶會迷路）；做成專案任務（事前準備不屬於任何評估目標） |
| **D2** 分類 | 只到大類，沒有子項目、必填、截止日、落點 | 顧問只是進來看看，有分類即可 | 每類下列細項與必填（變成交件清單，與「一個上傳的地方」不符） |
| **D3** 大類歸屬 | 跟著公司（租戶）。系統出貨 `system_menus` 四個起始值，公司第一次讀大類時帶進公司層表，之後只在公司層表增刪改名排序 | 每個顧問的大類不同；一個檔只屬於一個大類，大類若跟著資源庫，同一個檔在第二個框架要再歸一次類 | 跟著系統（全站一套，顧問不能改）；跟著資源庫（每框架一套） |
| **D4** 大類在哪設定 | 系統設定新頁「事前準備資料大類」，表格模式：排序、名稱、說明、資料數、最後更新、操作；新增與編輯開對話框；底下還有資料的不能刪。事前準備資料頁與程序書池只讀 | 設定不該藏在客戶也看得到的頁裡；字典類設定照慣例放系統設定 | 在事前準備資料頁放「管理大類」鈕 |
| **D5** 檔案歸屬 | 屬於公司，不屬於任何框架；公司＝租戶，與專案、資源庫同一套；顧問服務多家客戶＝帳號掛多個租戶、頂欄切換 | 網路架構、組織、資產、政策描述的是公司本身，不因換框架而變 | 屬於某個資源庫（第二個框架要客戶再傳）；另立「導入案」物件（與資源庫一對一重複） |
| **D6** 帶進資源庫 | **顧問挑選引用**：預設不帶入；資源庫程序書池「從事前準備資料選用」勾選才加列，「移出」刪列；四類都當程序書池的檔，不多開 tab | 「引用」的本意是顧問選了哪些、這個框架才有哪些；一個租戶有多家客戶時，自動帶入會讓每家資源庫都被別家資料塞滿 | 預設全部帶入、顧問只能反向排除（決策者在專案規劃頁看到被塞滿後改判）；另開「附件」「網路架構圖」tab |
| **D7** 時效性 | 一份資料可上傳新版，舊版留著可看；規劃中的資源庫跟最新版；超過一年沒更新標「可能過時」，不自動催 | 導入期間人員、網路會異動 | 每次重傳當新資料（同一份資料變多列） |
| **D8** 同名詢問 | 頁面不放提示文字；在某格上傳的檔，若與該格已有資料「去掉版次、日期、副本標記與開頭文件編號後」同名，對話框先問「當成新版／另存一份」，預設當成新版；只詢問、不自動合併 | 拿掉提示條後要有別的防線；真實檔名幾乎都帶版次或日期（`網路架構圖_v4.0_2026-09`），只做完全比對等於永遠不會問；同一份資料變兩列會讓兩份都進稽核副本、舊的刪不掉 | 靠提示文字；完全比對；模糊比對（相似度）——會把不同文件誤判成同一份 |
| **D9** 程序書池怎麼承接 | **被選用的事前準備資料，在那個資源庫程序書池落一列**（`ssp_reference_documents`，`context_type='ssp'`）。程序書池列加三欄：`category_id`（大類）、`pre_engagement_file_id`（來源；顧問上傳為空）、`is_excluded`（客戶標過時，預設 false）。已引用的列跟著上傳新版、搬大類、刪除、標過時同步；沒選用的不落列 | 送審凍結、存版、還原、複製資源庫四條複製路徑都只複製程序書池列，控制項掛載也指向程序書池列；只在讀取時合併顯示的話四條路徑都要各補一套 | 讀取時合併、不落列；另開排除表 |
| **D10** 稽核鎖定與刪除 | 事前準備資料永遠可更新。稽核鎖定沿用既有 `clone_pool_for_snapshot`：送審時程序書池整批複製到凍結 SSP，已引用的列指的是當時的 `file_id`，之後換新版只改規劃中的那列。某份事前準備資料只要有任何一個版本的 `file_id` 被凍結 SSP 的程序書池引用，就不能刪，只能標過時 | 稽核基準不能在稽核中被換掉；刪掉被引用的檔，稽核副本就破洞 | 送審後整個事前準備資料鎖住（導入期間不能更新） |

### D9 動 jedi-compliance-audit（決策者已同意）

`oscal.ssp_reference_documents` 的 ORM model 與複製邏輯在套件 jedi-compliance-audit（現版 1.4.0）裡。

| 動什麼 | 範圍 | 其他使用者的風險 |
|---|---|---|
| model `SspReferenceDocument` 加三個可空欄位 `category_id`、`pre_engagement_file_id`、`is_excluded`（預設 false） | 套件 migration 新增 005 | 三欄都可空或有預設值，舊程式不讀它們就不受影響 |
| `SspDocumentPoolQuery.clone_pool_docs` 複製時帶上三欄 | 一個函式 | 送審凍結會多帶三個欄位；稽核計畫頁讀程序書不看這三欄，行為不變 |
| `list_pool` 回傳三欄 | 一個函式 | 讀取端多出三個欄位，不影響既有使用者 |

主專案還有另一條複製路徑：`ssp_snapshot_extras` 依 `(file_id, description)` 配對池內文件，存版、還原、複製資源庫都經過這裡，要一起補上三欄。開發期間照規範走 poetry path dependency，完成後由決策者發版。

## 現況接入點盤點 {#touch nav="接入點"}

| 元件 | 現況 | 本案動作 |
|---|---|---|
| 程序書池表 `oscal.ssp_reference_documents` | 套件 jedi-compliance-audit；`context_type='ssp'` 是池、`_mappings` 是控制項與評估目標掛載；`file_id` 跨版共用；DEV 現有 57 列池內文件、欄位只有 id／uid／context_type／context_id／file_id／description＋審計四欄 | 加 `category_id`、`pre_engagement_file_id`、`is_excluded`（D9） |
| 程序書池服務 `app/oscal/service/ssp_document_pool_service.py`（249 行） | `list_pool`／`add_to_pool`／`delete_from_pool`／掛載／`clone_pool_for_snapshot` | `add_to_pool` 收 `category_id`；新增排除與恢復；`list_pool` 回大類與來源 |
| 送審凍結 | 套件 jedi-compliance-audit `audit_round_pass_app_service._freeze_ssp` → 主專案 `clone_pool_for_snapshot` → 套件 `SspDocumentPoolQuery.clone_pool_docs`（逐列複製 `file_id`、`description`） | 套件補三欄（D9） |
| 存版、還原、複製資源庫 | `app/module_frame/service/ssp_snapshot_extras.py`（106 行），三條路徑共用 | 補三欄 |
| 送審存版 | `ssp_submit_version_service.save_before_launch`，在 `oscal_stage_handlers` 的 launch 前呼叫 | 不動（經 snapshot_extras 自然帶上） |
| 程序書池 API | `/ssp/<ssp_uid>/document-pool`、`/document-pool/<doc_uid>` | 新增 `/document-pool/<doc_uid>/exclusion`（PUT／DELETE）；POST 收 `category_id` |
| 上傳 | FE 先 `POST /file/upload` 拿 `file_uid`，再送業務 API | 事前準備資料沿用同一條 |
| 程序書池 FE `src/components/grc/DocumentPoolPanel.vue`（346 行，專案頁用）＋ `ModuleFrameDocumentPoolPanel.vue`（資源庫頁用） | 平的清單 | 照大類分段、來源標籤、從事前準備資料選用、移出、上傳選大類；專案頁唯讀 |
| 資源庫編輯頁 `ModuleFrameTemplateEditView.vue`（**1,402 行，已超標**） | 程序書池 tab 嵌 `DocumentPoolPanel` | 只改子元件，不往這支加邏輯 |
| 資源庫列表 `ModuleFrame.vue`（579 行） | 列操作選單 | 加「事前準備資料」入口 |
| 網路架構圖 `oscal.ssp_diagrams` | 套件 jedi-oscal-v2 有 model 與 repo，主專案沒有程式寫它 | 程序書池「網路架構」段的檔勾「當網路架構圖匯出」時寫一筆、取消時刪 |
| 範本 SSP 寫入守門 | `common/authz/ssp.py` 的 `_guard_template_write`：先檢查能力點，再呼叫 `assert_template_writable` → `verify_template_writable` → `verify_module_frame_exists_by_uid(writable=True)`（租戶歸屬、非舊版檢視、稽核中鎖寫都在這支） | 同步服務共用不檢查能力點的 `is_template_writable` |
| 權限 | `public.capabilities`＋`route_capabilities`＋`role_capabilities`（先例 `2026-10-22-fr157-agent-console-capabilities.sql`） | 兩顆新能力點 |
| 選單字典 | `public.system_menus`（先例 `2026-10-22-fr158-seed-pdf-parser-menu.sql`） | group `pre_engagement_category` 四筆 |
| 側選單 | `public.ui_routes` | 新增兩個路由：事前準備資料、系統設定下的事前準備資料大類 |

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

### 資料表

三張新表都在 `compliance` schema，繼承 `TenantScopedMixinModel`。CREATE TABLE 必須直接帶 `tenant_id` 與 `org_unit_id INTEGER REFERENCES public.org_units(id)`。缺 `org_unit_id` 的話，多租戶模式下 INSERT 會 500。新表要 `GRANT ... TO cm_app`，sequence 也要授權。RLS 照其他 tenant-scoped 表：ENABLE＋FORCE，條件是超管或 `tenant_id = ANY(app.allowed_tenant_paths)`。

| 表 | 欄位 | 約束 |
|---|---|---|
| `compliance.pre_engagement_categories` | `id`、`uid`、`name` VARCHAR(100) NOT NULL、`description` TEXT、`sort` INT、`seed_key` VARCHAR(50)（從 `system_menus` 帶進來的才有值）、`tenant_id`、`org_unit_id`、審計四欄 | `UNIQUE (tenant_id, name)` |
| `compliance.pre_engagement_files` | `id`、`uid`、`category_id` FK→categories、`title` VARCHAR(255) NOT NULL、`is_obsolete` BOOL DEFAULT false、`tenant_id`、`org_unit_id`、審計四欄 | 刪大類前查這張表有沒有資料 |
| `compliance.pre_engagement_file_versions` | `id`、`uid`、`pre_engagement_file_id` FK CASCADE、`version_no` INT、`file_id` FK→`upload_files`、`created_user`、`created_at` | `UNIQUE (pre_engagement_file_id, version_no)`；最新版＝`version_no` 最大 |

大類的起始值走「讀時帶入」：公司第一次讀大類時，若公司層表是空的，就從 `system_menus` group `pre_engagement_category` 複製一份進來。不在 migration 裡預先替每家公司建，新租戶建立時也不用另外掛鉤。

### 引用與同步規則（D6、D9）

客戶上傳事前準備資料**不落任何程序書池列**。只有顧問在某個資源庫選用的那幾份，才在那個資源庫的範本 SSP 程序書池各有一列。之後的同步只動「已經有的列」。

| 事件 | 程序書池的動作 |
|---|---|
| 客戶上傳一份新資料 | 不動 |
| 顧問在資源庫「從事前準備資料選用」 | 這個資源庫的範本 SSP 程序書池，每份勾選的資料加一列：`file_id`＝最新版、`category_id`、`pre_engagement_file_id`、`description`＝`title`。已引用的跳過 |
| 顧問「移出」 | 刪掉這一列，連帶刪掉它的控制項與評估目標掛載。有掛載時先跳確認 |
| 客戶上傳新版 | 可編輯資源庫裡已引用的列，`file_id` 換成新版。已凍結的稽核 SSP 與存版快照不動 |
| 客戶搬到別的大類 | 已引用的列 `category_id` 跟著改 |
| 客戶標過時 | 已引用的列 `is_excluded=true`：畫面淡化並標「客戶已標過時」，**不自動移出**（顧問可能已掛在控制項上，由顧問決定）；選用清單不再列出過時的資料 |
| 客戶刪除 | 可編輯資源庫裡已引用的列刪除，連帶刪掛載。有任何版本被凍結 SSP 引用時拒絕刪除（D10） |
| 新建資源庫、第一次開程序書池 | 不動（不補齊） |

`is_excluded` 只代表「客戶標過時」，只由同步寫入，顧問沒有手動設定它的入口。

「可編輯」不另寫判斷，一律走範本 SSP 既有的寫入守門。`common/authz/ssp.py` 的 `_guard_template_write` 會依序做三件事：檢查 `oscal.resource-library.update`、呼叫 `verify_template_writable`（資源庫存在、不是舊版檢視、屬於本租戶）、在同一支裡檢查稽核中鎖寫（FR-139 D19：綁定專案最新一輪不是 `planning` 就 412）。同步服務要的是「這家公司哪些資源庫可寫」這份清單（用來決定新版、搬類、刪除要改哪些已引用的列），所以後兩步是一支不檢查能力點的判斷函式 `is_template_writable`，給守門與同步共用。不能另外寫一套。

同步要寫在事前準備資料 app service 的同一個 `@transaction` 裡，透過 domain service 呼叫程序書池服務，不直接碰程序書池的 ORM。

::: {.callout .warn}
**稽核中的資源庫，同步會跳過**

綁定專案最新一輪不是 `planning`（送審後到結案）時，資源庫整個鎖寫（FR-139 D19），同步不會改它，選用與移出也回 412。稽核輪讀的是送審時 `clone_pool_for_snapshot` 複製出來的凍結 SSP，客戶之後上傳新版，只會改到規劃中資源庫的那一列；鎖住期間換的新版不會回頭改已有的列，等專案回到 `planning`（例如覆核子輪）後由顧問決定要不要移出重選。
:::

控制項「從程序書池選用」的候選清單排除 `is_excluded=true` 的列。已經掛上控制項的列被客戶標過時，掛載保留、畫面標灰，不自動拆。

選用清單的「公司」用**資源庫的 tenant_id**，不是登入者的：RLS 是前綴比對，總公司看得到子公司的資源庫，用登入者的會把總公司的資料列給子公司的資源庫選。

### 既有資料升級

既有程序書池列沒有 `category_id`。讀取時，沒有大類的列一律歸到這家公司 `seed_key='policy'` 的那個大類；那個大類被改名或刪除時，歸到排序第一的大類。不寫回填 migration。原因是既有列分散在很多租戶的很多 SSP 裡，要先等公司層大類表帶入起始值，才有值可以回填，讀時歸類比較單純。

### 權限（FR-160 判準：沒有「專案」對象，屬系統層）

| 能力點 | 給誰 | 守哪些端點 |
|---|---|---|
| `oscal.pre-engagement.upload` | 已持有 `oscal.resource-library.update` 的系統角色＋`IT執行人員`（客戶的 IT 人員要能傳） | 事前準備資料的讀、上傳、上傳新版、刪除、標過時、下載 |
| `oscal.pre-engagement.manage-category` | 已持有 `oscal.resource-library.update` 的系統角色（不給 IT執行人員） | 大類增刪改排序 |

主體域守門用 `common/authz` 的 capability decorator，不另立 helper。程序書池的排除與恢復沿用程序書池既有的守門 `require_manager`。範本 SSP 會自動走上面那條資源庫守門，不用另外加判斷。

能力點的模組前綴用 `oscal`，和資源庫的 `oscal.resource-library.*` 放在一起。DEV 目前的前綴有 ai、asset、detection、evidence、flow、grc、iam、oscal、platform、proj、survey、system、workspace，沒有 governance。

### API

| 方法 | 路徑 | 說明 |
|---|---|---|
| GET | `/pre-engagement/categories` | 大類列表（第一次呼叫時帶入起始值），含每類資料數 |
| POST／PUT／DELETE | `/pre-engagement/categories[/<uid>]` | 新增、編輯（名稱、說明）、刪除（有資料回 409） |
| PUT | `/pre-engagement/categories/order` | 排序 |
| GET | `/pre-engagement/files` | 依大類分組，每份資料帶版本清單、最新版、`is_obsolete`、`is_locked`（是否被稽核引用）、`is_stale`（超過一年） |
| POST | `/pre-engagement/files` | 新增一份資料（`category_uid`、`file_uid`、`title`） |
| POST | `/pre-engagement/files/check-duplicate` | 同名檢查（`category_uid`、`file_name`），比對規則見 D8，回可能是同一份的資料 |
| POST | `/pre-engagement/files/<uid>/versions` | 上傳新版 |
| PUT | `/pre-engagement/files/<uid>` | 改顯示名稱、搬大類、標過時 |
| DELETE | `/pre-engagement/files/<uid>` | 刪除（被稽核引用回 409） |
| GET | `/pre-engagement/files/archive` | 下載全部最新版的 zip |
| GET | `/module-frame/<uid>/reference-documents/pre-engagement-candidates` | 選用清單：這家公司（資源庫的 tenant）未過時的事前準備資料，照大類分組，每份帶最新版與 `is_referenced` |
| POST | `/module-frame/<uid>/reference-documents/pre-engagement-references` | 選用：`pre_engagement_file_uids`，每份在範本 SSP 程序書池加一列（已有的跳過） |
| DELETE | `/module-frame/<uid>/reference-documents/<doc_uid>` | 既有刪除端點；客戶事前準備資料列也可刪（＝移出），連帶刪掛載 |
| PUT／DELETE | `/ssp/<ssp_uid>/document-pool/<doc_uid>/diagram` | 當網路架構圖匯出／取消（寫或刪 `ssp_diagrams`） |

回應中有 `created_user`／`updated_user` 的地方要一併帶 `*_name`，用 `common/util/audit_nickname.py` 的 `enrich_audit_nickname_objs`。

### Error code

新檔 `common/code/pre_engagement_error_code.py`，前綴 `PRE`：

| 代碼 | 情況 |
|---|---|
| `PRE_404001` | 大類不存在 |
| `PRE_404002` | 事前準備資料不存在 |
| `PRE_409001` | 大類名稱重複 |
| `PRE_409002` | 大類底下還有資料，不能刪 |
| `PRE_409003` | 這份資料已被稽核引用，不能刪，只能標過時 |
| `PRE_412001` | 資源庫不在可編輯狀態（保留；選用與移出走資源庫既有的 `MODULE_FRAME_412001`） |

新碼要同步加進 FE `error-code.json`，中英兩份都要。

### 畫面

| 頁 | 落點 | 要點 |
|---|---|---|
| 事前準備資料 | `src/views/pre-engagement/PreEngagementView.vue`，側選單入口；資源庫列表操作選單也能進 | 頁首只有大標與「公司：<租戶名>」，不放提示文字；每個大類一張 `dark-card`，卡標題下顯示大類說明；檔案列顯示顯示名稱（**點擊開最新版預覽**）、版本、檔名、大小、上傳者暱稱、時間、可能過時標記；預覽（最新版與每個舊版）、下載、上傳、上傳新版、同名詢問對話框、舊版展開、刪除（被引用時改成標過時）、下載全部。預覽用共用 `FilePreviewDialog` |
| 事前準備資料大類 | 新 view，掛在系統設定區 | PrimeVue DataTable；拖曳排序；新增與編輯走 Dialog；刪除按鈕在有資料時 disabled，加 tooltip 說明 |
| 程序書池（資源庫頁） | `ModuleFrameDocumentPoolPanel.vue` 與 `document-pool/` 子元件，不動 `ModuleFrameTemplateEditView.vue` | 上方「從事前準備資料選用」（照大類分段、可多選、已引用的勾著不可再選、名稱可預覽）；照大類分段；來源標籤（客戶事前準備資料、顧問上傳）；客戶資料列「移出」（有掛載先確認）、客戶標過時淡化；顧問上傳時選大類；「網路架構」段每檔有「當網路架構圖匯出」勾選 |
| 程序書池（專案規劃頁） | `DocumentPoolPanel.vue`（唯讀，FR-139：來自資源庫的專案 SSP 內容只能在資源庫改） | 顯示資源庫選用了哪些，沒有選用、移出、上傳鈕 |

元件照設計系統：`dark-card`、全站字級、`hint-bar` 只用在錯誤或警示。不准用左側粗色邊框。icon-only 按鈕要加 `v-tooltip.top`；非同步按鈕要配 loading 狀態。

## 拆分表 {#split nav="拆分"}

| 卡 | 做什麼 | 改哪些檔 | 依賴 | 驗收條件 |
|---|---|---|---|---|
| **1** 資料表與大類 | migration 建三張表（RLS、GRANT、`schema_migrations`）；`system_menus` 四個起始值；兩顆能力點與兩個路由；大類 domain、repo、app service、API；第一次讀時帶入起始值；error code 檔 | `scripts/sql/2026-10-xx-fr161-*.sql`、`domain/pre_engagement/`、`infra/pre_engagement/`、`app/pre_engagement/`、`api/pre_engagement/`、`di_containers/pre_engagement/`、`config/app_modules.py`、`common/code/pre_engagement_error_code.py` | — | DEV 套 migration 後呼叫大類 GET 回四筆；新增、改名、排序、刪除各打一次；有資料的大類刪除回 `PRE_409002` |
| **2** 事前準備資料 BE | 資料與版本的 domain、repo、app service、API；同名檢查；下載 zip；上傳新版；標過時；暱稱 enrich | `domain/` `infra/` `app/` `api/` 的 `pre_engagement/` 底下 | 卡 1 | 上傳一份、同名檢查回它、上傳新版變 v2、列表帶兩版、下載 zip 打得開 |
| **3** 程序書池承接（套件＋同步） | jedi-compliance-audit 加三欄（migration 005、model、`clone_pool_docs`、`list_pool`）；主專案 `ssp_snapshot_extras` 補三欄；從 `_guard_template_write` 抽出可寫判斷給同步共用；事前準備資料新增、新版、刪除、搬類、新建資源庫時同步程序書池；排除與恢復 API；選用清單排除不用的列；刪除守門查凍結引用 | 套件 `jedi_compliance_audit/{migrations,infra}`；`app/oscal/service/ssp_document_pool_service.py`；`app/module_frame/service/ssp_snapshot_extras.py`；`common/authz/ssp.py`（只抽函式）；新的同步服務檔；`api/oscal/routes/ssp/` | 卡 2；**決策者同意 D9** | 上傳一份事前準備資料，DEV 查到每個可編輯資源庫的程序書池各多一列；上傳新版那列的 `file_id` 換掉；排除後選用清單看不到；送審後凍結 SSP 那列 `file_id` 仍是舊版；被凍結引用的事前準備資料刪除回 `PRE_409003`；存版後還原，三欄都在 |
| **4** 事前準備資料頁與大類設定頁 FE | 兩個新 view；側選單與資源庫列表入口；同名詢問對話框；i18n；error code json | `src/views/pre-engagement/`、`src/service/`、`src/config/api/api.js`、i18n、`error-code.json`、`ModuleFrame.vue`（加入口一行） | 卡 1、2 | 對照雛形畫面 1 與畫面 3 逐項點一遍 |
| **5** 程序書池 FE 改版 | `DocumentPoolPanel.vue` 照大類分段、來源標籤、上傳選大類、網路架構圖勾選（「這框架不用」已由卡 10 改成選用與移出） | `src/components/grc/DocumentPoolPanel.vue`（必要時拆子元件） | 卡 3 | 對照雛形畫面 2；在 ISO 與 CMMC 兩個資源庫各點一次「這框架不用」，互不影響 |
| **6** 網路架構圖 | 主專案補 `ssp_diagrams` 的 app service（`diagram_context='network-architecture'`，`links` 指向檔案，`caption` 用顯示名稱）；勾選 API；OSCAL 匯出確認帶出 | `app/oscal/service/` 新檔、`api/oscal/routes/ssp/` | 卡 3 | 勾選後 DEV 查到 `ssp_diagrams` 一筆；匯出 OSCAL JSON 的 `system-characteristics.network-architecture.diagrams` 有它 |

| **10** 程序書池改成挑選引用＋同名比對放寬（CM-3050） | 拿掉自動帶入與補齊；選用清單與選用端點；「移出」刪列與掛載；拿掉 exclusion 端點；`_stem` 去版次與日期尾巴＋unit test；FE 選用對話框與移出確認；清 DEV 自動帶入列 | `app/pre_engagement/service/pre_engagement_pool_sync_service.py`、`pre_engagement_file_app_service.py`、`resource_library_app_service.py`、`ssp_document_pool_service.py`、`module_frame_reference_document_service.py`、`api/module_frame/`、`api/oscal/routes/ssp/ssp_document_pool_route.py`；FE `ModuleFrameDocumentPoolPanel.vue`、`document-pool/` | 卡 3、5 | 見端到端驗收第 3～9 步 |

```{.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'}}}%%
flowchart LR
  C1[卡 1 資料表與大類] --> C2[卡 2 事前準備資料 BE]
  C1 --> C4[卡 4 兩頁 FE]
  C2 --> C4
  C2 --> C3[卡 3 程序書池承接]
  C3 --> C5[卡 5 程序書池 FE]
  C3 --> C6[卡 6 網路架構圖]
```

決策者最早看得到畫面的是卡 4（事前準備資料頁與大類頁）。卡 3 的套件改動要等決策者同意，所以排在卡 4 之後也不會擋住畫面。

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

1. 到系統設定「事前準備資料大類」，看到四個起始值；新增「機房與實體環境」，說明填「門禁、監控、溫濕度」；把「網路架構」改名「網路與連線」；拖曳排序。
2. 用受稽方帳號登入，側選單只有「事前準備資料」。五個格子名稱與順序和第 1 步一致，「機房」卡標題下顯示說明；頁首只有大標與公司名。
3. 在「網路與連線」丟 `拓樸圖.vsdx`；再丟 `拓樸圖_v2_2026-10.vsdx`，被問「這格已經有一份『拓樸圖』」選當成新版，看到 v2 與可展開的 v1。點名稱開預覽，看到 v2。
4. 換顧問帳號，打開 ISO 資源庫的程序書池：**看不到**拓樸圖。按「從事前準備資料選用」，清單照大類分段，勾拓樸圖與兩份政策文件送出；程序書池照大類出現三份、標「客戶事前準備資料」、拓樸圖是 v2。
5. 打開另一個 CMMC 資源庫，程序書池**沒有**這三份。ISO 資源庫綁的專案，規劃頁程序書管理唯讀、看到那三份、沒有選用與移出鈕。
6. 受稽方對拓樸圖上傳 v3，ISO 程序書池那份換成 v3。
7. 在 ISO 程序書池勾拓樸圖「當網路架構圖匯出」，匯出 OSCAL JSON 看到 network-architecture diagram。
8. 把一份政策文件掛到某條控制項，再按「移出」：跳「已掛在 1 條控制項上」，確認後池與掛載都消失。
9. 受稽方把另一份已引用的政策文件標過時：ISO 那份淡化並標「客戶已標過時」，沒有被移出；選用清單不再列出它。
10. ISO 資源庫送審。受稽方對拓樸圖上傳 v4：稽核計畫頁看到的仍是 v3；選用與移出回 412。受稽方對拓樸圖按刪除，按鈕已變成「標為過時」。
11. 對 ISO 資源庫存一版再還原，程序書池的大類、來源標籤與引用都還在。

## 不做 {#notdo nav="不做"}

| 不做 | 理由 |
|---|---|
| 顧問審閱、看過勾、留言、通知 | 決策者裁太複雜；大多作業跟顧問綁，等系統更完善再補 |
| 子項目、必填、截止日、完成度 | 只要大類 |
| 組織圖轉成部門人員、資產 Excel 轉成設備主檔 | 第二版再談 |
| 接 Agent 掃描結果 | 資產自動盤點走既有檢測線 |
| 自動催客戶更新過時資料 | 只標記，不催 |
| 線上繪製網路圖、預覽 vsdx | 第一版只做勾選匯出 |
| OSCAL back-matter 匯出整條打通 | 匯出時 back-matter 目前是空的，是既有缺口，另開卡；本案只保證網路架構圖這一條 |

沿革見 [`handoff/`](handoff/) LOG 與 git log。
