---
title: "顧問訪談欄位與資源庫版本 — 設計文件 (FR-139)"
brand: "Guidant AI · **FR-139** 顧問訪談欄位與資源庫版本"
eyebrow: "FR-139 · 設計定案 · 2026-10-14"
h1: "顧問在資源庫訪談、存版、送審，專案只看不改"
subtitle: "缺失與修改建議欄、建議附件、訪談表累積匯入、資源庫版本、資源庫與專案一對一、送審鎖版"
lede: "顧問在合規資源庫把客戶的訪談結果寫完整——實施狀態、現況、備註、**缺失**、**缺失修改建議**，需要時在建議下面附一份可直接給客戶用的範本（**建議附件**）。訪談表可以反覆匯入、一次一次累積；每整理一輪就存一版，可看舊版、可比對、可整份還原。啟動專案時專案直接看資源庫那一份 SSP，規劃頁除任務設定外唯讀；顧問按「送審」那一刻存一版並凍結給稽核輪，之後資源庫就不能改，只有輪次在「規劃中」才可編輯。本文件寫定案規則、為什麼這樣定、踩到會怎樣。"
chips: [
  {text: "D1～D21 全部定案（2026-10-14）", kind: ok},
  {text: "三個子需求、十一張卡，待開卡", kind: accent},
  {text: "動一支套件 jedi-oscal-v2（props 逐鍵合併）", kind: crit},
  {text: "陷阱：範本 SSP 同時是專案 SSP，守門要兩類入口一起改", kind: crit},
  {text: "上版前查 STG／POC 有無一資源庫多專案", kind: warn}
]
footer: "FR-139 · 設計文件 · 2026-10-14 · 討論稿：[discussion.md](discussion.md) · 雛形：[prototype/index.html](prototype/index.html) · 沿革見討論稿與 git log"
---

> 狀態：**設計定案（2026-10-14），待開卡**｜討論稿：[`discussion.md`](discussion.md)｜雛形：[`prototype/index.html`](prototype/index.html)（四頁：控制項填寫、版本歷史、版本比對、專案規劃頁唯讀）

> 雛形是假資料畫面，欄位與選項以本文件為準（實施狀態選項以 §6.6 的六值為準）。檔案路徑與行號以 2026-10-14 為準，查不到時以函式名重新定位。路徑慣例：BE 路徑相對 `compliance-manager-be/`；`pkg/` 代表 `~/Projects/Jedicogy/module/jedi-python-package/`；FE 路徑相對 `compliance-manager-fe/`。

名詞：**資源庫**＝合規資源庫（`compliance.module_frames`）；**範本 SSP**＝資源庫底下那份 SSP（`module_frames.template_ssp_id`），也就是**工作副本**；**AO**＝評估目標（assessment objective），控制項底下的檢查項目；**IR**＝控制項實施紀錄（`oscal.ssp_implemented_requirements`）；**props**＝OSCAL 物件上的「名稱＋值」擴充屬性陣列；**新模式專案**＝`living_ssp_id` 等於某資源庫 `template_ssp_id` 的專案；**舊模式專案**＝啟動時複製了一份 SSP 的既有專案。

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

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-14 | v1：D1～D21 設計定案；資料模型、版本操作、權限分流、匯入兩路徑、畫面甲案；三個子需求十一張卡拆分與端到端驗收 | — |
| 2026-10-18 | D19 改判：資源庫只在綁定專案最新一輪 `planning` 時可改，送審後到結案（含改善中、已結案）一律鎖 | [CM-2888](https://app.notion.com/p/T-25-D19-3fd346da4cd08162b0a4c9653823d054) |
| 2026-10-14 | 開卡前補定七條：資源庫終身只綁一個專案（不分進行中／結案／封存）；匯入前自動存備份版、匯入後不另存；鎖寫區間含 `pending_reverify`、`remediation` 與 `closed` 解鎖；版本不提供刪除；看版本用 `oscal.resource-library.read`；實施狀態 `system_menus` seed 併入卡 1；雛形兩處與決策不符不修、以本文件為準 | 母卡 [CM-2745](https://app.notion.com/p/FR-139-3f9346da4cd081e0afbce457d08bea06)、子需求 CM-2746～2748、子任務 CM-2749～2759（T-1～T-10、T-2b；守門分流從卡 1 併入 T-9） |

---

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

合規資源庫是顧問替客戶導入法規框架的第一個入口。顧問到客戶現場訪談，手上拿的訪談表就是系統可下載的「現況資料 Excel」；回來把訪談表匯入，同一份表隨訪談次數反覆匯入。之後逐條整理控制項與 AO 的實施狀態、現況說明、備註。

整理到「部分實作」這類條目，顧問要寫出**缺了什麼**與**怎麼補**，手上若有可直接給客戶用的範本就附上——這一步就是正式稽核前的事前稽核。以 CRA 的漏洞通報條為例：

| 欄位 | 顧問寫的內容 |
|---|---|
| 實施狀態 | 部分實作 |
| 現況說明 | 只有提供 E-Mail 收信接收通報 |
| 備註 | 連 email 都未開通 |
| **缺失** | 無通報平台、也沒有公告、也沒有通報手冊 |
| **缺失修改建議** | 提供通報平台網址、官網公告通報方式、提供手冊下載 |
| **建議附件** | 通報流程手冊範本.pdf（說明：照這份補上收件、分級、修補、回覆四段） |

缺失若沒有自己的欄位，就只能塞進現況說明：一個欄位同時講「現在是什麼」和「少了什麼」，產缺失報告得人工再拆一次（FR-138 自家 CRA 自評就是這樣）。同一份訪談表重新匯入，還可能把上次填好的欄位清掉（§6.5）。

**流程由顧問主導**：

1. 顧問在資源庫訪談、匯入、整理，完成客戶的 SSP。
2. 啟動專案。專案直接看資源庫那份 SSP；客戶的專案管理者只指派工作與上傳證據，不編 SSP。
3. 顧問持續訪談、匯入、整理，專案即時看到。
4. 顧問判斷可以請外部稽核了，按「送審」（就是「啟動稽核」）：存一版、凍結一份給稽核輪、資源庫鎖寫。覆核送審走同一套。
5. 稽核流程本身照舊。送審之後到結案，資源庫都鎖住；只有覆核子輪回到規劃中時可以再整理，新年度走「複製資源庫」。

```{.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
    actor C as 顧問
    participant RL as 合規資源庫<br/>（範本 SSP＝工作副本）
    participant V as 版本索引
    participant P as 專案
    participant A as 稽核輪
    C->>RL: 下載訪談表、到客戶現場訪談
    C->>RL: 匯入訪談表（只蓋檔案有填的欄）
    RL->>V: 匯入前自動存一版當備份（source=import-backup）
    loop 整理現況、缺失、建議、附範本
        C->>RL: 暫存（不開版）
        C->>RL: 儲存成新版本
        RL->>V: 存一版（source=manual）
    end
    C->>P: 啟動專案（不複製，直接指向範本 SSP）
    P-->>RL: 規劃頁唯讀讀取同一份
    loop 持續訪談
        C->>RL: 再匯入、再整理、再存版
        RL-->>P: 專案即時看到
    end
    C->>A: 送審（＝啟動稽核）
    A->>V: 存一版（source=submit，記 audit_round_id）
    A->>A: 凍結快照給稽核輪
    Note over RL: 送審後到結案資源庫鎖寫
    C->>A: 覆核送審（同一套：存版、凍結、鎖寫）
```

---

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

整案一句話：**顧問在合規資源庫裡把訪談結果寫完整（現況、備註、缺失、修改建議、附範本），每整理一輪就存一版；顧問覺得可以了就送審，送審那刻鎖一版給外部稽核用。專案只看資源庫那一份，不能改。**

| 子需求 | 做什麼 | 產出 | 完成怎麼判定（決策者親手做什麼、看到什麼） |
|---|---|---|---|
| **一、訪談欄位與匯入**（卡 1、2、2b、3、4） | 控制項與 AO 加「缺失」「缺失修改建議」；建議下方加「建議附件」；訪談表兩條路徑加三欄；重複匯入只蓋檔案有填的欄 | props 新鍵、共用讀寫 helper、整條控制項批次存 API、建議附件表與 API、FE 欄位、Excel 三欄、實施狀態選項進 `system_menus`、jedi-oscal-v2 合併規則 | 在資源庫 460 某條控制項填缺失與建議、在建議附件掛一份範本並寫說明，存檔重開還在，且程序書區與程序書文件池看不到它；下載訪談表看到三個新欄，只填缺失欄重新匯入，其他欄沒被清掉 |
| **二、資源庫版本**（卡 5、6、7） | 暫存、儲存成新版本、匯入自動存版、版本歷史、唯讀看舊版、比對、整份還原 | 版本索引表、存版／暫存／列表／比對／還原 API、版本歷史頁、比對頁 | 改兩條控制項、存兩版；開第 1 版是唯讀；比對第 1、2 版只標出那兩條的差異；還原到第 1 版，確認框列出會改動的筆數與「目前工作副本會自動存成新版」，確定後歷史多一列，再還原回去內容一字不差 |
| **三、一對一與唯讀**（卡 8、9、10） | 啟動專案指向範本 SSP；一個資源庫終身只綁一個專案；複製資源庫；專案規劃頁唯讀（任務設定照舊可編）；每次送審鎖版；送審後鎖寫、只有規劃中可改 | 啟動流程改寫（先盤點複製鏈）、複製資源庫、守門分流、規劃頁唯讀、送審存版 | 用資源庫啟動專案後，在資源庫改一條現況，專案規劃頁立刻看到；規劃頁控制項與 AO 欄位全灰、任務指派與到期日仍可改；送審後資源庫存檔鈕不能按、版本歷史多一列「送審」並標對應稽核輪；輪次進整改、結案後仍不能存檔 |

依賴：子需求一、二可平行；子需求三的送審鎖版用到子需求二的存版。畫面最先出的是資源庫控制項欄位（卡 2）與建議附件（卡 2b）。

---

## 決策 D1～D21 {#decisions nav="決策"}

| 項 | 定案 | 理由 | 被排除 |
|---|---|---|---|
| **D1** 缺失與修改建議存哪 | OSCAL props 兩個新鍵 `gap`、`recommendation`（ns 見 D14），控制項 IR 與 AO statement 兩層都有 | 與「實施狀態」「現況說明」走同一條路；啟動、凍結、存版都是整列複製，props 自動帶走；OSCAL 允許自訂 props | 套件加實體欄（OSCAL 標準沒有這欄，要動 schema）；主專案側表（每條複製路徑都要手動補，程序書池就曾因此漏帶）；直接寫評估結果或 POA&M（啟動稽核才生成，資源庫階段沒有容器） |
| **D2** 備註 | 就是兩表既有的 `remarks`，不新增欄位 | 欄位本來就在；專案規劃頁控制項存檔沒送 remarks（`ProjectPlanningView.vue:1443`）的漏洞，規劃頁唯讀後自然消失 | — |
| **D3** 建議附件 | 獨立一張表 `compliance.ssp_control_attachments`，掛 IR 或 AO statement；不進程序書池、程序書池不加種類欄、jedi-compliance-audit 不改；存版、還原、複製資源庫時跟著複製，**稽核凍結不帶**；畫面放在「缺失修改建議」正下方、與「程序書」區塊分開；AO props 舊的 `reference-documents` JSON 不碰 | 程序書是客戶自己的 SOP、用來證明現況，要進稽核凍結；建議附件是建議的一部分，客戶照做完就沒用，不該混進稽核基準。同池靠種類欄過濾，稽核側每個讀程序書的地方都要多一個條件，漏一處建議範本就跑進稽核 | 程序書池加「文件種類」欄（稽核側讀取處逐一加過濾、要改套件）；用檔名或描述欄約定（查不了、篩不了） |
| **D4** 訪談表欄位 | 路徑 A（完整匯入）與路徑 B（控制項現況九欄）都加「備註」「缺失」「缺失修改建議」三欄，匯出同步帶出；docx 匯入不動 | 訪談表就是現況資料 Excel，顧問在現場直接填；兩條路徑共用欄名定義，只改一處兩份表欄位就不一致 | docx 一併加（docx 不在顧問訪談流程內） |
| **D5** 匯入四個坑全收 | ① 套件 update 模式 props 逐鍵合併：檔案有的鍵才蓋；② 資源庫完整匯入用合併語意、不清空重建；③ 實施狀態值域收進 `system_menus` 當唯一來源（D20）；④ props 讀寫 helper 收成一支共用，匯出、差異比對、合併決策同步認得新鍵 | 不修①②，第二次匯入只填缺失欄，上次的現況說明就被清掉；不修③，下拉、匯入、匯出各說各話；不修④，新鍵要改六處，漏一處就是某條路徑看不到缺失 | — |
| **D6** 誰能編範本 SSP | 具備資源庫編輯權即可（D18），拿掉「只有帳號層超級管理員」的守門（`common/authz/ssp.py:215`）；「顧問」角色之後再定義。這條守門目前走不到（雙模式解析器沒接上），實作是在範本 SSP 的**所有寫入入口**要求資源庫編輯權 | 顧問是主要編輯者，現行守門是開發期暫時做法 | 先建「顧問」角色再開權限（角色定義牽涉授權模型，會卡住整案） |
| **D7** 專案規劃頁 | 資源庫帶來的資料除任務設定外全部唯讀：控制項與 AO 實施層欄位、程序書、建議附件、其他 SSP 頁籤；任務設定（指派、到期日、說明）照舊可編。FE 可編判斷與 BE 專案側 SSP 寫入守門一起改 | 客戶專案管理者只指派工作與上傳證據；SSP 是顧問的成果，兩邊都能改就互相覆蓋 | 只在畫面做唯讀（共用端點擋不住，見 §6.4） |
| **D8** 資源庫與專案 | 一對一。啟動專案時 `living_ssp_id`＝`template_ssp_id`，不複製三件組；啟動時擋「這個資源庫已綁定過任何專案」（不分進行中、結案、封存；新年度一律走複製資源庫）；資源庫有專案綁著不能刪；新年度走「複製資源庫」（D17）；**資源庫沒有公版**——公版是框架層概念，`module_frames.scope` 欄留著、預設不開放、畫面不出現公版資源庫。卡 8 動手前必須先盤點啟動專案的複製鏈並回報 | 顧問啟動後仍持續訪談，只有指向同一份專案才能即時看到；複製一份就得設計同步，那是兩份真相 | 維持複製另做同步（雙向衝突與同步時機）；一個資源庫多專案共用一份 SSP（不同客戶的現況互相覆蓋）；資源庫層加公版旗標（與框架層重複） |
| **D9** 送審 | 送審＝啟動稽核。依序「存版（`source='submit'`、`audit_round_id`）→ 凍結快照給稽核輪 → 資源庫鎖寫」；覆核輪（close_out）送審走同一套；稽核流程本身不改 | 外部稽核看的是送審那一刻的內容，顧問要能回頭看送審時寫了什麼；主稽核與覆核各對應哪一版，要能從兩頭查到 | 稽核中仍允許改工作副本（規劃頁直接指向工作副本，客戶會看到與稽核中內容不一致）；覆核另做一套 |
| **D10** 版本放哪 | 版號與修訂史寫 OSCAL 原生 `metadata.version`、`metadata.revisions`（revision 的 links 帶 `rel=predecessor-version`）；舊版內容用套件 `snapshot_ssp` 整份複製成獨立 SSP；只多一張索引表 `compliance.ssp_versions`，設計成「SSP 的版本」（`owner_type`＋`owner_id`），第一版只有資源庫用；OSCAL 各表不加版本欄 | 匯出的 SSP 自帶版本資訊；整份 SSP 快照讓看舊版、比對、還原都直接用既有編輯器與 diff | 只寫 metadata（做不到回顧、比對、還原）；存 JSON 文件快照（還原要走匯入路徑，看舊版與比對要多一層轉換）；SSP 主表加 group_id／version_no（FR-019 早期做法，v2 已移除） |
| **D11** 版本操作範圍 | 快照是**整份 SSP**（主表、12 子表、程序書掛載、建議附件、風險）；暫存不開版、儲存成新版本先暫存再快照、匯入前自動存一版當備份（匯入後不另存）；唯讀開舊版；版本不提供刪除；看版本歷史與舊版要 `oscal.resource-library.read`；比對接既有 diff 的「SSP 對 SSP」入口，**只比控制項與 AO 實施層**（狀態、現況、備註、缺失、建議、程序書掛載、建議附件）；還原＝整份換回去，先列會改動筆數給顧問確認，確定後把目前工作副本存成一版（`pre_restore`）再換，永遠可逆；稽核凍結快照不進索引表 | 顧問在乎控制項層的內容變化，比對先做這一層避免範圍膨脹；逐項還原會讓工作副本變成哪一版都不是 | 逐項還原；比對擴到其他頁籤 |
| **D12** 控制項填寫畫面 | 甲案：控制項與 AO 表單各加兩個文字框，建議下方加建議附件區塊，其他七個頁籤不動；BE 照樣做「整條控制項連 AO 批次存」API 給暫存用 | 先讓資料有地方放，畫面改版等顧問實際用過再決定 | 這次重新設計填寫區 |
| **D13** 範本 460 | FR-138 塞在「現況說明」的缺失，新欄位上線後由小卡（卡 4）拆到「缺失」欄 | 460 是第一個真實案例，拆完正好驗證新欄位 | — |
| **D14** props ns 與 name | ns＝`https://guidant.ai/ns/oscal`，name＝`gap`、`recommendation`；全產品之後的自訂 props 用同一個 ns | OSCAL 規定自訂 props 帶自家 ns 避免與標準名稱撞 | — |
| **D15** 建議附件分類 | 不分類，只有檔案加一行說明，不進 `system_menus` | 與程序書已分表，「放哪張表」就是分類，沒有讀取端要用種類 | 建議附件分種類 |
| **D16** 兩張新表的 schema | `compliance.ssp_versions`、`compliance.ssp_control_attachments`；判準是 model 寫在哪個套件就放哪個 schema，兩張 model 都在主專案；跨 schema FK 指 `oscal.system_security_plans` 是正常的（先例 `project_audit_rounds`），FK 寫完整 schema 名 | 放 `oscal` 會讓人以為是 jedi-oscal-v2 的表 | 放 `oscal` |
| **D17** 複製資源庫 | 把啟動專案現有的複製邏輯（套件 `clone_resource_library` 三件組＋程序書池、表單設定、`ssp_risks`）搬成「複製資源庫」，再補帶建議附件；版本歷史不帶；啟動專案不再用它 | 複製路徑與現行啟動專案幾乎相同，搬過來等於重用 | 匯出 Excel 再匯入（帶不走程序書檔、依賴匯入坑修完）；資源庫加公版旗標 |
| **D18** 資源庫編輯權 | ＝`oscal.resource-library.update`，不另立能力點。範本 SSP 的**所有寫入入口**都要求它，含 SSP 共用端點：依「此 SSP 是否為某資源庫的 `template_ssp_id`」分流，是就走資源庫守門（能力點＋稽核中鎖寫），不是才走專案守門 | 只守資源庫自己的端點，客戶專案管理者仍能從共用端點改到顧問的 SSP | 另立新能力點 |
| **D19** 何時可改 | 只有綁定專案最新一輪是「規劃中」（`planning`，任務規劃與任務執行階段）可改；送審後到結案（含 `remediation`、`closed`）一律鎖。覆核子輪回到 `planning` 時可改，子輪送審後再鎖。判斷用白名單（只放行 `planning`），新增輪次狀態時預設是鎖 | 專案看到的 SSP 就是這一份，專案一開始稽核，客戶與稽核方看到的內容就不能再被改；新年度要改走複製資源庫 | 進整改就解鎖（顧問邊整改邊訪談）——結案後資源庫仍可任意改，專案那份 SSP 跟著變；等結案才解鎖 |
| **D20** 實施狀態值域 | 六值 `implemented`／`partial`／`planned`／`alternative`／`not_applicable`／`unknown`（＝還沒訪談），key 保留底線拼法；進 `system_menus` group `ssp_implementation_status`；FE 下拉、Excel 匯入驗證、BE 驗證三處讀同一份；匯出 OSCAL 時才轉標準拼法 `not-applicable` | DEV 現存 `implemented` 70,914、`unknown` 2,133、`partial` 47、`not_applicable` 13、`alternative` 2、`planned` 2；底線拼法散在讀取端與七萬多筆資料，硬改風險大、使用者無感；`unknown` 是顧問需要的真實狀態 | key 對齊 OSCAL 連字號並 migration 既有資料 |
| **D21** 既有資料 | 舊模式專案不遷移，維持各自獨立的 SSP 副本、照舊在專案側可寫；一對一檢查只算新模式專案；**STG／POC 上線前查有無一資源庫多專案，列進上版檢查項** | DEV 資源庫 460 綁 3 個專案、427 綁 1 個，全是舊模式；遷移要決定哪個專案接手範本 SSP，對既有稽核輪沒有收益 | 遷移既有專案 |

---

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

| 元件 | 現況 | 本案動作 |
|---|---|---|
| 實施狀態、現況說明 | props `implementation-status`、`implementation-description`；控制項在 `ssp_implemented_requirements.props`、AO 在 `ssp_statements.props`（`app/oscal/service/ssp_control_implementation_service.py`；資源庫側 `app/module_frame/service/module_frame_control_default_service.py`、`module_frame_control_objective_default_service.py`） | 欄位不動；值域收進 menu（D20） |
| 備註 | 兩表 `remarks` | 就是「備註」（D2） |
| AO 識別 | `statement_id` 格式 `<control>_obj.N` | 沿用 |
| 同一控制項多筆 IR | `_ir_for_control`（`ssp_control_implementation_service.py:888-900`）挑「已有說明」那筆 | 新欄位讀寫、批次存、比對走同一挑選 |
| props 讀寫 helper | 六份複製：`ssp_control_implementation_service.py:960-989`、資源庫兩支 default service、`export/ssp_v2_content_loader.py:203`、`ssp_resources_context_service.py:197`、`import_diff/snapshot_views.py:43` | 收成一支（D5④） |
| 程序書池 | `oscal.ssp_reference_documents`＋掛載表 `ssp_reference_document_mappings`（`control_implementation`→IR.id、`objective`→statement.id），model 在 jedi-compliance-audit；凍結時 `ssp_document_pool_service.py:91 clone_pool_for_snapshot` 補複製 | 不動；存版、還原、複製資源庫複用 `clone_pool_for_snapshot` |
| 訪談表路徑 A | `app/oscal/service/ssp_excel_import_app_service.py`；欄位 `app/module_frame/excel_template/sheet_definitions.py:452-494`、中文欄名 `header_i18n.py:127-136`；寫入 `import_adapter/excel_to_oscal_ssp.py:62-110`；資源庫模式 `mode="replace"` 清空重建（`:448-510`） | 加三欄；資源庫模式用合併語意 |
| 訪談表路徑 B | `app/oscal/service/ssp_control_impl_import_service.py`，端點 `/ssp/<ssp_uid>/control-implementations/import`；逐列 update、`_setp` 逐鍵合併；欄名與資源庫共用 `module_frame_template_import_service.py:92`；模板版本檢查 `excel_parser/version_check.py` | 加三欄 |
| 套件 update 模式 | `pkg/jedi-oscal-v2/.../oscal_io_service.py:1308` IR／statement 的 props 整份覆寫；None 跳過、清空要傳 `""` | 逐鍵合併（D5①） |
| 實施狀態值域 | 三處不一致：FE 下拉寫死在 `ModuleFrameTemplateEditView.vue:348-368`；Excel 匯入 `module_frame_template_import_service.py:79-86`；SQL 註解 `scripts/sql/2026-06-02-erd-chinese-comments.sql:720` | 三處讀 menu（D20） |
| 範本 SSP 寫入守門 | `common/authz/ssp.py:215 _require_admin_for_template` 只准超級管理員，但雙模式解析器沒接上（`di_containers/oscal/oscal_containers.py:345`），實際走不到；資源庫寫入靠 route 能力點＋service 直接拿 ssp_id 寫 | 兩類入口都要求 `oscal.resource-library.update`（D18） |
| 專案 SSP 寫入守門 | `common/authz/ssp.py:99-103,187-211` 專案管理者＋可寫判斷；可寫判斷 `domain/oscal/service/ssp_project_resolver.py:111-133`（凍結一律鎖；living 在無輪次或當前輪 `planning` 可寫） | 新模式專案改走資源庫守門 |
| SSP 歸屬判斷 | 生效的是 `ssp_project_resolver.py:97-108`：先查 `projects.living_ssp_id`、再查 `project_audit_rounds.ssp_id` | 範本判斷以 `template_ssp_id` 直比（§6.4） |
| 資源庫編輯頁 | `src/views/module_frame/ModuleFrameTemplateEditView.vue`（2,246 行、八頁籤），路由 `/module-frame/:uid/template-edit` | 只掛新子元件 |
| 專案規劃頁 | `src/views/project/ProjectPlanningView.vue`（2,609 行），`canEditSsp = apStatus === 'planning'`（`:158`） | 唯讀旗標與提示子元件 |
| 啟動專案 | `app/project/service/project_start_app_service.py:347` 呼叫套件 `clone_resource_library` 複製三件組；`:378` 寫 `living_ssp_id`；`:385-389` `_init_ssp_control_implementations` 補建控制項；程序書池與 `ssp_risks` 手動補複製（`:231-299, :392-409`） | 不再複製、直接指向；複製鏈先盤點（卡 8） |
| 讀 `living_ssp_id` 的地方 | 11 支（`flow_control/project_service.py`、`cloud_integration/project_tree_loader.py`、`task_alert/*`、`oscal/ssp_*_app_service.py`、`export/ssp_v2_content_loader.py` 等） | 照常讀；逐支確認不寫 |
| 複製資源庫 | 不存在：FE i18n 有 `from_sample: "複製資源庫"` 但無畫面使用，BE 無端點 | 新功能，搬啟動專案的複製邏輯（D17） |
| 稽核凍結 | 套件 `AuditRoundAppService.launch_audit`（`pkg/jedi-compliance-audit/.../audit_round_app_service.py:402`）`snapshot_ssp` 複製 living SSP 到 `project_audit_rounds.ssp_id` 並補程序書池；主專案由 `app/flow_control/service/oscal_stage_handlers.py` 的 planning 階段推進呼叫；輪次狀態 `planning → audit_planning → auditing → remediation → pending_reverify → closed`；覆核由 `launch_reverify` 另開 close-out 子輪 | 送審多存一版（卡 10） |
| 整份複製 | `pkg/jedi-oscal-v2/.../snapshot/oscal_snapshot_service.py:52 snapshot_ssp`（SSP＋12 子表＋一份 metadata，status=frozen）、`:105 clone_resource_library`、`:188 deep_clone_ssp` | 存版用 `snapshot_ssp` |
| 差異比對 | `app/oscal/service/import_diff/ssp_diff_service.py`（四態 unchanged／changed／added／gone、七區；`:725 annotate_against_snapshot`）、`decision_merge.py:107 merge_by_decisions` | 加「SSP 對 SSP」入口 |
| SSP 主表與 metadata | v2 主表只有 id／uuid／metadata_id／status／import_profile_id；`oscal.metadata` 有 `version`、`revisions` JSONB，主專案沒用 | 開始寫（D10） |

---

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

### 6.1 資料模型 {#design-model}

**① props 新鍵**（IR 與 AO statement 都有）：

| name | ns | 值 | 說明 |
|---|---|---|---|
| `gap` | `https://guidant.ai/ns/oscal` | 純文字 | 缺失：少了什麼 |
| `recommendation` | `https://guidant.ai/ns/oscal` | 純文字 | 缺失修改建議：怎麼補 |

規則：空字串＝刪掉該鍵（與 `_setp` 一致）；讀取時沒有該鍵視為空；全部欄位空時 props 回 None（與 `_map_to_props` 一致）。匯出 docx／Excel、差異比對、合併決策都認得這兩鍵，由共用 helper 一次處理。

**② 表 `compliance.ssp_control_attachments`**（建議附件；主線 migration，GRANT `cm_app` 含 sequence，RLS 依租戶）：

| 欄 | 型別 | 用途 |
|---|---|---|
| `id`／`uid`／`tenant_id`／`org_unit_id` | 標準欄 | RLS 依租戶 |
| `context_type` | varchar(32) NOT NULL | `control_implementation`（掛 IR）或 `objective`（掛 AO statement） |
| `context_id` | bigint NOT NULL | IR.id 或 statement.id |
| `file_id` | bigint NOT NULL | 指向 `upload_files` |
| `description` | text | 一行說明 |
| `created_user`／`created_at`／`updated_user`／`updated_at` | 審計欄 | API 回傳補 `*_user_name` |

**③ 表 `compliance.ssp_versions`**（版本索引；主線 migration，GRANT `cm_app` 含 sequence，RLS 依租戶）：

| 欄 | 型別 | 用途 |
|---|---|---|
| `id`／`uid`／`tenant_id`／`org_unit_id` | 標準欄 | |
| `owner_type` | varchar(32) NOT NULL | 擁有者種類，第一版只有 `module_frame` |
| `owner_id` | bigint NOT NULL | 資源庫 id |
| `ssp_id` | bigint NOT NULL | 這一版的整份快照 SSP（`snapshot_ssp` 產出，status＝frozen）；FK → `oscal.system_security_plans` |
| `version_no` | int NOT NULL | 同一擁有者遞增 1、2、3… |
| `version_label` | varchar(64) | 顯示版號，同步寫 `metadata.version` |
| `source` | varchar(32) NOT NULL | `manual`（手動存版）／`import`（匯入觸發）／`submit`（送審鎖版）／`pre_restore`（還原前自動存版） |
| `note` | text | 存版說明 |
| `audit_round_id` | bigint 可空 | 送審版對應的稽核輪（`project_audit_rounds.id`）；其他來源為空 |
| `created_user`／`created_at` | 審計欄 | API 回傳補 `created_user_name` |

唯一鍵 `UNIQUE (owner_type, owner_id, version_no)`。

**④ OSCAL 原生版本欄**（不加欄，只開始寫）：每存一版，該版快照與工作副本的 `metadata.version` 寫版號字串（與 `version_label` 同步）；`metadata.revisions` 追加一筆 `{version, last-modified, title, remarks, links:[{rel: "predecessor-version", href: "#<前一版 SSP uuid>"}]}`。

**⑤ `public.system_menus` 實施狀態 seed**（主線 `phase=seed`，`ON CONFLICT DO NOTHING`，先例 `scripts/sql/2026-05-22-seed-oscal-ssp-roles.sql`）：

| group | key | 顯示（zh-tw） |
|---|---|---|
| `ssp_implementation_status` | `implemented` | 已實作 |
| `ssp_implementation_status` | `partial` | 部分實作 |
| `ssp_implementation_status` | `planned` | 規劃中 |
| `ssp_implementation_status` | `alternative` | 替代措施 |
| `ssp_implementation_status` | `not_applicable` | 不適用 |
| `ssp_implementation_status` | `unknown` | 未評估 |

FE 走 `API.SYSTEM_MENU`，i18n 走 `lang.ssp_implementation_status.<key>`；Excel 匯入合法值與 BE 驗證讀同一份；匯出 OSCAL 時 `not_applicable` 轉 `not-applicable`。

### 6.2 三層關係 {#design-layers}

| 層 | 是什麼 | 誰改 | 誰看 |
|---|---|---|---|
| 工作副本 | 資源庫的範本 SSP，永遠是最新 | 顧問（稽核中鎖寫） | 顧問；新模式專案規劃頁即時唯讀 |
| 版本歷史 | 某時點的整份快照（`ssp_versions` 每列一份），建立後不再改 | 沒人 | 顧問回顧、比對、還原來源 |
| 稽核輪凍結快照 | 每次送審凍結給該輪的那份（`project_audit_rounds.ssp_id`） | 沒人 | 稽核輪 |

送審時存的版（`source='submit'`）與稽核凍結是兩份不同的快照，靠 `audit_round_id` 互指：前者給顧問回顧比對、帶建議附件；後者給稽核輪當基準、不帶建議附件。稽核凍結快照不進索引表。

### 6.3 版本操作 {#design-versions}

| 操作 | 做什麼 | 開版嗎 |
|---|---|---|
| 暫存 | 全頁未存的控制項與 AO 一次存進工作副本（整條控制項連 AO 批次存 API） | 否 |
| 儲存成新版本 | 先暫存；再 `snapshot_ssp` 複製工作副本、依 IR／statement 新舊 id 對照補程序書池掛載、建議附件與 `ssp_risks`；索引表加一列；寫 `metadata.version` 與 revision | 是（`manual`） |
| 匯入前備份 | 匯入確認前自動存一版，匯入後不另存 | 是（`import-backup`） |
| 看舊版 | 開版本快照，既有編輯器帶唯讀旗標；版本索引裡的 SSP 與 `status='frozen'` 一律不可寫 | — |
| 比對 | 兩版之間或某版對工作副本；只列有差異的控制項與 AO，四態著色；比對欄位：狀態、現況、備註、缺失、建議、程序書掛載、建議附件 | — |
| 還原到某版 | 先算出各頁籤會改動的筆數給確認框（「會改動：控制項 N 條、負責單位 M 筆……」）；確定後把目前工作副本存成一版（`pre_restore`），再把目標版整份複製成新工作副本，`module_frames.template_ssp_id` 與一對一專案的 `living_ssp_id` 同時換過去 | 是（`pre_restore`） |
| 送審 | 見 §6.4 | 是（`submit`） |

存版、還原都是寫入，受資源庫編輯權與稽核中鎖寫管（§6.4）。

### 6.4 權限分流與鎖寫 {#design-authz}

範本 SSP 的寫入入口有兩類，規則要在兩類都成立：

| 入口 | 現況 | 本案 |
|---|---|---|
| 資源庫自己的端點（`api/module_frame/__init__.py`） | route 能力點 `oscal.resource-library.*`，service 直接拿 ssp_id 寫 | 沿用能力點；寫入前加稽核中鎖寫判斷 |
| SSP 共用端點（系統特性、元件、外部服務、責任人員、控制項現況等，經 `SspPermissionChecker`） | 一律走專案分支 | 先判「此 SSP 是否為某資源庫的 `template_ssp_id`」（直接比對，不靠 profile 反查）：是→資源庫守門（`oscal.resource-library.update`＋稽核中鎖寫）；否→專案守門照舊 |

**送審後鎖寫**：範本 SSP 綁定的新模式專案，其當前輪次不是 `planning` 時（`audit_planning`、`auditing`、`remediation`、`pending_reverify`、`closed`），範本 SSP 的所有寫入回 412（D19）；覆核子輪回到 `planning` 時可改、送審時再鎖。

**唯讀的 SSP**：版本索引裡的 SSP 與 `status='frozen'` 的 SSP 一律唯讀；讀取入口要認得版本快照（目前它既不是 `living_ssp_id` 也不是稽核輪 `ssp_id`，走共用端點會「找不到」）。

**專案側**：新模式專案的規劃頁，SSP 資料一律唯讀（D7）；專案管理者帳號直接打寫入 API，因範本 SSP 走資源庫守門而被擋。舊模式專案維持專案守門與既有可寫判斷（D21）。

### 6.5 匯入兩路徑 {#design-import}

| | 路徑 A：完整匯入 | 路徑 B：控制項現況 |
|---|---|---|
| 入口 | `ssp_excel_import_app_service.py` | `ssp_control_impl_import_service.py`，`/ssp/<ssp_uid>/control-implementations/import` |
| 新欄 | 分頁「適用控制項」加「備註」「缺失」「缺失修改建議」（`sheet_definitions.py`、`header_i18n.py`、`excel_to_oscal_ssp.py`） | 九欄加三欄（欄名與資源庫共用 `module_frame_template_import_service.py`） |
| 模板版本 | 欄位變動要升模板版本，`excel_parser/version_check.py` 認得新版 | 同左 |
| 合併規則 | 資源庫模式不清空重建；套件 update 模式 props 逐鍵合併——檔案有填的欄才蓋，空白欄保留現值 | 既有 `_setp` 逐鍵合併，新欄比照 |
| 實施狀態驗證 | 讀 `ssp_implementation_status` menu | 同左 |
| 匯出 | 帶出三個新欄 | 同左 |

新模式專案指向範本 SSP，匯入一律從資源庫做；舊模式專案的專案側 `mode="update"` 照舊（D21）。docx 匯入不動。

### 6.6 畫面（甲案） {#design-ui}

雛形：[`prototype/index.html`](prototype/index.html)（[控制項填寫](prototype/control-edit.html)、[版本歷史](prototype/version-history.html)、[版本比對](prototype/version-compare.html)、[專案規劃頁唯讀](prototype/project-planning-readonly.html)）。

**資源庫「適用控制項」頁籤**：

- 控制項表單與 AO 表單各在「備註」之後加「缺失」「缺失修改建議」兩個多行文字框。
- 「缺失修改建議」正下方加「建議附件」區塊：上傳只選檔、寫一行說明，可改說明、可刪除。
- 既有「程序書」區塊放在建議附件下方，維持現狀、分開呈現。
- 實施狀態下拉讀 `ssp_implementation_status` 六值。
- 頁首加「暫存」「儲存成新版本」（填說明）兩顆鈕與「版本歷史」入口；稽核中兩顆鈕停用並提示原因。
- 其他七個頁籤不動。`ModuleFrameTemplateEditView.vue` 已超標，新欄位、建議附件區塊、頁首鈕一律做成新子元件，原檔只加一行掛載。

**版本歷史頁**：列版號、來源（手動存版／匯入自動存版／送審鎖版／還原前自動存版）、說明、誰、何時；送審版標對應稽核輪（主稽核或覆核）並顯示鎖頭；每列可檢視（唯讀）、比對、還原。還原確認框列出會改動的筆數，並說明目前工作副本會先存成新版。

**版本比對頁**：選兩版（或某版對工作副本），只列有差異的控制項與 AO，四態著色。

**專案規劃頁（新模式專案）**：控制項與 AO 實施層欄位、程序書、建議附件、其他 SSP 頁籤全部唯讀；頁首一行「內容即時同步自合規資源庫」，有資源庫編輯權的人多一個「前往合規資源庫」連結；任務設定照舊可編。`ProjectPlanningView.vue` 已超標，唯讀旗標與提示做成子元件。

---

## 拆分 {#breakdown nav="拆分"}

依「決策者何時看得到畫面」排序。e2e 屬母卡收口，不在單卡做。只動一支套件 jedi-oscal-v2（卡 3），開發期走 path dependency，發版等決策者明示。

| 卡 | 做什麼 | 主要檔案 | 依賴 | 驗收條件（決策者親手做什麼、看到什麼） | 會踩的鐵則 |
|---|---|---|---|---|---|
| **卡 1** BE 新欄＋共用 helper＋批次存 API | props `gap`／`recommendation` 讀寫；六份 helper 收一支（保留空字串刪鍵、全空回 None）；整條控制項連 AO 批次存端點；範本 SSP 兩類寫入入口要求 `oscal.resource-library.update`（依 `template_ssp_id` 分流） | `ssp_control_implementation_service.py`、資源庫兩支 default service、export loader、`snapshot_views.py`、`ssp_resources_context_service.py`、`decision_merge.py`、`common/authz/ssp.py` | — | 與卡 2 一起驗 | 先查再寫（helper 收斂不另寫第七份）；`common/authz/` 當次跑 reviewer；守衛測試 |
| **卡 2** FE 控制項欄位＋暫存鈕 | 甲案兩層表單各加兩框；頁首暫存鈕接批次存；實施狀態下拉讀 menu（seed 在卡 3，卡 2 先接 API） | `ModuleFrameTemplateEditView.vue` 只掛新子元件 | 卡 1 API 介面先定 | 在範本 460 填一條控制項與其 AO 的缺失、建議，按暫存後重新整理，內容都還在 | 檔案體積上限（只准拆）；下拉不寫死 |
| **卡 2b** 建議附件 | `compliance.ssp_control_attachments` migration；上傳／列表／改說明／刪除 API（權限同 D18）；FE 區塊放缺失修改建議正下方 | 新 domain／infra／app service、新 route、FE 新子元件 | 卡 1 | 在 460 某條控制項與其 AO 各上傳一份並寫說明，重新整理還在；程序書區與「程序書文件池」頁籤都看不到它 | migration 鐵則（日期註解、GRANT、`schema_migrations`、只套 DEV、回報出貨基線待重產）；審計欄位 enrich |
| **卡 3** Excel 三欄＋狀態選項＋套件合併規則 | 兩路徑加三欄（匯入、匯出、欄名、模板版本）；`ssp_implementation_status` seed 與 FE、Excel、BE 驗證讀 menu、匯出轉標準拼法；jedi-oscal-v2 update 模式 props 逐鍵合併；資源庫完整匯入不清空 | `sheet_definitions.py`、`header_i18n.py`、`ssp_control_impl_import_service.py`、`excel_to_oscal_ssp.py`、`ssp_excel_import_app_service.py`、`module_frame_template_import_service.py`、`pkg/jedi-oscal-v2/.../oscal_io_service.py`、seed migration | 卡 1 | 下載 460 的訪談表看到三個新欄；只填某條的「缺失」重新匯入，該條現況說明與其他條都沒被清掉；實施狀態下拉與 Excel 驗證一致、有「未評估」 | jedi 套件異動（plan 列範圍與其他 consumer 風險、path dependency 不 commit、發版等令）；`system_menus` 鐵則 |
| **卡 4** 範本 460 缺失回填 | 把塞在現況說明的缺失拆到缺失欄，可半自動（列出含缺失字樣的條目給人確認） | DEV 資料作業 | 卡 1、卡 2 | 在 460 抽五條，現況說明只剩現況，缺失欄有拆出來的那段 | 只動 DEV |
| **卡 5** 版本索引表＋存版 API | `compliance.ssp_versions` migration；存版（暫存→`snapshot_ssp`→依 id 對照補程序書池、建議附件、`ssp_risks`→索引→metadata 版號與 revision）；版本列表；匯入成功自動存版；版本快照讀取入口唯讀 | 新 service、`ssp_document_pool_service.py:91` 複用 | 卡 1、卡 2b | 與卡 6 一起驗 | migration 鐵則；`@transaction` 一個存版一個交易 |
| **卡 6** FE 版本歷史頁＋存版鈕 | 頁首「儲存成新版本」（填說明）、版本歷史頁、唯讀開舊版 | 新頁＋新子元件、`ui_routes` 登記 | 卡 5 | 改兩條控制項後存成新版本，版本歷史多一列且來源寫「手動」；點舊版打開，所有欄位不能改 | 新頁路由 name 要登記 `ui_routes`；檔案體積上限 |
| **卡 7** 比對與還原 | 既有 diff 加「SSP 對 SSP」入口，只比控制項與 AO 實施層；整份還原：算會改動筆數→確認框→存 `pre_restore`→複製→換 `template_ssp_id` 與 `living_ssp_id` | `ssp_diff_service.py`、還原 service、比對頁 | 卡 5 | 比第 1、2 版只看到那兩條的差異標記；還原到第 1 版，確認框列出「會改動：控制項 2 條……」與新版號；確定後歷史多一列，再還原回去內容一字不差、程序書與建議附件仍在 | 先查再寫（diff 用既有引擎不另寫）；查記住舊 id 的快取或背景工作 |
| **卡 8** 啟動改指向＋一對一守門＋複製資源庫 | **前置：先盤點 `project_start_app_service.py` 複製鏈，分「啟動要留／搬去複製資源庫／順手塞的」三類回報，回報後才動**；啟動不再複製、指向範本 SSP；擋「已綁定過任何專案」（新 error code）；有專案不可刪；複製資源庫（帶程序書池、建議附件、表單設定、`ssp_risks`，不帶版本歷史） | `project_start_app_service.py`、資源庫刪除、新複製端點與列表動作 | 卡 2b | 盤點清單先交；從資源庫啟動專案，在資源庫改一條現況，專案規劃頁立刻看到；同一資源庫再啟動一次被擋（含該專案已結案或封存後）；複製資源庫後新資源庫內容與兩種附件齊全、版本歷史是空的 | 分析後才改（盤點前置）；先查再寫（搬既有複製邏輯） |
| **卡 9** 規劃頁唯讀＋守門調整 | 規劃頁資源庫資料唯讀（任務設定照舊可編）、頁首提示；歸屬判斷認得範本（`template_ssp_id` 直比），專案側 SSP 寫入守門擋新模式專案；確認 11 支讀 `living_ssp_id` 的地方無寫入 | `ProjectPlanningView.vue`（只加唯讀旗標與提示子元件）、`common/authz/ssp.py`、`ssp_project_resolver.py` | 卡 8 | 新模式專案的規劃頁，控制項與 AO 欄位全灰、任務指派與到期日仍可改；用專案管理者帳號直接打寫入 API 被擋；舊模式專案行為不變 | `common/authz/` 當次跑 reviewer；檔案體積上限 |
| **卡 10** 送審鎖版＋稽核中鎖寫 | 主稽核與覆核送審都走「存版（`submit`＋`audit_round_id`）→凍結→鎖寫」；範本 SSP 兩類寫入入口加「當前輪不是 `planning` 就鎖」 | `oscal_stage_handlers.py`、範本 SSP 寫入守門 | 卡 5、卡 8 | 送審後回資源庫，暫存與存版鈕不能按、版本歷史多一列「送審」並標對應主稽核輪；輪次進整改、結案後仍不能編輯；覆核送審後再鎖、多一列標覆核輪 | 狀態轉換前置條件；新 error code 命名並同步 FE `error-code.json` |

```{.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
    subgraph S1[子需求一 訪談欄位與匯入]
      C1[卡1 BE 新欄＋共用 helper<br/>＋批次存 API] --> C2[卡2 FE 控制項欄位<br/>＋暫存鈕]
      C1 --> C2b[卡2b 建議附件<br/>表＋API＋畫面區塊]
      C1 --> C3[卡3 Excel 三欄＋狀態選項<br/>＋套件合併規則]
      C1 --> C4[卡4 範本 460 回填]
    end
    subgraph S2[子需求二 資源庫版本]
      C5[卡5 版本索引表＋存版 API] --> C6[卡6 版本歷史頁<br/>＋存版暫存鈕]
      C5 --> C7[卡7 比對與還原]
    end
    subgraph S3[子需求三 一對一與唯讀]
      C8[卡8 啟動改指向<br/>＋一對一守門] --> C9[卡9 規劃頁唯讀<br/>＋顧問編輯權]
      C8 --> C10[卡10 送審鎖版<br/>＋稽核中鎖寫]
    end
    C1 --> C5
    C2b --> C5
    C5 --> C10
```

**上版檢查項**：STG／POC 上線前查有無「一資源庫多專案」（D21）；兩張新表與 seed 的 migration 照環境異動鐵則只先套 DEV，寫完回報出貨基線待重產。

---

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

用 DEV 一個 CRA 資源庫（建議 460），批次走完再一次回報：

1. 資源庫「適用控制項」頁籤：某條控制項與其一個 AO 填實施狀態「部分實作」、現況、備註、缺失、缺失修改建議；在建議附件各掛一份檔案並寫說明 → 按「暫存」→ 重新整理，全部還在；程序書區與「程序書文件池」頁籤看不到建議附件；實施狀態下拉有六項、含「未評估」。
2. 下載訪談表，「適用控制項」分頁看到備註、缺失、缺失修改建議三欄且帶出剛填的值；在另一條只填「缺失」、其他欄留白，重新匯入 → 該條現況說明沒被清掉、其他條不變；版本歷史多一列「匯入自動存版」。
3. 再改兩條控制項 → 「儲存成新版本」填說明 → 版本歷史多一列「手動存版」；檢視上一版，所有欄位不能改。
4. 比對這兩版 → 只列出那兩條，差異著色。
5. 還原到較早那版 → 確認框列出會改動的筆數並說明目前工作副本會先存成新版 → 確定 → 歷史多一列「還原前自動存版」，內容回到那版；再還原回去，內容一字不差，程序書與建議附件都在。
6. 用此資源庫啟動專案 → 在資源庫改一條現況，專案規劃頁立刻看到；規劃頁控制項與 AO 欄位全灰、頁首提示同步自資源庫；任務指派與到期日仍可改。同一資源庫再啟動一次被擋（含該專案已結案或封存後）；有專案綁著時刪資源庫被擋。
7. 用專案管理者帳號直接打 SSP 共用寫入端點（例如系統特性）改這份 SSP → 被擋。
8. 專案送審（啟動稽核）→ 資源庫暫存與存版鈕停用；版本歷史多一列「送審鎖版」標主稽核輪；直接打寫入 API 回 412。
9. 輪次推進到整改、結案 → 資源庫仍唯讀。開覆核子輪回到規劃中時可再編輯；覆核送審後再鎖，版本歷史多一列標覆核輪。
10. 複製此資源庫 → 新資源庫控制項內容、程序書、建議附件、表單設定齊全，版本歷史是空的。
11. 一個舊模式專案（啟動時複製了 SSP 的既有專案）：規劃頁與寫入行為和上線前相同。

---

## 風險與陷阱 {#risks nav="風險"}

::: {.callout .crit}
**🔴 範本 SSP 同時是專案的 SSP，唯讀只做在畫面上擋不住**

指向後範本 SSP 的 id 就是專案的 `living_ssp_id`。所有走 `SspPermissionChecker` 的共用端點會把它判成專案 SSP，套「專案管理者＋規劃階段可寫」——客戶專案管理者能透過這些端點改到顧問的 SSP。範本判斷要用 `template_ssp_id` 直比，命中就走資源庫守門。快照沿用來源的 profile（`ssp_clone_service.py:129-131` 整列複製根節點），若日後接上雙模式解析器、靠 profile 反查，快照會被判成範本而變可寫——所以不靠 profile。
:::

::: {.callout .crit}
**🔴 重複匯入會清掉上次的內容**

套件 update 模式 props 整份覆寫、資源庫完整匯入清空重建，第二次匯入只填缺失，現況說明就沒了。修完要拿「只填一欄重匯」實測，不能只看程式。
:::

::: {.callout .crit}
**🔴 啟動專案後的初始化會作用在範本 SSP 上**

`_init_ssp_control_implementations`（`project_start_app_service.py:385-389`）對「剛複製出來的新 SSP」補建控制項；指向後它會直接寫範本 SSP，程序書池與 `ssp_risks` 的補複製會變成自己複製給自己。卡 8 的盤點前置是硬性的。
:::

| 陷阱 | 後果 | 對策 |
|---|---|---|
| props helper 有六份 | 漏改一處，那條路徑看不到缺失且不報錯；共用版沒保留「空字串刪鍵」「全空回 None」，清空欄位會存成空字串鍵 | 卡 1 收成一支並保留兩個行為 |
| 同一控制項多筆 IR | 缺失寫進 A 筆、畫面讀 B 筆 | 讀寫、批次存、比對都走 `_ir_for_control` |
| 守門只改一類入口 | 另一類入口就是漏洞 | 兩類入口同時套用能力點與鎖寫；卡 1 到卡 10 之間範本 SSP 沒有稽核中鎖，卡 8 上線前不會有新模式專案，可接受 |
| `snapshot_ssp` 重產 IR／statement id | 程序書掛載與建議附件指向舊 id | 存版、還原、複製資源庫都複用 `clone_pool_for_snapshot` 的 id 對照，建議附件照同一份對照複製；任務與控制項對應（`workflow_execution_control_mapping`）用 `control_id` 字串＋輪次，不受影響 |
| `snapshot_ssp` 不含 `ssp_risks` | 版本快照少了風險 | 存版與還原另外補複製（啟動專案現行做法） |
| 還原換掉範本 SSP 的 id | 記住舊 id 的快取或背景工作讀到舊版 | `template_ssp_id` 與 `living_ssp_id` 同交易換；卡 7 查 `task_alert` 兩支等記 id 的地方 |
| 不可為空欄位（例 `by_component.description`） | 複製路徑寫入失敗 | 沿用套件既有處理 |
| 送審產生兩份快照 | 容易誤以為重複 | 刻意保留：送審版帶建議附件、稽核凍結不帶，靠 `audit_round_id` 互指 |
| `ModuleFrameTemplateEditView.vue` 2,246 行、`ProjectPlanningView.vue` 2,609 行 | 往裡加會被 reviewer 判阻斷 | 新邏輯一律新子元件，原檔只加一行掛載 |

---

## 不做與後續 {#future nav="後續"}

| 項目 | 去向 |
|---|---|
| 控制項填寫區重新設計（D12） | 全部做完、顧問實際用過再看 |
| 其他七個頁籤改動、版本比對擴到控制項以外（D11） | 另案 |
| AO props 舊的 `reference-documents` JSON（D3） | 不碰 |
| docx 匯入加欄位（D4） | 不在顧問訪談流程內 |
| 「顧問」角色定義（D6） | 之後定義時把 `oscal.resource-library.update` 配進去 |
| 稽核流程本身的改動（D9） | 不做 |
| 既有一資源庫多專案資料遷移（D21） | 不做；上版前查 STG／POC |
| 版本歷史隨複製資源庫帶走（D8） | 不做 |
| 資源庫層公版與公版旗標（D8、D17） | 公版屬框架層 |
| 建議附件分類（D15） | 不做 |
| 逐項還原（D11） | 不做 |
| 缺失報告產出 | 卡 4 回填完成後另議 |
