✅ 已往前推進(2026-06-15)— 接手者請改看新 handoff

本檔的「下一棒 = B2」已過時:B2 / B3a / B4.1 已 shipped(commits 2ba7a7c48111626e)。 下一棒 = B4.2 → B5,請改讀 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:37962f7dbefbbc583512e4f9 + 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:
M jedi-oscal-v2/.../catalog/catalog_service.py(add_catalog + import_catalog_from_pdf/excel + metadata NN 預設)
M jedi-oscal-v2/.../framework/framework_service.py(get_version)
?? jedi-oscal-v2/tests/catalog/test_catalog_service_import.py
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 ✓ 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 §0~§4(舊 6 問題 / 三層四邊界 / 輪次 7 態狀態機 / 新舊業務流程對照 / Q1 job 綁定)
  • 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 §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(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 DBoscal 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/exceladd_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(開工必跑)

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.pystart_oscal_projectstart_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。