決策者 2026-09-16 審完本稿後裁:先不動。理由:oscal 與主專案牽連太多(61 條 route 每條都帶本產品守門或概念),反轉只解模組間方向、不解模組內三支千行大檔的糾纏;等新功能開發帶著真實需求回頭釐清再處理。本稿的實查數字與方案取捨留作那時的起點。
系統裡只有一種 OSCAL SSP,開了兩個前門:資源庫範本(module_frame)與專案 SSP(oscal)。 SSP 的欄位對應表、Excel 範本產生器現在住在 module_frame,純粹因為資源庫先做。 把它們搬回 oscal,讓 module_frame 依賴 oscal、oscal 不再依賴 module_frame。 不改任何行為、不動套件、不動 DB。
系統把一份「系統安全計畫書」(SSP)拆成幾種子物件:參與方、元件、資產清冊、外部授權、系統特性。 套件 jedi_oscal_v2 存的是 OSCAL 標準格式,前端要的是自己的欄位名,中間要一張對應表翻譯。 這張表在兩個地方用到:資源庫範本與專案 SSP。兩者底下都是同一種 SSP——資源庫範本的資料 就是一份 SSP(module_frames.template_ssp_id 指向套件的 SSP 表)。
FR-038 做 v2 時先做資源庫那側,對應表就寫在 module_frame;後來專案 SSP 要用同一份,oscal 直接去 module_frame 拿。結果是:
oscal,反過來依賴 module_frame(實查 app 層 6 檔對 1 檔,加 DI 容器 6 處)。_build_props、_to_dict),三個模組在用。oscal_containers.py,不在 module_frame_containers.py。module_frame 下。問題不是重複(實查真正重複只有 25 行),是 SSP 的東西放錯家。
oscal| 類 | 從哪來 | 搬去哪 | 規模 | 說明 |
|---|---|---|---|---|
| ① 子物件對應表的純函式 | 五支 app/module_frame/service/module_frame_{party,components,inventory,leveraged,system_characteristic}_service.py 裡的模組層級函式與 @staticmethod(_build_props/_to_dict/_props_to_map/_PROP_* 常數/_NIL_PARTY_UUID/_parse_date/_to_email_list/_empty_placeholder/_apply_payload 等) |
app/oscal/service/ssp_mapping/{party,components,inventory,leveraged,system_characteristic}.py |
約 400 行 | 只搬純函式;五支 service 本體(查 template_ssp_id、呼叫 SspService、暱稱 enrich)留在 module_frame,改 import oscal。函式去掉底線改公開名——它們本來就是跨模組契約 |
| ② SSP Excel 範本產生器 | app/module_frame/excel_template/ 整包 7 檔 |
app/oscal/service/excel_template/(與既有 excel_parser/ 並列:一個產、一個讀) |
1,656 行 | 整包平移,零邏輯改動 |
| ③ 控制項 Excel 欄位契約 | module_frame_template_import_service.py:78-107(CM-1841 剛去重到這裡,oscal 側 ssp_control_impl_import_service.py:44 現在 import 它) |
app/oscal/service/excel_template/control_impl_contract.py |
25 行 | 再搬一次;兩邊改引用。CM-1841 的落點與本案相反,但只是常數位置,本案覆蓋即可 |
| ④ SSP 資源脈絡 service | app/oscal/service/ssp_resources_context_service.py |
不動 | 230 行 | 已在 oscal。反轉後 module_frame_ssp_resources_service.py 對它的 import 從「唯一反向依賴」變成合法方向 |
| ⑤ 通用 Excel 樣式 | excel_template/styles.py(38 行)與 data_validation_builder.py(54 行)——app/auth/service/user_import_template_app_service.py 只借這兩檔的三個字型/填色常數與一個下拉建構函式 |
隨 ② 進 oscal,auth 那條 import 改指 oscal | 92 行 | 兩檔只依賴 openpyxl、無業務知識。不另抽 common/:common/ 目前沒有任何 openpyxl 工具,為三個常數開新目錄不划算;auth 依賴 oscal 是可接受方向(使用者匯入範本借 SSP 範本的視覺風格是刻意一致的) |
jedi_oscal_v2 / jedi_common
▲
app/oscal ← SSP 本家:子物件對應表、Excel 產/讀、資源脈絡、匯入匯出
▲ ▲ ▲
app/module_frame │ app/auth(借 Excel 樣式)
│
app/flow_control(assessment_plan 用 party 對應表)
module_frame → oscal 現有 4 處(1 支 service、1 支 route、1 支 serializer 引 2 個 schema)反轉後全是合法方向,不用動。
test/test_module_boundaries.py)app/oscal/、api/oscal/、domain/oscal/、infra/oscal/、di_containers/oscal/ 不得 import app/module_frame、api/module_frame、domain/module_frame。實查現況違規 16 處(app 10、di_containers 6),本案清到 0。app/oscal 加一行 from app.module_frame...,守衛要紅。module_frame_ssp_resources_service 的 DI 註冊從 oscal_containers.py(import 在 57-88 行,Factory 在 257-285、373-376 行)搬回 module_frame_containers.py——它們是 module_frame 的 service。FlowControlErrorCode 被這些檔當通用錯誤碼用(GRC_MODULE_FRAME_NOT_FOUND 等)——不動,只記錄。| 方案 | 為什麼不選 |
|---|---|
| 新開共用模組(前一版設計稿) | 為同一個概念開第三個箱子,名字要硬湊。決策者問「SSP 不就是 OSCAL 的一環,為什麼獨立出來」——答不出來,就代表不該獨立 |
| 維持現狀 | 「SSP 本家依賴資源庫」會一直誘人反轉;FR-105 兩棒待裁欄都提到,代表它會反覆冒出來 |
塞進 common/ |
common/ 有「不得反向 import」守衛,且對應表帶產品業務知識(matched-user-id 軟參照、空值哨兵、中文欄位標題) |
推進套件 jedi_oscal_v2 |
對應表另一端是本產品前端欄位名與中文 Excel 標題,套件不該知道;空值哨兵是主專案替套件補位,塞回套件會變成套件契約 |
| 連五支 service 本體一起搬 | 本體綁 module_frames.template_ssp_id(資源庫概念),搬走等於把資源庫業務塞進 oscal |
7b017144)| 動作 | 檔數 | 行數 |
|---|---|---|
| 純平移(excel_template 7 檔) | 7 | 1,656 |
| 抽出純函式成新檔(五支對應表 → oscal 五檔) | 5 新增 | 約 400 |
| Excel 契約搬成一檔 | 1 新增 | 25 |
| 改 import 路徑(不改邏輯) | 約 20 | — |
| DI 容器搬註冊 | 2 | — |
| 守衛測試加規則 | 1 | 約 40 |
| 合計 | 約 36 檔 | 邏輯零改動 |
改 import 的檔(runner 開工先重跑 grep): app/module_frame/service/ 五支對應 service + ssp_import_template_app_service + module_frame_template_import_service; app/oscal/service/ 五支 ssp_*_app_service + ssp_control_impl_import_service + excel_parser/sheet_handlers; app/flow_control/service/assessment_plan_app_service;app/auth/service/user_import_template_app_service; di_containers/oscal/oscal_containers、di_containers/module_frame/module_frame_containers; scripts/archive/regenerate_reference_templates.py;test/ 四支(test_excel_template_generator_required_fill、test_fr032_excel_device_roundtrip、test_fr038_b3_mf_inventory_mapping、test_module_boundaries)。
pytest test/test_module_boundaries.py test/test_fr038_b3_mf_inventory_mapping.py test/test_excel_template_generator_required_fill.py test/test_fr032_excel_device_roundtrip.py test/test_import_adapter_excel_to_oscal_ssp.py test/test_party_reconciliation_v2.py -q。grep -rn "from app.module_frame" app/oscal api/oscal di_containers/oscal 回 0。一棒做完(Opus,effort high)。36 檔全是同一個搬遷動作的不同面,拆兩棒會有「新路徑存在但舊路徑還在」的半套狀態。
與 CM-1841 的關係:CM-1841 已在跑(BE 22c65991、7b017144 已進,FE 未 commit)。它的 C 段把 25 行契約去重到 module_frame 側,與本案方向相反,但只是常數落點——不中斷 1841,本案第 ③ 類再搬一次。本案開工前提:1841 收完並驗收。
_build_props、_to_dict)各有一份、簽名不同,改名時要逐支對;守衛擋不到「同名不同模組」。用 grep -rn "_build_props\|_to_dict\|_props_to_map\|_map_to_props" --include=*.py app api test 逐一核。ssp_party_app_service.py:158,164 有兩處方法內延遲 import(原因是避循環)。反轉後循環消失,可提到檔頭;runner 要驗 python -c "import app.oscal.service.ssp_party_app_service" 無循環。module_frame)與 SSP 一起進套件,本案搬的東西跟著進去當套件的「宿主呈現層」;不會因為本案搬了而更難。FlowControlErrorCode 被當通用錯誤碼——只記錄。oscal 模組改名(它裝的是「框架+SSP」兩個產品功能,不是整個 OSCAL)——另案。