> **✓ DONE — 2026-06-10**：Phase 2（P2a/P2b/P2a-E）+ P2c（DROP 退役表 + 移除 inert 子系統）已於 **DEV** 完工並經 user 手測通過。收尾見 [`2026-06-10-phase2-SUMMARY.md`](2026-06-10-phase2-SUMMARY.md)、design.md §9。stg/poc 留獨立遷移弧（未排程）。以下為接手當時的原始 handoff，保留供溯源。

# FR-036 換 session Handoff — Phase 1 完工，接手做 Phase 2

| 項目 | 內容 |
|------|------|
| 緣由 | module_frame 範本內容整併進「樣板 SSP」(`is_template=True`)。Phase 1（schema + 遷移 + 啟動專案 clone + 資源庫編輯改源）**已完工並經 user 在 dev 驗證**。本 handoff 交接 **Phase 2**（匯入/填值改源 + gate 清理 + DROP 舊表）。 |
| branch | `feature/oscal-refactor`（BE）。**不要切 branch**。 |
| 影響 repo | BE（主專案）+ jedi-oscal（套件，走 dev path-dep）+ 3 DB 環境（dev 已遷移；stg/poc 待） |
| 接手前必讀 | 本檔 → `docs/features/FR-036-2606-module-frame-ssp-merge/design.md`（含 §3.0 架構修正）→ `implementation-plan.md`（P2a/P2b/P2c 精確到 method/行號）|
| 預估時間 | P2a 大（~M5a 規模，3 service 讀+寫）；P2b 小；P2c 破壞性、待前置 |

---

## §0 接手讀序

1. **本檔**（全貌 + 現況 + 前置 + 雷區）
2. `docs/features/FR-036-2606-module-frame-ssp-merge/design.md` — 整體設計，**特別 §3.0 架構修正**（專案 SSP 子表是 AP 投影；樣板 SSP 用 AP-independent key；clone 走 re-point copy() 而非 SspVersioningService）
3. `docs/features/FR-036-2606-module-frame-ssp-merge/implementation-plan.md` — **P2a/P2b/P2c 精確 surface（method + 行號）**，本期主要照它做
4. 主嫌路徑檔（P2a 要改的）：
   - `app/module_frame/service/ssp_import_template_app_service.py`（樣板下載填值，6 個讀 helper）
   - `app/oscal/service/ssp_docx_import_app_service.py`（`_run_mf_default_confirm` L1078-1268，docx 匯入確認）
   - `app/oscal/service/ssp_excel_import_app_service.py`（`_apply_write_mf_defaults` L1707-1866，excel 匯入確認）
5. 參考已完工的 M5a 改法範本：`app/module_frame/service/module_frame_control_default_service.py`（control default → 樣板 SSP 的 pattern）

---

## §1 現況（Phase 1 已完工 + 驗證）

### 1.1 模型已成形
```
module_frame ──(module_frames.template_ssp_id)──► 樣板 SSP（oscal.ssps, is_template=True, version_no=0）
                                                     └ 完整 ssp_* 子表（取代 module_frame_*_defaults）
資源庫編輯（M5a/M5b）──► 寫樣板 SSP 子表
啟動專案（M4）──► copy() 從樣板 SSP clone 內容到專案 SSP（AP 橋接）
新建資源庫（M5c）──► 自動建一份樣板 SSP
```

### 1.2 DB 現況（dev `guidant_ai_dev` @ 192.168.50.188:25432，密碼查 .env DB_SECRET）
- `oscal.ssps.is_template`（boolean, default false）+ `compliance.module_frames.template_ssp_id`（int）已加（migration `scripts/sql/2026-06-10-fr036-template-ssp-columns.sql`，**只套了 dev**）。
- 7 個 active module_frame 各有一份 `is_template=True` 樣板 SSP，內容零丟失（M3 遷移 script `scripts/sql/fr036_migrate_defaults_to_template_ssp.py`，**只跑了 dev**）。

對照（dev）：
```
mf#276 CMMC L1        → ssp#304
mf#277 CMMC L2        → ssp#305
mf#313 超精簡          → ssp#306（1 ctrl / 12 程序書）
mf#343 艾爾航空        → ssp#307（15 ctrl / 57 AO / 2 元件 / 1 SC）
mf#383 系統安全計畫    → ssp#308（15/54/4 元件/1 LA/1 SC）
mf#388 系統安全計畫６… → ssp#309（14/54/4 元件/14 資產/1 LA/1 SC）
mf#389 測試…ㄅ123      → ssp#310
```
驗 dev 連結完整：
```bash
PGPASSWORD='<查.env DB_SECRET>' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c \
"SELECT id, left(name,20) name, template_ssp_id FROM compliance.module_frames WHERE is_delete=0 ORDER BY id;"
# 預期 7 筆都有 template_ssp_id（非 NULL）
```

### 1.3 ⚠️ Phase 1 期間踩過 + 已修的雷（別重蹈）
**bug：編輯資源庫 metadata 會把 `template_ssp_id` 洗成 NULL** → 該資源庫所有樣板 SSP endpoint 報 `BadRequestError: 此資源庫尚未建立樣板 SSP`。
- 根因：jedi-common `update_with_translation`（base_repository_impl.py ~L635）更新非翻譯欄位**無 `value is not None` 防呆**（與 base `update` 不一致），`update_module_frame` 建的 entity 沒帶 template_ssp_id（=None）→ 覆寫成 NULL。
- 已修（`f04ec7c7`）：`update_module_frame` 從 old entity 帶 template_ssp_id 進去保留。
- **follow-up（更廣的 latent bug）**：jedi-common `update_with_translation` 的 None-overwrite 會洗掉任何 translatable entity 的 partial-update 未帶欄位。jedi-common 為 **pin 版**、blast radius 大，本次只做 BE targeted 修。下個 session 若有 jedi-common 改動需求可一併修（加 None 防呆，對齊 base update）。

---

## §2 Phase 2 要做什麼（照 implementation-plan）

### P2a — 匯入 + 樣板下載填值改源（code，可逆，最大塊）
把三個 service 的 `*_defaults` 讀寫改成樣板 SSP（key=`mf.template_ssp_id`）。**好消息：這三個 service 已注入 jedi-oscal 的 domain service（control_implementation/objective/component/inventory/leveraged/system_characteristic），給專案 SSP 路徑用——re-point 就是把 `_mf_*_default` 換成那些既有 service + `mf.template_ssp_id`，entity 轉 `ComponentEntity`/`InventoryItemEntity`/`LeveragedAuthorizationEntity`/`SystemCharacteristicEntity`（帶 tenant_id + is_active）。**

精確 surface（method + 行號）見 `implementation-plan.md` §P2a A/B/C/D。摘要：
- **A 讀（`ssp_import_template_app_service` 樣板下載填值）**：6 個讀 helper（`_fetch_ref_docs_mf_lookup`/`_build_ref_docs`/`_build_sc_from_mf_default`/`_fetch_mf_default_items`/`_fetch_mf_inventory_entities`/`_load_existing_ctrl_map`+`_load_existing_obj_map`）。最簡做法：`_populate_filled_data` 解析 `tssp=mf.template_ssp_id`，SC/components/leveraged/inventory 走既有 `_*_by_ssp_id` helper（已存在）；control/AO/refdoc helper 改吃 template_ssp_id。
- **B 寫（docx `_run_mf_default_confirm` L1078-1268）**：LA/Component/Inventory/SC 四段 DELETE-all + upsert，從 `self._mf_*_default` 改成 `self._leveraged_authorization_domain_service` 等（已注入），keyed by template_ssp_id。控制項/AO 寫入（`module_frame_control_default_repo`/`_objective_default_repo`）改寫樣板 SSP control_implementations/objectives。觸發條件 L870 一併調整。
- **C 寫（excel `_apply_write_mf_defaults` L1707-1866）**：同 B 模式；`ModuleFrameWriteStrategy.write_excel_control_defaults`（控制項/AO，L1619）改寫樣板 SSP。
- **D DI**：`di_containers/oscal/oscal_containers.py` 993-996 / 1047-1051（+ excel container）三個 import service 停注入 `mf_*_default_domain_service`，改注入 jedi-oscal domain service。

> ⚠️ `ssp_import_template_app_service` 的讀 helper（如 `_load_existing_ctrl_map`）**fill 與 confirm 共用**，改的時候整批一起、跑完 import smoke 確認沒破匯入。

### P2b — gate + copy service 清理（code，可逆，小）
1. `oscal_project_service._setup_ssp_system_implementation` 的 `_mf_has_defaults` gate（~L665，現查 `module_frame_component_defaults` count）→ 改查 `module_frames.template_ssp_id IS NOT NULL`；移除舊式「profile 第一個 SSP 當 template」clone 概念（已被樣板 SSP 取代）。
2. `app/module_frame/service/module_frame_template_copy_service.py` `__init__` 移除已不用的 4 個 `*_default` domain service 注入（M4 後 copy() 全走 raw SQL on 樣板 SSP，這些注入沒人用）。

### P2c — DROP `*_defaults` 子系統（**破壞性、不可逆**）
- **前置（不可跳，安全底線）**：① stg / poc 套用 M1 欄位 migration + M3 資料遷移（目前**只有 dev**）② 三環境驗證零丟失 ③ **user 明確點頭**。
- 內容：DROP `module_frame_*_defaults`（7 張）+ `module_frame_reference_documents`/`_mappings`；移除整個子系統 model/repo/domain service/mapper/entity/query-entity/DTO/route。

---

## §3 開工順位（建議）
1. **Pre-flight**（§6）：確認 branch / working tree / BE listener。
2. **Verify Phase 1**（§7）：dev 7 個 MF template_ssp_id 完整 + 樣板 SSP 內容在。
3. **P2a**：先做讀（樣板下載填值，自包含、低風險），跑 import smoke；再做 docx confirm、excel confirm 寫入；每塊 commit + `py_compile` + DI import smoke。
4. **P2b**：gate + copy cleanup，commit。
5. **P2a/P2b 完成 + dev 驗證後**，再規劃 P2c（先備份 stg/poc → 套 migration → 驗 → 問 user 才 DROP）。

---

## §4 該讀 / 預期改動範圍
| 檔案 | 為何 read | 改動 |
|------|----------|------|
| `app/module_frame/service/ssp_import_template_app_service.py` | P2a 填值讀 helper | 6 helper 改讀樣板 SSP |
| `app/oscal/service/ssp_docx_import_app_service.py` | P2a docx 寫 | `_run_mf_default_confirm` 改寫樣板 SSP |
| `app/oscal/service/ssp_excel_import_app_service.py` | P2a excel 寫 | `_apply_write_mf_defaults` + `ModuleFrameWriteStrategy` 改寫樣板 SSP |
| `di_containers/oscal/oscal_containers.py` | P2a DI | 三 import service 改注入 jedi-oscal domain service |
| `app/project/service/oscal_project_service.py` | P2b gate | `_mf_has_defaults` 改查 template_ssp_id |
| `app/module_frame/service/module_frame_template_copy_service.py` | P2b 清理 | `__init__` 移除 unused 注入 |
| `app/module_frame/service/module_frame_control_default_service.py` | **M5a 範本**（已完工，照抄 pattern）| 不改，當參考 |

跨 repo：jedi-oscal 已注入的 domain service 足夠，P2a/P2b **預期不需再改 jedi-oscal**（除非要修 §1.3 的 update_with_translation follow-up）。

---

## §5 Pre-flight Command（必跑）
```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
git branch --show-current          # 應為 feature/oscal-refactor；不對停下問 user
git status --short                 # pyproject.toml(M) 是 dev path-dep（勿 commit）；FR-037 檔是別的 arc
grep -n "jedi-oscal" pyproject.toml # 確認 dev-dependencies 的 jedi-oscal path 那行「未註解」（dev path-dep 生效中）
lsof -i :8000 | grep LISTEN || echo "BE 沒在跑"   # 改 BE 後提醒 user 重啟（無 hot reload）
# import smoke（改完 DI 必跑）：
python -c "import di_containers.module_frame.module_frame_containers, di_containers.oscal.oscal_containers; print('DI OK')"
```

## §6 Verify Phase 1 確實 close（必跑，避免走回頭路）
```bash
PW=$(python3 -c "import json;[print(json.loads(l.split('=',1)[1].strip().strip(chr(34)).strip(chr(39)))['rds_master_password']) for l in open('.env') if l.startswith('DB_SECRET=')]")
# 7 個 MF 都有 template_ssp_id：
PGPASSWORD="$PW" psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -tA -c \
"SELECT id||':'||COALESCE(template_ssp_id::text,'NULL') FROM compliance.module_frames WHERE is_delete=0 ORDER BY id;"
# 樣板 SSP 內容在（例 ssp#307 艾爾航空）：
PGPASSWORD="$PW" psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -tA -c \
"SELECT 'ctrl='||count(*) FROM oscal.ssp_control_implementations WHERE system_security_plan_id=307;"
```

---

## §7 行為規範重要提醒
- **不切 branch**（`git checkout`/`switch` 一律不執行）；branch 不對停下問 user。
- **push 永遠等 user 明示**（BE 10 個 FR-036 commit + jedi-oscal 3 個 **都還沒 push**）。
- **jedi-oscal 走 dev path-dep**：`pyproject.toml` dev-dependencies 的 jedi-oscal path 行已取消註解（dev-only，**勿 commit**）；改套件源碼 BE 重啟即生效。**發版（bump + 推 Nexus）等 user 明示**，且要等整個 FR-036 完成。
- **改 BE service 後提醒 user 重啟**（無 hot reload；user 自己起服務，不要替他下 `python main_*.py`）。
- **顯式 git add 檔名，禁用 `-am`**（working tree 有 FR-037 別的 arc 檔 + dev-only pyproject，別掃進來）。
- **changelog 收尾才 batch 寫**（平時 commit message 寫詳細即可）。
- **不晶晶體**：動詞/連接詞用中文。
- **import-confirm 改完必跑 import smoke**，確認沒把匯入功能改壞。

---

## §8 收尾流程（P2a/P2b 完成 + user 驗證後才做）
1. changelog batch（feat：FR-036 整併；分 M-series / P2a / P2b 主題）
2. design.md：標 Phase 2 完工
3. memory `feedback_*.md`：本 arc 教訓（樣板 SSP 模型 / update_with_translation None-overwrite 雷 / AP-independent key），`MEMORY.md` 加索引行
4. Notion「GuidantAI Issue 任務清單」開/填一條（先搜尋避免跟 PM 重複）
5. 對話紀錄 extract → `docs/conversation-history/`（redact secret）

---

## §9 不在本期 scope（別順手做）
- **DROP 舊表（P2c）**：未到前置（stg/poc + user ok）不做。
- **stg/poc 遷移**：要 user 先備份才跑。
- **jedi-common `update_with_translation` 全域修**：pin 版、blast radius 大；除非 user 要，留 follow-up。
- **FR-037**（OSCAL schema gap audit）：是另一條 arc，working tree 那些 `docs/features/FR-037-*` / `fr037_*.py` / schema HTML 是它的，別碰。

---

## §10 本期（Phase 1）commits 清單（origin 皆**未 push**）

**BE（`feature/oscal-refactor`）：**
```
1148d566 docs(FR-036): 整併設計（樣板 SSP 模型）
12228389 feat(FR-036): 地基 schema migration（dev 已套）
5a00b2eb feat(FR-036): M3 資料遷移 script（dev 零丟失）
b82d62e4 feat(FR-036): M4 啟動專案 clone 換源
b7afacdd docs(FR-036): M5 + Phase 2 計畫
9aca4a16 feat(FR-036): M5a 控制項預設改寫樣板 SSP（參考樣板）
fe8859e9 feat(FR-036): M5a AO 預設改寫
8e0913d3 feat(FR-036): M5a leveraged/components/inventory/system_characteristic 改寫
b797fc57 feat(FR-036): M5c 新建 module_frame 建樣板 SSP
222bc8f2 feat(FR-036): M5b 程序書改寫
bdf284f6 feat(FR-036): M5d 匯出/資源讀取改源
f04ec7c7 fix(FR-036): update_module_frame 保留 template_ssp_id（修 metadata 編輯洗掉連結）
6f2e1d84 docs(FR-036): P2a/P2b/P2c 詳細計畫
（+ 本 handoff commit）
```
> 注：`eaf6a975` 是中途進度 doc，已被後續取代。

**jedi-oscal（未 push）：**
```
4735b84 feat(ssp): is_template 欄位
d7155ec feat(ssp): list/get-by-fields 預設排除樣板 SSP
c62ff62 feat(ssp): add_empty_ssp 支援 is_template / version_no
```

---

## §11 給 fresh session 的超短 prompt
```
讀 docs/features/FR-036-2606-module-frame-ssp-merge/handoff/2026-06-10-phase2-handoff.md
了解現況（Phase 1 已完工驗證，接手做 Phase 2）。先跑 §5 pre-flight + §6 verify Phase 1，
再照 implementation-plan §P2a 開始做匯入/填值改源。branch=feature/oscal-refactor，不切 branch、不自動 push。
```
