> ## ✅ 已往前推進（2026-06-15）— 接手者請改看新 handoff
> 本檔的「下一棒 = B2」已過時：**B2 / B3a / B4.1 已 shipped**（commits `2ba7a7c4`、`8111626e`）。
> 下一棒 = **B4.2 → B5**，請改讀 [`2026-06-15-B4.2-B5-START-HERE-handoff.md`](2026-06-15-B4.2-B5-START-HERE-handoff.md)。
> 本檔保留作 B1/B2 開工依據與 disable 清單參照。

# 🟢 START HERE — FR-038 Wave 2B 冷接交接：在 v2 地基上重建 B1~B5 業務

> **給下個 session 的 prompt**：「讀 `docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-14-2B-START-HERE-handoff.md`，先過 §0 讀序硬 gate 懂需求，跑 §4 pre-flight 確認現況，再接手 **B2**（B1 已完成）：依 §3 re-enable 計畫 + B1 的 canonical 模式（§1c）在 v2 上逐一重建。」
> 本檔自包含。所有狀態為 **2026-06-14 verified**。

> ## 🔖 交接現況（2026-06-14 換 session 當下）
> | 項目 | 值 |
> |------|----|
> | **進度** | 2A ✅ shipped+closed · 2.0b ✅ defer-DROP · **B1 ✅ 完成（框架 CRUD+匯入 + 資源庫三件組）** · **下一棒 = B2**（專案成立 clone 三件組）|
> | 主專案 branch / HEAD | `feature/oscal-refactor` / `edab519e`（B 進度 commits：`37962f7d`→`befbbc58`→`3512e4f9` + docs）|
> | 主專案 working tree | **乾淨，只有 `M pyproject.toml`**（jedi-oscal-v2 dev path-dep，**照規範勿 commit**）|
> | ⚠️ **v2 套件有未 commit 的 dev 改動（B 依賴，別搞丟/別發版）** | repo `~/Projects/Jedicogy/module/jedi-python-package`（branch `feature/oscal-refactor`）working tree：<br>`M jedi-oscal-v2/.../catalog/catalog_service.py`（add_catalog + import_catalog_from_pdf/excel + metadata NN 預設）<br>`M jedi-oscal-v2/.../framework/framework_service.py`（get_version）<br>`?? jedi-oscal-v2/tests/catalog/test_catalog_service_import.py`<br>**dev path-dep（主專案 pyproject 指本地源碼），BE 重啟即生效；v2 測試 197 passed。發版（bump+推 Nexus+主專案 pin）等整個 feature 完成 + user 明示才做。** |
> | My Jobs / GRC 產品 | 2A 後 GRC 大面 dark；B1 已 re-enable 框架+資源庫 9 條 route；其餘 B2~B5 route 仍在 §5.4 disable 清單，逐棒 re-enable |
> | 跑得起來嗎 | `create_app()` BOOT OK on v2；`python -m pytest test/` = 56 failed + 50 errors（=106，與 2A 後持平、零新回歸）+ 9 skipped |

| 項目 | 值 |
|------|----|
| 緣由 | FR-038 OSCAL 重設計。**2A 地基翻轉已完成**（BE 已 boot 在 `jedi_oscal_v2` 上，B1~B5 業務全 disable）。**2B = 在 v2 地基上把 B1~B5 業務真正落地**。|
| branch | `feature/oscal-refactor`（兩 repo）|
| 2A 狀態 | ✅ 完成：`create_app()` BOOT OK on v2、pytest 106 紅 ≤ baseline 117（zero new regression）、My Jobs view 回 131 列。見 [2A handoff](2026-06-14-2A-corrected-gut-and-disable-handoff.md) `✓ FIXED` 標頭 + §5.4 disable 清單。|
| 2B 範圍 | 依 design §4.x 把 B1~B5 業務 service 接真 v2，逐一 re-enable 2A disable 的 route/provider/service/test。**FE 是 2C/Wave 3。**|

---

## 🧭 開工前必懂：2B 在大圖的位置（先懂才准碰 code）

**一句話**：2A 只是把 BE 從舊套件翻到 v2、讓它能 boot；**2A 之後整個 GRC 產品是 dark 的**（framework/資源庫/專案成立/SSP/AP/AR/POA&M/輪次 endpoint 全 404 或 stub 空回）。**2B 就是把這些業務在 v2 的新模型上重新實作**，讓產品恢復功能並落地新設計（三層 clone/snapshot 邊界 + 稽核輪次 first-class + Phase 3 補齊）。

**新模型 WHY 與細節**（必讀，與 2A 同一份 gate）：
- [`requirement-analysis.md`](../requirement-analysis.md) §0~§4（舊 6 問題 / 三層四邊界 / 輪次 7 態狀態機 / 新舊業務流程對照 / Q1 job 綁定）
- [`design.md`](../design.md) §3（engagement 模型 initial/surveillance/close-out ↔ AP/AR/ar_results）、**§4.2（v2 套件對外 service 簽章契約 — 2B 接 v2 的依據）**、§4.4（套件 vs 主專案邊界）、§5（API spec 骨架）
- [`api-contract.md`](../api-contract.md) §1~§10（B1~B5 端點清單 — 2B 照此重建 route）

> **冷接自檢**：你應能回答 ① 三層四 clone/snapshot 邊界是哪四個 ② engagement 模型怎麼把 round ↔ AP ↔ AR ↔ ar_results wire 起來 ③ 哪些留主專案、哪些在套件（design §4.4）④ My Jobs 為何綁 SSP 控制項而非 AP task。答不出 → 回去讀。

---

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

1. 本檔「🧭」節 + 通讀本檔
2. 🔒 gate：requirement §0~§4 + design §3/§4.2/§4.4/§5 + api-contract
3. **2A handoff §5.4「✅ 最終 disable 清單」** ← 這是 2B 的 re-enable 地圖（每個 disable 項對應 2B 復原動作）
4. 2A handoff §5.2 v2 wiring map（v2 service class 名 + 核心 method 契約，開工前 pre-flight 複驗簽章）
5. [`wave2-migration-plan.md`](../wave2-migration-plan.md)（B1~B5 phase 拆解）

---

## §1 2A 留下的地基（現況 verified）

- **OscalContainer** = 18 個 v2 primitive（零參 Factory），見 `di_containers/oscal/oscal_containers.py`。2B 直接消費這些（或加 repo 注入）。
- **舊 jedi_oscal 已從 boot graph 完全剝離**；主專案 source 對舊套件零 boot-time import。舊套件仍裝著（`jedi_oscal 0.0.22`），但任何 import 它的檔都會與 v2 collide → **2B 重建時一律接 v2，禁止再 import 舊 jedi_oscal**。
- **dev DB**：`oscal` schema 48 表（v2）、`compliance.project_audit_rounds` 在；v2 業務表多為空（SSP/AP/AR=0 列）→ 2B 的專案成立 clone 流程會把資料灌進去。
- **My Jobs view** 已重綁 live 工作流資料（control_* 暫 NULL）；2B 要把 control_* 接到 `oscal.ssp_control_implementations`（Q1）。

## §1b 2.0b + B1 已釐清的前置（2026-06-14 後續）

- **2.0b `assessment_plan_extensions`：決議 defer DROP 到 B3**（不 rebuild FK、不現在 DROP）。理由：目前**無 broken FK** 卡 boot（`fk_ape` 早已消失）、表 inert（193 列舊資料）但仍有 live-ish consumer `stage_advance_service`。等 B3 `project_audit_rounds` 取代 + `stage_advance_service` 重寫後再跑安全四查 DROP。完整取捨見 `docs/analysis/2026-06-14-fr038-2.0b-assessment-plan-extensions-defer-drop.md`。
- **B1 v2 套件依賴已補（dev path-dep，未 commit/未發版）**：原本 v2 `CatalogService` 沒有建立/匯入 catalog 的方法、`OscalIoService` 沒有 import_ssp → B1 匯入端點無法做。已在 `jedi-oscal-v2` 補：
  - `CatalogService.add_catalog(catalog: CatalogEntity) -> CatalogEntity`（persist catalog→groups→controls→parts，caller 持 @transaction）
  - `CatalogService.import_catalog_from_pdf(stream, framework_code, start_page=None, end_page=None, metadata=None) -> CatalogEntity`
  - `CatalogService.import_catalog_from_excel(stream, framework_code, metadata=None) -> CatalogEntity`
  - CatalogEntity 巢狀屬性（parser 產出）：`catalog.oscal_metadata` / `catalog.groups` / `catalog.controls`(flat) / `group.controls` / `control.parts`；FK：group.catalog_id、control.catalog_id+catalog_group_id、part.catalog_control_id。
  - v2 測試 197 passed（194+3）。**這份套件改動還在 working tree（dev path-dep），feature 全完成 + user 明示才 bump+發 Nexus。**
  - **仍 deferred（follow-up）**：`OscalIoService.import_ssp_docx/excel`（SSP 匯入大；資源庫可先用空 SSP 範本 + clone 起步，非 B2~B5 e2e 必需）→ 需要時再補套件。
- **B1 主專案實作怎麼接**：framework CRUD 走 `oscal_container.framework_service`（v2 FrameworkService：add/get/list/update_framework、add_framework_version、list_versions、publish_version）；資源庫建立走 `oscal_container.oscal_snapshot_service.clone_resource_library` + `profile_service.resolve_profile`；框架版本匯入走 `catalog_service.import_catalog_from_pdf/excel` 後回填 `framework_versions.catalog_id`。端點清單 + request/response shape 見 api-contract §1/§2。舊 framework route 檔（`api/oscal/routes/framework/*`）仍在但 import 舊 jedi_oscal → 2B 重寫成 v2 或新建，再從 `EXCLUDE_MODULES` + `api/oscal/__init__.py create_module` 恢復註冊。

## §1c B1 進度（2026-06-14/15 已做的部分）

- ✅ **B1 框架半部 SHIPPED**（commit `37962f7d` CRUD + `befbbc58` 匯入）：
  - `app/oscal/service/framework_app_service.py` 包 v2 FrameworkService + CatalogService。
  - **框架 CRUD**（list/get/create/update/menu）接 v2，route `oscal_framework_route` 重寫 + re-enable（`/oscal/frameworks/list`、`/menu`、`/oscal/framework/<uid>`、`/oscal/frameworks`）。
  - **框架版本匯入** `import_framework_version`（PDF/Excel → `CatalogService.import_catalog_from_*` → 建 framework_version 回填 catalog_id），route `oscal_import_route` 重寫 + re-enable（`POST /oscal/framework-versions/import`）。**real CMMC L2 PDF 實測 → 14 groups / 110 controls / 1988 parts 正確落地。**
  - canonical B 模式定型：新 app service 包 v2 primitive（補 pager/filter/uid/enrich）→ OscalContainer wire → route 重寫 → EXCLUDE_MODULES 移除 + create_module 恢復註冊 → boot + smoke + pytest（持平 106 紅）+ commit。
  - **uid 由 app service 層產**（v2 repo 不自動生 uid，與 catalog 一致）。
- ✅ **B1 資源庫半部 SHIPPED**（commit `3512e4f9`）— **B1 整體完成**：
  - `app/oscal/service/resource_library_app_service.py`：建立資源庫 = 選 framework_version → `oscal_clone_service.clone_catalog_tree`（邊界① catalog 副本）→ `profile_service.add_profile`（ProfileImportEntity 指向 cloned catalog + include 規則）→ `ssp_service.add_empty_ssp`（空範本，需先建 metadata）→ 寫 `compliance.module_frames` link record。**無 schema 變更**（module_frames 已有 oscal_framework_version_uid/oscal_profile_uid/template_ssp_id；catalog 副本 id 經 profile import 取得，不另存欄位）。
  - list/get（resolve_profile 控制數摘要）/publish；route `/oscal/resource-libraries[/list]`、`/oscal/resource-library/<uid>[/publish]`。
  - **end-to-end 實測**：framework→import CMMC L2→建資源庫 → cloned catalog≠src（邊界① 確認）、profile resolve 110 控制項、SSP 範本 + module_frame link 建立。
  - **module_frames RLS**：tenant-scoped；create 由 route 帶 `user.tenant_id`；無 user context 時 session_scope `else` 開 super_admin 繞 RLS（smoke 可直接跑）。
  - import-ssp（資源庫匯入 SSP）仍卡 `OscalIoService.import_ssp_*`（follow-up）。
- **v2 套件本波累計改動（dev path-dep 未 commit/未發版，v2 測試 197 passed）**：`CatalogService.add_catalog`+`import_catalog_from_pdf/excel`、`add_catalog` 補 metadata NN 預設、`FrameworkService.get_version`。發版時機：feature 全完成 + user 明示。

## §2 ⚠️ 2B 必守約束

1. **v1/v2 不可共存**（記憶 `project_oscal_v1_v2_cannot_coexist`）：2B 任何新 code 只接 `jedi_oscal_v2`。重新 enable 一個 service 前，確認它內部 import 的是 v2，不是被 2A stub 的舊符號。
2. **2A 的 stub 是「import-clean 佔位」不是實作**：2A 把 dark service 的方法 stub 成空回傳 / no-op（保留簽章）。2B re-enable = 把 stub 換成真 v2 邏輯，不是「拿掉 skip 就好」。
3. **disable 清單是 re-enable 契約**：2A handoff §5.4 D 表逐檔列了 stub 了什麼。2B 對著它把每個 stub 還原成 v2 實作 + 移除對應 test 的 module-level skip。
4. **container 注入**：2A 把 grc/project/module_frame/associations container 對 oscal_container 的注入移除或改 Object(None)。2B 重接時，oscal_container 已是 v2，provider 名稱與舊不同（v2 收掉了大部分 domain service，只剩 3 個）→ 照 design §4.2 用 v2 app service。
5. **pytest baseline**：2B 完成的每塊，移除對應 test skip 並讓它在 v2 上綠；不得讓 `python -m pytest test/` 比現況（106 紅）多紅。
6. **plan 假設先 verify**（v2 簽章 / 欄位）才開工。不晶晶體。

## §3 2B re-enable 計畫（依 design Wave 2 B1~B5）

| Phase | 內容 | 對應 2A disable | 主要 v2 service（design §4.2）|
|-------|------|----------------|------------------------------|
| **B1** | 合規框架 + 資源庫 API 對齊 v2（profile resolve、資源庫三件組 snapshot）| api.oscal framework/catalog/profile route + module_frame 資源庫 service | FrameworkService / CatalogService / ProfileService / OscalSnapshotService |
| **B2** | 專案成立流程重寫（clone 資源庫三件組、AP/AR 延後建）| oscal_project_service / module_frame_template_copy | OscalSnapshotService.clone_resource_library + SspService |
| **B3** | SSP 維護 + 啟動稽核 snapshot + `project_audit_rounds` 狀態機（7 態）| api.oscal ssp_* route + ssp_versioning | SspService 子物件 CRUD + snapshot_ssp；輪次狀態機留主專案 |
| **B4** | AP 後端（草稿生成 + reviewed-controls/subjects/tasks）| api.oscal ap route | AssessmentPlanService + ApDraftService |
| **B5** | AR 後端（AO 全量判定矩陣 + 風險總結）+ POA&M + 結案/覆核 | api.oscal ar route + grc poam/stage handlers | AssessmentResultService + AssessmentRiskService + PoamService |
| **My Jobs** | view control_* 接 v2 SSP 控制項；專案成立時建 job（綁 SSP 控制項，Q1）| vw_user_job_queue + task_assignee | — |

> 各 phase re-enable 時：(1) 把對應 leaf service 的 stub 換真 v2 實作 → (2) container 重接 v2 provider → (3) route 從 EXCLUDE_MODULES 移除 + create_module 恢復註冊 → (4) 移除對應 test skip 並補 v2 測試 → (5) 跑 boot + pytest 驗不多紅。

## §4 Pre-flight（開工必跑）

```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
git branch --show-current                      # feature/oscal-refactor
poetry run python -c "import jedi_oscal_v2; print('v2 OK')"
# BE boot（補 dummy env，否則撞 jedi_issue / deepcopy 假象）
set -a; source .env; set +a 2>/dev/null
export GITLAB_API_VERSION=4 GITLAB_URL=http://localhost GITLAB_PRIVATE_TOKEN=dummy GITHUB_PRIVATE_TOKEN=dummy
poetry run python -c "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,405 passed,9 skipped,50 errors
```

## §4b 下一棒 = B2 立即開工指南（專案成立 clone 三件組，邊界②）

**canonical 模式（B1 已驗，照抄）**：新 app service 包 v2 primitive（補主專案職責 pager/filter/uid/RLS/tenant）→ OscalContainer wire → route 重寫/新建 → `EXCLUDE_MODULES` 移除 + `api/oscal/__init__.py`（或對應 api 模組）`create_module` 恢復註冊 → boot + **app-service smoke（用真資料）** + pytest（不多紅）+ 顯式 git add commit。參考 commit `37962f7d`/`3512e4f9` 怎麼接。

**B2 要做的**（requirement §4.3 + design §3 + api-contract §3.2）：
- 重寫 `app/project/service/oscal_project_service.py` 的 `start_oscal_project` → **`start_project`**：
  1. 選一個 **published 資源庫**（B1 建的 module_frame 三件組）→ 取其 catalog/profile/ssp。
  2. `oscal_container.oscal_snapshot_service.clone_resource_library(catalog_id, profile_id, ssp_id)`（**邊界② 脫鉤公版**）→ 得專案自己的三件組副本。
  3. 建 `compliance.projects` 主檔 + participant + **為專案 SSP 控制項建 workflow job（Q1，專案成立就建，非 AP task）**。
  4. **AP/AR 不在此建**（延到輪次啟動 B3）。移除舊「啟動時建 AP/AR + 為 AP task 建 workflow」。
- `clone_resource_library` 簽章 = `(catalog_id, profile_id, ssp_id)`（不是 framework_version_uid；主專案先從資源庫 module_frame 解出三件組 id）。
- resource library 的三件組 id：module_frame 有 `oscal_profile_uid`（→ profile → profile_imports.source_catalog_id = catalog）、`template_ssp_id`（= ssp）。catalog 經 profile import 取。
- pre-flight：先用 B1 的 route 建一個真資源庫（import CMMC L2 → 建資源庫）當 B2 的 clone 來源測資料。
- ⚠️ 老 `project_route` / `oscal_project_service` 在 2A 被 gut（stub），B2 是把它接回 v2，不是從零；先讀 2A 對它做了什麼（§5.4 D 表）。

## §5 不在 2B scope

- FE（Wave 3 / 2C）。
- 套件發 Nexus（全完成 + user 明示才做）；`pyproject.toml` 改回 pin。
- 正式環境 schema 遷移（D7）。
- ISO/NIST（本期 CMMC-only，design §7）。

---
### 冷接可行性自檢 ✅
看本檔 + §0 讀序 + 跑 §4 → 能確認 2A 地基（v2 boots、舊套件已剝離）、知道 2B 是「在 v2 上重建 B1~B5」非「翻 import」、知道 disable 清單是 re-enable 地圖、知道每 phase 接哪個 v2 service。
