# 🟢 START HERE — FR-038 Wave 2B：B3 SSP 維護（import-ssp 全弧 done + B3 地基 done，接手 16 route）

> **給下個 session 的 prompt**：「讀 `docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-B3-ssp-maintenance-START-HERE-handoff.md`，先過「🧭 WHY」+ §0 讀序硬 gate（api-contract §9 + design §4.2/§4.4 + import-ssp-design），能答冷接自檢 4 問再碰 code。跑 §6 pre-flight + §7 verify（import-ssp + B3 地基），接手 **B3 SSP 維護**：用已建好的套件 `SspService` 子物件 CRUD，把 16 條 disabled route（7 資源庫範本 + 9 專案 SSP）逐一接 v2、re-enable。」
> 本檔自包含。狀態為 **2026-06-15**（import-ssp 全弧 ship + B3 套件地基 ship + verified）。

> ## 🔖 交接現況（2026-06-15 換 session 當下）
> | 項目 | 值 |
> |------|----|
> | **進度** | **import-ssp 全弧 DONE**（Excel + docx 匯入資源庫範本 SSP，full v1 parity）+ **B3 套件地基 DONE**（`SspService` 子物件 CRUD）。**下一棒 = B3 SSP 維護：re-enable 16 route** |
> | 主專案 branch / HEAD | `feature/oscal-refactor` / **`4e448ce1`** |
> | 主專案 working tree | **乾淨，只有 `M pyproject.toml`**（jedi-oscal-v2 dev path-dep，**照規範勿 commit**）|
> | 套件 branch / HEAD | `~/Projects/Jedicogy/module/jedi-python-package` `feature/oscal-refactor` / **`0196582`**（領先 origin 3，**未 push**）|
> | ⚠️ 套件未 commit 的 B1 dev 改動（別搞丟/別發版） | `M catalog_service.py`、`M framework_service.py`、`?? tests/catalog/test_catalog_service_import.py`。**dev path-dep，BE 重啟即生效；發版等整 feature + user 明示。** |
> | 跑得起來嗎 | `create_app()` BOOT OK on v2；BE `python -m pytest test/` = **56 failed + 50 errors**（持平 baseline、零新回歸）+ 9 skipped；套件 io_export **10 passed** + ssp **44 passed** |
> | push | **BE + 套件都未 push**，等 user 明示 |
> | **未收尾**（user 還沒下收尾命令） | import-ssp + B3 地基的 changelog / analysis / SUMMARY / memory / Notion 全未寫。連更前面 Q1/Wave 2B 的收尾也掛著。下次 user 說「收尾」一起補。 |

---

## 🧭 WHY：這整件事要幹嘛 + B3 在大圖位置（先懂才准碰 code）

**FR-038 一句話**：把舊「只套 OSCAL 概念、大量自定義欄位」的實作打掉重練成「正式 OSCAL v1.2.2 物件模型 + 三層 clone/snapshot 邊界 + 稽核輪次 first-class」。**Wave 2A 已把 BE 從舊 `jedi_oscal` 全翻到 `jedi_oscal_v2`、B1~B5 業務全 disable**；**Wave 2B 在 v2 地基上逐一把業務 re-enable**。

**三層 + 四邊界**：框架母版(catalog/profile) →①resolve→ 資源庫三件組(catalog+profile+**SSP 範本**) →②clone脫鉤→ 專案副本 →③啟動稽核 snapshot→ 凍結快照。

**import-ssp（剛做完的前一棒）**：合規資源庫的「範本 SSP」原本就有「從 Word/Excel 匯入填答案卷」功能，v2 翻地基後寫入層斷掉。本弧把寫入層接到 v2 `OscalIoService.import_ssp`，**Excel + docx、Model A（更新既有資源庫）+ Model B（新建資源庫）都做完、達 v1 parity**（control 實作 by-component 敘述+狀態 / per-AO statements / parties / components / leveraged-authorizations / inventory 全寫）。

**B3 SSP 維護（本棒）= 把「匯入填好的 SSP」能在 FE 檢視／編輯**：
- **資源庫範本編輯**（FE `ModuleFrameTemplateEditView.vue`，7 tabs：受評標的 / 參與人員 / 元件 / 資產清冊 / 外部服務 / 控制實作 / 程序書）—— 落點是 `compliance.module_frames.template_ssp_id` 指的那份 SSP（= import-ssp 灌進去的同一份）。
- **專案 SSP 維護**（B2 clone 出來的專案 living SSP，同類子物件 + control_implementation / 程序書池）。
- 這 16 條 route 在 2A 全被 disable（service chain 還 import 舊 jedi_oscal）。**B3 = 把它們的 app service 重寫接 v2 `SspService`、re-enable route**。

> **冷接自檢 4 問**（答不出回 §0 讀序）：① B3 SSP 維護的落點是哪兩種 SSP、各從哪來？② 為什麼 import-ssp 做完接著就該做 B3（同一份 SSP 的寫 vs 讀/編輯）？③ 套件 `SspService` 這次補了哪些子物件 CRUD、給誰用？④ 為什麼 16 route 不能直接 re-enable、要先重寫 app service？

---

## §0 接手讀序（按序，1~3 是硬 gate）

1. 本檔「🧭 WHY」+ 通讀本檔
2. 🔒 gate：[`api-contract.md`](../api-contract.md) **§9（SSP 維護端點清單 — B3 照此重建 route）** + §2（資源庫）
3. 🔒 gate：[`design.md`](../design.md) §4.2（套件對外 service 契約，含 SspService）/ §4.4（套件 vs 主專案邊界：OSCAL CRUD 在套件、業務在主專案）
4. context（前一棒，已完成）：[`import-ssp-design.md`](../import-ssp-design.md)（兩模型 / 落點 = template_ssp_id / this-system 元件當 by-component 錨點）
5. **B3 地基成品（套件，已 ship）**：`~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/jedi_oscal_v2/app/service/ssp/ssp_service.py`（子物件 CRUD method 清單見 §3）
6. v1 待重寫的 MF app service（B3 要改）：`app/module_frame/service/module_frame_components_service.py` 等（§5 清單）；v1 專案 SSP route 對應 service
7. canonical re-enable pattern 範本：B1 的 `app/oscal/service/resource_library_app_service.py` + import-ssp 的 `app/oscal/service/ssp_excel_import_app_service.py`（怎麼包 v2 primitive + DI wire + route 重寫 + EXCLUDE 移除 + blueprint 註冊）

---

## §1 現況：import-ssp 全弧 + B3 地基都做了什麼（2026-06-15，全 verified）

### import-ssp 全弧 SHIPPED（BE 4 commit + 套件 2 commit）
- **套件**：`OscalIoService.import_ssp` / export 擴到**完整 SSP 子樹**（by-components 控制敘述+狀態、per-AO statements、leveraged-authorizations、inventory、information-types；component uuid→id map 解 by-component 軟參照）。`import_ssp(oscal_ssp, *, target_ssp_id, curr_user, mode='create')`，create-only（非空丟 ValueError）。
- **adapter**（主專案 `app/oscal/service/import_adapter/`）：`excel_to_oscal_ssp` / `docx_to_oscal_ssp` + 共用 `_common`（synthesize OSCAL `this-system` 元件當 by-component 錨點 / LA 補合成 provider party / inventory / SoA props）。
- **app service**：`ssp_excel_import_app_service.py` / `ssp_docx_import_app_service.py` 精簡重寫 → confirm delegate `import_ssp`。Model A（既有資源庫 → template_ssp_id 填）+ Model B（新建資源庫 = `ResourceLibraryAppService.create_resource_library` 建殼再填）。docx parse 用新 `v2_candidate_loader`（framework_version → v2 catalog → controls + AO）。
- **route**：excel/docx import route 移出 EXCLUDE_MODULES + `api/oscal/__init__.py` 重新註冊。
- verify：real-DB smoke（Excel/docx/create-path）全過、零新回歸。

### B3 套件地基 SHIPPED（套件 `0196582`）
`SspService` 擴子物件 CRUD（讀寫 import-ssp 灌進去的同一批 v2 子表）：
- SI/CI：`get_system_implementation` / `get_or_create_system_implementation` / `get_or_create_control_implementation`
- components：`list_components_for_ssp` / `get_component` / `update_component` / `delete_component`（+既有 add/list）
- inventory：`list_inventory_items(ssp_id)` / `add` / `get` / `update` / `delete_inventory_item`
- leveraged：`list_leveraged_authorizations(ssp_id)` / `add` / `get` / `update` / `delete_leveraged_authorization`
- implemented-requirements：`list_implemented_requirements_for_ssp` / `get` / `update` / `delete`（+既有 add/list）
- by-components：`update_by_component` / `delete_by_component`
- parties（metadata-scoped）：`list_parties(metadata_id)` / `add_party` / `get` / `update` / `delete_party`
- verify：package ssp+io 44 passed、live CRUD smoke 全過。

---

## §2 ⚠️ 會讓你做錯的陷阱 / 已驗地基

1. **v1/v2 不可共存**（memory `project_oscal_v1_v2_cannot_coexist`）：B3 任何新 code **只准 import `jedi_oscal_v2`**。重寫 app service 前確認它內部不再 import 舊 `jedi_oscal`，否則 wire 進 boot 撞同名表 MetaData 直接炸。16 route 被 disable 的根因就是 service chain 還 import 舊套件。
2. **落點**：資源庫範本編輯 → `compliance.module_frames.template_ssp_id` 指的 SSP（= import-ssp 填的那份）；專案 SSP 維護 → 專案 living SSP（B2 clone 出來）。`module_frames` 在 **compliance** schema（不是 public）。
3. **v2 SSP 子表 schema 落 `oscal` schema**（`system_security_plans` 主表 + `ssp_*` 子表）；jedi_common Base `DEFAULT_SCHEMA` 會讓部分表走 search_path。查表存在性查對 schema（compliance.module_frames vs oscal.ssp_*）。
4. **子物件掛點**：components/inventory/leveraged 掛 `ssp_system_implementations`（用 `get_or_create_system_implementation`）；implemented-requirements 掛 `ssp_control_implementations`（`get_or_create_control_implementation`）；parties 掛 SSP 的 `metadata_id`（不是 ssp_id）。SspService 的 list_*_for_ssp 已封裝 resolve，直接用。
5. **v1 MF app service 寫的是 MF-defaults 表**（`module_frame_control_defaults` 等）；v2 改成讀寫 template_ssp_id 的 SSP 子物件（MF-defaults 在 v2 退役，範本 SSP 才是 source of truth，memory `feedback_mf_defaults_single_source_of_truth`）。別照抄 v1 寫 MF-defaults。
6. **RLS**：`module_frames` tenant-scoped；route 帶 `user.tenant_id`；無 user context 時 session_scope `else` 開 super_admin 繞 RLS（smoke 可直接跑）。
7. **審計欄位**：response 帶 `created_user`/`updated_user`/`matched_*_id` 要 enrich nickname（CLAUDE.md 審計欄位規範）。
8. **pytest 用 `python -m pytest`**；BE boot 補 dummy GITLAB/GITHUB env（見 §6）。
9. **plan 假設先 verify**：開工前讀 route 檔 + v1 service 實際 import/method，不憑本檔摘要刻。

---

## §3 B3 計畫（方法，開工前 pre-flight 驗）

**目標**：16 條 disabled SSP 維護 route 接 v2、re-enable。user 拍板「兩組一起做、直接擴 SspService（地基已完成）」。

**16 route（現 EXCLUDE_MODULES，= re-enable 地圖）**：
- **資源庫範本（`api.module_frame.routes.*`，7 條）**：`module_frame_system_characteristic` / `module_frame_components` / `module_frame_inventory` / `module_frame_leveraged` / `module_frame_party` / `module_frame_ssp_resources` / `mf_ssp_export`。落點 template_ssp_id。
- **專案 SSP（`api.oscal.routes.ssp.*`，9 條）**：`ssp_system_characteristic` / `ssp_components` / `ssp_inventory_items` / `ssp_leveraged` / `ssp_party` / `ssp_resources` / `ssp_control_implementation` / `ssp_control_impl_import` / `ssp_document_pool`。落點專案 living SSP。
- （`ssp_scoped_excel_import_route` / `ssp_export_route` 是匯入/匯出，**非本棒**，見 §10。）

**canonical pattern（每條 route，照 B1 / import-ssp 同套）**：
1. **重寫 app service** → v2：注入 `oscal_container.ssp_service`（+ `resource_library_app_service` 解 template_ssp_id 或 ssp domain 解專案 ssp）→ 用 §1 的子物件 CRUD method 讀寫；拔掉所有 `from jedi_oscal...` v1 import 與 MF-defaults 寫入。
2. **DI wire**：`di_containers/oscal/oscal_containers.py` 加 provider（mirror `ssp_excel_import_app_service` 的 wiring）。
3. **route 重寫**：route 只解 request / 呼 app service / 序列化；Provide 路徑指新 provider。
4. **re-enable**：`config/di_modules.py` 從 EXCLUDE_MODULES 移除該 route；`api/oscal/__init__.py`（oscal.ssp.*）或 `api/module_frame/__init__.py`（module_frame.*）的 `create_module()` 重新註冊 blueprint。
5. **boot + real-DB smoke（用 import-ssp 填好的真範本 SSP，如 mf 412 / template_ssp_id 1481）+ pytest 零回歸 + 顯式 git add commit**。

> **建議子順序**：先做資源庫範本那 7 條（跟 import-ssp 同落點，可直接拿匯入結果驗讀/編輯），再做專案 SSP 9 條。先立一條完整 vertical slice（建議 `module_frame_system_characteristic`，1:1 子物件最單純）當 canonical，再複製其餘。

---

## §4 開工順位（每段 smoke + 顯式 git add commit）

1. **pre-flight**：跑 §6 + §7（確認 import-ssp + B3 地基在、BE boot、pytest baseline）。
2. **讀 live**：選定第一條 route（建議 module_frame_system_characteristic）→ 讀其 route 檔 + v1 app service（看它讀寫什麼、v1 import 哪些）+ FE `ModuleFrameTemplateEditView.vue` 打哪個端點 / 期望 response shape。
3. **第一條 vertical slice**：重寫 app service → DI wire → route → re-enable → boot + smoke + commit（立 canonical pattern）。
4. **複製其餘 6 條資源庫範本** route。
5. **9 條專案 SSP** route（落專案 living SSP；control_implementation / document_pool 較複雜，留後段）。
6. 每組 BOOT OK + pytest 不多於 baseline 紅 + commit。

---

## §5 該讀 / 預期改動的檔案

| 檔案 | 為何 |
|------|------|
| `~/Projects/.../jedi-oscal-v2/.../ssp/ssp_service.py` | B3 地基（子物件 CRUD），B3 app service 都呼它（已完成，讀懂 method）|
| `app/module_frame/service/module_frame_*_service.py`（~7 個）| 資源庫範本編輯 app service，**重寫接 v2**（v1 寫 MF-defaults → v2 寫 template_ssp_id SSP 子物件）|
| `app/oscal/routes/ssp/*` 對應的 app service | 專案 SSP 維護 app service，同上 |
| `api/module_frame/routes/*` + `api/oscal/routes/ssp/*` | route 重寫（Provide 指新 provider）|
| `di_containers/oscal/oscal_containers.py` | 加 B3 app service provider（mirror ssp_excel_import_app_service）|
| `config/di_modules.py` | 從 EXCLUDE_MODULES 逐條移除 |
| `api/oscal/__init__.py` + `api/module_frame/__init__.py` | `create_module()` 重新註冊 blueprint |
| `~/Projects/.../compliance-manager-fe/src/views/module_frame/ModuleFrameTemplateEditView.vue` | FE 消費端，確認 request/response shape（動 FE 前讀 FE CLAUDE.md）|

---

## §6 Pre-flight（必跑，可複製貼）

```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
git branch --show-current                      # feature/oscal-refactor
git status --short                             # 僅 ' M pyproject.toml'
git log --oneline -3                            # 4e448ce1 / 37c1100c / a082c0b2
( cd ~/Projects/Jedicogy/module/jedi-python-package && git log --oneline -1 )  # 0196582
set -a; source .env 2>/dev/null; set +a
export GITLAB_API_VERSION=4 GITLAB_URL=http://localhost GITLAB_PRIVATE_TOKEN=dummy GITHUB_PRIVATE_TOKEN=dummy
poetry run python -c "
from dotenv import load_dotenv; load_dotenv()
import eventlet; eventlet.monkey_patch(all=False, socket=True)
import sys; sys.setrecursionlimit(5000)
from core.app_factory import create_app; create_app(); print('BOOT OK')"
poetry run python -m pytest test/ -q -p no:cacheprovider --continue-on-collection-errors 2>&1 | tail -1  # 56 failed,...,50 errors（持平）
```

---

## §7 Verify import-ssp + B3 地基確實 close（必跑，可複製貼）

```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
export GITLAB_API_VERSION=4 GITLAB_URL=http://localhost GITLAB_PRIVATE_TOKEN=dummy GITHUB_PRIVATE_TOKEN=dummy
# 套件：import_ssp/export 全子樹 round-trip + SspService 子物件 CRUD
poetry run python -m pytest ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/tests/io_export/ ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/tests/ssp/ -q -p no:cacheprovider 2>&1 | tail -3  # 10 + 44 passed
# 主專案 adapter 單元測試
poetry run python -m pytest test/test_import_adapter_excel_to_oscal_ssp.py test/test_import_adapter_docx_to_oscal_ssp.py -q -p no:cacheprovider 2>&1 | tail -3
# SspService 新子物件 CRUD method 都在
poetry run python -c "from jedi_oscal_v2.app.service.ssp.ssp_service import SspService; print([m for m in dir(SspService) if 'inventory' in m or 'leveraged' in m or 'party' in m])"
```

---

## §8 行為規範重要提醒（適用本棒）

- **不切 branch**（兩 repo 都在 `feature/oscal-refactor`；branch 不對停下問 user）。
- **可自行階段性 commit**（顯式 `git add` 檔名、**禁 `-am`**；各 repo 分開）；**push / 收尾 / 套件發版等 user 明示**。
- `pyproject.toml` path-dep **勿 commit**；套件 dev 改動 commit 進套件 branch 防搞丟，但**不發 Nexus**。
- **改 BE 後提醒 user 重啟 BE**（無 hot reload）；服務 user 自己起。
- **動 FE 前先讀 FE CLAUDE.md**（本棒理論上 FE 已 v2 不用動，但若要碰先讀）。
- **B3 只接 v2**：重寫 app service 前確認拔乾淨 v1 import（§2-1）。
- **遇到架構/資料模型取捨、scope 邊界 → 停下問 user**。

---

## §9 收尾流程（整 B3 arc 做完 + user 明示才做）

盤點 commits（BE + 套件）→ changelog（type=feat，B3 各組 + import-ssp 補）→ analysis（this-system 錨點決策 / 完整 parity / MF-defaults 退役）→ SUMMARY → 回頭更新橫向文件（api-contract §9 標完工、design §4.2 SspService 補子物件 CRUD、frontend-overview 若動 payload）→ memory feedback（≥1 條）→ Notion。**push 等 user。**

> **本 session 自身的收尾（import-ssp + B3 地基的 changelog / SUMMARY / analysis / memory / Notion）也都還沒做** —— user 還沒下收尾命令，全留著。

---

## §10 不在本期 scope（別順手做）

- **P4 update-diff**（資源庫範本 SSP 重複匯入做差異更新）—— import-ssp 目前 create-only（非空丟 412 `GRC_IMPORT_SSP_TEMPLATE_NOT_EMPTY`）。差異模式留 P4。
- **專案 SSP 匯入 / 匯出**（`ssp_scoped_excel_import_route` / `ssp_export_route` / `mf_ssp_export` 的匯出半部）—— 跟 B3 維護 CRUD 不同，另議。
- **B4.2（AP 編輯）/ B5（AR/風險/POA&M/結案覆核）** —— B3 之後才接（見 `2026-06-15-B4.2-B5-START-HERE-handoff.md`）。
- **租戶連結**（parties→使用者 / inventory→device 配對）—— 已釐清範本落點不需要（原樣存即可）。
- **套件發 Nexus / `pyproject.toml` 改回 pin / 收尾文件** —— 整 feature 完 + user 明示。

---

## §11 本 session commits

### 主專案（branch `feature/oscal-refactor`，未 push；HEAD `4e448ce1`）
```
4e448ce1 fix(FR-038): import-ssp P3 docx create — confirm 自建資源庫（對齊 FE 預期）
37c1100c feat(FR-038): import-ssp P3 — Docx 匯入落 v2 範本 SSP（full parity）
a082c0b2 feat(FR-038): import-ssp adapters full v1 parity + docx adapter
b70339b0 feat(FR-038): import-ssp P2 — Excel 匯入兩 source 落 v2 範本 SSP
```
（更前面 cb091369 起為前一棒 import-ssp 設計/P1。）

### 套件（`~/Projects/Jedicogy/module/jedi-python-package` branch `feature/oscal-refactor`，未 push；HEAD `0196582`，領先 origin 3）
```
0196582 feat(oscal-v2): SspService 子物件 CRUD — B3 SSP 維護地基
798a274 feat(oscal-v2): import_ssp/export full SSP subtree — v1 parity
a930bfd feat(oscal-v2): OscalIoService.import_ssp — OSCAL SSP → v2 relational tree (P1)
```
- 套件另有 B1 未 commit dev 改動：`M catalog_service.py`、`M framework_service.py`、`?? tests/catalog/test_catalog_service_import.py`（別搞丟、別發版）。

---

## §12 給 fresh session 的超短 prompt

```
讀 docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-B3-ssp-maintenance-START-HERE-handoff.md。
先過「🧭 WHY」+ §0 讀序硬 gate（api-contract §9 + design §4.2/§4.4 + import-ssp-design），
能答冷接自檢 4 問（B3 兩落點 / 為何接著 import-ssp 做 / SspService 補了什麼 / 為何不能直接 re-enable）才往下。
跑 §6 pre-flight（BOOT OK + pytest 56f/50e）+ §7 verify（套件 io_export 10 + ssp 44 passed）。
接手 B3 SSP 維護：用已建好的套件 SspService 子物件 CRUD，把 16 條 disabled route（先 7 資源庫範本、再 9 專案 SSP）
逐一接 v2 — 重寫 app service（拔 v1 import、改讀寫 template_ssp_id 的 SSP 子物件）→ DI wire → route 重寫 →
移出 EXCLUDE_MODULES + 重新註冊 blueprint → real-DB smoke + commit。先立一條 vertical slice 當 canonical 再複製。
每段 smoke + 顯式 git add commit。push/收尾/套件發版等 user 明示；不切 branch；pyproject.toml dev path-dep 勿 commit。
```

---
### 冷接可行性自檢 ✅
看本檔 + §0 讀序 + 跑 §6/§7 → 能確認現況（import-ssp 全弧 done + B3 套件地基 done + commits）、懂 WHY（FR-038 替換、import-ssp 寫入 / B3 讀編輯同一份 SSP、三層四邊界）、知道 B3 怎麼接（SspService 子物件 CRUD + canonical re-enable pattern + 16 route 地圖）、知道地基陷阱（只接 v2 / 落點 template_ssp_id / 子物件掛點 / MF-defaults 退役）、知道規範界線。不需 user 額外解釋即可開工。
