---
title: "顧問訪談欄位與資源庫版本 — 討論稿 (FR-139)"
brand: "Guidant AI · **FR-139** 顧問訪談欄位與資源庫版本"
eyebrow: "FR-139 · 討論稿 · 2026-10-14"
h1: "顧問在資源庫訪談、存版、送審，專案只看不改"
subtitle: "D1～D21 全部已裁；三個子需求、十一張卡；附四頁假資料雛形"
lede: "這份把決策者 2026-10-14 裁定的 D1～D21 展開成可以開工的規格：控制項與評估目標多「缺失」「缺失修改建議」兩欄、建議下方多一塊「建議附件」（獨立一張表，不進程序書池）、訪談表重複匯入可以累積、資源庫有版本可存可比可整份還原、資源庫與專案改一對一且專案規劃頁唯讀、每次送審（主稽核與覆核）都鎖一版。每一項都附現況座標（程式碼行號或 DEV 唯讀查詢）。請先開 [雛形索引](prototype/index.html) 點五頁，再回來看規格。"
chips: [
  {text: "D1～D21 全部已裁", kind: ok},
  {text: "上版前查一資源庫多專案", kind: warn},
  {text: "十一張卡，資源庫控制項畫面先出", kind: accent},
  {text: "動一支套件 jedi-oscal-v2", kind: crit}
]
footer: "FR-139 · 討論稿 · 2026-10-14 · 輸入：決策者 2026-10-14 當面裁示、FR-138 自評實作經驗、雛形 [index](prototype/index.html) · 現況依據：程式碼行號與 DEV DB 唯讀查詢（2026-10-14）"
---

> 設計定案拍板後寫 design.md，本稿不再改寫。

**狀態**：討論稿（D1～D21 全部已裁，下一步寫 design.md、拆卡開 Notion）　｜　**日期**：2026-10-14　｜　**雛形**：[prototype/index.html](prototype/index.html)

路徑慣例：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**＝控制項實施紀錄（`ssp_implemented_requirements`）；**props**＝OSCAL 物件上的「名稱＋值」擴充屬性陣列。

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

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-14 | 初稿：已裁 D1～D13 展開、待補裁 P1～P8；資料模型草案、端到端流程、十卡拆卡草案、四頁雛形引用 | — |
| 2026-10-14 | 決策者審雛形與 P1～P8 後定案 | D3、D7～D11、D14～D21、卡 2b |

---

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

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

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

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

```{.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 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
```

---

## 需求背景與使用情境 {#why nav="背景"}

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

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

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

### 流程改成顧問主導

1. 顧問在資源庫訪談、匯入、整理，完成客戶的 SSP（系統安全計畫）。
2. 啟動專案。客戶的專案管理者只負責指派工作與上傳證據，**不再編輯 SSP**。
3. 顧問持續訪談、持續匯入，SSP 繼續在資源庫更新，專案即時看到。
4. 顧問判斷可以請外部稽核了，按「送審」（就是現在的「啟動稽核」）。那一刻存一版、凍結一份給稽核輪；之後的覆核送審也走同一套。
5. 之後走正常稽核流程，不改。

### 現況痛點

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

---

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

### ① 欄位落點（資源庫範本 SSP 與專案 SSP 同結構）

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 實施狀態 | 存 props `implementation-status`；控制項在 `ssp_implemented_requirements.props`、AO 在 `ssp_statements.props`（`app/oscal/service/ssp_control_implementation_service.py:40-41,93-94,864-865`；資源庫側 `app/module_frame/service/module_frame_control_default_service.py:36-43`、`module_frame_control_objective_default_service.py:1-10`） | 不動欄位；值域收斂（D5③） |
| 現況說明 | props `implementation-description` | 不動 |
| 備註 | 兩表的 `remarks` 欄 | 就是本案的「備註」（D2） |
| 缺失、缺失修改建議 | 沒有 | 新 props 兩鍵，控制項與 AO 都有（D1） |
| AO 識別 | `statement_id` 格式 `<control>_obj.N` | 沿用 |
| 同一控制項多筆 IR | `_ir_for_control` 挑「已有說明」那筆（service `:888-900`） | 新欄位讀寫走同一挑選 |
| props 讀寫 helper | 至少六份複製：`ssp_control_implementation_service.py:960-989`、`module_frame_control_default_service.py:45,58`、`module_frame_control_objective_default_service.py:39,51`、`export/ssp_v2_content_loader.py:203`、`ssp_resources_context_service.py:197`、`import_diff/snapshot_views.py:43` | 收成一支共用（D5④） |

DEV 唯讀查詢（2026-10-14），兩表 `implementation-status` 現存值：`implemented` 70,914 筆、`unknown` 2,133、`partial` 47、`not_applicable` 13、`alternative` 2、`planned` 2。

### ② 附件（程序書池與建議附件）

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 池 | `oscal.ssp_reference_documents`（context_type='ssp'），欄位只有 id／uid／context_type／context_id／file_id／description／審計欄，**沒有種類欄**（DEV 查證）；model 在 jedi-compliance-audit 套件 | 不動；套件不改（D3） |
| 掛載 | `ssp_reference_document_mappings`，context_type `control_implementation`→IR.id、`objective`→statement.id（`app/oscal/service/ssp_document_pool_service.py:1-14`） | 沿用 |
| 實體檔 | `upload_files` | 沿用 |
| AO 舊機制 | props `reference-documents` 塞 JSON（service `:44, :861-862`） | 不碰（D3） |
| 兩線分工 | FR-135 D7：程序書掛控制項或 AO、證據掛工作，兩線不混（`docs/features/FR-135-2610-cra-compliance-framework/design-cra.md:379-389`） | 照舊 |
| 建議附件 | 沒有 | 新表 `compliance.ssp_control_attachments`，同樣掛控制項或 AO，但不進程序書池、不與證據混（D3） |

### ③ 訪談表匯入（兩條路徑）

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 路徑 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`（statement_id 隱藏／控制項識別碼／名稱／AO 編號／AO 名稱／實作狀態／現況描述／參考程序書／納入 Profile），**沒有備註欄**；寫入 `import_adapter/excel_to_oscal_ssp.py:62-110` | 加三欄（D4） |
| 路徑 A 資源庫模式 | `mode="replace"` 清空重建（`ssp_excel_import_app_service.py:448-510`） | 改合併語意（D5②） |
| 路徑 A 專案模式 | `mode="update"` upsert | 新模式專案指向範本 SSP、不再從專案側匯入；既有舊模式專案照舊（D21） |
| 路徑 B：控制項現況九欄 | `app/oscal/service/ssp_control_impl_import_service.py`（欄位 docstring `:9-10`），端點 `/ssp/<ssp_uid>/control-implementations/import`（`api/oscal/__init__.py:191`）；逐列呼叫 update、逐 key 合併（`_setp`）；欄名與資源庫共用 `module_frame_template_import_service.py:92`；模板版本檢查 `excel_parser/version_check.py` | 加三欄（D4） |
| 套件 update 模式 | `pkg/jedi-oscal-v2/.../oscal_io_service.py:1308` IR／statement 的 props **整份覆寫**；None 跳過、要清空要傳 `""`（`:356-360`） | 改逐 key 合併（D5①） |
| 實施狀態值域 | 三處不一致：FE 下拉 implemented／partial／planned／alternative／not_applicable（`ModuleFrameTemplateEditView.vue:348-368` 寫死）；Excel 匯入 `module_frame_template_import_service.py:79-86` 為 implemented／partial／not_applicable／not_implemented／inherited／unknown；SQL 註解 `scripts/sql/2026-06-02-erd-chinese-comments.sql:720` 另一組。OSCAL 標準五值 implemented／partial／planned／alternative／not-applicable | 收進 `system_menus`（D5③、D20） |
| docx 匯入 | 另一條路徑 | 不動（D4） |

### ④ 權限與編輯畫面

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 資源庫編輯頁 | `src/views/module_frame/ModuleFrameTemplateEditView.vue`（2,246 行、八頁籤；控制項表單 `:1555-1610`、AO `:1760-1810`、附件 `:724-880`），路由 `/module-frame/:uid/template-edit` | 加欄位，新邏輯開子元件（超標檔） |
| 範本 SSP 寫入守門 | `common/authz/ssp.py:215 _require_admin_for_template`：只有帳號層超級管理員能寫，註解留「未來考慮加 mf-owner role」。**但雙模式解析器沒有接上**（`di_containers/oscal/oscal_containers.py:345` 註明 `ssp_context_resolver` 至今未注入），checker 一律走專案分支，這條實際上走不到；資源庫的寫入靠 route 上的能力點與資源庫 service 直接拿 ssp_id 寫 | 守門要放在真正會經過的入口（D6、D18，見坑 2） |
| 專案 SSP 寫入守門 | `common/authz/ssp.py:99-103,187-211`：專案管理者＋可寫判斷 | 專案側改唯讀（D7） |
| 可寫判斷 | `domain/oscal/service/ssp_project_resolver.py:111-133`：凍結快照一律鎖；living 在無輪次或當前輪 `planning` 才可寫 | 規則搬到資源庫側（D9） |
| SSP 歸屬判斷 | 實際生效的是 `ssp_project_resolver.py:97-108`：先查 `projects.living_ssp_id`、再查 `project_audit_rounds.ssp_id`，都查不到就「找不到」；雙模式的 `ssp_context_resolver.py:98-118`（用 profile 反查資源庫）寫好但沒接上 | 改指向後要重看（見坑 2） |
| 資源庫端點 | `api/module_frame/__init__.py:96-244`，權限 `oscal.resource-library.{read,create,update,delete}` | 沿用 `update`（D18） |
| 專案規劃頁 | `src/views/project/ProjectPlanningView.vue`（2,609 行），`canEditSsp = apStatus === 'planning'`（`:158`）；控制項存檔 `:1443` 沒送 remarks、AO `:1463` 有送 | 唯讀（D7） |

### ⑤ 專案啟動、凍結、版本

| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 啟動專案 | `app/project/service/project_start_app_service.py:347` 呼叫套件 `clone_resource_library` 複製 catalog／profile／SSP 三件組，IR／statement 整列複製；`:378` 寫 `living_ssp_id=new_ssp_id`；程序書池與 `ssp_risks` 手動補複製（`:231-299, :392-409`） | 不再複製，直接指向（D8） |
| 啟動後初始化 | `:385-389` `_init_ssp_control_implementations` 對新 SSP 補建控制項 | 改指向後會作用在範本 SSP 上；卡 8 先盤點再動（D8、坑 3） |
| 專案表 | `compliance.projects` 有 `module_frame_id`、`living_ssp_id`（DEV 查證） | 一對一約束 |
| 一資源庫多專案現況 | DEV：資源庫 460 綁 3 個專案、427 綁 1 個；沒有任何專案的 `living_ssp_id` 等於範本 SSP | 不遷移，上版前查 STG／POC（D21） |
| 讀 `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 沒有複製端點 | 把啟動專案現有的複製邏輯搬過來（D8、D17） |
| 稽核凍結 | 輪次啟動稽核時 `snapshot_ssp` 複製 living SSP 到 `project_audit_rounds.ssp_id`；程序書池由 `ssp_document_pool_service.py:91 clone_pool_for_snapshot` 補 | 沿用，送審時多存一版並記對應稽核輪（D9） |
| SSP 主表 | v2 只有 id／uuid／metadata_id／status／import_profile_id（DEV 查證），早期 FR-019 的 group_id／version_no 已不存在 | 不加欄（D10） |
| OSCAL 版本欄 | `oscal.metadata` 有 `version`、`revisions` JSONB，主專案目前沒用 | 啟用（D10） |
| 整份複製 | `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` 可對快照 dict 比）、`decision_merge.py:107 merge_by_decisions` | 接「SSP 對 SSP」入口（D11） |

---

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

### props 新鍵（控制項 IR 與 AO statement 都有）

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

規則：空字串＝刪掉該鍵（與 `_setp` 現行行為一致）；讀取時沒有該鍵視為空。匯出 docx／Excel、差異比對、合併決策都要認得這兩鍵（D5④ 共用 helper 一次處理）。

### 新表：建議附件 `compliance.ssp_control_attachments`（主專案）

顧問附在「缺失修改建議」下面的檔案。不分類，只有檔案加一行說明。

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

程序書池（`oscal.ssp_reference_documents`）與 jedi-compliance-audit 套件都不動。存版、還原、複製資源庫要把這張表跟著複製並把 `context_id` 換成新的 IR／statement id（見坑 7）；**稽核凍結不複製這張表**——建議是給客戶改善用的，不是稽核基準（D3）。

### OSCAL 原生版本欄（不加欄，只開始寫）

| 欄 | 寫法 |
|---|---|
| `metadata.version` | 每存一版寫成版號字串，與索引表 `version_label` 同步 |
| `metadata.revisions` | 每存一版追加一筆：`{version, last-modified, title, remarks, links:[{rel: "predecessor-version", href: "#<前一版 SSP uuid>"}]}` |

### 新表：SSP 版本索引 `compliance.ssp_versions`（主專案）

設計成「SSP 的版本」，不是「資源庫的版本」：第一版只有資源庫用，日後別的擁有者也能掛。

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

唯一鍵 `UNIQUE (owner_type, owner_id, version_no)`。新表照 SQL migration 鐵則加 `GRANT ... TO cm_app` 與 sequence 權限。

**放哪個 schema 的判準**：model 寫在哪個套件就放那個 schema。上面兩張 model 都寫在主專案，所以放 `compliance`；`ssp_id` 跨 schema FK 指 `oscal.system_security_plans` 是正常的（先例 `project_audit_rounds`），FK 要寫完整 schema 名。

稽核凍結快照**不進**這張表——那份已經記在 `project_audit_rounds.ssp_id`。送審時存的版（`source='submit'`）與稽核凍結是兩份不同的快照，靠 `audit_round_id` 互指：前者給顧問回顧比對，後者給稽核輪當基準（坑 10）。

### 三層關係

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

### `public.system_menus` 實施狀態 seed

主線 `phase=seed` migration，`ON CONFLICT DO NOTHING`，先例 `scripts/sql/2026-05-22-seed-oscal-ssp-roles.sql`。group `ssp_implementation_status`，key 六個：`implemented`／`partial`／`planned`／`alternative`／`not_applicable`／`unknown`（D20）。FE 改走 `API.SYSTEM_MENU`，i18n 走 `lang.<group>.<key>`；Excel 匯入的合法值清單與 BE 驗證改讀同一份。

---

## 端到端流程 {#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
    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）
    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）
    A->>A: 凍結快照給稽核輪
    Note over RL: 稽核中資源庫鎖寫
    A-->>RL: 輪次進整改即解鎖，顧問可繼續整理
    C->>A: 覆核送審（同一套：存版、凍結、鎖寫）
```

### 儲存與版本操作

| 操作 | 做什麼 | 開版嗎 |
|---|---|---|
| 暫存 | 全頁未存的控制項與 AO 一次存進工作副本（整條控制項連 AO 批次存 API） | 否 |
| 儲存成新版本 | 先暫存，再 `snapshot_ssp` 複製一份、補程序書池、索引表加一列、`metadata.version` 加一並追加 revision | 是 |
| 匯入成功 | 匯入確認成功後自動存一版 | 是 |
| 看舊版 | 開舊版 SSP，既有編輯器帶唯讀旗標 | — |
| 比對 | 兩版之間（或某版對工作副本），第一版只比控制項與 AO 的實施層欄位 | — |
| 還原到某版 | 整份換回去，不逐項挑。先列「會改動：控制項 N 條、負責單位 M 筆……」給顧問確認；確定後先把現在的工作副本存成一版（`pre_restore`），再把那一版整份複製成新工作副本，資源庫與專案的指向一起換過去 | 是 |

---

## 畫面放法 {#ui nav="畫面"}

雛形五頁（假資料，另一棒刻）：

| 頁 | 檔案 | 看什麼 |
|---|---|---|
| 索引 | [prototype/index.html](prototype/index.html) | 五頁入口 |
| 控制項填寫 | [prototype/control-edit.html](prototype/control-edit.html) | 資源庫「適用控制項」頁籤：控制項與 AO 表單多兩個文字框、建議正下方多「建議附件」區塊（與下方程序書分開）、頁首「暫存」「儲存成新版本」 |
| 版本歷史 | [prototype/version-history.html](prototype/version-history.html) | 版本列表（版號、來源、說明、誰、何時；送審版標對應稽核輪）、看舊版、比對、還原 |
| 版本比對 | [prototype/version-compare.html](prototype/version-compare.html) | 兩版之間只列有差異的控制項與 AO，四態著色；整份還原的確認框 |
| 專案規劃頁唯讀 | [prototype/project-planning-readonly.html](prototype/project-planning-readonly.html) | 控制項欄位全灰、頁首提示「內容即時同步自合規資源庫」、任務設定照舊可編輯 |

::: {.callout .decided}
**控制項填寫區這次只加欄位（甲案）**

- 控制項表單與 AO 表單各在「備註」之後加「缺失」「缺失修改建議」兩個多行文字框。
- 「缺失修改建議」正下方加「建議附件」區塊：上傳只選檔、寫一行說明，不分種類。
- 原本的「程序書」區塊放在建議附件下方，維持原樣、分開呈現。
- 頁首加「暫存」與「儲存成新版本」兩顆鈕，以及「版本歷史」入口。
- 其他七個頁籤不動。

**理由**：先讓顧問能把缺失寫進正確欄位，重新設計填寫區等全部做完、實際用過再看。`ModuleFrameTemplateEditView.vue` 已 2,246 行，新增的欄位與建議附件區塊一律做成新子元件掛進去。
:::

專案規劃頁：資源庫帶來的資料（控制項與 AO 實施層欄位、程序書、建議附件、系統特性等其他 SSP 頁籤）全部唯讀，頁首一行提示「內容即時同步自合規資源庫」並附「前往合規資源庫」連結（有權限的人才看得到連結）；任務設定（指派、到期日、說明）照舊可編，不動。

---

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

::: {.callout .decided}
**✅ D1 — 缺失與缺失修改建議存 OSCAL props**

**定案**：新增兩個 props 鍵（`gap`、`recommendation`，帶自家 ns），控制項與 AO 兩層都有。不加實體欄、不開側表、不寫 POA&M。

**理由**：與既有「實施狀態」「現況說明」走同一條路；啟動專案、稽核凍結、存版都是整列複製，props 自動帶走；OSCAL 標準允許自訂 props。

**被排除**：套件加實體欄（OSCAL 標準沒有這欄，還要動 schema）；主專案側表（每條複製路徑都要手動補，程序書池就曾因此漏帶）；直接寫評估結果或 POA&M（啟動稽核才會生成，資源庫階段沒有容器可放）。
:::

::: {.callout .decided}
**✅ D2 — 「備註」就是既有 `remarks`**

**定案**：不新增欄位。

**理由**：兩表本來就有 `remarks`；資源庫 AO 有在用，只是專案規劃頁控制項存檔沒送（`ProjectPlanningView.vue:1443`），改唯讀後這個漏洞自然消失。
:::

::: {.callout .decided}
**✅ D3 — 建議附件獨立一張表，不進程序書池**

**定案**：顧問附在建議下面的檔案存新表 `compliance.ssp_control_attachments`（`context_type` 為 `control_implementation`／`objective`，加 `context_id`、`file_id`→`upload_files`、`description`、審計欄）。程序書池不動、不加文件種類欄，jedi-compliance-audit 套件不改。版本存版、還原、複製資源庫時這張表跟著複製；稽核凍結不帶。畫面放在「缺失修改建議」正下方，區塊名「建議附件」，與下方「程序書」分開。不分類，只有檔案加一行說明（D15）。AO 層 props 裡舊的 `reference-documents` JSON 不碰。

**理由**：程序書是客戶自己的 SOP，用來證明現況，要進稽核凍結；顧問附件是建議的一部分，客戶照著做完就沒用了，不該混進稽核基準。兩者若同池、靠種類欄過濾，稽核側每個讀程序書的地方都要多一個條件，漏一處就讓建議範本跑進稽核。

**被排除**：程序書池加「文件種類」欄（稽核側讀取處要逐一加過濾，且要改套件）；用檔名或描述欄約定（查不了、篩不了）。
:::

::: {.callout .decided}
**✅ D4 — 訪談表兩條路徑都加三欄，docx 不動**

**定案**：路徑 A（完整匯入）與路徑 B（控制項現況九欄）的欄位定義都加「備註」「缺失」「缺失修改建議」；匯出同步帶出。docx 匯入不動。

**理由**：訪談表就是現況資料 Excel，顧問在現場直接填；兩條路徑共用欄名定義，只改一處會造成兩份表欄位不一致。docx 是另一種來源，不在顧問訪談流程內。
:::

::: {.callout .decided}
**✅ D5 — 四個坑全收**

**定案**：

1. 套件 update 模式的 props 改成逐鍵合併：檔案裡有的鍵才蓋，沒有的鍵保留。
2. 資源庫完整匯入改用合併語意，不清空重建，讓多次匯入可以累積。
3. 實施狀態值域收進 `system_menus` 當唯一來源（六值，拼法見 D20），FE、Excel 驗證、BE 驗證都讀它。
4. props 讀寫 helper 收成一支共用，匯出、差異比對、合併決策同步認得新鍵。

**理由**：不修①②，顧問第二次匯入只填了缺失欄，上次的現況說明就被清掉——這正是「多次匯入累積」的反面。不修③，三處各自一組值，新欄位上線後下拉、匯入、匯出會各說各話。不修④，新鍵要改六個地方，漏一處就是某條路徑看不到缺失。
:::

::: {.callout .decided}
**✅ D6 — 有資源庫編輯權的人都能編範本 SSP**

**定案**：拿掉「只有帳號層超級管理員能寫範本 SSP」的守門（`common/authz/ssp.py:215`），改成具備資源庫編輯權限即可。「顧問」角色之後再定義。這條守門目前其實走不到（雙模式解析器沒接上，見盤點 ④），所以實作是「在範本 SSP 的所有寫入入口要求資源庫編輯權」，不是改一行判斷。

**理由**：顧問是主要編輯者，現行守門是開發期的暫時做法（註解自己寫著「dev-stage gate」）。

**被排除**：先建「顧問」角色再開權限（角色定義牽涉授權模型，會卡住整案）。
:::

::: {.callout .decided}
**✅ D7 — 專案規劃頁唯讀，編輯權只在資源庫**

**定案**：專案規劃頁上，資源庫帶來的資料除任務設定外全部唯讀。任務設定（指派、到期日、說明）照舊可編、不動；唯讀的是控制項與 AO 實施層欄位、程序書、建議附件、其他 SSP 頁籤。FE 的可編判斷與 BE 專案側 SSP 寫入守門一起改。

**理由**：客戶專案管理者只指派工作與上傳證據；SSP 是顧問的工作成果，兩邊都能改就會互相覆蓋。
:::

::: {.callout .decided}
**✅ D8 — 資源庫與專案一對一，啟動不再複製 SSP**

**定案**：

- 啟動專案時專案直接指向資源庫的範本 SSP（`living_ssp_id`＝`template_ssp_id`），不再複製三件組。
- 啟動時擋「這個資源庫已經有進行中的專案」。
- 資源庫有專案綁著不能刪。
- 新年度走「複製資源庫」產生新資源庫，程序書池、建議附件與表單設定要帶過去，版本歷史不帶。做法見 D17。
- **資源庫沒有公版**。公版是框架層（framework／catalog）的概念；`module_frames.scope` 欄位保留不拆，概念預留但預設不開放使用者，畫面不出現公版資源庫。顧問從框架層選控制集，自建、自維護資源庫。

**卡 8 硬性前置**：runner 動手前先逐項盤點 `app/project/service/project_start_app_service.py` 的複製鏈（三件組、程序書池、`ssp_risks`、控制項初始化 `:385-389`），分成三類列出——「啟動專案要留的」「搬去複製資源庫的」「當初順手塞進來的」——回報後才動。這段程式本身寫得亂，不盤點直接改，容易把該留的一起拿掉。

**理由**：顧問啟動專案後仍持續訪談，只有指向同一份，專案才能即時看到；複製一份就得再設計「資源庫改了怎麼同步到專案」，那是兩份真相。

**被排除**：維持複製、另做同步（雙向衝突與同步時機都要另外設計）；一個資源庫多個專案共用一份 SSP（不同客戶的現況會互相覆蓋）；資源庫層加「公版」旗標（公版屬框架層，資源庫層再做一套是兩份概念）。
:::

::: {.callout .decided}
**✅ D9 — 送審即鎖版，稽核中資源庫鎖寫**

**定案**：「送審」就是現在的「啟動稽核」。按下時依序做「存版→鎖版→凍結→鎖寫」：存一版進版本索引（`source='submit'`，`audit_round_id` 記對應稽核輪），照舊凍結快照給稽核輪，資源庫進入鎖寫。輪次進入整改（remediation）就解鎖（D19）。**覆核輪（close_out）送審走同一套**，不做特例；覆核送審時再鎖一次。版本歷史與稽核輪靠 `audit_round_id` 互指，三層關係見資料模型「三層關係」表。稽核流程本身不改。

**理由**：外部稽核看的是送審那一刻的內容，顧問要能回頭看「送審時寫了什麼」；主稽核與覆核各對應哪一版，要能從兩頭查到。

**被排除**：稽核中仍允許改工作副本——專案規劃頁直接指向工作副本，客戶專案管理者會看到規劃頁與稽核中的內容不一致；覆核另做一套（兩套鎖版規則容易走歪）。
:::

::: {.callout .decided}
**✅ D10 — 版本走 OSCAL 原生欄位＋一張索引表**

**定案**：版號與修訂史寫 OSCAL 原生 `metadata.version` 與 `metadata.revisions`（每筆 revision 的 links 帶 `rel=predecessor-version` 指前一版 uuid）；舊版內容用套件 `snapshot_ssp` 整份複製成獨立 SSP；版本這件事只多一張索引表，設計成「SSP 的版本」（`owner_type`＋`owner_id`），第一版只有資源庫用。OSCAL 各表不加任何版本欄。

**理由**：OSCAL 本來就有版本與修訂史欄位，匯出的 SSP 自帶版本資訊；整份 SSP 快照讓看舊版、比對、還原都能直接用既有的編輯器與 diff，不用多一層轉換。

**被排除**：只寫 metadata（做不到回顧、比對、還原）；存 JSON 文件快照（還原要走匯入路徑，匯入的坑沒修之前不可靠；看舊版與比對都要多一層轉換）；在 SSP 主表加 group_id／version_no（早期 FR-019 的做法，v2 已移除）。
:::

::: {.callout .decided}
**✅ D11 — 版本操作的範圍**

**定案**：

- 版本快照是**整份 SSP**：主表、12 子表、程序書池掛載、建議附件、風險。
- 暫存不開版；儲存成新版本先暫存再快照；匯入確認成功自動存一版；版本歷史列表；唯讀開舊版（既有編輯器加唯讀旗標）。
- **比對**接既有 diff 的「SSP 對 SSP」入口，第一版只比控制項與 AO 的實施層欄位：狀態、現況、備註、缺失、建議、程序書掛載、建議附件。其他頁籤不比。
- **還原＝整份換回去，不做逐項**。按下先列「會改動：控制項 N 條、負責單位 M 筆……」給顧問確認；確定後把舊工作副本自動存成一版（`pre_restore`），再把那一版複製成新工作副本並換指向，所以永遠可逆。
- 稽核凍結快照不進索引表。

**理由**：顧問在乎的是控制項層的內容變化，比對先做這一層，避免範圍膨脹；還原逐項挑會讓「還原後的工作副本」變成哪一版都不是，整份換加上確認清單，顧問知道會動到哪些頁籤。
:::

::: {.callout .decided}
**✅ D12 — 控制項填寫區這次只加欄位**

**定案**：甲案——控制項與 AO 表單各加兩個文字框，建議下方加「建議附件」區塊。重新設計留到全部做完再看。BE 照樣做「整條控制項連 AO 批次存」API，給暫存用。

**理由**：先讓資料有地方放，畫面改版等顧問實際用過再決定。
:::

::: {.callout .decided}
**✅ D13 — 範本 460 的缺失回填另開小卡**

**定案**：FR-138 在範本 460 塞進「現況說明」的缺失，新欄位上線後開一張小卡，人工或半自動拆到「缺失」欄。

**理由**：460 是第一個真實使用者案例，拆完正好驗證新欄位與缺失報告的需求。
:::

::: {.callout .decided}
**✅ D14 — props 的 ns 與 name**

**定案**：ns＝`https://guidant.ai/ns/oscal`，name＝`gap`、`recommendation`。全產品之後的自訂 props 都用同一個 ns。

**理由**：OSCAL 規定自訂 props 帶自家 ns，避免與標準名稱撞；全產品一個 ns，之後不必再為每個欄位另想。
:::

::: {.callout .decided}
**✅ D15 — 建議附件不分類**

**定案**：建議附件只有檔案加一行說明，不分種類、不進 `system_menus`。

**理由**：建議附件與程序書已經分表（D3），種類的作用已由「放在哪張表」承擔，再分類沒有讀取端要用。
:::

::: {.callout .decided}
**✅ D16 — 兩張新表的 schema 與名稱**

**定案**：`compliance.ssp_versions`（版本索引）、`compliance.ssp_control_attachments`（建議附件）。判準是「model 寫在哪個套件就放那個 schema」——兩張 model 都在主專案，放 `compliance`。跨 schema FK 指 `oscal.system_security_plans` 是正常的，先例 `project_audit_rounds`。

**理由**：放 `oscal` 會讓人以為是 jedi-oscal-v2 套件的表；schema 跟著 model 歸屬走，查表時不必猜誰管。
:::

::: {.callout .decided}
**✅ D17 — 複製資源庫走搬家，不另寫**

**定案**：把啟動專案現有的複製邏輯（套件 `clone_resource_library` 複製三件組，加程序書池、表單設定、`ssp_risks`）搬成「複製資源庫」，再補帶建議附件。啟動專案不再用它。不做公版旗標，`module_frames.scope` 欄位留著（D8）。搬之前照 D8 的卡 8 前置先盤點。

**理由**：複製路徑與現在的啟動專案幾乎相同，搬過來等於重用；匯出 Excel 再匯入帶不走程序書檔，也依賴匯入坑修完。

**被排除**：匯出再匯入；資源庫加公版旗標（公版屬框架層，D8）。
:::

::: {.callout .decided}
**✅ D18 — 「有資源庫編輯權」＝`oscal.resource-library.update`**

**定案**：沿用 `oscal.resource-library.update`（目前 51 處使用，資源庫寫入端點已全用它），不另立新能力點。**範本 SSP 的所有寫入入口都要求此能力點**，包含坑 2 提到的 SSP 共用端點：這些端點依「此 SSP 是否為某資源庫的 `template_ssp_id`」分流，是就走資源庫守門（此能力點＋稽核中鎖寫），不是才走專案守門。之後定義「顧問」角色時把這個能力點配進去即可。

**理由**：只守資源庫自己的端點，客戶專案管理者仍能從共用端點改到顧問的 SSP（坑 2、坑 6）。
:::

::: {.callout .decided}
**✅ D19 — 輪次進整改就解鎖**

**定案**：輪次進入整改（remediation）資源庫就解鎖，不必等結案；覆核送審時再鎖（D9）。

**理由**：顧問在整改期正好要邊訪談、邊更新現況與建議；稽核基準已凍結在稽核輪自己的快照，改工作副本不影響稽核結果。
:::

::: {.callout .decided}
**✅ D20 — 實施狀態保留底線拼法、多收 `unknown`**

**定案**：key 保留底線 `not_applicable`，OSCAL 五值之外多收 `unknown`（＝還沒訪談），共六值：`implemented`／`partial`／`planned`／`alternative`／`not_applicable`／`unknown`。值域進 `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；底線拼法已散在讀取端與七萬多筆資料裡（`ssp_control_implementation_service.py:42` 的註解即依賴它），硬改風險大而對使用者沒有可見收益。`unknown` 是真實狀態，顧問需要。

**被排除**：key 對齊 OSCAL 連字號並 migration 改既有資料（要找出所有依賴底線拼法的讀取端，`unknown` 也無處可去）。
:::

::: {.callout .decided}
**✅ D21 — 既有資料不搬，只對新啟動專案生效**

**定案**：既有「一資源庫多專案」的舊模式專案不遷移，維持各自獨立的 SSP 副本，照舊在專案側可寫（守門依「專案 SSP 是否就是某資源庫的 `template_ssp_id`」分流）；一對一檢查只算新模式專案。**STG／POC 上線前檢查有無一資源庫多專案，列進上版檢查項。**

**理由**：DEV 實況資源庫 460 綁 3 個專案、427 綁 1 個，全部是舊的「複製一份」模式；遷移要決定哪個專案接手範本 SSP，對既有客戶的稽核輪沒有收益。
:::

---

## 拆卡草案（依「決策者何時看得到畫面」排序） {#cards nav="拆卡"}

| 順序 | 卡 | 做什麼 | 主要檔案 | 前置 | 驗收（決策者親手做什麼、看到什麼） |
|---|---|---|---|---|---|
| 1 | **卡 1 BE 新欄＋共用 helper＋批次存 API** | props `gap`／`recommendation` 讀寫（ns 照 D14）；六份 helper 收一支；整條控制項連 AO 批次存端點；範本 SSP 寫入一律要求 `oscal.resource-library.update`（兩類入口都要，依 `template_ssp_id` 分流，見 D18、坑 6） | `ssp_control_implementation_service.py`、資源庫兩支 default service、export loader、diff、`decision_merge.py`、`common/authz/ssp.py` | — | 與卡 2 一起驗 |
| 1 | **卡 2 FE 資源庫控制項欄位＋暫存鈕** | 甲案：兩層表單各加兩框；頁首暫存鈕（接卡 1 批次存） | `ModuleFrameTemplateEditView.vue` 只掛新子元件 | 卡 1 API 介面先定 | 在範本 460 填一條控制項與其 AO 的缺失、建議，按暫存後重新整理頁面，內容都還在 |
| 1 | **卡 2b 建議附件** | `compliance.ssp_control_attachments` migration（GRANT 齊全）；上傳／列表／改說明／刪除 API（權限同 D18）；FE「建議附件」區塊放缺失修改建議正下方，與程序書區分開 | 新 domain／infra／app service、新 route、FE 新子元件 | 卡 1 | 在 460 某條控制項與其 AO 各上傳一份建議附件並寫說明，重新整理還在；下方程序書區與「程序書文件池」頁籤都看不到它 |
| 2 | **卡 3 Excel 三欄＋狀態選項＋套件合併規則** | 兩路徑加三欄（匯入匯出、欄名、模板版本）；實施狀態 seed（D20 六值）與 FE、Excel 驗證、BE 驗證讀 menu、匯出 OSCAL 轉標準拼法；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:448-510`、`pkg/jedi-oscal-v2/.../oscal_io_service.py`、seed migration | 卡 1 | 下載 460 的訪談表看到三個新欄；只填某條的「缺失」重新匯入，該條現況說明與其他條都沒被清掉；實施狀態下拉選項與 Excel 驗證一致、有「未評估」 |
| 3 | **卡 5 版本索引表＋存版 API** | `compliance.ssp_versions` migration（含 `audit_round_id`）；存版（暫存→`snapshot_ssp`→補程序書池與建議附件→索引→metadata 版號與 revision）；版本列表；匯入成功自動存版 | 新 service、`ssp_document_pool_service.py:91` 複用 | 卡 1、卡 2b | 與卡 6 一起驗 |
| 3 | **卡 6 FE 版本歷史頁＋存版鈕** | 頁首「儲存成新版本」（填說明）、版本歷史頁、唯讀開舊版 | 新頁＋新子元件 | 卡 5 | 改兩條控制項後存成新版本，版本歷史多一列且來源寫「手動」；點舊版打開，所有欄位不能改 |
| 4 | **卡 7 比對與還原** | 既有 diff 加「SSP 對 SSP」入口，只比控制項與 AO 實施層（含程序書掛載、建議附件），其他頁籤不比；整份還原：先算出各頁籤會改動的筆數給確認框，確定後存 `pre_restore`、複製、換指向 | `ssp_diff_service.py`、還原 service、比對頁 | 卡 5 | 比第 1、2 版只看到那兩條的差異標記；按還原到第 1 版，確認框列出「會改動：控制項 2 條……」與新版號；確定後歷史多一列，再還原回去內容一字不差、兩種附件仍在 |
| 5 | **卡 4 範本 460 缺失回填** | 把塞在現況說明的缺失拆到缺失欄，可半自動（找出含「缺失」字樣的條目列給人確認） | DEV 資料作業（只動 DEV） | 卡 1、卡 2 | 在 460 抽五條，現況說明只剩現況，缺失欄有拆出來的那段 |
| 6 | **卡 8 啟動改指向＋一對一守門＋複製資源庫** | **前置：先盤點 `project_start_app_service.py` 複製鏈，分「啟動要留／搬去複製資源庫／順手塞的」三類回報，回報後才動（D8）**；啟動不再複製、指向範本 SSP；擋重複啟動；有專案不可刪；複製資源庫（搬現有複製邏輯，帶程序書池、建議附件、表單設定、`ssp_risks`，D17） | `project_start_app_service.py`、資源庫刪除、新複製端點與列表動作 | 卡 2b | 盤點清單先交；從資源庫啟動專案，在資源庫改一條現況，專案規劃頁立刻看到；同一資源庫再啟動一次被擋；複製資源庫後新資源庫內容與兩種附件齊全、版本歷史是空的 |
| 7 | **卡 9 規劃頁唯讀＋守門調整** | 規劃頁資源庫資料唯讀（任務設定照舊可編）、頁首提示；專案側 SSP 寫入守門擋掉「指向範本」的專案；確認 11 支讀 `living_ssp_id` 的地方無寫入 | `ProjectPlanningView.vue`（只加唯讀旗標與提示子元件）、`common/authz/ssp.py`、`ssp_project_resolver.py`（範本判斷改 `template_ssp_id` 直比，見坑 2） | 卡 8 | 新模式專案的規劃頁，控制項與 AO 欄位全灰、任務指派與到期日仍可改；用專案管理者帳號直接打寫入 API 被擋 |
| 8 | **卡 10 送審鎖版＋稽核中鎖寫** | 主稽核與覆核送審都走「存版（`submit`＋`audit_round_id`）→鎖版→凍結→鎖寫」；資源庫寫入守門加「綁定專案當前輪在稽核中就鎖」；進整改解鎖（D19） | `oscal_stage_handlers.py`、範本 SSP 寫入守門 | 卡 5、卡 8 | 送審後回資源庫，存檔與暫存鈕不能按、版本歷史多一列「送審」並標對應主稽核輪；輪次進整改後又能編輯；覆核送審後再鎖、多一列標覆核輪 |

e2e 屬母卡收口，不在單卡做。只動一支套件 jedi-oscal-v2（卡 3），開發期走 path dependency，發版等決策者明示；jedi-compliance-audit 不動（D3）。

**上版檢查項**：STG／POC 上線前查有無「一資源庫多專案」（D21）；兩張新表的 migration 照環境異動鐵則只先套 DEV。

---

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

- 控制項填寫區重新設計（D12，這次只加欄位）。
- 其他七個頁籤的任何改動，與版本比對擴到控制項以外的頁籤（D11）。
- AO 層 props 裡舊的 `reference-documents` JSON（D3）。
- docx 匯入加欄位（D4）。
- 「顧問」角色定義（D6，先用資源庫編輯權）。
- 稽核中允許改工作副本（D9）。
- 既有「一資源庫多專案」資料的遷移（D21）。
- 稽核流程本身的任何改動（D9）。
- 版本歷史跟著「複製資源庫」帶到新資源庫（D8）。
- 資源庫層的公版與公版旗標（D8、D17）。
- 建議附件分類（D15）。
- 逐項還原（D11）。
- 缺失報告產出（D13 的 460 回填完成後另議）。

---

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

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

套件 update 模式的 props 是整份覆寫（`oscal_io_service.py:1308`），資源庫完整匯入是清空重建（`ssp_excel_import_app_service.py:448-510`）。第二次匯入只填缺失，現況說明就沒了。D5①② 修；修完要拿「只填一欄重匯」實測，不能只看程式。
:::

::: {.callout .crit}
**🔴 2. 改指向後，同一份 SSP 會同時被當成「資源庫的」與「專案的」**

現在實際生效的歸屬判斷只有專案那條（`ssp_project_resolver.py:97-108`：`living_ssp_id` → 稽核輪 `ssp_id`）；雙模式解析器沒接上，`_require_admin_for_template` 走不到；資源庫的寫入靠 route 能力點加 service 直接寫。D8 之後：

- 範本 SSP 的 id 就是專案的 `living_ssp_id`。所有走 `SspPermissionChecker` 的 SSP 共用端點（系統特性、元件、外部服務、責任人員、控制項現況等）會把它判成**專案 SSP**，套用「專案管理者＋規劃階段可寫」——客戶專案管理者能透過這些端點改到顧問的 SSP，D7 的唯讀只做在畫面上是擋不住的。
- 版本快照（索引表那些）既不是 `living_ssp_id` 也不是稽核輪 `ssp_id`，走這些端點會「找不到」——唯讀開舊版得另開讀取入口，或讓解析器認得版本快照。
- 快照沿用來源的 profile（`pkg/jedi-oscal-v2/.../ssp/ssp_clone_service.py:129-131` 以 `replace(src, id=None, …)` 整列複製根節點），若日後把雙模式解析器接上，快照會被 profile 反查判成範本、變成可寫。

判斷要改成：範本＝`module_frames.template_ssp_id` 直接比對（不靠 profile），命中就走資源庫守門（能力點＋稽核中鎖寫），不再走專案管理者那條；版本索引裡的 SSP 與 `status='frozen'` 一律唯讀。能力點分流在卡 1（D18），歸屬判斷與專案側擋法在卡 9，稽核中鎖寫在卡 10。
:::

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

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

::: {.callout .warn}
**4. props 讀寫 helper 有六份**

新鍵要在六處都認得；`_setp` 遇空字串刪鍵、`_map_to_props` 全空回 None，共用版要保留這兩個行為，否則「清空某欄」會變成存一個空字串鍵。export loader、差異比對、合併決策漏改，那條路徑就看不到缺失，且不會報錯。
:::

::: {.callout .warn}
**5. 同一控制項可能有多筆 IR**

`_ir_for_control`（service `:888-900`）挑「已有說明」那筆。新欄位讀寫、批次存、比對都要走同一個挑選，否則缺失寫進 A 筆、畫面讀 B 筆。
:::

::: {.callout .warn}
**6. 守門要兩條一起改**

範本 SSP 的寫入入口有兩類：資源庫自己的端點（route 能力點＋service 直接寫）與 SSP 共用端點（經 `SspPermissionChecker`）。「有資源庫編輯權才能寫」與「稽核中鎖寫」兩條規則要在兩類入口都成立。只改一類，另一類就是漏洞。卡 1 先處理能力點、卡 10 補稽核中鎖寫，中間這段時間範本 SSP 沒有稽核中鎖——卡 8 上線前不會有新模式專案，風險可接受。
:::

::: {.callout .warn}
**7. 存版與還原時 IR 會重產 id，程序書掛載要跟著複製**

程序書掛載表與建議附件表都用 IR.id 與 statement.id，`snapshot_ssp` 會重產 id。存版、還原、複製資源庫都要複用 `clone_pool_for_snapshot`（`ssp_document_pool_service.py:91`），建議附件另外照同樣的 id 對照表複製；稽核凍結只帶程序書、不帶建議附件。任務與控制項的對應（`workflow_execution_control_mapping`）是用 `control_id` 字串＋輪次，不受 IR id 變動影響。
:::

::: {.callout .warn}
**8. 還原會換掉範本 SSP 的 id**

還原＝換一份新工作副本，`template_ssp_id` 與一對一專案的 `living_ssp_id` 要同時換。11 支讀 `living_ssp_id` 的地方照常讀即可，但有快取或背景工作記住舊 id 的要查（例如 `task_alert` 兩支）。
:::

::: {.callout .warn}
**9. 不可為空的欄位與資源庫 service 的權限繞道**

`by_component.description` 等欄位不可為空，存版與還原的複製路徑要沿用套件既有處理。資源庫的 default service 直接拿 ssp_id 寫，不經 `SspPermissionChecker`（見坑 2、6）。
:::

::: {.callout .warn}
**10. 送審時會產生兩份快照**

送審存一版（`submit`，進索引，帶 `audit_round_id`）＋稽核凍結（進 `project_audit_rounds.ssp_id`），各一份；差別在稽核凍結不帶建議附件。第一版接受兩份，求兩邊各自單純，靠 `audit_round_id` 互指。
:::

::: {.callout .warn}
**11. 前端兩支超標檔**

`ModuleFrameTemplateEditView.vue` 2,246 行、`ProjectPlanningView.vue` 2,609 行，都超過 800 行上限。新增邏輯一律新開子元件，原檔只加一行掛載；ddd-compliance-reviewer 遇到往裡塞會標阻斷。
:::
