FR-038 框架維護 2a/2b 再啟用 — 實作計畫

範圍:把 Wave 2A disable 的三組「框架維護」route 重寫接 v2、註冊回舊 URL、對齊舊 shape → FE 零改動。 上游決策:framework CRUD 已用此原則 ship(BE 路由接回 /oscal-frameworks*/oscal-framework*,FE 未動)。 撰寫日:2026-06-16 branch:feature/oscal-refactor(BE + 套件)

0. 核心原則(每階段共用)

  1. FE 零改動:route 一律註冊回舊 URL/oscal-framework-versions/oscal-catalog-group …),主專案 app service 當 adapter 對齊舊 request/response shape。
  2. 絕不 import v1 jedi_oscal 進 boot graph(撞 v2 MetaData 直接炸 — FR-038 第一鐵則)。所有重寫只接 jedi_oscal_v2 / v2 repo / raw SQL。
  3. canonical pattern(B1 已驗):新/改 app service 包 v2 primitive → OscalContainer wire → route 改 import 接新 service → EXCLUDE_MODULES 移除 + api/oscal/__init__.py 註冊舊 URL → BOOT + smoke + pytest 不多紅 → 顯式 git add commit。
  4. 動 v2 套件:dev path-dep(BE 重啟生效),不發 Nexus、不 commit pyproject,整 feature 完 + user 明示才發版。
  5. 每階段獨立可 ship、可手測;FE 全程不需改。

1. 三階段排序(風險低 → 高)

階段 內容 FE 頁面 風險 主要 BE 工
P1 = 2a 版本管理 版本 list(樹)/menu/get/create/update/delete ComplianceFrameworkVersionManage 低(B1 pattern) FrameworkVersionAppService;v2 FrameworkServiceupdate_version/delete_version/樹過濾
P2 = 2b catalog 編輯 catalog-tree + group/control/AO 改/刪(draft+無 profile 引用守門) FrameworkVersionEditView 改寫 framework_version_edit_service 接 v2 catalog repos;v2 CatalogService 補 group/control/part update/delete
P3 = 兩階段 PDF 匯入 parse-job:parse→預覽→confirm(decisions/overrides) FrameworkImportPage parse-job service 重寫成 v1-clean;需 v2「parse PDF→dict(不寫入)」能力

P1 — 2a 版本管理

端點(舊 URL,FE 既有)

URL method 用途
/oscal-framework-versions POST 版本分頁列表,filters:framework_uid / parent_version_uid / is_root
/oscal-framework-version/<uid> GET 版本詳情
/oscal-framework-version POST 新增版本
/oscal-framework-version/<uid> PUT 更新版本
/oscal-framework-version/<uid> DELETE 刪版本
(版本 menu 若 FE 有用) GET 版本樹 menu

FE response shape(adapter 要回的欄位)

uid, version, release_date, publish_status, framework_uid, framework_name, parent_version_uid, parent_version, catalog{概覽}, is_root, children[], created_user/_name, updated_user/_name, created_at, updated_at

註:v2 framework_versions 欄位 = uid/framework_id/parent_id/version/catalog_id/publish_status/published_at;release_date v2 schema 是否有需驗(沒有就回 null 或用 published_at)。

步驟

  1. v2 套件 FrameworkService(dev path-dep):
    • update_version(entity)delete_version(uid)(repo 已有 update/delete_by_uid
    • list_versions_by(query) 支援 parent_id / is_root(parent_id IS NULL)過濾(query entity 已有 framework_id + parent_id)
  2. app/oscal/service/framework_version_app_service.py@transaction,包 v2 FrameworkService):menu / list+pager(樹過濾 + framework_name enrich)/ get / create / update / delete;對齊舊 shape。
  3. DIoscal_containers.pyframework_version_app_service provider。
  4. routeoscal_framework_version_route.py 改 import 新 app service(拔 v1 OscalFrameworkVersionService)。
  5. re-enableconfig/di_modules.py 移除 oscal_framework_version_routeapi/oscal/__init__.py 註冊舊 URL。
  6. BOOT + smoke(用既有 framework)+ pytest baseline diff + commit。

待驗

  • v2 framework_versions 有無 release_date 欄位(FE 讀 release_date);無 → adapter 補 null 或對應欄位。
  • FE 版本列表是否硬依賴 catalog 概覽(控制數);需要就 enrich。

P2 — 2b catalog 編輯

端點(舊 URL)

URL method 守門
/oscal-framework-version/<uid>/catalog-tree GET
/oscal-catalog-group/<uid> PUT / DELETE draft + 無 profile 引用
/oscal-catalog-control/<uid> PUT / DELETE 同上(PUT 支援跨 group 搬家)
/oscal-catalog-control-assessment/<uid> PUT / DELETE draft(PUT 支援跨 control 重綁)

步驟

  1. v2 套件 CatalogService(dev path-dep):get_catalog_tree_by_version 或重用既有 get_control_treeupdate_group/update_control/update_partdelete_group/delete_control/delete_part(repo 已有 update/delete_by_uid + 級聯需自寫)。
  2. 改寫 app/oscal/service/framework_version_edit_service.py:拔 v1 catalog domain service import,改接 v2 CatalogService / v2 catalog repos;保留雙軌守門(_require_draft + _has_profile_references 改查 v2 profile_imports)。
  3. DI:provider 改 wire v2。
  4. re-enable:移除 framework_version_edit_route(含 catalog_control_assessment 同檔);註冊舊 URL(/oscal-catalog-group 等)。
  5. BOOT + smoke(改/刪一個 draft catalog 的 group/control/AO)+ pytest + commit。

待驗

  • has_references 判定:v2 profile 引用控制集的查法(profile_imports → 控制清單)。
  • 跨 group 搬家 / 跨 control 重綁在 v2 model 的外鍵(control.catalog_group_id / part.catalog_control_id)。

P3 — 兩階段 PDF 匯入(parse-job)

端點(舊 URL)

URL method 用途
/oscal-framework-parse-jobs GET 列進行中草稿(status filter)
/oscal-framework-parse-jobs/parse POST Step1 上傳 PDF(multipart)→ 解析存 job
/oscal-framework-parse-jobs/<uid> GET 取 parsed_result 預覽
/oscal-framework-parse-jobs/<uid>/confirm POST Step2 decisions+overrides → 寫 catalog/version
/oscal-framework-parse-jobs/<uid> DELETE 軟刪

parsed_result 結構(FE 既有)

{ groups:[{uid,name,description,parent_group_uid,order_no}], controls:[{uid,control_id,control_title,description,guidance,group_uid,order_no}], assessments:[{uid,name,description,control_uid,order_no}] }

關鍵風險 / 設計

  • parse-job service 現用 v1 OscalImportService.parse_pdf_to_dict + import_from_dict → 不能進 boot graph。
  • 需 v2「parse PDF → dict(不寫入)」能力:v2 CatalogService.import_catalog_from_pdf 是 parse+寫一次完成;兩階段需「只 parse 出 dict 供預覽」。
    • 方案 A:v2 補 parse_pdf_to_dict(stream, parser_code) -> dict(把 v1 parser 邏輯搬進 v2,與 import_catalog_from_pdf 共用底層 parser)。
    • 方案 B:confirm 時用 decisions/overrides 整理出最終 dict,再走 v2「from dict 寫入」(需 v2 有 add_catalog_from_dict 或用 add_catalog + entity 組裝)。
  • oscal.framework_parse_jobs 表在主專案 infra(已存在);parsed_result/import_summary 是 JSONB,與套件無關,可留。
  • parse-job domain service / repo 在主專案,檢查是否 v1-clean(不 import jedi_oscal)。

步驟

  1. 先釘死 v2 parse 能力(讀 import_catalog_from_pdf 內部 parser,抽出 parse-only)。
  2. v2 套件補 parse_pdf_to_dict + 「from dict 寫入(含 decisions 後的最終 dict)」。
  3. 重寫 framework_parse_job_service 接 v2(拔光 v1 import)。
  4. DI wire + 移除 EXCLUDE + 註冊舊 URL(4 條)。
  5. BOOT(先 import-probe 確認無 v1 NameError)+ real PDF smoke(CMMC L2)+ pytest + commit。

2. 共用驗證 / 收尾

  • 每階段:create_app() BOOT OK + python -m pytest test/ 不多於 baseline 紅(baseline-diff)+ 顯式 git add commit(BE 與套件分開)。
  • FE 全程不需改;手測走既有三頁。
  • push / 收尾(changelog/SUMMARY/memory/Notion)/ 套件發版 → 等 user 明示
  • pyproject.toml dev path-dep 勿 commit。

3. 不在本計畫

  • profile route(oscal_profile_route)/ catalog_control_assessment 獨立 route — 視 FE 是否用到再評估。
  • 資源庫 / 專案 / SSP / AP / AR 等其他 B 區塊。