module_frame 逐檔分類

module_frame 逐檔分類

實查日期:2026-09-16 | 當時 HEAD:98d756bb | branch:feature/review

🔴 本表是某一天的快照,引用前先重驗。 每個數字下方都附了產生它的指令,重跑一次比相信這份表安全。


§1

一句話結論

module_frame 不是「真業務與轉接殼各半」,真業務比殼多:81 支檔案裡, 5,563 行(50.8%)是主專案自己寫的業務(Excel 範本產生器、YAML/Excel 匯入、BPMN 流程編排), 3,831 行(35.0%)是薄薄一層的欄位對應殼(把前端的欄位名翻成 OSCAL 的形狀、再交給 jedi_oscal_v2 套件存), 其餘 1,548 行(14.1%)是序列化器、DTO、mapper 這類接線與型別。

而那六支被懷疑「與 oscal 那邊各寫一份」的殼,實查後發現沒有兩份真相—— oscal 那五支是直接 import 這邊的對應表來用,這邊才是唯一的定義處。


§2

① 現況盤點

四層規模

目錄 檔數 行數
api/module_frame 25 2,763
app/module_frame 31 7,416
domain/module_frame 12 348
infra/module_frame 13 415
合計 81 10,942

另有 di_containers/module_frame/module_frame_containers.py(不在四層內,判 DI 註冊時要看)。

怎麼重跑
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git rev-parse --short HEAD
for L in api app domain infra; do
  d=$L/module_frame
  printf '%-22s %s 檔 / %s 行\n' $d \
    "$(git ls-files $d | grep '\.py$' | wc -l | tr -d ' ')" \
    "$(git ls-files $d | grep '\.py$' | xargs wc -l | tail -1 | awk '{print $1}')"
done

用 git ls-files 不用 find——find 會撈到 __pycache__ 與未入版控的殘留。

⚠️ 行數會因「怎麼數」差 9 行:wc -l 數的是換行符個數, Python 的 sum(1 for _ in open(f)) 數的是行物件個數——檔尾沒有換行符時後者多算一行。 本文全篇用 wc -l(10,942);FR-102 總表 A 用的是 Python 數法(10,951)。 兩個數字都對,差的是數法不是程式碼,比對時要用同一種。

對外 route:45 條

用 AST 數(不是 regex——add_resource( 常換行寫,regex 會漏抓)。 45 條全部掛在 blueprint module-frame、前綴 /api/1.0。

怎麼重跑

FR-102 README「怎麼重跑」③ 的腳本,把掃描目錄改成 api/module_frame。

⚠️ 與派工卡上的「43 條」不符——實際是 45 條。 這不是新增的 route: FR-102 總表 A 該列(README.md:58)寫的就是 45,卡片轉述時誤植為 43。 45 是本棒 AST 實跑的結果。

另有 2 條被註解掉不計入(grep -rn "^\s*#.*add_resource" api/module_frame/):

被註解的 route 註解說明
ModuleFramesRoute → /module-frames [FR-038 DEAD-V1|2026-06-18 停用待清理] 清單已改走 resource-library;FE 無 caller
ModuleFrameStartRoute → /module-frame/start [FR-038 DEAD-V1|2026-06-18 停用待清理] 建立走 /module-frame/clone;FE 無 caller

route 清單與前端四層查結果

四層=① FE src/config/api/api.js 常數 ② FE 路由 src/config/router/index.js ③ DB public.ui_routes(DEV localhost:5432/guidant_ai_dev)④ 非瀏覽器呼叫者(scripts/、e2e)。

第三層先講清楚:ui_routes 全庫只有 1 列與本模組相關—— id=24, name='module-frame', url='/module-frame/module-frame', enable=1。 它是選單項不是 API,管的是「左側選單看不看得到資源庫」這一件事; 45 條 API route 不會逐條出現在 ui_routes 裡。因此下表第三層對所有 route 都是同一個答案 (該選單開著=整組頁面可達),逐條列會變成 45 個一樣的格子,故不設欄,在此統一交代。

PGPASSWORD=<查 .env> psql -h localhost -p 5432 -U cm_app -d guidant_ai_dev \
  -c "SELECT id, name, url, enable FROM public.ui_routes WHERE name ILIKE '%module%' OR url ILIKE '%module%';"

FE 路由第二層共 9 條 /module-frame/*(router/index.js:412-491): module-frame/module-frame-items/:uid/template-edit/import-docx/:uid/import-docx/ import-excel/:uid/import-excel/import-excel/preview/:parseUid。

# route 路徑 吃哪支 service FE①常數 FE②路由 ④非瀏覽器 判定
1 GET/POST /module-frame、GET/PUT/DELETE /module-frame/<uid> ModuleFrameService MODULE_FRAME ✅ scripts/e2e_test_module_frame_with_docx.py:174 POST/e2e module-frame-import.steps.js:211 DELETE 活
2 GET/POST/PUT/DELETE /module-frame/item[/<uid>] ModuleFrameItemService MODULE_FRAME_ITEM ✅ ModuleFrameItems.vue:182,196,218,245 — 活
3 PUT /module-frame/item/xml/<uid> ModuleFrameItemService MODULE_FRAME_ITEM_XML ✅ WorkflowSetupEditor.vue:391,406,416 — 活
4 POST /module-frame/clone ModuleFrameService MODULE_FRAME_CLONE ✅ ModuleFrame.vue:634 — 活
5 GET /module-frames/menu ModuleFrameService MODULE_FRAME_MENU ✅ menuStore.js:99,233 — 活
6 GET /module-frame/download/template (route 層直接 send_from_directory) MODULE_FRAME_TEMPLATE_DOWNLOAD ⚠️ 僅在被註解的程式碼中(ModuleFrame.vue:668) — 疑似死 — 見 ④
7 GET /module-frame/download/yaml-template (同上) MODULE_FRAME_YAML_TEMPLATE_DOWNLOAD ⚠️ 僅在被註解的程式碼中(ModuleFrame.vue:672) — 疑似死 — 見 ④
8 POST /module-frame/import/template ModuleFrameImportService MODULE_FRAME_IMPORT_TEMPLATE ⚠️ 僅在被註解的程式碼中(ModuleFrame.vue:737) — 疑似死 — 見 ④
9 POST /module-frame/import/verify ModuleFrameImportService MODULE_FRAME_IMPORT_VERIFY ✅ ModuleFrame.vue:1019 — 活
10 POST /module-frame/import ModuleFrameImportService MODULE_FRAME_IMPORT ✅ ModuleFrame.vue:1050 — 活
11 POST /module-frame/import/yaml ModuleFrameImportService MODULE_FRAME_IMPORT_YAML ✅ ModuleFrame.vue:847 — 活
12 GET/PUT /module-frame/<uid>/system-characteristic ModuleFrameSystemCharacteristicService 有 ✅ SspBasicSection.vue — 活
13-14 /module-frame/<uid>/components[/<item_uid>] ModuleFrameComponentsService 有 ✅ SspComponentsLeveragedInventoryTab.vue — 活
15-16 /module-frame/<uid>/inventory[/<item_uid>] ModuleFrameInventoryService 有 ✅ 同上 e2e module-frame-party-factory 活
17-18 /module-frame/<uid>/leveraged[/<item_uid>] ModuleFrameLeveragedService 有 ✅ SspLeveragedSection.vue e2e 同上 活
19-20 /module-frame/<uid>/parties[/<party_uid>] ModuleFramePartyService 有 ✅ ModuleFramePartiesPanel.vue e2e 同上 活
21-23 /module-frame/<uid>/ssp-resources、/items[/<item_uid>] ModuleFrameSspResourcesService 有 ✅ ModuleFrameSspDevicesPanel.vue/…InfoSystemsPanel.vue — 活
24-25 /module-frame/<uid>/control-defaults[/<ci>] ModuleFrameControlDefaultService MODULE_FRAME_CONTROL_DEFAULT(S) ✅ e2e module-frame-defaults.feature 活
26-27 /module-frame/<uid>/objective-defaults[/<od_uid>] ModuleFrameControlObjectiveDefaultService 有 ✅ e2e 同上 活
28-29 /module-frame/<uid>/reference-documents[/<doc_uid>] ModuleFrameReferenceDocumentService 有 ✅ — 活
30-33 control/objective 的 …/reference-documents[/<doc_uid>] attach/detach 四條 同上 有 ✅ — 活
34-39 /module-frame/<uid>/control-defaults/ 的 export/template/download/import/template/import/verify/import/validate/import ModuleFrameTemplateImportService MODULE_FRAME_TEMPLATE_* 六個 ✅ — 活
40 GET /module-frame/<uid>/ssp-import-template SspImportTemplateAppService MODULE_FRAME_SSP_IMPORT_TEMPLATE ✅ — 活
41 GET /ssp/<ssp_uid>/excel-template 同上 SSP_EXCEL_TEMPLATE ✅ — 活
42 GET /ssp-import-template 同上 SSP_IMPORT_TEMPLATE_BY_FRAMEWORK_VERSION ✅ — 活
43 GET /module-frame/<uid>/ssp-export SspExportAppService(oscal 的) MODULE_FRAME_SSP_DOCUMENT_EXPORT ✅ — 活

表列 43 列是把成對的 list/item route 併列顯示,展開後即 45 條路徑。

這模組自己持有的表

compliance.module_frames + compliance.module_frames_trans(多語系翻譯表)兩張, ORM 在 infra/module_frame/models/。模組所有其他資料——控制項預設值、AO 預設值、 程序書池、元件、資產清冊、外部授權、系統特性、參與方——都不在這兩張表, 而是存在該 module_frame 的樣板 SSP(module_frames.template_ssp_id → oscal.ssps)底下, 由 jedi_oscal_v2 的 SspService 負責讀寫。這是本模組「殼」那一半的由來。


§3

② 逐檔分類表

四類的判準(依賴方向,不是檔名)

  • 真業務:主要邏輯自己寫,jedi_* 引用少或零;刪掉這支功能就消失。
  • 轉接殼:主體是「開 @transaction → 呼叫套件 service → 欄位對應」,商業規則在套件。 殼不等於可刪——它是「主專案回答套件問題」的必要接線。要另外標「有沒有帶主專案獨有規則」。
  • 共用設施:沒有 route、沒有自己的業務語意,是給別的模組或別的檔用的(DTO、序列化器、error code、port、mapper)。
  • 死碼:全 repo 零 import、零 route 註冊、零 DI 註冊(四層都要查才算)。
  • 還看不準:允許存在,要寫卡在哪。

api/module_frame(25 檔/2,763 行)

檔 行 類別 依賴證據 殼帶主專案規則?
__init__.py 263 共用設施 blueprint 組裝;21 個 host import(全是自己模組的 route 類別),jedi=0。零 DB、零業務 —
routes/__init__.py 0 共用設施 空 __init__ —
routes/module_frame_route.py 144 轉接殼 呼叫 ModuleFrameService;jedi=2(get_user_context/return_response)。ModuleFramesRoute(44-62)與 ModuleFrameStartRoute(113-131)兩個類別已無 route 註冊(__init__.py:88,93 被註解) ✅ 帶 — @require_capability("module-frame.create/update/delete") 三處守門
routes/module_frame_item_route.py 105 轉接殼 呼叫 ModuleFrameItemService;jedi=2 ✅ 帶 — require_capability 三處
routes/module_frame_import_route.py 162 真業務(殼+業務混住) jedi=4;route 層自己用 pandas 讀 Excel、比對 7 欄 header、比不上就 raise ConflictError(:57-77),不是純轉呼叫 ✅ 帶 — require_capability + header 驗證是產品規則
routes/module_frame_control_default_route.py 139 轉接殼 呼叫 ModuleFrameControlDefaultService;serializer dump ✅ 帶 — require_capability
routes/module_frame_control_objective_default_route.py 147 轉接殼 同上,吃 objective service ✅ 帶 — require_capability
routes/module_frame_reference_document_route.py 281 轉接殼 6 個 Resource 類別(池 CRUD + control/objective 兩組 attach/detach)全轉呼叫 ModuleFrameReferenceDocumentService ✅ 帶 — require_capability
routes/module_frame_template_import_route.py 260 轉接殼 6 個 Resource 轉呼叫 ModuleFrameTemplateImportService;blob 回傳走 send_file ✅ 帶 — require_capability
routes/ssp_import_template_route.py 169 轉接殼 3 個 Resource 轉呼叫 SspImportTemplateAppService;jedi=1 ✅ 帶 — require_capability
routes/module_frame_system_characteristic_route.py 56 轉接殼 轉呼叫;service 從 oscal_container 注入(不是自己模組的 container) ✅ 帶 — require_capability
routes/module_frame_components_route.py 78 轉接殼 同上,Containers.oscal_container.module_frame_components_service ✅ 帶 — require_capability
routes/module_frame_inventory_route.py 78 轉接殼 同上 ✅ 帶 — require_capability
routes/module_frame_leveraged_route.py 91 轉接殼 同上 ✅ 帶 — require_capability
routes/module_frame_party_route.py 95 轉接殼 同上,多一層 marshmallow serializer ✅ 帶 — require_capability
routes/module_frame_ssp_resources_route.py 99 轉接殼 同上 ✅ 帶 — require_capability
routes/mf_ssp_export_route.py 71 轉接殼 呼叫的是 oscal 的 SspExportAppService(app/oscal/service/export/),本模組零實作 ✅ 帶 — require_capability + RFC 5987 中文檔名編碼(_send_attachment)
serializers/__init__.py 0 共用設施 空 —
serializers/module_frame.py 183 共用設施 9 個 marshmallow Schema;host=2(引 oscal 的 profile/framework schema),jedi=1 —
serializers/module_frame_control_default.py 46 共用設施 純 Schema,零 import —
serializers/module_frame_control_objective_default.py 49 共用設施 同上 —
serializers/module_frame_item.py 38 共用設施 同上 —
serializers/module_frame_party.py 43 共用設施 同上 —
serializers/module_frame_reference_document.py 61 共用設施 同上 —
serializers/module_frame_template_import.py 105 共用設施 同上 —

app/module_frame(31 檔/7,416 行)

service/ — 13 支,模組的重心

檔 行 類別 依賴證據 殼帶主專案規則?
ssp_import_template_app_service.py 1,500 真業務(含殼段,見下方拆解) jedi=18/host=4。這 18 個 jedi import 全是讀資料的來源(jedi_iam 使用者/單位、jedi_asset 設備/資訊系統、jedi_oscal_v2 SSP/profile/catalog、jedi_compliance_audit 專案擴充),不是把工作交出去;檔內 70+ 個私有方法做的是「把八個來源的資料揉成 Excel 九張表的列」 —
module_frame_template_import_service.py 1,008 真業務 jedi=4/host=1。openpyxl 手刻 9 欄 Excel(建 workbook、套配色邊框、下拉、隱藏 A 欄)+解析+逐列驗證+批次 upsert。套件只提供 IR/statement 兩個 entity 型別 —
module_frame_import_service.py 415 真業務 jedi=4/host=5。YAML/Excel → module_frame 結構的遞迴轉換(save_import_module_frame_data_recursive)+BPMN 產生(create_module_frame_bpmn_format)+HTML 標籤剝除。核心是自寫的樹狀遞迴 —
module_frame_party_service.py 323 轉接殼+業務 jedi=6/host=2。主體是 FE 欄位 ↔︎ OSCAL props 對應表(檔頭 10 行對照表),寫入委派 SspService 🔴 帶三條:① 產品擴充軟參照 matched-user-id/matched-org-unit-id(OSCAL parties 無此核心欄)② 同名去重(同 metadata 下同 type+name 擋下,raise Conflict;2026-06-24 從 idempotent 改成 raise)③ 暱稱 enrich(_enrich_user_org_names 反查 jedi_iam user/org_unit 補名字)。另有 legacy 容忍:role prop 舊值 fallback
module_frame_service.py 304 真業務+殼 jedi=11/host=9,模組內依賴最雜。clone_module_frame(129-224,96 行)是自寫的 BPMN 深拷貝——複製主流程、逐個複製子流程、把主流程 XML 裡的 called_element_id 全部換成新 id;start_project_from_module_frame(226-265)編排啟動專案。其餘 CRUD 是薄殼轉 domain service ✅ 帶 — assert_scope_writable(租戶 scope 守門);BPMN clone 是主專案自有邏輯
module_frame_control_objective_default_service.py 269 轉接殼+業務 jedi=4/host=3。AO 預設值落樣板 SSP 的 statement props;lazy upsert ✅ 帶 — ① AO∈control 守門(_is_objective_in_control)② 暱稱 enrich ③ 「delete=清空值不刪 IR」的產品語意
module_frame_reference_document_service.py 257 轉接殼+業務 jedi=4/host=4。程序書池 CRUD +跨控制項 mapping,落 jedi_compliance_audit 的 ssp_reference_documents ✅ 帶 — ① 檔名 enrich(_enrich_file_metadata,ssp_reference_documents 無 title 欄,FE 顯示靠這層補)② mapping context_type 的 control_implementation/objective 慣例
module_frame_control_default_service.py 240 轉接殼+業務 jedi=3/host=3。控制項預設值落樣板 SSP 的 IR props 🔴 帶 — control∈profile 守門(_is_control_in_profile,只擋新建不擋既有;檔頭記 2026-07-17 DEV 有 23 條 guard 加嚴前的歷史違規資料,刻意不動)+暱稱 enrich
module_frame_inventory_service.py 220 轉接殼+業務 jedi=3/host=3 🔴 帶 — ① FR-032 軟參照欄 asset-type/ref-type/ref-id(產品擴充)② 同名去重 raise ③ M2M 清空修正:_build_implemented 一律回 [] 不回 None,繞開 BaseRepositoryImpl.update() 對 None silent-skip 的坑 ④ 暱稱 enrich
module_frame_item_service.py 219 真業務 jedi=5/host=2。BPMN Call Activity 的增刪改——add_module_frame_item 自己組子流程 BPMN、把 call activity 插回主流程 XML;delete 反向拆 ✅ 帶 — assert_scope_writable(檔內註解說明「守門留這裡不是套件內:套件沒有 route 也不持有宿主的租戶語意」)
module_frame_leveraged_service.py 211 轉接殼+業務 jedi=4/host=3 🔴 帶 — ① 五個 FE 欄位走 props 擴充(v2 LA 無核心欄)② NN 欄空值哨兵:party_uuid 空落 NIL-UUID(uuid 型別吃不下 "")、date_authorized 空落 date.today(),讀回時 reshape 成 None ③ 同名去重 ④ 暱稱 enrich
module_frame_components_service.py 211 轉接殼+業務 jedi=4/host=3 ✅ 帶 — ① protocol/port_ranges/security_auth 三鍵+LA 軟參照走 props ② 同名去重 raise ③ build_leveraged_title_map/enrich_leveraged_titles 兩個 module-level helper(批次查避 N+1)④ 暱稱 enrich
module_frame_system_characteristic_service.py 186 轉接殼+業務 jedi=4/host=3 ✅ 帶 — ① target-type/owner-uid 走 props ② NN 欄安全預設(首次 insert 時 5 個 NN 欄先給空字串/空陣列,避免部分 payload 撞 NOT NULL)③ 空 SC 時回 placeholder、PUT 首次 auto-create 的產品語意 ④ 暱稱 enrich
module_frame_ssp_resources_service.py 101 轉接殼(薄,真的只是轉呼叫) jedi=2/host=2。全部委派 app/oscal/service/ssp_resources_context_service.py;自己只做 mf uid → template_ssp_id 反查 ⚠️ 帶一點 — 缺 template SSP 時的兩種回法:list 回空陣列(合法狀態:剛建還沒匯入)、寫入 raise 412。這是產品的 UX 判斷
service/__init__.py 0 共用設施 空 —
ssp_import_template_app_service.py 的殼段/業務段拆解

卡片問「一支檔混了多少種事」。按方法區塊估算:

區段 約行數 性質
兩張 OSCAL 中英對照常數表(OSCAL_COMPONENT_TYPE_ZH 15 條、OSCAL_PARTY_ROLE_ZH 10 條) 35 業務(產品的中文標籤,對齊 FE i18n 檔)
3 支對外方法(generate/generate_for_ssp/generate_by_framework_version)+入口解析 約 130 殼(守門、取 mf、組 bundle、丟給 generator)
lookup 抓取(_fetch_*_lookup/_fetch_*_lookup_helpers 共 12 支) 約 225 殼(逐一去四個套件的 domain service 撈清單)
資料預填(_populate_filled_data*/_build_*_row/_build_parties/_build_components_v3…) 約 640 業務(entity → Excel 列的欄位揉合,含 FR-032 device/info-system 標籤反查、LA title map、控制項樹壓平)
控制項與 AO 組列(_build_controls_*/_flatten_tree_to_rows/_load_existing_*_map) 約 300 業務(catalog 樹壓平成列 + 既有值 overlay)
小工具(label 解析、檔名組裝、locale 判定、props 解析) 約 170 共用設施(同檔內用)

粗分約 殼 355 行/業務 975 行/工具 170 行。這個拆法是按方法區塊估的,不是逐行計數。

excel_template/ — 7 檔/1,656 行,整包零 jedi 引用

grep -rnE '^\s*(from|import) jedi_' app/module_frame/excel_template/ | wc -l → 0。 卡片要求實查確認,已確認。

檔 行 類別 依賴證據
generator.py 652 真業務 純 openpyxl,零 jedi、零 DB。建 9 張資料 sheet + 6 張隱藏 lookup sheet、header 黃底必填/灰底選填、DataValidation(inline enum 與 named range 兩種)。被三個模組外的地方 import:ssp_import_template_app_service、scripts/archive/regenerate_reference_templates.py、test/test_excel_template_generator_required_fill.py
sheet_definitions.py 617 真業務(亦是跨模組契約) 零 import(只有 dataclass/StrEnum)。定義 ColumnDef/SheetDef/LookupSource 與 8 張 sheet 的欄位規格。app/oscal/service/excel_parser/sheet_handlers.py:43 直接 import 這裡的 10 個 sheet 常數——匯出端與匯入端共用同一份欄位定義
header_i18n.py 176 真業務 零 import。欄位標題的 zh_Hant_TW/en 對照 dict(檔頭寫明「第一版 hardcode,後續整合 babel .po 時改 _resolve_header() 內部即可」)
lookup_builder.py 108 共用設施 host=1(引 sheet_definitions.LookupSource)。建隱藏 lookup sheet + 註冊 openpyxl DefinedName
data_validation_builder.py 54 共用設施 零 import。兩支 helper:build_enum_dv(inline,超 255 字元拋 EnumValuesTooLongError)、build_named_range_dv。app/auth/service/user_import_template_app_service.py:28 借用後者
styles.py 38 共用設施 只有 openpyxl style 常數。app/auth/…user_import_template_app_service.py:29 借用三個常數
__init__.py 11 共用設施 只有 docstring

dto/ 與 code/ — 8 檔/240 行

檔 行 類別 依賴證據
dto/module_frame_dto.py 70 共用設施 jedi=1(WorkflowTemplateDTO)/host=1。被 module_frame_service 用
dto/module_frame_reference_document_dto.py 57 共用設施 零 import dataclass
dto/module_frame_control_default_dto.py 49 共用設施 零 import;另被 test/test_api_module_frame_control_default_route.py:23 用
dto/module_frame_control_objective_default_dto.py 47 共用設施 同上
dto/module_frame_menu_dto.py 34 共用設施 host=1
code/module_frame_error_code.py 32 共用設施 jedi=1(BaseCode)。13 個 error code;檔頭記 2026-07-23 與 common/code/error_code.py 撞號 9 組後改本檔側新號
__init__.py/dto/__init__.py/code/__init__.py 7/0/0 共用設施 空或只有 docstring

domain/module_frame(12 檔/348 行)

檔 行 類別 依賴證據
entity/module_frame_entity.py 113 真業務 零 import。ModuleFrameEntity(24 個欄位)+四個 JOIN 用的輕量 summary entity(framework/framework_version/catalog_group/catalog_control/profile)。四個 summary entity 的存在理由:列表頁要顯示框架與 profile 資訊,但不想把整個 oscal entity 拖進來
service/module_frame_domain_service.py 76 真業務 jedi=3/host=5。9 支方法全是薄轉發到 repo,但 verify_module_frame_exists_by_uid 帶「不存在就 raise NotFound」的產品語意。被 8 處模組外注入:oscal_containers 8 處、flow_control_containers:339、domain/oscal/service/ssp_context_resolver.py:80
dto/module_frame_dto.py 49 共用設施 jedi=1/host=1。ModuleFrameDTO/WorkflowDTO/JobDTO/ActionDTO——這是 YAML 匯入用的中介型別,與 app/module_frame/dto/module_frame_dto.py 同名不同物(後者是 API response DTO)
entity/module_frame_query_filter.py 37 共用設施 零 import。ModuleFrameQueryEntity。⚠️ is_delete: Optional[int] = 0 是非-None 預設——不明確傳就永遠帶 is_delete=0 的 WHERE(這裡是刻意的:軟刪除預設過濾掉已刪列)
repository/module_frame.py 33 共用設施 jedi=3。IModuleFrameRepo 抽象介面,7 支 abstract method
entity/module_frame_menu_entity.py 23 共用設施 零 import
code/module_frame_action_type_enum.py 15 共用設施 零 import。ActionType(5 值)+ParserAdapterType(1 值)。ActionType 被 app/flow_engine/service/workflow_execution_service.py:12 用——本模組唯一被 flow_engine 使用的東西
repository/__init__.py/service/__init__.py 1/1 共用設施 一行
__init__.py/code/__init__.py/entity/__init__.py 0/0/0 共用設施 空

infra/module_frame(13 檔/415 行)

檔 行 類別 依賴證據
repository/module_frame_repo_impl.py 136 真業務 jedi=3/host=8。繼承 BaseRepositoryImpl;含 module_frames ↔︎ oscal 跨 schema 的 JOIN 查詢與多語系 failover
models/module_frame.py 91 真業務 jedi=3/host=1。ModuleFrame ORM,18 欄。含 scope(FR-042 租戶 scope)、template_ssp_id(FR-036 跨 schema soft-ref,刻意不建 FK constraint)、兩個 created/updated_user_name hybrid_property。被 infra/flow_control/repository/flow_control_project_repo_impl.py:19 import(跨模組唯一一處)
adapter/yaml_to_module_frame_parser_adapter.py 65 真業務 jedi=1/host=4。YAML → ModuleFrameDTO 的遞迴解析
mapper/module_frame_mapper.py 41 共用設施 host=2。entity ↔︎ model
models/module_frame_trans.py 29 真業務 jedi=1。多語系翻譯表 ORM,ondelete='CASCADE'
mapper/module_frame_menu_mapper.py 23 共用設施 host=2
adapter/parser_adapter_factory.py 17 共用設施 jedi=1/host=4。依 ParserAdapterType 回對應 adapter;DI 註冊為 Singleton(module_frame_containers.py:120),注入 module_frame_import_service(:129),實際被呼叫於 module_frame_import_service.py:254。⚠️ 工廠只有一個分支(YAML),非 YAML 一律拋 BadRequestError
ports/parser_port.py 9 共用設施 host=1。ParserPort ABC,單一 abstract method
models/__init__.py 4 共用設施 re-export 兩個 model
__init__.py/adapter/__init__.py/mapper/__init__.py/repository/__init__.py 0×4 共用設施 空

§4

③ 分類統計

分類單位是檔。混住兩種性質的檔(如 module_frame_party_service.py 是殼+業務、 module_frame_service.py 是業務+殼)按主體歸類,另在 ② 表的證據欄註明混住情形。

類別 檔數 佔比 行數 佔比
真業務 15 18.5% 5,563 50.8%
轉接殼 23 28.4% 3,831 35.0%
共用設施 43 53.1% 1,548 14.1%
死碼 0 0% 0 0%
還看不準 0 0% 0 0%
合計 81 100% 10,942 100%

共用設施佔過半檔數但只佔 14.1% 行數——因為裡面有 16 支空的或只有一兩行的 __init__.py,加上一批純 dataclass 的 DTO 與 mapper。看行數比看檔數準。

怎麼重跑統計

把 ② 表的每一列(檔路徑 → 類別)餵進下面這段,行數用 wc -l 現算:

# 逐檔行數(分類請照 ② 表)
git ls-files api/module_frame app/module_frame domain/module_frame infra/module_frame \
  | grep '\.py$' | while read f; do printf '%s\t%s\n' "$(wc -l < "$f"|tr -d ' ')" "$f"; done

行數合計必須等於 10,942,檔數必須等於 81;對不上就是分類有漏或重複。

按層分布:

層 真業務 轉接殼 共用設施
api/ 1 檔/162 行 14 檔/1,813 行 10 檔/788 行
app/ 8 檔/4,891 行 9 檔/2,018 行 14 檔/507 行
domain/ 2 檔/189 行 0 10 檔/159 行
infra/ 4 檔/321 行 0 9 檔/94 行

三件事從這張表看得出來:

  1. 殼全部集中在 api/ 與 app/ 兩層,domain/ 與 infra/ 零殼—— 代表這模組的「自持資料那一半」(兩張表的 entity/repo/model)是乾淨的主專案資產, 混住只發生在對外那兩層。
  2. app/ 一層就佔了真業務的 88%(4,891/5,563 行),其中光三支大檔 (ssp_import_template 1,500、template_import 1,008、import_service 415)就 2,923 行。
  3. api/ 的 14 支殼共 1,813 行是典型的 route 轉呼叫層,每支 56~281 行,沒有異常肥大的。

對 FR-102 那句「真業務與 v2 轉接殼約各半」的回答:以行數算是 真業務 5,563 / 轉接殼 3,831 = 1.45 : 1,真業務明顯多於殼,不是各半。 FR-102 該句是模組層級的粗估,方向(兩種都有、混住)對,比例要修正。


§5

④ 可以立刻收的

判準是「說得出證據才叫可收」。下面每一項都附了怎麼驗它真的沒人用。

4.1 兩個已無 route 註冊的 Resource 類別

api/module_frame/routes/module_frame_route.py 裡的 ModuleFramesRoute(第 44-62 行) 與 ModuleFrameStartRoute(第 113-131 行),以及 __init__.py:14 對它們的 import。

四層查證結果:

層 結果
route 註冊 ❌ __init__.py:88 與 :93 兩行 add_resource 皆被註解,註解標 [FR-038 DEAD-V1|2026-06-18 停用待清理]
模組內 import 只有 __init__.py:14 那一行(就是為了那兩行被註解的 add_resource 而留)
模組外 import 0(grep -rn "ModuleFramesRoute|ModuleFrameStartRoute" --include='*.py' . 只命中定義本身、__init__.py 那行 import、兩行註解,以及 docs/api/module-frame/generate_docx.py:40 的一張圖說文字)
DI 註冊 不適用(Resource 類別不進 DI)

怎麼驗:

grep -rn "ModuleFramesRoute\|ModuleFrameStartRoute" --include='*.py' . | grep -v __pycache__
grep -rn "/module-frames\b\|/module-frame/start" ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/

⚠️ 但它們背後的 service 方法不能一起收:

  • ModuleFramesRoute 吃的 get_module_frames_and_pager 是 ModuleFrameDomainService 也有的同名方法,而 domain 那支被 repo 層用著。
  • ModuleFrameStartRoute 吃的 start_project_from_module_frame(module_frame_service.py:226-265) 確實只有這個被註解的 route 在呼叫——但它裡面編排的「啟動專案 → 建摘要報告」是完整功能, 砍掉等於砍功能。列在此處只是標出「目前對外不可達」,不是建議砍。

4.2 兩支只在被註解的前端程式碼中出現的下載端點

GET /module-frame/download/template 與 GET /module-frame/download/yaml-template (module_frame_import_route.py 的 ModuleFrameTemplateDownloadRoute/ModuleFrameYamlTemplateDownloadRoute)。

四層查證結果:

層 結果
FE① api.js 常數 ✅ 有定義(MODULE_FRAME_TEMPLATE_DOWNLOAD:154、MODULE_FRAME_YAML_TEMPLATE_DOWNLOAD:155)
FE② 實際呼叫 ⚠️ 只出現在被註解掉的函式裡:ModuleFrame.vue:668(// const downloadExcelTemplate = () => {)與 :672(// const downloadYamlTemplate)
③ ui_routes 不適用(不是選單)
④ 非瀏覽器 0 命中(grep -rn "download/template|download/yaml-template" bin/ scripts/ 與 e2e repo 皆空)

同組還有 POST /module-frame/import/template(ModuleFrameImportTemplateRoute), FE 的 importFromExcel() 整支被註解(ModuleFrame.vue:737)。 注意這三支是同一組:下載 Excel 範本 → 填 → 上傳匯入,前端把整條流程註解掉了。 但同檔的 import/verify/import/import/yaml 三支仍在用(:1019/:1050/:847), 所以 ModuleFrameImportService 這支 service 不可整支收。

怎麼驗:

cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
grep -rn "MODULE_FRAME_TEMPLATE_DOWNLOAD\|MODULE_FRAME_YAML_TEMPLATE_DOWNLOAD\|MODULE_FRAME_IMPORT_TEMPLATE\b" src/
# 三處命中全部前綴 "//"

⚠️ 這兩支 route 讀的是 GLOBAL_DOWNLOAD_DIR 下的 audit_frame_template_v1.xlsx/.yaml 兩個隨版檔案;收 route 時要一併確認那兩個檔還有沒有別的用途。

4.3 沒有找到的東西(如實記錄)

卡片點名的兩個「可以立刻收」候選,實查後都不成立:

(a) 六支 v2 殼與 oscal 五支「兩份真相」——不成立。 oscal 那五支直接 import module_frame 這邊的對應表,不是各寫一份:

oscal 的檔 從 module_frame import 了什麼
ssp_components_app_service.py:28,31 ModuleFrameComponentsService as _CompMap、_build_props、build_leveraged_title_map、enrich_leveraged_titles
ssp_inventory_items_app_service.py:27 ModuleFrameInventoryService as _InvMap、_build_props、_build_implemented
ssp_leveraged_app_service.py:37 ModuleFrameLeveragedService as _LaMap、_NIL_PARTY_UUID、_build_props、_parse_date
ssp_party_app_service.py:43,158,164 _build_props、_to_email_list
ssp_system_characteristic_app_service.py:24 ModuleFrameSystemCharacteristicService as _ScMap

五支的檔頭都明寫「與資源庫範本版完全相同……故直接共用其純 mapping 靜態方法, 確保兩端一致、單一真相。差別只在 ssp_id 從哪來 + 權限」。 module_frame 這邊是定義處,oscal 那邊是引用者,收不掉也不該收。

第六支 module_frame_ssp_resources_service.py 方向相反——它 import app/oscal/service/ssp_resources_context_service.py,共用的實作在 oscal 那邊。

另外 app/flow_control/service/assessment_plan_app_service.py:57 也 import module_frame_party_service 的 _build_props 與 _to_email_list——三個模組共用同一份對應表。

(b) module_frame_template_import_service.py(1,008 行)與 app/oscal/service/ssp_control_impl_import_service.py(431 行)是不是重複實作——部分重複,但不是可立刻收的。

實查逐項對照:

項目 MF 端 oscal 端 是否相同
Excel 9 欄標題 EXPECTED_HEADERS(:88-98) HEADER_LABELS(:47-57) 逐字相同(兩份各寫一份)
VALID_STATUSES 六值 :78-86 :43-46 逐字相同(兩份)
樣式常數(字型/填色/框線/對齊) :101-112 :59-70 逐字相同(兩份)
props 名(implementation-status/-description) 自己定義 _P_STATUS/_P_DESC(:56-57) 從 ssp_control_implementation_service import(:35-39) 值相同、來源不同
控制項母體 profile → catalog(_load_profile_controls) ssp.import_profile_id → catalog 路徑不同
寫入落點 樣板 SSP 的 IR/statement living SSP,且 delegate 給 SspControlImplementationService 不同
檔案大小上限 無 MAX_FILE_SIZE = 10MB(:71) MF 端缺這道

結論:可抽出共用的是「Excel 欄位契約與樣式」那約 40 行常數(三組逐字相同), 不是整支 service。而那 40 行要抽到哪裡、抽了會不會又變成 module_frame 與 oscal 互相 import 的第三條線,屬設計問題——列進 ⑤。

(c) 沒有死碼。 81 支全部通過四層查(模組內 import/模組外 import/DI 註冊/route 註冊)。 最接近的是 4.1 那兩個 Resource 類別,但它們在 __init__.py 有 import, 不符「四層全零」的死碼定義,故歸「疑似死碼」記錄而非死碼。


§6

⑤ 要設計才能動的

只說「為什麼現在動不了、需要什麼前置」,不給完整方案。

5.1 Excel 欄位契約散在三處,抽出來會製造新的跨模組依賴

現況:SSP 控制項現況說明的 Excel 格式(9 欄標題、6 個狀態值、配色邊框) 在 module_frame_template_import_service.py(資源庫範本端)與 app/oscal/service/ssp_control_impl_import_service.py(專案端)各寫一份逐字相同的常數。 第三處是 app/module_frame/excel_template/sheet_definitions.py——它定義的是另一套 (SSP 匯入的九張表),被 app/oscal/service/excel_parser/sheet_handlers.py import。

為什麼現在動不了:抽到哪裡沒有現成答案。抽進 common/ 要問「Excel 欄位標題算不算跨模組共用型別」; 留在 module_frame 讓 oscal import,會讓 oscal → module_frame 的依賴再多一條 (目前已有六條:五支 service mapping + sheet_definitions); 抽進 jedi_oscal_v2 套件則要問「中文欄位標題是不是套件該知道的事」—— CLAUDE.md 對套件的判準是「屬套件 domain 的才進套件」,中文標題像是產品知識。

需要的前置:先確定 module_frame 與 oscal 這兩個模組的最終疆界(誰是誰的上游), 再決定共用常數往哪邊放。這件事等 oscal 的逐檔分類結果(FR-105 另一支產出)。

5.2 六支 v2 殼「殼裡混著業務」,拆不拆會影響套件契約 → 需要決策者裁

現況:六支殼每一支都不是純轉呼叫,都帶了主專案獨有的規則(見 ② 表的「殼帶主專案規則」欄)。 以 module_frame_leveraged_service.py 為例,它做三件事:

  1. 欄位對應(FE 名 ↔︎ OSCAL props)— 這是殼該做的
  2. NN 欄空值哨兵(party_uuid 空落 NIL-UUID、date_authorized 空落 date.today())— 這是在繞套件 schema 的 NOT NULL 約束
  3. 同名去重、暱稱 enrich — 這是產品規則

第 2 項是關鍵:檔內註解寫「套件 import_ssp 用 party-uuid or "" 對 uuid 欄是 latent bug (無 party-uuid 的 SSP 匯入會 500),但屬 import 路徑、本期非 scope」。 也就是說主專案這一側在替套件的設計缺口補位。

兩種合理判法,後果不同:

A 案:維持現狀(哨兵留主專案) B 案:把 NN 約束的問題退回套件解
做法 六支殼繼續各自帶哨兵邏輯 套件把 party_uuid/date_authorized 改成 nullable,主專案拿掉哨兵
好處 不動套件,零風險 主專案的殼真正變薄;套件 import_ssp 那個 latent bug 一起解掉
代價 哨兵是「看不見的約定」——寫入時塞 NIL-UUID、讀出時 reshape 回 None,兩邊必須配對。任何第三個寫入者(例如 docx 匯入)不知道這個約定就會寫出前端看得到 NIL-UUID 的資料 動 jedi_oscal_v2 的 schema=動 DB 欄位約束=要 migration,且 oscal-v2 有其他 consumer

這題不自選,列出來問決策者。 判斷需要的資訊:jedi_oscal_v2 除了本專案還有沒有別的 consumer、以及決策者對「主專案替套件補位」的容忍度。

5.3 module_frame_service.py 是模組內依賴最雜的一支,拆之前要先決定 BPMN 歸誰

現況:304 行、jedi=11/host=9。它同時碰四個外部套件 (jedi_flow_engine 的 workflow template 與 execution、jedi_task_platform 的 participant、 jedi_common 的分頁與 session)與三個主專案模組 (app/flow_engine、app/project_summary_report、domain/module_frame)。

裡面 96 行的 clone_module_frame 是自寫的 BPMN 深拷貝 (複製主流程 → 逐個複製子流程 → 把主流程 XML 裡的 called_element_id 全換成新 id)。 module_frame_item_service.py 的 219 行也幾乎全是 BPMN 增刪改。

為什麼現在動不了:「資源庫範本要不要綁一條 BPMN 流程」這件事本身 正在 FR-104(流程疆界重新分析)被重新檢視。 在那邊有結論之前,動這兩支 BPMN 邏輯等於押注一個還沒定的方向。

需要的前置:FR-104 的結論。

5.4 di_containers/oscal/oscal_containers.py 註冊了六支 module_frame 的 service

oscal_containers.py:57,60,63,66,69,88 註冊了 module_frame_system_characteristic_service、 components、inventory、leveraged、party、ssp_resources 六支, 而它們的 route(在 api/module_frame/)也是從 Containers.oscal_container 取注入。

這不是錯——這六支殼的資料落點在 oscal.ssp_* 表,DI 依落點分組有其道理。 但它造成「module_frame 的 route 從 oscal 的 container 取 service」這個反直覺的接線, 新人照 module_frame_containers.py 找會找不到。

為什麼現在動不了:移動 DI 註冊會同時改到 oscal 那側的 wiring, 而 DI 簽章漂移是靜默的(Factory 是 lazy,對不上只在該端點被打時才炸)。 要動需要把六支的端點逐一實打驗過。

需要的前置:oscal 的逐檔分類,確認 oscal 那側對這六支的依賴全貌。


§7

對 FR-102 總表 B 該列的逐句核對

FR-102 README.md:101 對 module_frame 的證據欄共 5 句。逐句核對結果:

FR-102 原句 核對結果
「自己持有 module_frames/module_frames_trans 兩張表」 ✅ 成立。infra/module_frame/models/ 恰兩支 ORM,__init__.py re-export 兩個
「最大兩支全自寫(ssp_import_template_app_service.py 1,500 行、module_frame_template_import_service.py 1,008 行)」 ⚠️ 行數成立,「全自寫」要修正。1,500 行那支 jedi=18(從四個套件讀資料),說「全自寫」會讓人以為零套件依賴;準確說法是「主體邏輯自寫、資料來源在套件」。1,008 行那支 jedi=4 且只用到兩個 entity 型別,「全自寫」對它成立
「excel_template/generator.py(652 行)零 jedi 引用」 ✅ 成立且可再強化。不只該檔,excel_template/ 整包 7 檔 1,656 行零 jedi 引用(實跑 grep -rnE '^\s*(from|import) jedi_' → 0)
「module_frame_party_service.py(323 行)檔頭自陳……主體是 FE 欄位 ↔︎ OSCAL props 對應表」 ✅ 成立。檔頭第 24-38 行就是那張對照表
「components/inventory/leveraged 同型」 ✅ 成立,且應補上 system_characteristic 與 ssp_resources——同型的是六支不是三支(前五支同型;ssp_resources 略不同,它委派 oscal 的 context service)
總表 A 的「對外 route 45」 ✅ 成立(本棒 AST 重跑得 45)。派工卡上轉述的「43」是筆誤
需另開卡欄「真業務與 v2 轉接殼約各半」 ⚠️ 方向成立,比例要修正。實際行數比是真業務 50.8%/轉接殼 35.0%/共用設施 14.1%,真業務約為殼的 1.45 倍,不是各半

沒有任何一句已失效;兩句需要精確化(「全自寫」與「約各半」),一句需要補充(同型是六支)。


§8

需要決策者裁的

  1. 六支 v2 殼裡的「NN 欄空值哨兵」要不要退回套件解(§5.2 的 A/B 兩案)。 關鍵未知:jedi_oscal_v2 除本專案外還有沒有別的 consumer。

  2. Excel 欄位契約那約 40 行逐字重複的常數要抽到哪裡(§5.1)。 三個候選落點各有代價,且與「module_frame 和 oscal 誰是誰的上游」這個更大的疆界問題綁在一起。

  3. §4.1 與 §4.2 那五個對外不可達的端點要不要清(兩個被註解的 Resource + 三個前端只在註解中呼叫的下載/匯入端點)。 證據已備齊(四層查證結果見該節),但「清」屬異動、本棒只分析不動程式。