# 🟢 START HERE — FR-038 B3 收尾：解開最後 1.5 條（document_pool mapping 半 + control_impl_import）

> **給下個 session 的 prompt**：「讀 `docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-B3-finish-docpool-mappings-and-control-impl-import-START-HERE-handoff.md`，先過「🧭 WHY」+ §0 讀序硬 gate，能答冷接自檢 4 問再碰 code。跑 §6 pre-flight + §7 verify。接手：把『control_identifier / AO → v2 id』的解析 + catalog 標題/AO 清單，從**已不存在的舊 AP 表**改建到 **v2 catalog 鏈**，解開 document_pool 的控制項/AO mapping 半 + control_impl_import 整條。」
> 本檔自包含。狀態 **2026-06-15**（B3 15/16 route ship + verified；剩 1.5 條卡同一塊地基）。

## 🔖 交接現況（換 session 當下）
| 項目 | 值 |
|------|----|
| **進度** | **B3 SSP 維護 15/16 route-group 接 v2 + commit、零新回歸**。剩 **1.5 條**卡在「舊 AP 表已不存在」：① `document_pool` 的控制項/AO **mapping 半**（4 條 route，池 CRUD ✅ 能用）② `control_impl_import`（SoA Excel 匯入匯出，整條 disabled） |
| 主專案 branch / HEAD | `feature/oscal-refactor` / **`fd668423`** |
| 主專案 working tree | **乾淨，只有 `M pyproject.toml`**（jedi-oscal-v2 dev path-dep，照規範勿 commit）|
| 套件 branch / HEAD | `~/Projects/Jedicogy/module/jedi-python-package` `feature/oscal-refactor` / **`c2b7502`**（B3 statement CRUD；領先 origin，未 push）|
| ⚠️ 套件未 commit 的 B1 dev 改動（別搞丟/別發版） | `M catalog_service.py`、`M framework_service.py`、`?? tests/catalog/test_catalog_service_import.py`、`?? jedi-device/CLAUDE.md`、`?? jedi-project/CLAUDE.md`。dev path-dep、BE 重啟即生效；發版等整 feature + user 明示 |
| 跑得起來嗎 | `create_app()` BOOT OK；BE `python -m pytest test/` = **47 failed + 50 errors**（≤ baseline 56f/50e，零新回歸）+ 9 skipped |
| push | **BE + 套件都未 push**，等 user 明示 |
| **未收尾** | 整個 B3 弧（13+ commits）的 changelog / analysis / SUMMARY / memory / Notion 全未寫。user 還沒下收尾命令。下次 user 說「收尾」一起補 |

---

## 🧭 WHY：這件事要幹嘛 + 這 1.5 條卡在哪（先懂才准碰 code）

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

**B3 = SSP 維護**：把「答案卷（SSP）」的 7 個編輯 tab（受評標的 / 參與人員 / 元件 / 資產清冊 / 外部服務 / 控制實作 / 程序書）接回 v2，兩種落點：① 資源庫範本 SSP（`compliance.module_frames.template_ssp_id`）② 專案 living SSP（B2 clone 出來，存 `compliance.project_extensions.living_ssp_id`）。**前 15 條都做完了**（見 §11 commits）。

**剩這 1.5 條為什麼卡 —— 舊 AP 結構在新模型不存在**：

`document_pool` 的「控制項/AO 程序書關聯」+ `control_impl_import` 的「Excel 匯出（控制項標題 + AO 清單）」，舊版都靠這條鏈：
```
SSP → assessment_plans → assessment_plan_controls / assessment_plan_tasks / assessment_task_controls
```
但 FR-038 **重設計了 AP 模型**（B4 已 ship）：新 AP 用 `ap_reviewed_controls` / `ap_tasks`（per-round），**舊的 `assessment_plan_controls` / `assessment_plan_tasks` / `assessment_task_controls` 三張表在新 schema 根本不存在**（已實測 MISSING，見 §7）。所以：
- `infra/grc/repository/ssp_document_pool_query.py` 的 `get_ap_control_id_by_identifier` / `get_ap_task_id_by_uid` 在 2A 被 stub 成 `return None`（dark）→ document_pool 的 mapping route 查不到 id、形同壞掉。
- `infra/oscal/repository/ssp_catalog_title_query.py` 直接 `from jedi_oscal.infra.model.ap...` import 舊 AP ORM model 查那三張表 → **① 表不存在 ② import v1 jedi_oscal 進 boot graph 會撞 v2 同名表 MetaData 直接炸**（我實測過，§2 trap 1）。

**目標模型（這一棒要鋪的地基）**：把「control_identifier / AO → 一個穩定 v2 id」的解析，從舊 AP 改建到 **v2 結構**：
- **控制項 id** → 該控制的 `SspImplementedRequirement.id`（per control，套件 `SspService` 已有 get/list/add）。
- **AO id** → 該 AO 的 `SspStatement.id`（per AO，套件 `SspService` 本棒已補 statement CRUD）。
- **控制標題 + AO 清單**（匯出用）→ 從 **v2 catalog**：`ssp.import_profile_id → profile_imports.source_catalog_id → catalog_controls.title`（控制標題）+ `catalog_control_parts`（filter `name=='assessment-objective'`）（AO 清單）。**這就是 B5/Q1 已在用的 AO 推導**（見 §5 範本 `app/grc/service/assessment_result_app_service.py:_project_catalog_controls` / `_derive_ao_map` + 共用 `ao_derivation.derive_ao_pairs`）。

> **冷接自檢 4 問**（答不出回 §0 讀序）：① 這 1.5 條卡的根因是哪三張表不存在、新模型改用哪兩張？② v2 裡「控制項 → id」「AO → id」分別該對應哪個 entity 的 id？③ 控制標題 + 完整 AO 清單在 v2 從哪條鏈拿（不經 AP）？④ 為什麼不能直接 import `SspCatalogTitleQuery`（現況）進 DI？

---

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

1. 本檔「🧭 WHY」+ 通讀本檔
2. 🔒 gate：[`api-contract.md`](../api-contract.md) §9（SSP 維護端點）；[`design.md`](../design.md) §4.2（套件 SspService 契約）/ §4.4（套件 vs 主專案邊界）
3. 🔒 gate：**B5/Q1 的 AO 推導範本**（這一棒的核心參照）：`app/grc/service/assessment_result_app_service.py` 的 `_project_catalog_controls`（L127）+ `_derive_ao_map`（L141）+ 共用 helper `app/grc/service/ao_derivation.py` 的 `derive_ao_pairs`（grep 確認路徑）。**control_impl_import 的 catalog 標題/AO 清單必用同一套定義**（只算 `assessment-objective` part；見 Q1 handoff `2026-06-15-Q1-prep-jobs-START-HERE-handoff.md` §2「AO 定義」）
4. 已 ship 的 B3 canonical 範本：`app/oscal/service/ssp_control_implementation_service.py`（**本棒最重要參照** — 它已把控制實作接到 v2 IR + statement，含 `_get_or_create_ir` / `_get_or_create_statement` / `_ir_for_control` / `_statement_for`，document_pool mapping 要 reuse 同樣的 IR/statement 解析 + **同一個 context_id 空間**）
5. 待改檔：`app/oscal/service/ssp_document_pool_service.py`（mapping 半）/ `infra/oscal/repository/ssp_catalog_title_query.py`（整個重寫）/ `app/oscal/service/ssp_control_impl_import_service.py`（v2 改寫，**我上一棒寫過一版又還原了，可從本檔 §3-B 復原**）

---

## §1 現況：1.5 條的精確症狀

### document_pool（程序書池）— 池 CRUD ✅ / mapping 半 ❌
- **能用**：`/ssp/<uid>/document-pool` GET/POST/DELETE（上傳/列出/刪除程序書到池）。已 commit（`e309cb9c`）+ real-DB smoke 過。
- **壞**：控制項/AO 關聯 4 條 route（`/control-implementation/<ci>/document-mappings` GET/POST、`.../document-mapping/<doc_uid>` DELETE、`.../objective/<si>/document-mappings` GET/POST、對應 DELETE）。原因：service 內 `_get_ap_control_id_or_404` / `_get_ao_task_id_or_404` 呼 `_pool_query.get_ap_control_id_by_identifier` / `get_ap_task_id_by_uid`，這兩個在 `ssp_document_pool_query.py:216,221` 是 `return None`（2A dark）→ 永遠 raise `GRC_CONTROL_NOT_FOUND` / `GRC_ASSESSMENT_OBJECT_NOT_FOUND`。
- ⚠️ 我 commit `e309cb9c` 時 message 寫「6 條 route 接好」，**這是不準的**——mapping 半其實沒通（smoke 只驗了池 CRUD，沒 AP 資料沒驗 mapping）。下一棒修好後回頭把這點在 changelog 講清楚。

### control_impl_import（SoA Excel 匯入匯出）— 整條 disabled
- 4 條 route：`/ssp/<uid>/control-implementations/export`（GET 匯出 Excel）/ `/import`（POST 上傳驗證）/ `/import/validate`（POST 重驗）/ `/import/confirm`（POST 確認寫入）。
- 仍在 `config/di_modules.py` EXCLUDE_MODULES、blueprint 未註冊、`ssp_control_impl_import_service.py` 是 v1 原版（import 舊 jedi_oscal）。
- **confirm_import 其實已可接 v2**（它 delegate 給已 v2 化的 `SspControlImplementationService.update_control_implementation/update_objective`）；卡的是 **export + validate** 需要 `SspCatalogTitleQuery`（控制標題 + AO 清單），而那支查 MISSING 的舊表 + import v1 model。

---

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

1. **絕不 import v1 `jedi_oscal` 進 boot graph**（memory `project_oscal_v1_v2_cannot_coexist`）。我上一棒把 `SspControlImplImportService` wire 進 DI、它 chain 到 `SspCatalogTitleQuery`（`from jedi_oscal.infra.model.ap...`）→ `create_app()` 直接炸 `Table 'oscal.assessment_plans' is already defined for this MetaData instance`。**重寫 `SspCatalogTitleQuery` 必須拔光 v1 import，改用 raw SQL（`text()`）或 v2 catalog repo**。
2. **舊 AP 三表不存在**（§7 已驗）：`assessment_plan_controls` / `assessment_plan_tasks` / `assessment_task_controls` = MISSING；新 AP = `ap_reviewed_controls` / `ap_tasks` = EXISTS。**但本棒的標題/AO 來源不是新 AP，是 v2 catalog**（AP 是 per-round 凍結快照，維護期的 living SSP 不一定有 AP；catalog 經 profile 才是 always-available 的控制母體）。
3. **context_id 空間要跟 control_implementation 一致**：`ssp_control_implementation_service.py`（已 ship）的程序書用 `ssp_reference_documents` 表、context_type `control_implementation`→`IR.id` / `objective`→`statement.id`。document_pool 的 mapping 用 `ssp_reference_document_mappings` 表（不同表），**context_id 要用同一組 IR.id / statement.id**，兩邊才對得起來。
4. **程序書池 2 表是本棒前一段補建的**（migration `scripts/sql/2026-06-15-fr038-b3-ssp-reference-documents.sql`，DEV 已套；**stg/poc/prod 未套**）。`SspDocumentPoolQuery` 的 `list_mappings` / `add_mappings` / `resolve_doc_uids_to_ids` / `get_pool_name_to_uid_map` / `list_pool` 都查這 2 表 + 是 raw SQL、**v1-clean、能用**——只有 `get_ap_*` 兩個是 dark。
5. **套件 SspService 本棒已補 statement CRUD**（pkg `c2b7502`）：`list_statements(ir_id)` / `get_statement(id)` / `add_statement` / `update_statement` / `delete_statement`，加既有 `list_implemented_requirements_for_ssp(ssp_id)` / `get_or_create_control_implementation(ssp_id, user)` / `add_implemented_requirement`。dev path-dep、BE 重啟生效。
6. **AO 識別子一致性（關鍵設計點，動手前先釘死）**：FE 對「同一個 AO」在 control_implementation route 傳 `statement_identifier`、在 document_pool AO mapping 也傳 `statement_identifier`、export Excel 的隱藏欄存 AO 識別子。三處**必須是同一個值**才串得起來。建議：統一用 **catalog AO part 的 `part_id`**（OSCAL AO token，如 `ac-1_smt.a` 或 `[a]`）當 `statement_id`。先讀 `catalog_control_parts` 實際 `part_id` 長相 + control_implementation route 目前 FE 傳什麼，對齊後再實作（memory `feedback_plan_vs_reality_verify_first`）。
7. **權限**：document_pool / control_impl_import 都走 `SspPermissionChecker`（已 wire 在 oscal_container）：讀=`require_participant`、寫=`require_manager`。`ssp_uid → ctx.ssp.id`。
8. **pytest 用 `python -m pytest`**；BE boot 補 dummy GITLAB/GITHUB env（見 §6）。
9. **plan 假設先 verify**：開工前讀 `catalog_control_parts` / `profile_imports` 實際欄位、`derive_ao_pairs` 簽章、control_implementation route FE 實傳值，不憑本檔摘要刻。

---

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

### §3-A：document_pool mapping 半（較單純，建議先做）
改 `app/oscal/service/ssp_document_pool_service.py`：
1. 建構子加 `ssp_service`（v2 SspService）dep。DI provider（`di_containers/oscal/oscal_containers.py` 的 `ssp_document_pool_service`）加 `ssp_service=ssp_service`。
2. 把 `_get_ap_control_id_or_404(ssp_id, control_identifier)` 改成 `_resolve_ir_id(ssp_id, control_identifier, create)`：reuse `ssp_control_implementation_service` 的 `_ir_for_control`（讀）/ `_get_or_create_ir`（寫）邏輯（可抽共用 helper 或複製那幾行）。回傳 `IR.id`。
3. 把 `_get_ao_task_id_or_404(statement_identifier)` 改成 `_resolve_stmt_id(ssp_id, control_identifier, statement_identifier, create)`：先解 IR、再 `_statement_for`（讀）/ `_get_or_create_statement`（寫）→ 回 `statement.id`。**注意現簽章只有 statement_identifier 沒 control_identifier**——AO mapping route 其實有帶 control_identifier（`list_ao_mappings(ssp_uid, control_identifier, statement_identifier)`），把它一路傳進來。
4. `list_*_mappings`（讀）用 create=False，解不到回 `[]`；`add_*_mappings` / `remove_*_mapping`（寫）用 create=True / 解不到回 False。其餘 `_pool_query.list_mappings/add_mappings/remove_mapping/resolve_doc_uids_to_ids` 不動（查 mapping 表、能用）。
5. smoke：用專案 266 / living_ssp_id 1482 / manager 17，先 add_to_pool 一筆 → add_control_mappings(控制項) + add_ao_mappings(AO) → list 驗 → remove → 清乾淨。控制項用 SSP 真有 IR 的 control_id（先 `SELECT control_id FROM oscal.ssp_implemented_requirements ir JOIN oscal.ssp_control_implementations ci ON ci.id=ir.control_implementation_id WHERE ci.ssp_id=1482 LIMIT 1`；沒有就先用 control_implementation route 建一個）。

### §3-B：control_impl_import（重寫 catalog 標題查詢 + 接回 service）
1. **重寫 `infra/oscal/repository/ssp_catalog_title_query.py`**（拔光 v1 import）：實作 `ISspCatalogTitleQuery` 三個 method，改用 v2 catalog 鏈：
   - 先 ssp_id → `ssp.import_profile_id` → `profile_imports.source_catalog_id` → catalog_id（raw SQL 或 v2 repo；範本 `_project_catalog_controls`）。
   - `get_titles_by_ssp_id(ssp_id)`：`{control_id: catalog_controls.title}`（該 catalog 的 controls）。
   - `get_ao_list_by_ssp_id(ssp_id)`：`{control_id: [(stmt_id, ao_title, stmt_id), ...]}`——AO 從 `catalog_control_parts` filter `name=='assessment-objective'`（reuse `derive_ao_pairs` 或同邏輯）。**第三個 tuple 元素（舊版是 task_uid）改成 stmt_id 自己**（沒 AP task 了；隱藏欄存 AO part_id，confirm 時當 statement_identifier）。
   - `get_objective_titles_by_ssp_id`：照 §6 AO 識別子一致性回 `{control_id: {stmt_id: title}}`。
   - ⚠️ 簽章現在收 `ssp_id: str`（舊版因 `assessment_plans.ssp_id` 是 VARCHAR）。v2 直接用 int ssp_id，呼叫端配合改（或內部 `int(ssp_id)`）。
2. **重寫 `app/oscal/service/ssp_control_impl_import_service.py` 接 v2**（我寫過一版、為救 boot 還原了；可從本對話 JSONL 復原，或照下列重做）：
   - 建構子：`(ssp_permission_checker, ssp_service, ssp_ctrl_impl_service, catalog_title_query, document_pool_query)`，拔掉 v1 `ssp_domain_service` / `ctrl_impl_domain_service` / `objective_domain_service`。
   - `_get_ssp_or_404` → permission checker（export/validate/revalidate=`require_participant`、confirm=`require_manager`），`ssp_id = ctx.ssp.id`。
   - 加 `_controls_view(ssp_id)`：`ssp_service.list_implemented_requirements_for_ssp` + `list_statements`，組出 mirror 舊 `ControlImplementationEntity` 介面的 `SimpleNamespace`（`.control_identifier`=ir.control_id、`.implementation_status`/`.implementation_description` 從 IR props 讀、`.objectives`=statements 各自 props）。取代所有 `_ctrl_ds.get_all_by_ssp_id`。
   - status/description 的 props 名跟 control_implementation service 一致：`implementation-status` / `implementation-description`。
   - Excel 格式（HEADER_LABELS / 樣式 / dropdown）原樣保留；只換資料來源。
   - confirm_import 已 delegate `self._ssp_svc.update_control_implementation/update_objective`（v2）+ doc pool mapping（用 §3-A 修好的 resolve）——把 mapping 部分也改成走新的 IR/statement id（別再用 `get_ap_*`）。
3. **wire + re-enable**：DI provider（`ssp_catalog_title_query` Singleton + `ssp_control_impl_import_service` Factory）→ `config/di_modules.py` 移除 `api.oscal.routes.ssp.ssp_control_impl_import_route` → `api/oscal/__init__.py` 註冊 4 條 route（URL 見 §11 git 對照）。
4. smoke：export_excel(ssp_uid) 回非空 xlsx（`data[:2]==b'PK'`）+ confirm_import 一筆控制項 + 權限負面（stranger ForbiddenError）。

---

## §4 開工順位
1. **pre-flight**：跑 §6 + §7（確認 BOOT、pytest baseline、舊 AP 三表 MISSING、新表在）。
2. **§3-A document_pool mapping**（單純）→ smoke → 顯式 git add commit。
3. **§3-B-1 重寫 SspCatalogTitleQuery**（v2 catalog 鏈、拔 v1 import）→ 單獨先驗它不炸 boot（import 它進一個 throwaway script）。
4. **§3-B-2 重寫 import service** → wire → re-enable → smoke → commit。
5. 每段 BOOT OK + pytest ≤ baseline 紅（用 §6 baseline-diff）+ 顯式 git add commit。

---

## §5 該讀 / 預期改動的檔案
| 檔案 | 為何 |
|------|------|
| `app/grc/service/assessment_result_app_service.py`（`_project_catalog_controls` L127 / `_derive_ao_map` L141）| **v2 catalog→controls→AO 的範本**，照抄解析鏈 |
| `app/grc/service/ao_derivation.py`（`derive_ao_pairs`）| 共用 AO 推導（只算 assessment-objective），catalog_title_query 重寫 reuse |
| `app/oscal/service/ssp_control_implementation_service.py` | **已 ship 的 IR/statement 解析範本**；document_pool mapping reuse 它的 `_get_or_create_ir`/`_get_or_create_statement`，context_id 同空間 |
| `app/oscal/service/ssp_document_pool_service.py` | §3-A 改：加 ssp_service + IR/statement 解析取代 dark AP 查詢 |
| `infra/oscal/repository/ssp_catalog_title_query.py` | §3-B-1 整支重寫（拔 v1 import、改 v2 catalog 鏈）|
| `app/oscal/service/ssp_control_impl_import_service.py` | §3-B-2 v2 改寫（現為 v1 原版）|
| `infra/grc/repository/ssp_document_pool_query.py` | 讀懂：`get_ap_*` 是 dark（不用它）；mapping 表 CRUD 能用 |
| `di_containers/oscal/oscal_containers.py` / `config/di_modules.py` / `api/oscal/__init__.py` | wire provider + re-enable EXCLUDE + 註冊 blueprint |
| `~/Projects/Jedicogy/.../jedi-oscal-v2/.../catalog/catalog_control_part_repo_impl.py` / `catalog_control_repo_impl.py` | v2 catalog repo（標題 + AO parts 來源）|

---

## §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                            # fd668423 / 0662360e / e309cb9c
( cd ~/Projects/Jedicogy/module/jedi-python-package && git log --oneline -1 )  # c2b7502
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
# BOOT（含 blueprint 註冊用 main_app.py 的迴圈才驗得到 route；create_app 只驗 DI）
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')"
# baseline 存起來，之後每次改完 comm -23 比新回歸
poetry run python -m pytest test/ -q -p no:cacheprovider --continue-on-collection-errors 2>&1 | grep -E '^(FAILED|ERROR)' | sort > /tmp/b3fin_base.txt
poetry run python -m pytest test/ -q -p no:cacheprovider --continue-on-collection-errors 2>&1 | tail -1  # 47 failed,...,50 errors
```

---

## §7 Verify 地基事實（必跑，可複製貼）
```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
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
# 舊 AP 三表 MISSING、新 AP 兩表在、catalog parts 在
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(enable_socketio=False)
from jedi_common.session.database.db import session_scope
from jedi_common.session.database.session_context import get_session
from sqlalchemy import text
with session_scope():
    for t in ['assessment_plan_controls','assessment_plan_tasks','assessment_task_controls','ap_reviewed_controls','ap_tasks','catalog_control_parts','ssp_reference_documents','ssp_reference_document_mappings']:
        r=get_session().execute(text(f\"SELECT count(*) FROM information_schema.tables WHERE table_name='{t}'\")).scalar()
        print(f'{t}:', 'EXISTS' if r else 'MISSING')"
# 套件 statement CRUD 在
poetry run python -c "from jedi_oscal_v2.app.service.ssp.ssp_service import SspService; print(sorted(m for m in dir(SspService) if 'statement' in m))"
# 測試資料座標：專案 266 / living_ssp_id 1482 / manager user 17；範本 mf template_ssp_id 1481
```
預期：舊三表 MISSING、`ap_reviewed_controls`/`ap_tasks`/`catalog_control_parts`/程序書池 2 表 EXISTS；statement CRUD 5 method 在。

---

## §8 行為規範重要提醒
- **不切 branch**（兩 repo 都在 `feature/oscal-refactor`；branch 不對停下問 user）。
- **可自行階段性 commit**（顯式 `git add` 檔名、**禁 `-am`**；各 repo 分開）；**push / 收尾 / 套件發版等 user 明示**。
- `pyproject.toml` path-dep **勿 commit**；套件 dev 改動 commit 進套件 branch 防搞丟、**不發 Nexus**。
- **絕不 import v1 `jedi_oscal` 進 boot graph**（§2-1）；重寫 query 用 raw SQL / v2 repo。
- **改 BE 後提醒 user 重啟 BE**（無 hot reload）；服務 user 自己起。
- **plan 假設先 verify**（§2-6/9）；AO 識別子一致性先釘死再實作。
- 遇到架構/資料模型取捨、scope 邊界 → 停下問 user。

---

## §9 收尾流程（整 B3 弧做完 + user 明示才做）
盤點 commits（BE+套件）→ changelog（B3 各 route group + 本棒 + **更正 `e309cb9c` document_pool mapping 半當時沒通的事實**）→ analysis（IR/statement 當 v2 control/AO id 空間 / catalog 鏈取代舊 AP / props 承載 status-description 的決策）→ SUMMARY → 回頭更新橫向文件（api-contract §9 標完工、design §4.2 SspService 補 statement CRUD、frontend-overview 若動 payload）→ memory feedback（≥1 條，建議「舊 AP 表退役、id 解析改建 v2 catalog/IR/statement」+「import 半路 v1 query 炸 boot 的 baseline-stash-diff 抓法」）→ Notion。**push 等 user。** stg/poc/prod 記得跟著套程序書池 migration。

---

## §10 不在本期 scope（別順手做）
- **mf_ssp_export / ssp_export**（SSP 匯出 docx/pdf）—— §10 deferred，跟本棒無關。
- **P4 update-diff**（資源庫範本 SSP 重複匯入差異更新）—— import-ssp create-only，留 P4。
- **Q1 準備期 My Jobs（5 增量）** —— 另一條 handoff `2026-06-15-Q1-prep-jobs-START-HERE-handoff.md`，跟 B3 平行、不互卡。
- **B3 弧的 changelog/SUMMARY/memory/Notion** —— 等 user 下收尾命令。
- **套件發 Nexus / pyproject 改回 pin** —— 整 feature 完 + user 明示。

---

## §11 本 session（B3）commits

### 主專案（branch `feature/oscal-refactor`，未 push；HEAD `fd668423`）
```
fd668423 feat(FR-038): B3 — 專案 SSP 控制實作 SoA（control-implementation）接 v2
0662360e feat(FR-038): B3 — 受評標的 ssp-resources（devices/info-systems）接 v2（MF+專案）
e309cb9c feat(FR-038): B3 — 專案 SSP document-pool（程序書池）接 v2 + 補回 2 張漏建表  ← mapping 半當時未通,§1 已註
f15c55b2 feat(FR-038): B3 — 專案 SSP components/inventory/leveraged/party 接 v2
b14af00c feat(FR-038): B3 — 重建 SspProjectResolver(v2) + 專案 SSP system-characteristic 接 v2
96becd58 feat(FR-038): B3 — module_frame inventory / leveraged / party 接 v2 SspService
3e311aca feat(FR-038): B3 — module_frame components 接 v2 SspService
e232ada5 feat(FR-038): B3 vertical slice — module_frame system-characteristic 接 v2 SspService
```
（更前面 cf9694dd 起為前一棒 import-ssp 設計/實作。）

### 套件（`~/Projects/Jedicogy/module/jedi-python-package`，未 push；HEAD `c2b7502`）
```
c2b7502 feat(oscal-v2): SspService statement CRUD — B3 控制實作 SoA per-AO 編輯地基
```

### control_impl_import 4 route 原始 URL（re-enable 用，git pre-2A `d9a93f1e^`）
```
/ssp/<ssp_uid>/control-implementations/export            (GET)
/ssp/<ssp_uid>/control-implementations/import            (POST 上傳驗證)
/ssp/<ssp_uid>/control-implementations/import/validate   (POST 重驗 JSON)
/ssp/<ssp_uid>/control-implementations/import/confirm    (POST 確認寫入)
```
Resource 類名：`SspControlImplExportRoute` / `SspControlImplImportRoute` / `SspControlImplRevalidateRoute` / `SspControlImplImportConfirmRoute`（在 `api/oscal/routes/ssp/ssp_control_impl_import_route.py`）。

---

## §12 給 fresh session 的超短 prompt
```
讀 docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-B3-finish-docpool-mappings-and-control-impl-import-START-HERE-handoff.md。
先過「🧭 WHY」+ §0 讀序硬 gate（B5/Q1 的 AO 推導範本 + 已 ship 的 control_implementation IR/statement 解析），
能答冷接自檢 4 問（卡的三張舊表/新模型用哪兩張 / 控制與AO各對哪個 v2 id / 標題+AO從哪條 catalog 鏈 / 為何不能 import 現況 SspCatalogTitleQuery）才往下。
跑 §6 pre-flight（BOOT OK + pytest 47f/50e）+ §7 verify（舊 AP 三表 MISSING、catalog/新表在、statement CRUD 在）。
接手解開最後 1.5 條：①§3-A document_pool mapping 半 — service 加 ssp_service、control_identifier→IR.id / AO→statement.id（reuse control_implementation 的 get_or_create，同 context_id 空間）取代 dark 的 get_ap_*。
②§3-B control_impl_import — 重寫 SspCatalogTitleQuery 走 v2 catalog 鏈（ssp.import_profile_id→profile_imports.source_catalog_id→catalog_controls.title + catalog_control_parts 的 assessment-objective）、拔光 v1 import（否則炸 boot）；再 v2 改寫 import service + wire + re-enable。
每段 BOOT OK + baseline-diff 零新回歸 + 顯式 git add commit。push/收尾/套件發版等 user 明示；不切 branch；pyproject path-dep 勿 commit。
```

---
### 冷接可行性自檢 ✅
看本檔 + §0 讀序 + 跑 §6/§7 → 能確認現況（15/16 done、剩 1.5 條卡同一塊地基、commits）、懂 WHY（舊 AP 三表退役 → 控制/AO id 改建 v2 IR/statement、標題/AO 改走 catalog 經 profile）、知道怎麼接（§3-A/§3-B 步驟 + 範本檔 + context_id 一致 + AO 識別子一致性）、知道陷阱（v1 import 炸 boot / dark get_ap_* / migration 多環境）、知道規範界線。不需 user 額外解釋即可開工。
