實查日期:2026-09-16 | 當時 HEAD:98d756bb | branch:feature/review
🔴 本表是某一天的快照,引用前先重驗。 每個數字下方都附了產生它的指令,重跑一次比相信這份表安全。
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 這邊的對應表來用,這邊才是唯一的定義處。
| 目錄 | 檔數 | 行數 |
|---|---|---|
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)。 兩個數字都對,差的是數法不是程式碼,比對時要用同一種。
用 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 |
四層=① 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 負責讀寫。這是本模組「殼」那一半的由來。
jedi_* 引用少或零;刪掉這支功能就消失。@transaction → 呼叫套件 service → 欄位對應」,商業規則在套件。 殼不等於可刪——它是「主專案回答套件問題」的必要接線。要另外標「有沒有帶主專案獨有規則」。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 行)| 檔 | 行 | 類別 | 依賴證據 | 殼帶主專案規則? |
|---|---|---|---|---|
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 行。這個拆法是按方法區塊估的,不是逐行計數。
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/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 | 共用設施 | 空 |
分類單位是檔。混住兩種性質的檔(如 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 行 |
三件事從這張表看得出來:
api/ 與 app/ 兩層,domain/ 與 infra/ 零殼—— 代表這模組的「自持資料那一半」(兩張表的 entity/repo/model)是乾淨的主專案資產, 混住只發生在對外那兩層。app/ 一層就佔了真業務的 88%(4,891/5,563 行),其中光三支大檔 (ssp_import_template 1,500、template_import 1,008、import_service 415)就 2,923 行。api/ 的 14 支殼共 1,813 行是典型的 route 轉呼叫層,每支 56~281 行,沒有異常肥大的。對 FR-102 那句「真業務與 v2 轉接殼約各半」的回答:以行數算是 真業務 5,563 / 轉接殼 3,831 = 1.45 : 1,真業務明顯多於殼,不是各半。 FR-102 該句是模組層級的粗估,方向(兩種都有、混住)對,比例要修正。
判準是「說得出證據才叫可收」。下面每一項都附了怎麼驗它真的沒人用。
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 在呼叫——但它裡面編排的「啟動專案 → 建摘要報告」是完整功能, 砍掉等於砍功能。列在此處只是標出「目前對外不可達」,不是建議砍。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 時要一併確認那兩個檔還有沒有別的用途。
卡片點名的兩個「可以立刻收」候選,實查後都不成立:
(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, 不符「四層全零」的死碼定義,故歸「疑似死碼」記錄而非死碼。
只說「為什麼現在動不了、需要什麼前置」,不給完整方案。
現況: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 另一支產出)。
現況:六支殼每一支都不是純轉呼叫,都帶了主專案獨有的規則(見 ② 表的「殼帶主專案規則」欄)。 以 module_frame_leveraged_service.py 為例,它做三件事:
party_uuid 空落 NIL-UUID、date_authorized 空落 date.today())— 這是在繞套件 schema 的 NOT NULL 約束第 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、以及決策者對「主專案替套件補位」的容忍度。
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 的結論。
di_containers/oscal/oscal_containers.py 註冊了六支 module_frame 的 serviceoscal_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 那側對這六支的依賴全貌。
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 倍,不是各半 |
沒有任何一句已失效;兩句需要精確化(「全自寫」與「約各半」),一句需要補充(同型是六支)。
六支 v2 殼裡的「NN 欄空值哨兵」要不要退回套件解(§5.2 的 A/B 兩案)。 關鍵未知:jedi_oscal_v2 除本專案外還有沒有別的 consumer。
Excel 欄位契約那約 40 行逐字重複的常數要抽到哪裡(§5.1)。 三個候選落點各有代價,且與「module_frame 和 oscal 誰是誰的上游」這個更大的疆界問題綁在一起。
§4.1 與 §4.2 那五個對外不可達的端點要不要清(兩個被註解的 Resource + 三個前端只在註解中呼叫的下載/匯入端點)。 證據已備齊(四層查證結果見該節),但「清」屬異動、本棒只分析不動程式。