狀態:設計定稿(2026-06-15,與 user 互動拍板)。本檔為 brainstorm 收斂結果。 上游:requirement-analysis / design §4.2、§4.4 / api-contract §2 / 2A handoff §10(import-ssp defer)。 FR-038 Wave 2 收尾項:把唯一還沒接 v2 的 BE 功能(SSP 匯入)補上。
受評公司 / 顧問常常已經有一份寫好的 SSP 文件(Word 或 Excel)。目前要把內容搬進系統,只能 人工一條條打。import-ssp = 上傳該文件 → 系統自動解析 → 填進「答案卷(SSP)」。
落點 = 合規資源庫的「範本 SSP」(公版範本):顧問/管理員建一份標準範本 SSP 放資源庫,之後 每個新專案成立時 clone 這份範本當起點(B2 已有 clone 機制)。不是填某個專案自己的 SSP。
UI 掛在合規資源庫編輯頁(FE ModuleFrameTemplateEditView.vue,已是 v2:module-frame-import-docx-update / module-frame-import-excel-update 路由 + ssp-docx-import-v2 頁)。
v1 confirm 以 source_type 分派三條(_VALID_SOURCE_TYPES={framework_version, module_frame, ssp}):
| source_type | v1 flow | 模型 | 本期 |
|---|---|---|---|
framework_version |
_confirm_superset_flow |
Model B:從文件建新資源庫(選框架版本 → 建 module_frame + profile + 填範本 SSP) | ✅ 做 |
module_frame |
_confirm_update_flow |
Model A:更新既有資源庫的範本 SSP(FE ...-update 路由) |
✅ 做 |
ssp |
_confirm_ssp_update_flow |
專案 SSP(Project Planning) | ❌ 範圍外 |
OscalIoService.import_ssp 填範本 SSP(共同核心)。差別:Model B 多一步「先建殼」 —— 從 framework_version clone catalog→profile→空範本 SSP(= B1 既有 clone_resource_library 能力), 再 import_ssp 填;Model A 直接解析既有 module_frame 的 template_ssp_id 來填/更新。...-update,程式碼註解「ensure_shell 後填入」。首次匯入空殼 = create(P1~P3 涵蓋);再次匯入既有內容 = update-diff(P4,核心非附加)。Model B 每次建新資源庫 =永遠 create;Model A 才會碰 update-diff。DocxParserCore / ExcelParser,OSCAL-agnostic)、parse-job 表、reconciliation、 Phase 2 update-diff、8+ 測試 —— 都還在,但被 config/di_modules.py:EXCLUDE_MODULES 擋掉。domain/oscal/strategy/ssp_write_strategy.py、 module_frame_write_strategy.py)還 import 舊 jedi_oscal v1 entity(ControlImplementationEntity / ControlImplementationObjectiveEntity / PartyEntity / ResponsiblePartyEntity),與 v2 撞同一 SQLAlchemy MetaData,wire 進去 BE 就炸。jedi_oscal_v2 的 OscalIoService 只有 export_oscal(已 ship),import 整個不存在。| 區塊 | 狀態 | 處置 |
|---|---|---|
DocxParserCore / ExcelParser(解析) |
OSCAL-agnostic,不依賴 v1/v2 | 重用 |
| parse-job 表 + domain service(暫存解析結果 + 狀態機) | 主專案、與 v1 OSCAL 無關 | 重用 |
| route + app service 的 preview / confirm / discard 編排 | 主專案;confirm 內呼 v1 strategy | 重用編排,換寫入 |
| party reconciliation(解析出的人 ↔︎ DB user 配對) | 主專案 | 重用 |
| Phase 2 update-diff 標注(parsed vs current) | 主專案;current 端讀 v1 entity | 重用編排,current 端改讀 v2 |
| 寫入核心(control-impl / SoA / statements / parties / SC) | v1 jedi_oscal strategy |
重建於 v2(套件) |
SspImportConfirmService(v2-bundle confirm) |
只寫 LA / component / inventory(FR-032 era) | 併入新寫入路徑,補齊核心 |
jedi_oscal_v2 寫入機具齊全:14 個 SSP repo/entity(SspControlImplementation / SspImplementedRequirement / SspStatement / SspByComponent / SspSystemCharacteristics / SspComponent / SspLeveragedAuthorization / SspInventoryItem 等)+ SspCloneService.deep_clone_ssp 已示範 12 子表完整寫入 pattern(含 by-component 的 component_id 軟指標 remap)。OscalIoService.export_oscal → _export_ssp / _build_metadata / _build_system_characteristics / _build_system_implementation / _build_control_implementation:import 是其精確反向,照同一結構鏡像。module_frames.template_ssp_id 指該 SSP;import 解析 resource-library {uid} → template_ssp_id → 寫入該 ssp_id。⚠️ 實作前 pre-flight(per memory
feedback_preflight_before_touching_pipeline):每 phase 開工 必先讀關鍵檔本身(parser 輸出 dataclass 欄位、export_build_*實際結構、SspCloneService 寫入 順序、B1 建殼後 template SSP 是否已有 implemented_requirements),不憑本設計摘要刻。欄位級 對照在 implementation-plan 各 phase pre-flight 段釘死。
┌─ 主專案(compliance-manager-be)────────────────────────────────┐
│ route(re-enable)→ app service(preview / confirm / discard 編排)│
│ ├─ DocxParserCore / ExcelParser → 解析成「正規化 SSP 中介」 │
│ ├─ party reconciliation → 解析人 ↔ DB user │
│ ├─ update-diff 標注 → 中介 vs 套件 snapshot │
│ └─ confirm 寫入 ── delegate ──▶ │
└──────────────────────────────────────────┼──────────────────────────┘
▼
┌─ 套件(jedi-oscal-v2 OscalIoService)──────────────────────────────┐
│ import_ssp(intermediate, *, target_ssp_id, catalog_id, mode, user) │
│ → 把正規化中介寫成 v2 SSP entity 樹(鏡像 export / clone pattern)│
│ build_ssp_snapshot(ssp_id) → 可比對 dict(給主專案 diff 用) │
└─────────────────────────────────────────────────────────────────────┘
| # | 決策 | 值 |
|---|---|---|
| D-IS-1 | 落點 | 合規資源庫範本 SSP(module_frames.template_ssp_id)。兩種模型都做:Model A(module_frame source,填/更新既有)+ Model B(framework_version source,先建殼=clone_resource_library 再填)。ssp source(專案)範圍外 |
| D-IS-2 | 寫入 mapper 位置 | 套件 OscalIoService.import_ssp(與 export 對稱,套件擁有 OSCAL↔︎entity 知識) |
| D-IS-3 | 正規化中介契約 | 主專案 parse 端定義、套件 import 端 consume;穩定 dataclass(OSCAL-shaped、不綁 docx/excel) |
一個 OSCAL-shaped、與來源格式無關的中介物,承載:
props)+ statements + by-components。docx 與 excel parser 各自把自己的解析結果正規化成此中介(docx adapter / excel adapter 各一), 之後 confirm + diff + 套件 import 全部只認中介 → 兩格式共用下游,避免兩套寫入。
| Phase | 範圍 | 驗收 |
|---|---|---|
| P1 套件寫入核心 ✅ DONE | OscalIoService.import_ssp(oscal_ssp, *, target_ssp_id, curr_user, mode='create'):OSCAL ssp dict(中介=export 同形狀)→ v2 SSP 樹。涵蓋 export round-trip 面:metadata roles/parties + system-characteristics + system-implementation/components + control-implementation/implemented-requirements(SoA props)。statements / by-components / LA / inventory 不在 P1(export 也不 round-trip;LA/component/inventory 另有 FR-032 v2-bundle 路徑)。create-only(空殼;非空丟錯)。pkg test tests/io_export/test_oscal_import.py。 |
io_export 10 passed(含 round-trip + 非空 guard);BE BOOT OK;BE pytest 持平 |
| **P2 Excel(兩 source) | excel parsed → OSCAL ssp dict adapter;SspExcelImportAppService confirm 改 delegate import_ssp(丟 v1 strategy);re-enable excel route。Model A**(module_frame 解析 template_ssp_id 填)+ Model B(framework_version → 先 clone_resource_library 建殼[=B1 能力] → 再 import_ssp 填)。create 模式(首次/空殼)。 |
e2e:(A) 既有資源庫上傳 excel → 範本 SSP 填好;(B) 選框架+上傳 → 建新資源庫且範本 SSP 填好;BOOT OK + pytest 不多紅 |
| P3 Docx(兩 source) | docx parsed → OSCAL ssp dict adapter;SspDocxImportAppService 同 P2(A+B);re-enable docx route。 |
e2e:docx → 兩 source 範本 SSP 填好 |
| **P4 update-diff(Model A 再匯入)✅ 實作(接回 v2,W2;smoke 待驗收) | import_ssp 加 mode='update' + 套件 build_ssp_snapshot;current 端改讀 v2;移植 Phase 2 diff 標注(parsed vs current);preview 逐項 diff_status;confirm 依 decision 在 OSCAL-dict 層 merge(W2,單一 writer)後寫入。主要服務 Model A 再匯入**(Model B 永遠建新=不需 diff),並擴及專案 SSP(Model C)。 |
既有資源庫重複匯入 → side-by-side 差異 + 逐項決定生效。單元/整合測試 + BOOT + baseline 零回歸已過;real-DB HTTP smoke 待 user 驗收(2026-06-16) |
P1 已完成(套件 commit
a930bfd,OSCAL-dict 即中介、無獨立 dataclass、create-only)。中介契約 = OSCAL ssp dict(與 export 同形狀),故 §3「正規化中介契約=dataclass」改為「= OSCAL ssp dict」; docx/excel adapter 產出此 dict。 每 phase 開工先 pre-flight 驗 live 路徑(§2 ⚠️)。改套件走 dev path-dep,不 commit pyproject 改動; 整弧完 + user 明示才發 Nexus + pin。FE 已是 v2(多數情況不需動 FE,BE 契約對齊既有頁面)。
{uid} → template_ssp_id → 主專案在 @transaction 內呼 OscalIoService.import_ssp(中介, target_ssp_id=template_ssp_id, catalog_id, mode='create', user) → 套件寫 v2 SSP 樹 → 回 count 摘要 → parse-job status=completed。build_ssp_snapshot(template_ssp_id) → 標 diff_status → FE 逐項決定 → confirm 依 decision 寫。jedi_common.handler.exception + GrcErrorCode(新碼 grep common/code/grc_error_code.py 最大值續編): resource-library 不存在 / template_ssp_id 為空 / 解析失敗(落 parse-job.error_code+message,不 500)/ control-id 對不到專案 catalog(warning 不擋,記 import_warnings)。@transaction 內(套件 import_ssp 不自開 scope,caller 控制;mirror confirm_service 慣例)。feedback_test_logger_patch_db_handler); 重用既有 8+ 匯入測試檔當回歸基準,逐一改成 v2 斷言。python -m pytest test/ 不多於 baseline 紅(零新回歸)。ssp_write_strategy / module_frame_write_strategy 退役(不刪檔,停用;確認無 live consumer 後另開 cleanup)。ssp_control_implementations / implemented_requirements —— 決定 create 是純 insert 還是 upsert + remap,直接改變 P1 mapper 形狀。必須在 P1 寫 code 前驗,不可邊做邊發現。_build_control_implementation 抓 applicability / inclusion-justification / implementation-status 的精確 prop name,讓 docx 與 excel adapter 對齊同一組鍵(否則兩格式寫出不同 props,下游 SoA 讀不到)。ParsedExcel / ParsedDocx / ParsedSsp dataclass 實體)。_build_* 的精確 OSCAL 結構(import 鏡像)。{uid} → template_ssp_id 的解析路徑與權限(manager 守門)。