實查日期:2026-09-16 | 當時 HEAD:84b8426a | branch:feature/review
🔴 本表是某一天的快照,引用前先重驗。 每個數字下方都附了產生它的指令,重跑一次比相信這份表安全。
oscal 是主專案最大的一塊自有資產,不是套件的殼:163 支檔案裡, 11,494 行(57.5%)是主專案自己寫的業務(Word/Excel 解析、兩份文件比對、 CMMC 格式轉換、匯入管線),4,757 行(23.8%)是轉接殼(把前端的欄位名翻成 jedi_oscal_v2 套件的形狀、再交給套件存),其餘 3,619 行(18.1%)是序列化器、 DTO、mapper 這類接線與型別。
真業務是殼的 2.4 倍——比 module_frame 那邊的 1.45 倍更懸殊。原因是這個模組 獨佔了整個「把客戶的既有文件吃進系統」那條線:domain/oscal/parser/(2,027 行) 與 app/oscal/service/import_diff/(1,384 行)合計 3,411 行零套件引用, 套件完全不參與。
殼的部分則高度集中在兩處:框架維護四支(framework_*,1,101 行, 主體是 v1 形狀 ↔︎ v2 形狀的來回翻譯)與 SSP 子物件五支(ssp_party /components /inventory_items/leveraged/system_characteristic,714 行)。
| 目錄 | 檔數 | 行數 |
|---|---|---|
api/oscal |
67 | 3,984 |
app/oscal |
46 | 10,964 |
domain/oscal |
36 | 4,549 |
infra/oscal |
14 | 499 |
| 合計 | 163 | 19,996 |
另有 di_containers/oscal/oscal_containers.py(509 行,不在四層內,判 DI 註冊時要看)。
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git rev-parse --short HEAD
for L in api app domain infra; do
d=$L/oscal
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__ 與未入版控的殘留。 行數一律 wc -l(數換行符),與 FR-102 總表 A 的 Python 數法在本模組恰好同值 (19,996),module_frame 那邊則差 9 行。
用 AST 數(不是 regex——add_resource( 常換行寫,regex 會漏抓)。 61 條全部在 api/oscal/__init__.py 統一註冊,掛 blueprint oscal、前綴 /api/1.0。 注意:所有 route 註冊都寫在 __init__.py,各 route 檔本身只定義 Resource 類別。
另有 3 條被註解掉不計入(grep -rn "^\s*#.*add_resource" api/oscal/):
| 被註解的 route | 註解說明 |
|---|---|
OscalExportRoute → /oscal/export/<doc_type>/<uid> |
無停用註解,連 import 那行一起被註解(__init__.py:88-89) |
SspScopedExcelImportRoute → /ssp/<ssp_uid>/excel-import/<parse_uid> |
[FR-038 DEAD-V1|2026-06-18 停用待清理] scoped excel 只用 upload,預覽走非 scoped 端點 |
SspScopedExcelImportConfirmRoute → …/confirm |
同上 |
ui_routes 全庫只有 1 列與本模組相關—— id=33, name='compliance-framework-manage', url='/compliance-framework/compliance-framework-manage', enable=1。 它是選單項不是 API,管的是「左側選單看不看得到合規框架管理」這一件事; 61 條 API route 不會逐條出現在 ui_routes 裡。SSP 那批(/ssp/*,佔 61 條的 40 條) 更是完全不經選單——它們掛在專案頁面內的分頁裡(/project/projects/:id/...), 由專案選單進入。因此下表不設第三層欄位,在此統一交代。
PGPASSWORD=<查 .env> psql -h localhost -p 5432 -U cmmgr -d guidant_ai_dev \
-c "SELECT id, name, url, enable FROM public.ui_routes
WHERE name ILIKE '%framework%' OR name ILIKE '%ssp%' OR name ILIKE '%oscal%';"四層=① 前端 API 常數表 src/config/api/api.js ② 前端實際呼叫(.vue/service/*.js) ③ DB public.ui_routes(見上段)④ 非瀏覽器呼叫者(scripts/、e2e)。
| # | route | 吃哪支 service | ①常數 | ②呼叫 | ④非瀏覽器 | 判定 |
|---|---|---|---|---|---|---|
| 1-2 | POST /oscal-frameworks、GET /oscal-frameworks/menu |
FrameworkAppService |
OSCAL_FRAMEWORKS(_MENU) |
✅ | — | 活 |
| 3-4 | POST /oscal-framework、GET/PUT/DELETE /oscal-framework/<uid> |
同上 | OSCAL_FRAMEWORK |
✅ | — | 活 |
| 5 | POST /oscal-framework-versions |
FrameworkVersionAppService |
OSCAL_FRAMEWORK_VERSIONS |
✅ | e2e compliance-framework-import-version.steps.js:171 |
活 |
| 6-7 | GET /oscal-framework-version/download/oscal/<uid>[/token] |
同上 | OSCAL_FRAMEWORK_VERSION_DOWNLOAD |
✅ signedDownloadUtil.js:73-78 |
— | 活(簽章下載兩段式) |
| 8 | GET/PUT/DELETE /oscal-framework-version/<uid>、POST /oscal-framework-version |
同上 | OSCAL_FRAMEWORK_VERSION |
✅ | — | 活 |
| 9 | GET /oscal-framework-version/<uid>/catalog-tree |
FrameworkVersionEditService |
…CATALOG_TREE |
✅ 9 處 | — | 活 |
| 10-12 | PUT/DELETE /oscal-catalog-group|control|control-assessment/<uid> |
同上 | OSCAL_CATALOG_* 三個 |
✅ FrameworkVersionEditService.js |
— | 活 |
| 13-16 | /oscal-framework-parse-jobs、/parse、/<uid>、/<uid>/confirm |
FrameworkParseJobService |
OSCAL_FRAMEWORK_PARSE_JOBS |
✅ 5 處 | e2e compliance-framework-import-version.steps.js:50,155,164 |
活 |
| # | route | ①常數 | ②呼叫 | ④非瀏覽器 | 判定 |
|---|---|---|---|---|---|
| 17 | POST /oscal/resource-libraries/list |
RESOURCE_LIBRARIES_LIST |
✅ ModuleFrame.vue:453 |
e2e module-frame.steps.js:70 |
活 |
| 18 | POST /oscal/resource-libraries |
RESOURCE_LIBRARY_CREATE |
✅ ModuleFrame.vue:773、SspDocxImportPage.vue:718 |
e2e resource-library-factory.js:53 |
活 |
| 19 | GET /oscal/resource-library/<uid> |
RESOURCE_LIBRARY(有定義) |
❌ 全前端零呼叫 | ❌ | 疑似死 — 見 ④ 4.2 |
| 20 | POST /oscal/resource-library/<uid>/publish |
同上(同一常數) | ❌ 全前端零呼叫 | ❌ | 疑似死 — 見 ④ 4.2 |
| # | route | 吃哪支 service | ①常數 | ②呼叫 | ④非瀏覽器 | 判定 |
|---|---|---|---|---|---|---|
| 21-23 | /ssp-excel-imports/parse、/ssp-excel-import/<uid>[/confirm] |
SspExcelImportAppService |
SSP_EXCEL_IMPORT(_PARSE) |
✅ | e2e module-frame-import.steps.js:242,269,286 |
活 |
| 24-26 | /ssp-docx-imports/parse、/ssp-docx-import/<uid>[/confirm] |
SspDocxImportAppService |
SSP_DOCX_IMPORT(_PARSE) |
✅ | scripts/smoke_test_ssp_docx_parser.py、scripts/e2e_test_module_frame_with_docx.py、e2e project-ssp-import-docx.steps.js:58 |
活 |
| 27 | POST /ssp/<ssp_uid>/excel-import/upload |
同 Excel | SSP_EXCEL_IMPORT_SSP |
✅ SspExcelImportService.js:56 |
— | 活 |
| # | route | 吃哪支 service | ①常數 | ②呼叫 | ④非瀏覽器 | 判定 |
|---|---|---|---|---|---|---|
| 28 | GET/PUT /ssp/<uid>/system-characteristic |
SspSystemCharacteristicAppService |
SSP_SYSTEM_CHARACTERISTIC |
✅ 9 處 | e2e project-ssp-basic.steps.js:81,90 |
活 |
| 29-30 | /ssp/<uid>/components[/<item_uid>] |
SspComponentsAppService |
SSP_COMPONENTS |
✅ | e2e ssp-helpers.js:54 |
活 |
| 31-32 | /ssp/<uid>/inventory-items[/<item_uid>] |
SspInventoryItemsAppService |
SSP_INVENTORY_ITEMS |
✅ | e2e ssp-helpers.js:56 |
活 |
| 33-34 | /ssp/<uid>/leveraged[/<item_uid>] |
SspLeveragedAppService |
SSP_LEVERAGED(_ITEM) |
✅ 20 處 | e2e ssp-helpers.js:55 |
活 |
| 35-36 | /ssp/<uid>/parties[/<party_uid>] |
SspPartyAppService |
SSP_PARTIES/SSP_PARTY |
✅ 21 處 | e2e ssp-helpers.js:53 |
活 |
| 37-39 | /ssp/<uid>/ssp-resources、/items[/<item_uid>] |
SspResourcesAppService → SspResourcesContextService |
SSP_RESOURCES/SSP_RESOURCE_ITEM(S) |
✅ 22 處 | — | 活 |
| # | route | 吃哪支 service | ①常數 | ②呼叫 | ④非瀏覽器 | 判定 |
|---|---|---|---|---|---|---|
| 40-41 | /ssp/<uid>/document-pool[/<doc_uid>] |
SspDocumentPoolService |
SSP_DOCUMENT_POOL |
✅ 12 處 | — | 活 |
| 42-45 | control/objective 各一組 document-mapping(s) 四條 |
同上 | SSP_DOCUMENT_MAPPING(S)/SSP_OBJECTIVE_MAPPING(S) |
✅ | — | 活 |
| 46 | GET /ssp/<uid>/control-tree |
SspControlImplementationService |
SSP_CONTROL_TREE |
✅ 22 處 | e2e agent-task-management.steps.js:49、my-tasks-detection.steps.js:35、project-task-agent-dispatch.steps.js:37 |
活 |
| 47 | GET /ssp/<uid>/control-implementations |
同上 | SSP_CONTROL_IMPLEMENTATIONS |
✅ SspService.js:11 |
— | 活 |
| 48 | GET/PUT /ssp/<uid>/control-implementation/<ci> |
同上 | SSP_CONTROL_IMPLEMENTATION |
✅ | — | 活 |
| 49 | PUT …/objective/<si> |
同上 | SSP_OBJECTIVE |
✅ | — | 活 |
| 50 | DELETE …/objective/<si>/document/<file_id> |
同上 | SSP_OBJECTIVE_DOCUMENT |
✅ | — | 活 |
| 51-54 | control/objective 各一組 reference-document(s) 四條 |
同上 | SSP_REFERENCE_DOCUMENT(S) |
✅ | — | 活 |
| 55-58 | /ssp/<uid>/control-implementations/export、/import、/import/validate、/import/confirm |
SspControlImplImportService |
SSP_EXPORT/SSP_IMPORT(_VALIDATE/_CONFIRM) |
✅ SspService.js:90-94 等 6 處 |
— | 活 |
| # | route | 吃哪支 service | ①常數 | ②呼叫 | ④非瀏覽器 | 判定 |
|---|---|---|---|---|---|---|
| 59 | GET /ssp/<uid>/export |
SspExportAppService |
SSP_DOCUMENT_EXPORT |
✅ ProjectPlanningView.vue:2238 |
e2e planning-batch-ops.steps.js:211 |
活 |
| 60 | GET /module-frame/<mf_uid>/template-ssp |
ResourceLibraryAppService |
拼 MODULE_FRAME |
✅ ModuleFrameTemplateService.js:23 |
— | 活 |
| 61 | GET /oscal/catalog-controls/<uid>/assessments(BE 無此 route) |
— | CATALOG_CONTROL_ASSESSMENTS |
⚠️ 前端有打(ModuleFrameTemplateService.js:198) |
— | 前端打空氣 — 見 ⑤ 5.4 |
第 61 列不是 BE 的 route(AST 只數到 60 條掛在 oscal blueprint 上、加上第 59 的
/ssp/<uid>/export共 61)。列在此是因為前端確實會打這個路徑,而 BE 全 repo 沒有 任何地方註冊它——屬於前端打得到但後端不存在的缺口,詳見 ⑤。
compliance.framework_parse_jobs、compliance.ssp_docx_parse_jobs、 compliance.ssp_excel_parse_jobs 三張匯入工作表,ORM 在 infra/oscal/model/。 模組所有其他資料——框架、版本、catalog、profile、SSP 本體與所有子物件—— 都不在主專案的表,而是由 jedi_oscal_v2 持有。這是本模組「殼」那一半的由來: 三張 job 表記的是「這份檔案解析到哪、使用者選了什麼」,解析完寫進套件就結束。
jedi_* 引用少或零;刪掉這支功能就消失。@transaction → 呼叫套件 service → 欄位對應」,商業規則在套件。 殼不等於可刪——它是「主專案回答套件問題」的必要接線。要另外標「有沒有帶主專案獨有規則」。api/oscal(67 檔/3,984 行)| 檔 | 行 | 類別 | 依賴證據 | 殼帶主專案規則? |
|---|---|---|---|---|
__init__.py |
207 | 共用設施 | blueprint 組裝+全模組 61 條 route 的唯一註冊處;19 個 host import(全是自己模組的 Resource 類別),jedi=0。零 DB、零業務 | — |
routes/ssp/ssp_control_implementation_route.py |
309 | 轉接殼 | 10 個 Resource 類別(控制樹/IR CRUD/AO/程序書關聯)全轉呼叫 SspControlImplementationService;jedi=2、host=4 |
⚠️ 守門不在 route——全部落在 app service 的 SspPermissionChecker(資源域守門,需先 resolve ssp_uid 才知道判誰) |
routes/ssp/ssp_document_pool_route.py |
183 | 轉接殼 | 6 個 Resource 轉呼叫 SspDocumentPoolService;jedi=2 |
⚠️ 同上 |
routes/framework/framework_parse_job_route.py |
178 | 轉接殼 | 4 個 Resource 轉呼叫 FrameworkParseJobService;multipart 檔案接收在 route |
⚠️ 守門在 service(require_platform_admin) |
routes/framework/oscal_framework_version_route.py |
157 | 轉接殼 | 轉呼叫 FrameworkVersionAppService+一支簽章下載(OscalExportAppService.export('catalog',…) 回裸 OSCAL JSON) |
✅ 帶 — 唯一在 route 層 import common.authz 的檔;簽章下載 token 兩段式是主專案機制 |
routes/framework/framework_version_edit_route.py |
132 | 轉接殼 | 4 個 Resource 轉呼叫 FrameworkVersionEditService |
⚠️ 守門在 service |
routes/ssp/ssp_scoped_excel_import_route.py |
130 | 轉接殼 | 3 個 Resource,其中 2 個已無 route 註冊(__init__.py:116-117 被註解,標 FR-038 DEAD-V1) |
⚠️ 守門在 service |
routes/ssp/ssp_control_impl_import_route.py |
114 | 轉接殼 | 4 個 Resource 轉呼叫 SspControlImplImportService;blob 回傳走 send_file |
⚠️ 守門在 service |
routes/ssp/ssp_excel_import_route.py |
112 | 轉接殼 | 3 個 Resource 轉呼叫 SspExcelImportAppService |
⚠️ 守門在 service |
routes/ssp/ssp_docx_import_route.py |
105 | 轉接殼 | 3 個 Resource 轉呼叫 SspDocxImportAppService |
⚠️ 守門在 service |
routes/framework/oscal_framework_route.py |
104 | 轉接殼 | 4 個 Resource 轉呼叫 FrameworkAppService;marshmallow dump |
⚠️ 守門在 service |
routes/ssp/ssp_resources_route.py |
88 | 轉接殼 | 3 個 Resource 轉呼叫 SspResourcesAppService |
⚠️ 守門在 service |
routes/ssp/ssp_components_route.py |
85 | 轉接殼 | 2 個 Resource 轉呼叫 SspComponentsAppService |
⚠️ 守門在 service |
routes/resource_library_route.py |
84 | 轉接殼 | 4 個 Resource 轉呼叫 ResourceLibraryAppService。ResourceLibraryRoute(詳情)與 ResourceLibraryPublishRoute(發布)兩支已註冊但前端零呼叫——見 ④ 4.2 |
✅ 帶 — @require_platform_admin_route 掛在 publish;建立端點有 12 行註解說明為何刻意不掛(建立只寫租戶私有庫,公版治理靠 publish 守住) |
routes/ssp/ssp_inventory_items_route.py |
84 | 轉接殼 | 2 個 Resource 轉呼叫 SspInventoryItemsAppService |
⚠️ 守門在 service |
routes/ssp/ssp_party_route.py |
84 | 轉接殼 | 2 個 Resource 轉呼叫 SspPartyAppService+marshmallow serializer |
⚠️ 守門在 service |
routes/ssp/ssp_leveraged_route.py |
78 | 轉接殼 | 2 個 Resource 轉呼叫 SspLeveragedAppService |
⚠️ 守門在 service |
routes/ssp/ssp_export_route.py |
72 | 轉接殼 | 1 個 Resource 轉呼叫 SspExportAppService;blob 回傳 |
✅ 帶 — RFC 5987 中文檔名編碼(與 module_frame 的 mf_ssp_export_route 同法) |
routes/ssp/ssp_system_characteristic_route.py |
51 | 轉接殼 | 1 個 Resource 轉呼叫 SspSystemCharacteristicAppService |
⚠️ 守門在 service |
routes/oscal_export_route.py |
39 | 🔴 死碼 | 定義 OscalExportRoute。四層全零:route 註冊被註解(__init__.py:89)、模組內 import 那行也一起被註解(:88)、模組外零 import、不進 DI。詳見 ④ 4.1 |
— |
routes/__init__.py/routes/framework/__init__.py/routes/ssp/__init__.py |
0×3 | 共用設施 | 空 | — |
這模組的守門位置與
module_frame相反,是刻意的。module_frame的 route 幾乎每支 都掛@require_capability(...)(主體域守門:只看「你是誰」);oscal的 SSP 那批 route 一個也沒掛——因為它們是資源域守門:要先把ssp_uid反查成專案、才知道 要判這個人在哪個專案裡的角色。route 層表達不了,所以統一落在 app service 的SspPermissionChecker。這符合common/authz/的雙軌設計(CLAUDE.md 授權守門段)。
除下列四支外,其餘 43 支全是 共用設施(純 marshmallow Schema,零業務、零 DB; oscal_common/ 下 19 支另被 api/project/serializers/project.py 與 api/module_frame/serializers/module_frame.py 借用,是跨模組的欄位契約)。
| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
serializers/ssp/ssp.py |
29 | 🔴 死碼 | SystemSecurityPlanResponseSchema 只被 serializers/__init__.py:12 re-export,全 repo 無其他命中(含字串式 Nested)。詳見 ④ 4.3 |
serializers/ssp/ssp_system_characteristic.py |
18 | 🔴 死碼 | SystemCharacteristicResponseSchema 只被上面那支死 schema 用字串式 Nested 引用(ssp.py:19)+__init__ re-export。⚠️ 與活的 ssp_system_characteristic_inproject.py 的 SspSystemCharacteristicResponseSchema 名字只差 Ssp 前綴,清理時極易誤刪 |
serializers/ssp/ssp_system_implementation.py |
17 | 🔴 死碼 | 同上(ssp.py:20 字串式引用+__init__ re-export) |
serializers/framework/oscal_framework_menu.py |
12 | 🔴 死碼 | OscalFrameworkMenuResponseSchema 只被 __init__ re-export。route 用的是同名不同物的 OscalFrameworkMenuSchema(定義在 oscal_framework.py:70,無 Response 三字) |
serializers/framework/oscal_framework_version_menu.py |
11 | 🔴 死碼 | 只被上面那支死 schema 與自己(children 遞迴)引用+__init__ re-export |
serializers/framework/oscal_framework.py |
165 | 共用設施 | 6 個 Schema,含 route 實際在用的 OscalFrameworkMenuSchema/OscalFrameworkPageQueryResponse/OscalFrameworkResponseSchema |
serializers/ssp/ssp_docx_import.py |
185 | 共用設施 | 12 個 Schema(匯入決策/預覽/confirm 請求回應),被 ssp_docx_import_route 用 |
serializers/ssp/ssp_excel_import.py |
137 | 共用設施 | 7 個 Schema,被 ssp_excel_import_route 用 |
serializers/framework_parse_job/framework_parse_job_schemas.py |
201 | 共用設施 | 被 framework_parse_job_route 用 |
| (其餘 38 支 serializer) | — | 共用設施 | 純 Schema/空 __init__ |
⚠️ 這五支死 schema 用 grep import 抓不到、也不能只看 grep 命中數。 它們互相用 marshmallow 的字串式
fields.Nested("X")引用,形成一個封閉的小圈:ssp.py引SystemCharacteristic/SystemImplementation,oscal_framework_menu引oscal_framework_version_menu,而圈外沒有任何人引用這個圈。守衛測試test/test_module_boundaries.py:2028-2042把這幾行列為字串式Nested白名單, 理由寫的是「本卡範圍外未動」——那是說明沒清,不是說明它們是活的。
app/oscal(46 檔/10,964 行)| 檔 | 行 | 類別 | 依賴證據 | 殼帶主專案規則? |
|---|---|---|---|---|
ssp_control_implementation_service.py |
1,004 | 真業務(殼+大量業務混住,見下方拆解) | jedi=19/host=4。19 個 jedi import 多數是讀資料的來源(v2 IR/statement/catalog、jedi_compliance_audit 的輪次與 AP、jedi_flow_engine 的 job)。單一方法 build_control_tree_by_ssp_id 就 400 行(408-807),做的是「把 catalog 控制樹 + 已填值 + 本輪任務狀態 + 上一輪結果」四份資料揉成前端要的一棵樹 |
✅ 帶 — SspPermissionChecker 守門;輪次凍結範圍(_frozen_round_control_scope)與「規劃期重驗」(_is_planning_stage_reverify)是產品的稽核輪次語意,套件不知道 |
ssp_excel_import_app_service.py |
937 | 真業務 | jedi=5/host=8。26 支方法,主體是自寫的匯入編排:上傳→解析→比對→預覽→確認四階段,含 TTL 逾期清理、參與方鉤稽、決策合併、內容覆寫。套件只在最後一步收寫入 | ✅ 帶 — ① 三種落點分流(資源庫範本/專案 SSP/新建資源庫)② 專案角色守門(_require_project_manager/_require_project_participant)③ 檔案 TTL 與暫存清理 |
ssp_docx_import_app_service.py |
787 | 真業務 | jedi=2/host=8。與上一支同形(四階段匯入編排),多一段「依框架挑 adapter 跑格式轉換」(_run_framework_adapter)與 docx 修訂記號正規化(_normalize_docx_revisions,54 行) |
✅ 帶 — 同上三條,另加 CMMC 格式判定 |
framework_parse_job_service.py |
674 | 真業務(殼+業務混住) | jedi=14/host=4。parse(112 行)與 confirm(118 行)是自寫編排,_apply_decisions_and_overrides(83 行)處理使用者逐項決策,_persist_catalog(54 行)才把結果交給套件 |
✅ 帶 — ① require_platform_admin 守門 ② _effective_tenant_id(多租戶落點判定)③ _guard_version_replaceable(版本可否覆寫的產品規則) |
resource_library_app_service.py |
541 | 真業務(非純殼——卡片問的那支) | jedi=11/host=2。看行數分布就知道不是殼:create_resource_library 單一方法 104 行,做的是「clone catalog → 依規則建 profile → 建空範本 SSP → 寫 link record」四段編排;_build_catalog_tree(65 行)與 update_applicable_controls(60 行)是自寫的樹狀組裝與差異更新;_init_ao_workflows(38 行)掛預設流程範本。套件提供的是 clone/CRUD 原語,編排與資源庫這個概念本身是主專案的 |
✅ 帶 — ① 資源庫的 link record 存主專案的 compliance.module_frames(原生 SQL,_MF 常數)② 暱稱 enrich(enrich_audit_nicknames)③ _fmt_dt(本 route 直接回 dict 未套 Schema,須自行格式化時間,否則回 GMT 格式) |
ssp_control_impl_import_service.py |
431 | 真業務 | jedi=3/host=3。openpyxl 手刻 9 欄 Excel(建 workbook、套配色邊框、下拉、隱藏 A 欄)+解析+逐列驗證+批次 upsert。與 module_frame_template_import_service.py 共用同一份欄位契約——見 ⑤ 5.1 |
— |
framework_version_edit_service.py |
351 | 轉接殼 | jedi=14/host=2、零主專案業務 import。檔頭 12 行就是 v1↔︎v2 欄位對照表(control.description ↔︎ statement part prose、group.description ↔︎ v2 無此欄讀 None 寫忽略、uid ↔︎ str(id))。主體是「收 FE 的 v1 形狀 → 翻成 v2 → 呼叫套件」 |
✅ 帶兩條 — ① 雙軌守門:publish_status != 'draft' 拒絕、catalog 被 profile 引用時不允許刪 ② require_platform_admin |
ssp_document_pool_service.py |
313 | 轉接殼+業務 | jedi=8/host=3。程序書池本體落 jedi_compliance_audit 的 ssp_reference_documents,控制項/AO 關聯的 context_id 要反查 v2 的 IR.id/statement.id |
✅ 帶 — ① SspPermissionChecker ② IR/statement 不存在時的三種行為(寫入自動建、讀取回 []、刪除回 False)是產品的 UX 判斷 ③ context_type 的 control_implementation/objective 慣例 |
framework_app_service.py |
247 | 轉接殼 | jedi=7/host=2。檔頭自陳「補套件沒有的主專案職責——分頁、filter、main_version 字串解析、建框架時順手建首版」。模組層級的 apply_sorts_in_memory(在 Python 端做多欄排序,因為套件 query 不支援跨欄位 OR 搜尋)被 framework_version_app_service.py:26 借用 |
✅ 帶 — require_platform_admin;分頁/排序/搜尋是主專案的 API 契約 |
ssp_resources_context_service.py |
230 | 轉接殼+業務 | jedi=3/host=1。落 v2 ssp_components,以 type 區分 hardware/system。兩個入口共用這一支:專案走 SspResourcesAppService、資源庫範本走 module_frame 的 ModuleFrameSspResourcesService |
✅ 帶 — ① FR-032 軟參照欄(device_id/system_characteristic_id/responsible_party 走 props)② 讀取時 enrich 設備/資訊系統主檔名稱 ③ sys_impl_main_id/ensure_sys_impl_main_id 是刻意保留的 no-op 簽章,讓兩個入口不必改 |
framework_version_app_service.py |
229 | 轉接殼 | jedi=5/host=3。檔頭自陳「對外對齊舊 v1 版本管理 response shape(FE 零改動)」。版本樹(parent/children/is_root)在本層用 Python 算,避免動套件 repo 的 NULL 過濾 | ✅ 帶 — require_platform_admin;v1 shape 相容是主專案對 FE 的契約 |
ssp_inventory_items_app_service.py |
194 | 轉接殼+業務 | jedi=4/host=6。直接 import module_frame 的 ModuleFrameInventoryService 當欄位對應表(_InvMap/_build_props/_build_implemented) |
🔴 帶 — FR-032 master-snapshot:picker 帶 ref_id 時由後端從主檔撈明細寫入(不信前端傳值、單向凍結),free input 才原樣存。檔頭寫明這是相對範本版的差異——範本可以 store-as-is,專案是租戶實際資料不行 |
ssp_party_app_service.py |
169 | 轉接殼 | jedi=5/host=3。共用 module_frame 的 ModuleFramePartyService 純 mapping 靜態方法。自己只做「metadata_id 從哪來+權限」 |
✅ 帶 — SspPermissionChecker;metadata_id 解析(party 在 v2 掛 SSP 的 metadata 而非 SSP 本體,get_ssp 只吃 uuid 故繞 list_ssps 取 root) |
ssp_components_app_service.py |
142 | 轉接殼 | jedi=4/host=5。同上,共用 ModuleFrameComponentsService 的 _to_dict/_build_props |
✅ 帶 — SspPermissionChecker;uid(OSCAL uuid)↔︎ int id 的對外識別轉換 |
ssp_leveraged_app_service.py |
141 | 轉接殼 | jedi=4/host=5。同上,共用 ModuleFrameLeveragedService |
🔴 帶 — 繼承範本版的空值哨兵:party_uuid(uuid 非空欄)空值落全零 UUID、date_authorized(非空欄)空值落今天,讀回時換成 None。這是主專案替套件的設計缺口補位——見 ⑤ 5.2 |
ssp_system_characteristic_app_service.py |
68 | 轉接殼(薄) | jedi=3/host=3。共用 ModuleFrameSystemCharacteristicService 的 mapping |
✅ 帶 — 空 SC 時 GET 回 placeholder、PUT 首次自動建立 |
ssp_resources_app_service.py |
52 | 轉接殼(薄,真的只轉呼叫) | jedi=2/host=2。全部委派 SspResourcesContextService,自己只做 ssp_uid → ssp_id 反查與守門 |
✅ 帶 — SspPermissionChecker |
oscal_export_app_service.py |
47 | 轉接殼 | jedi=2/host=1。接套件 OscalIoService.export_oscal。注意它註冊在 flow_control_container 不是 oscal_container(flow_control_containers.py:230)。使用者是 oscal_framework_version_route.py:138(框架簽章下載),不是被註解掉的 OscalExportRoute |
⚠️ 帶一點 — 本期權限只有 jwt 認證,檔頭自陳 per-project 授權是 follow-up |
service/__init__.py |
0 | 共用設施 | 空 | — |
ssp_control_implementation_service.py 的殼段/業務段拆解這支 1,004 行、19 個套件引用,是全模組最像「殼」卻最不是殼的一支。按方法區塊估算:
| 區段 | 約行數 | 性質 |
|---|---|---|
build_control_tree_by_ssp_id(408-807) |
400 | 業務——四份資料(catalog 樹/已填值/本輪任務/上輪結果)揉成前端的樹 |
_resolve_inscope_catalog(160-223)+_frozen_round_control_scope(295-333)+_prev_round_context(359-405) |
155 | 業務——稽核輪次的控制項範圍判定,是產品的輪次模型 |
build_classifier_catalog_by_ssp_id(237-292) |
56 | 業務——給證據分類器用的控制項母體 |
CRUD 三支(get/update/list_control_implementations) |
57 | 殼——轉呼叫套件 IR CRUD |
程序書關聯六支(add_reference_documents 等) |
47 | 殼——全部一行轉呼叫 SspDocumentPoolService |
get-or-create 與 props 解析小工具(_ir_for_control/_get_or_create_*/_props_to_map 等 9 支) |
82 | 共用設施(同檔內用,另 _P_DESC/_P_STATUS/_props_to_map 三個被 ssp_control_impl_import_service.py:34-38 借用) |
粗分約 業務 611 行/殼 104 行/工具 82 行,其餘是 import 與類別骨架。 這個拆法是按方法區塊估的,不是逐行計數。
grep -rnE '^\s*(from|import) jedi_' app/oscal/service/import_diff/ | wc -l # → 0| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
ssp_diff_service.py |
751 | 真業務 | 零 jedi、零 DB、無狀態。7 個區段(控制項說明/逐 AO/參與方/元件/外部授權/資產清冊/系統特性)× 4 種差異狀態(未變/已改/新增/消失)的比對演算法+智慧預設表。呼叫端自己餵資料進來 |
decision_merge.py |
318 | 真業務 | 零 import(只有標準庫)。把使用者逐項選的「用新的/保留現況/略過」摺成一份最終 OSCAL dict。關鍵設計是「保留現況=省略不寫」——套件的 update 模式不刪缺漏的列、也不把省略欄位設成 null,所以省略剛好等於保留 |
snapshot_views.py |
315 | 真業務 | 零 import。把套件產的 OSCAL 快照 dict 重塑成比對器要的「現況側」視圖,鍵用的是自然身分(參與方 name/email、元件 title+type、控制項 control-id) |
import_diff/__init__.py |
0 | 共用設施 | 空 |
| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
_common.py |
620 | 真業務 | 零 import(只有標準庫)。Excel 與 Word 兩條匯入線共用的 OSCAL 組裝器——系統特性/元件/外部授權/資產清冊/SoA props。核心設計:OSCAL 要求實作說明掛在 by-component 上、by-component 又要指向一個 component,故每份匯入的 SSP 都合成一個標準的 this-system component 當錨點 |
docx_to_oscal_ssp.py |
152 | 真業務 | host=1(引 _common)、jedi=0。Word 專屬的參與方與控制項實作抽取 |
excel_to_oscal_ssp.py |
144 | 真業務 | host=1、jedi=0。Excel 專屬的同上 |
party_match_enrich.py |
78 | 共用設施 | jedi=1。把比對到的 matched_user_id/matched_org_unit_id 補上暱稱與帳號(CLAUDE.md 審計欄位規範)。批次查避 N+1、失敗靜默略過(預覽不可因此爆) |
v2_candidate_loader.py |
54 | 轉接殼 | host=1、jedi=0(但實際跑時吃注入的套件 service)。從 v2 catalog 撈控制項候選清單餵給 Word 解析器 |
import_adapter/__init__.py |
9 | 共用設施 | re-export |
| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
sheet_handlers.py |
428 | 真業務 | jedi=0、host=4。9 張 sheet 的逐列解析。:43 直接 import module_frame 的 excel_template/sheet_definitions.py 的 10 個 sheet 常數——匯出端定義、匯入端引用,單一真相 |
v2_bundle.py |
217 | 真業務 | 零 import。把 Excel 解析出的舊鍵(devices/info_systems/leveraged)換成新的 OSCAL 鍵(components/inventory_items/leveraged_authorizations)。缺這層匯入確認會直接 412 |
parser.py |
153 | 真業務 | jedi=1、host=4。openpyxl 入口,逐 sheet 分派給 handler |
types.py |
111 | 共用設施 | 零 import。ParsedExcel/ValidationError 兩個 dataclass |
validators.py |
66 | 真業務 | host=1。逐列驗證器,含「參與方角色四值」與「勾選欄的 14 種真值寫法」(含 是、Y、1) |
version_check.py |
63 | 真業務 | 零 import。樣板版本相容性檢查。檔內 20 行註解說明為何拒絕 v2.x 與 v1.x:sheet 名與欄位鍵全不同,硬讀會雙重錯位、資料全錯,強制使用者重下樣板是唯一安全路徑 |
excel_parser/__init__.py |
9 | 共用設施 | re-export |
| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
ssp_docx_generator.py |
258 | 真業務 | 零 jedi、host=2。docxtpl + python-docx 排版:渲染靜態段落 → 存檔 → 重開 → 逐控制項動態 append(標題三 + AO 表格 + 實作說明)→ 附錄。範本檔 app/oscal/templates/ssp/ssp_cmmc_template.docx 進版控,由 scripts/generate_ssp_docx_template.py 產生 |
ssp_v2_content_loader.py |
217 | 轉接殼 | jedi=1(實際跑吃注入的套件 service)、host=1。六個資料來源全部向套件拿(系統特性/人員/受評標的/外部服務/控制實作/控制標題),組成 SspExportDataModel |
ssp_export_model.py |
169 | 共用設施 | 零 import。純 dataclass,讓 generator 不必知道資料來源差異 |
ssp_libreoffice_converter.py |
124 | 真業務 | 零 import(只有標準庫)。LibreOffice headless 子行程包裝(docx → pdf/odt),含三平台 binary 名稱解析與暫存檔清理 |
ssp_export_app_service.py |
115 | 轉接殼 | jedi=2、host=3。統一入口,協調 loader + generator + converter |
export/__init__.py |
0 | 共用設施 | 空 |
| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
dto/ssp/ssp_reference_document_dto.py |
29 | 共用設施 | jedi=1。被程序書池 service 與 route 用 |
dto/__init__.py/dto/ssp/__init__.py/app/oscal/__init__.py |
0/0/7 | 共用設施 | 空或只有 docstring |
domain/oscal(36 檔/4,549 行)grep -rnE '^\s*(from|import) jedi_' domain/oscal/parser/ | wc -l # → 0| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
docx_section_extractors.py |
892 | 真業務 | 零 import(只有 python-docx)。依「章節標題 + 表格欄位標題」雙錨點在 Word 裡定位特定表格並抽出欄位。回純 dict/list,不回 OSCAL 型別,讓各框架的轉換器自由組裝 |
docx_parser_core.py |
555 | 真業務 | 零 jedi、host=4。Word 檔的通用結構解析(段落/表格依文件順序走訪)+控制項識別碼比對 |
ssp_intermediate.py |
369 | 共用設施 | 零 jedi、host=2。OSCAL 對齊的中間層 dataclass(ParsedSsp/ParsedParty/ParsedComponent 等)。被 7 處模組內引用,是整條匯入線的共同語言。命名一律用 OSCAL 術語、不用 CMMC 專有名詞,框架專屬屬性統一塞 framework_specific_props |
control_id_matcher.py |
120 | 真業務 | 零 jedi、host=1。控制項識別碼的樣式比對與模糊比對(rapidfuzz)。檔頭記 FR-069 P0-② 把它從 common/util/ 搬來的理由:它是 OSCAL 專屬業務知識,放 common/ 會造成 common/ → domain.oscal 的反向依賴 |
docx_intermediate.py |
72 | 共用設施 | 零 import。ParsedDocx/CandidateControl 等 dataclass,被 5 處引用 |
framework_patterns.py |
19 | 真業務 | 零 import。四種框架(CMMC L1/L2、NIST 800-171、ISO 27001)的控制項識別碼正規式。CMMC 那條同時吃 1.0 與 2.0 兩種寫法,因為實際客戶文件常混用 |
parser/__init__.py |
0 | 共用設施 | 空 |
| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
cmmc_ssp_adapter.py |
860 | 真業務 | 零 jedi、host=4。把框架無關的 ParsedDocx 轉成 OSCAL 對齊的 ParsedSsp。整支是 CMMC 的文件慣例知識:封面表格哪一格是版本、五個固定角色的順序、哪個角色對應「單位」哪些對應「人員」(含一條反直覺的:Information Provider 字面是上游廠商,但實際填的是窗口聯絡人,要對使用者不對單位) |
i_ssp_docx_adapter.py |
78 | 共用設施 | 零 jedi、host=2。轉換器的抽象介面 |
adapter_registry.py |
21 | 共用設施 | host=1。框架代號 → 轉換器的查表。設計理由:加 ISO 27001 只要在 DI 加一行註冊,不必動 app service |
adapter/__init__.py |
0 | 共用設施 | 空 |
| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
ssp_project_resolver.py |
184 | 共用設施(跨模組) | jedi=3、host=1。ssp_uid → SSP/專案的反查鏈+寫入階段判定。被 common/authz/ssp/SspPermissionChecker 用(授權軸的資源解析),屬全專案共用的守門基礎設施 |
ssp_context_resolver.py |
158 | 共用設施 | jedi=1、host=2。判斷一份 SSP 屬專案還是資源庫範本,讓同一個端點雙端共用。走 profile 反查而非 SSP.template_module_frame_id——檔頭寫明後者雖有欄位但從沒接上、DB 該欄 100% 是 null |
framework_parse_job_domain_service.py |
123 | 真業務 | 零 jedi、host=2。框架解析工作的 domain service(自持表) |
reconciliation/base.py |
107 | 共用設施 | host=1。比對演算法的抽象基底(三段回退:完全相符 → 正規化後相符 → 模糊相符),子類只實作 5 個掛鉤 |
reconciliation/person_reconciler.py |
101 | 真業務 | jedi=1、host=4。解析出的人員 ↔︎ 系統使用者的比對 |
reconciliation/organization_reconciler.py |
101 | 真業務 | jedi=1、host=4。解析出的單位 ↔︎ 系統組織單位的比對 |
ssp_docx_parse_job_domain_service.py |
71 | 真業務 | 零 jedi、host=2。自持表的 domain service |
ssp_excel_parse_job_domain_service.py |
71 | 真業務 | 同上 |
reconciliation/_normalizers.py |
51 | 共用設施 | 零 import。字串正規化 helper |
party_reconciliation_service.py |
30 | 共用設施 | host=1。門面:依 party_type 把混合清單分派給兩個比對器。檔頭寫明簽章 100% 不變、既有呼叫端零行變動 |
reconciliation/__init__.py |
18 | 共用設施 | host=2。只 re-export 不 import ssp_intermediate 的那兩支——檔頭寫明 re-export 具體比對器會觸發循環 import,呼叫端與 DI 必須從各自模組路徑 import |
reconciliation/match_method.py |
10 | 共用設施 | 零 import。比對方式列舉,被 7 處引用 |
service/__init__.py |
0 | 共用設施 | 空 |
| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
entity/framework_parse_job_entity.py |
76 | 共用設施 | 零 import。entity + query entity,被 5 處引用 |
entity/ssp_docx_parse_job_entity.py |
70 | 共用設施 | 同上,4 處 |
entity/ssp_excel_parse_job_entity.py |
70 | 共用設施 | 同上,4 處 |
import_pipeline/bundle_restore.py |
208 | 真業務 | 零 jedi、host=2。確認階段的 dict ↔︎ dataclass 還原。只支援新版形狀,舊版拋 LegacyConfirmRequired 讓呼叫端掉回舊路徑 |
repository/i_framework_parse_job_repo.py |
39 | 共用設施 | host=1。抽象介面 |
repository/i_ssp_docx_parse_job_repo.py/i_ssp_excel_parse_job_repo.py |
25/25 | 共用設施 | 同上 |
repository/i_ssp_catalog_title_query.py |
24 | 共用設施 | 零 import。控制標題查詢的抽象介面,被 module_frame 那側也用到(同一份 v2 catalog 鏈) |
entity/__init__.py/repository/__init__.py/import_pipeline/__init__.py/domain/oscal/__init__.py |
0/0/1/0 | 共用設施 | 空 |
infra/oscal(14 檔/499 行)| 檔 | 行 | 類別 | 依賴證據 |
|---|---|---|---|
repository/ssp_catalog_title_query.py |
93 | 真業務 | jedi=4、host=1。跨疆界唯讀查詢:ssp_id → import_profile_id → source_catalog_id → 控制標題與 AO 清單。檔頭記舊版走已退役的 AP 鏈、且 from jedi_oscal.infra.model.ap... 會把 v1 套件拉進啟動圖撞 v2 同名表直接炸 create_app |
repository/framework_parse_job_repo_impl.py |
91 | 真業務 | jedi=1、host=4。繼承 BaseRepositoryImpl |
repository/ssp_excel_parse_job_repo_impl.py |
56 | 真業務 | 同上 |
repository/ssp_docx_parse_job_repo_impl.py |
54 | 真業務 | 同上 |
mapper/framework_parse_job_mapper.py |
39 | 共用設施 | host=2。entity ↔︎ model |
model/framework_parse_job.py |
36 | 真業務 | jedi=2。ORM,含 TenantScopedMixinModel(多租戶) |
mapper/ssp_docx_parse_job_mapper.py/ssp_excel_parse_job_mapper.py |
35/35 | 共用設施 | host=2 各 |
model/ssp_docx_parse_job.py/ssp_excel_parse_job.py |
30/30 | 真業務 | jedi=2 各 |
__init__.py×4(infra/oscal/mapper/model/repository) |
0×4 | 共用設施 | 空 |
分類單位是檔。混住兩種性質的檔(如 ssp_control_implementation_service.py 是業務+殼、 ssp_document_pool_service.py 是殼+業務)按主體歸類,另在 ② 表的證據欄註明混住情形。
| 類別 | 檔數 | 佔比 | 行數 | 佔比 |
|---|---|---|---|---|
| 真業務 | 37 | 22.7% | 11,494 | 57.5% |
| 轉接殼 | 34 | 20.9% | 4,757 | 23.8% |
| 共用設施 | 86 | 52.8% | 3,619 | 18.1% |
| 死碼 | 6 | 3.7% | 126 | 0.6% |
| 還看不準 | 0 | 0% | 0 | 0% |
| 合計 | 163 | 100% | 19,996 | 100% |
共用設施佔過半檔數但只佔 18.1% 行數——裡面有 22 支空的或只有一兩行的
__init__.py, 加上 43 支純 marshmallow Schema 與一批 dataclass。看行數比看檔數準。
把 ② 表的每一列(檔路徑 → 類別)餵進下面這段,行數用 wc -l 現算:
git ls-files api/oscal app/oscal domain/oscal infra/oscal \
| grep '\.py$' | while read f; do printf '%s\t%s\n' "$(wc -l < "$f"|tr -d ' ')" "$f"; done行數合計必須等於 19,996,檔數必須等於 163;對不上就是分類有漏或重複。
按層分布:
| 層 | 真業務 | 轉接殼 | 共用設施 | 死碼 |
|---|---|---|---|---|
api/ |
0 | 19 檔/2,188 行 | 42 檔/1,670 行 | 6 檔/126 行 |
app/ |
19 檔/7,983 行 | 15 檔/2,569 行 | 12 檔/412 行 | 0 |
domain/ |
11 檔/3,121 行 | 0 | 25 檔/1,428 行 | 0 |
infra/ |
7 檔/390 行 | 0 | 7 檔/109 行 | 0 |
四件事從這張表看得出來:
api/ 一支真業務都沒有,全是殼與序列化器。這是乾淨的形狀——route 層不該有業務。 對照 module_frame 那邊 api/ 還有一支 162 行的真業務(route 層自己用 pandas 讀 Excel 比對欄位標題), oscal 這邊沒有同類問題。domain/ 3,121 行真業務、零殼——這是全模組最值錢的一塊:Word 解析(2,027)、 CMMC 轉換(860)、比對器(202)、匯入還原(208)。套件完全不參與。app/ 與 api/(4,757 行的 100%),與 module_frame 同形。api/,且全是序列化器與一支被註解掉的 route,沒有一支在 app/ 以下。對 FR-102 總表 B 該列(README.md:100)逐句核對:
| FR-102 原句 | 核對結果 |
|---|---|
「domain/oscal 4,549 行是真 domain 資產(docx 解析 892 + CMMC adapter 860 + parser core 555)」 |
成立,三個數字逐一重跑相符。但「4,549 行全是真 domain 資產」需精確化:實查 3,121 行是真業務,另 1,428 行是 dataclass/抽象介面/resolver 這類共用設施 |
「import_diff/ssp_diff_service.py(751 行)零個 jedi_oscal_v2 引用、全自寫」 |
成立,且整包 import_diff/ 4 檔 1,384 行全都零套件引用(比原句涵蓋更廣) |
「infra/oscal 自己持有三張匯入 job 表」 |
成立。三張表的 repo 全都有寫入者也有讀取者(卡片問的「只寫不讀/只讀不寫孤兒」不成立):三支 domain service 各被對應的 app service 注入使用 |
「framework_app_service.py(247 行)幾乎每支就一行轉呼叫套件,檔頭自陳補套件沒有的主專案職責——分頁、filter」 |
成立。另補:同檔的 apply_sorts_in_memory 被 framework_version_app_service 借用,是兩支殼共用的排序工具 |
| 「真業務與轉接殼混住同一 service 目錄,模組層級單一標籤蓋不住」 | 成立,這正是本表存在的理由 |
FR-102 對本模組的五句證據沒有一句已失效,兩句需要精確化。
判準是「說得出證據才叫可收」。下面每一項都附了怎麼驗它真的沒人用。 本卡只分析不動程式,列在此處是備妥證據,清不清屬異動、要決策者裁。
api/oscal/routes/oscal_export_route.py 整檔(定義 OscalExportRoute)。
四層查證結果:
| 層 | 結果 |
|---|---|
| route 註冊 | ❌ __init__.py:89 被註解 |
| 模組內 import | ❌ __init__.py:88 那行 import 也一起被註解 |
| 模組外 import | ❌ 0(grep -rn "OscalExportRoute" --include='*.py' . 只命中定義本身、兩行註解,以及三處提到它名字的文字說明) |
| DI 註冊 | 不適用(Resource 類別不進 DI) |
怎麼驗:
grep -rn "OscalExportRoute" --include='*.py' . | grep -v __pycache__
grep -rn "oscal/export/" ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/
grep -rn "oscal/export/" ~/Projects/Billows/Audit-Manager/compliance-manager-test/site-regression/⚠️ 但它背後的 OscalExportAppService(47 行)是活的,不能一起收—— api/oscal/routes/framework/oscal_framework_version_route.py:138 用它做框架的 OSCAL 下載 (export('catalog', catalog_uid, fmt='json')),走 flow_control_container 注入。 另外三處註解文字(ssp_export_app_service.py:6,113、oscal_containers.py:406) 寫著「json/yaml/xml 匯出已移至 OscalExportRoute」——那個目的地其實是關著的, 這三行說明與現況不符。
GET /oscal/resource-library/<uid>(詳情)與 POST /oscal/resource-library/<uid>/publish(發布), 對應 resource_library_route.py 的 ResourceLibraryRoute(67-73 行)與 ResourceLibraryPublishRoute(76-84 行)。
四層查證結果:
| 層 | 結果 |
|---|---|
FE① api.js 常數 |
✅ 有定義(RESOURCE_LIBRARY:145,註解寫「+ /<uid> [GET 詳情] [+ /publish POST]」) |
| FE② 實際呼叫 | ❌ 全前端零命中——grep -rn "API.RESOURCE_LIBRARY\b" src/ 回 0 行,grep -rn "resource-library" src/ 只命中三行註解文字 |
③ ui_routes |
不適用(不是選單) |
| ④ 非瀏覽器 | ❌ 0——e2e step code 零命中(只在 docs/test-cases/module-frame.md 的文字說明裡被提到)、scripts/ 與 bin/ 零命中 |
怎麼驗:
cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
grep -rn "RESOURCE_LIBRARY\b" src/ | grep -v "config/api/api.js"
grep -rn "resource-library/" src/
cd ~/Projects/Billows/Audit-Manager/compliance-manager-test
grep -rn "resource-library/" site-regression/steps/ site-regression/support/ site-regression/pages/⚠️ 兩支背後的 service 方法不能一起收:
get_resource_library(resource_library_app_service.py:205)被六處後端內部呼叫 (ssp_excel_import_app_service.py:367,464,817、ssp_docx_import_app_service.py:165,337,454,603), 是匯入線驗證「這個資源庫存不存在」的手段。publish_resource_library(:533)只有這個端點呼叫,收掉端點等於收掉整個發布功能。 「發布」是資源庫從租戶私有變成系統公版的唯一途徑(建立端點刻意不掛平台管理員守門, 就是因為治理靠發布這關擋)。列在此處只是標出「目前對外不可達」,不是建議砍。| 檔 | 行 | 類別名 |
|---|---|---|
api/oscal/serializers/ssp/ssp.py |
29 | SystemSecurityPlanResponseSchema |
api/oscal/serializers/ssp/ssp_system_characteristic.py |
18 | SystemCharacteristicResponseSchema |
api/oscal/serializers/ssp/ssp_system_implementation.py |
17 | SystemImplementationResponseSchema |
api/oscal/serializers/framework/oscal_framework_menu.py |
12 | OscalFrameworkMenuResponseSchema |
api/oscal/serializers/framework/oscal_framework_version_menu.py |
11 | OscalFrameworkVersionMenuResponseSchema |
以及 api/oscal/serializers/__init__.py:10-14 對它們的五行 re-export。
四層查證結果:
| 層 | 結果 |
|---|---|
| route 使用 | ❌ 0——沒有任何 route 的 @marshal_with 或 .dump() 用到這五個類別 |
| 模組內 import | 只有 serializers/__init__.py 的 re-export,以及圈內互相的字串式 Nested |
| 模組外 import | ❌ 0(唯一一處 from api.oscal.serializers import ... 是 oscal_framework_version_route.py:11,拿的是 OscalFrameworkVersionResponseSchema,不在這五支裡) |
| DI 註冊 | 不適用 |
怎麼驗(必須連字串式 Nested 一起查,只 grep import 會漏):
for c in SystemSecurityPlanResponseSchema SystemCharacteristicResponseSchema \
SystemImplementationResponseSchema OscalFrameworkMenuResponseSchema \
OscalFrameworkVersionMenuResponseSchema; do
echo "--- $c"
grep -rn "\b$c\b" --include='*.py' . | grep -v __pycache__ | grep -v '^./docs/'
done每一支的命中應該只有三種:自己的 class 定義、serializers/__init__.py 的 re-export、 圈內另一支的字串式 Nested。有第四種就表示它是活的。
🔴 兩個清理時會踩的坑:
名字只差三個字母。 活的是 ssp_system_characteristic_inproject.py 的 SspSystemCharacteristicResponseSchema(有 Ssp 前綴,被 ssp_system_characteristic_route.py:14,23,38 用);死的是 ssp_system_characteristic.py 的 SystemCharacteristicResponseSchema(無前綴)。 同理框架選單:活的是 oscal_framework.py:70 的 OscalFrameworkMenuSchema(無 Response), 死的是 oscal_framework_menu.py 的 OscalFrameworkMenuResponseSchema(有 Response)。 grep 時不加 \b 邊界會兩邊一起命中,直接誤判。
守衛測試的白名單不是「它是活的」的證明。 test/test_module_boundaries.py:2028-2042 把這幾支的字串式 Nested 行列在白名單,理由寫「本卡範圍外未動」——那是 FR-092 第 18 棒的作業範圍註記。真要清,這幾行白名單也要一起移除,否則守衛會 指向不存在的檔案行號。
以下曾被懷疑、實查後確認不可收,列在此處避免下一個人重查:
infra/oscal 三張匯入工作表的 repo:三支都有寫入者也有讀取者(各自的 domain service 被對應的 app service 注入),沒有孤兒。ssp_scoped_excel_import_route.py 的兩個被註解類別:雖然 route 註冊被註解,但 同檔的 SspScopedExcelImportUploadRoute 仍註冊且前端在用(SspExcelImportService.js:56), 整檔不可收,只有那兩個類別可議——而它們的 service 方法是共用的,收了也省不到行數。app/oscal/service/oscal_export_app_service.py:見 4.1 的警告,框架 OSCAL 下載在用。app/oscal/service/ssp_control_impl_import_service.py(431 行)與 app/module_frame/service/module_frame_template_import_service.py(1,008 行) 各自定義了同一份 Excel 樣板的欄位契約。實查逐項比對結果:
| 常數 | 比對結果 |
|---|---|
| 九欄中文標題 | 逐字相同,但變數名不同:oscal 叫 HEADER_LABELS、module_frame 叫 EXPECTED_HEADERS |
六個狀態值(VALID_STATUSES) |
值相同、排版不同(一個寫成三行、一個寫成六行) |
| 四個配色常數(標題字/標題底/唯讀底/可編輯底) | 逐字相同 |
換行對齊(WRAP_ALIGNMENT) |
逐字相同 |
邊框(THIN_BORDER) |
逐字相同 |
兩個 props 名(_P_STATUS/_P_DESC) |
oscal 側不自己定義,直接 import 自 ssp_control_implementation_service;module_frame 側自己寫一份 |
MAX_FILE_SIZE(10 MB) |
只有 oscal 側有 |
逐字重複的約 25 行(module_frame 那側的檔頭第 3 行明寫「格式對齊 app/oscal/service/ssp_control_impl_import_service.py」)。
為什麼現在動不了:三個候選落點各有代價,且與「這兩個模組誰是誰的上游」這個更大的 疆界問題綁在一起——
common/:兩邊都只是引用者,但「Excel 樣板的欄位契約」不是通用工具, 放 common/ 會讓 common/ 開始長業務知識。oscal → module_frame(v2 子物件 的欄位對應表、Excel sheet 定義都在 module_frame 那側定義),照這個方向應該是 module_frame 定義、oscal 引用。但 _P_STATUS/_P_DESC 的方向恰好相反 (module_frame 自己寫一份,oscal 從自己模組內 import)——兩個方向並存。需要什麼前置:先定「module_frame 與 oscal 誰是誰的上游」。目前跨模組 import 共 9 處、方向全是 oscal → module_frame(app/oscal 六支引用 app/module_frame 的 service 與 excel_template/sheet_definitions),反向只有 3 處 (module_frame 的 ssp-resources service、兩支 serializer、一支匯出 route)。 方向已經很清楚是 module_frame 在上游,但這與直覺相反(oscal 比較大、比較底層), 需要決策者確認這是刻意的還是長歪的。
卡片問「五支 ssp_*_app_service 與 module_frame 六支殼是否鏡像」。實查結論: 不是兩份真相,是同一份——五支全都直接 import module_frame 那側的對應表:
| oscal 側 | 行 | import 了 module_frame 的什麼 |
|---|---|---|
ssp_inventory_items_app_service.py |
194 | ModuleFrameInventoryService as _InvMap、_build_props、_build_implemented |
ssp_party_app_service.py |
169 | ModuleFramePartyService(另在方法內兩處延遲 import 避循環) |
ssp_components_app_service.py |
142 | ModuleFrameComponentsService as _CompMap、_to_dict、_build_props |
ssp_leveraged_app_service.py |
141 | ModuleFrameLeveragedService |
ssp_system_characteristic_app_service.py |
68 | ModuleFrameSystemCharacteristicService as _ScMap |
五支的檔頭都明寫「欄位 mapping 與資源庫範本版完全相同……故直接共用其純 mapping 靜態方法,確保兩端一致、單一真相。差別只在『ssp_id 從哪來 + 權限』」。 module_frame 那一側的分類表記的也是同一件事,兩邊對得上。
module_frame 那側是六支(多一支 ModuleFrameSspResourcesService), oscal 這側對應的 SspResourcesAppService 走的是第三支共用 service (ssp_resources_context_service.py,住在 oscal 這側,被兩邊共用)—— 所以嚴格說是 5 + 1 對 5 + 1,不是六對六。
卡在哪:這五支殼繼承了範本版的空值哨兵——套件的資料表有幾個欄位不准留空, 前端允許留空,主專案就塞假值進去(party_uuid 塞全零 UUID、date_authorized 塞今天), 讀出來時再換回空白。這件事在 module_frame 那一側的分類表也列為待裁。 在 oscal 這一側看,問題更嚴重一級: 範本 SSP 是內部資料,專案 SSP 是租戶的實際稽核資料,全零 UUID 與「今天」這個假日期 會跟著匯出進客戶的系統安全計畫書。目前 _to_dict 有換回 None,但那是讀路徑的補救—— 任何第三個寫入者(例如未來新增的批次匯入)不知道這個看不見的約定,就會把假值寫成真值。
需要什麼前置:與 module_frame 那一側同一題——jedi_oscal_v2 除本專案外還有沒有別的使用者。 有就只能維持現狀;沒有就該把欄位改成可留空。
下列三支的「殼」與「業務」纏在同一個方法裡,不是分開的區段,拆解會動到套件契約:
| 檔 | 混住情形 | 拆了會動到什麼 |
|---|---|---|
ssp_control_implementation_service.py(1,004 行) |
build_control_tree_by_ssp_id 一個方法 400 行,同時做「向套件拿 catalog 與 IR」(殼)與「疊上本輪任務狀態、上一輪結果、輪次凍結範圍」(業務)。兩者在同一個迴圈裡 |
要拆就得讓套件回一個「帶洞的樹」讓主專案填,等於在套件開一個主專案專屬的回傳形狀 |
framework_parse_job_service.py(674 行) |
confirm(118 行)裡,使用者的逐項決策(業務)與 catalog 落地(殼)交錯 |
同上,套件的 catalog 匯入要能接受「部分覆寫」的參數 |
resource_library_app_service.py(541 行) |
create_resource_library(104 行)四段編排全都是「呼叫套件 → 拿結果 → 決定下一步」,中間夾主專案的 link record 原生 SQL |
「資源庫」這個概念要不要進套件是更大的問題(套件只有 catalog/profile/SSP 三個原語,三件組是主專案組出來的) |
這三支不列在 ④「可以立刻收的」,因為它們不是可收,是可能該搬——而搬的前提是 先決定套件契約要不要變。
前端 src/service/ModuleFrameTemplateService.js:198 打 GET /oscal/catalog-controls/<control_uid>/assessments,常數定義在 api.js:180。 後端全 repo 沒有註冊這條 route(grep -rn "catalog-controls" --include='*.py' . 零命中, 套件側也零命中)。
呼叫者是 ModuleFrameTemplateEditView.vue:566 的 loadAOsForControl()。 現況不會壞,因為那段程式碼上方註解寫明「AO 已在 buildTreeFromProfile 從 module_frame.oscal_profile 預載,此處 cache 應該總是命中。走 API 只是防呆」, 且整段包在 try/catch 裡、失敗只 console.warn 並回空陣列。
卡在哪:這是個沉默的後備路徑——真的走到(快取沒命中)時會拿到 404, 然後靜默回空陣列,前端的 AO 清單變空白、不報錯。要嘛後端補這條 route, 要嘛前端把這個後備拿掉改成明確報錯。屬前端與後端契約問題,不在本卡的分析範圍內。
另外兩條同型的(都在 ComplianceFrameworkVersionManage.vue): OSCAL_FRAMEWORK_VERSION_IMPORT(/oscal-framework-version/import/<type>)與 OSCAL_FRAMEWORK_VERSION_SAMPLE_DOWNLOAD(/oscal-framework-version/download/sample) 後端也都不存在。但這兩條的前端入口本身是關著的——開啟該對話框的選單項 onUpdateVersionDocClick 在 :596-599 被整段註解(註解寫「v2 兩階段化後暫時隱藏」), 所以使用者點不到。三條合起來看是同一件事:框架匯入從舊的「上傳整份文件」 改成新的「解析工作」兩階段流程後,前端留下了指向舊端點的殘骸。
本表遇到「兩種合理判法、不自選」的有三件:
module_frame 與 oscal 誰是誰的上游跨模組 import 的方向已經很清楚是 oscal 依賴 module_frame(9 處對 3 處), 包括五支 SSP 子物件的欄位對應表、Excel sheet 定義。但這與規模直覺相反 (oscal 兩倍大、概念上更底層)。
module_frame 是上游):零風險,但每個新來的人看到「大模組依賴小模組」 都會想反過來改,而改了會壞。oscal):符合直覺,但要動五支 SSP 子物件 service +六支 module_frame service,且 module_frame 那側的 Excel 樣板產生器(1,656 行) 也在被 oscal 引用,一併反轉範圍會很大。判斷需要的資訊:這兩個模組未來會不會有一個進套件。若 module_frame(資源庫) 要進套件,現在的方向剛好(套件不能依賴主專案);若 oscal 要進,方向就是錯的。
module_frame 那一側的分類表也把這題列為待裁,建議兩個模組的結果一起看。 從 oscal 這一側實測,逐字重複約 25 行,但變數名不一致 (HEADER_LABELS vs EXPECTED_HEADERS)反而是更麻煩的部分——兩邊各改一次時, grep 同一個名字只會找到一半。
三個候選落點與代價見 5.1。這題與第一題綁死,建議一起裁。
| 項目 | 行數 | 收掉的風險 |
|---|---|---|
4.1 被註解的 oscal_export_route.py |
39 | 低——但要一併修三處指向它的過期註解 |
| 4.2 資源庫詳情與發布兩條端點 | — | 🔴 高——發布是公版治理的唯一途徑,收了等於砍功能。建議只清「詳情」那條或兩條都不動 |
| 4.3 五支死序列化器 + 五行 re-export | 87 | 中——要同步移除守衛測試白名單三行,且名字極易誤刪(見 4.3 的兩個坑) |
| 5.4 前端三條指向不存在端點的殘骸 | — | 中——屬前端 repo,且其中一條是沉默的後備路徑,拿掉前要確認快取真的總是命中 |
「清」屬異動,本案只分析不動程式。