FR-092 第 5 棒盤點報告——活檔裡的死方法與死類別

第 5 棒 盤點報告:活檔裡的死方法與死類別

§1

一句話結論

vulture 在 60% 信心下報了 394 條「沒人用的方法/類別/函式」,逐條核過動態呼叫路徑後, 真正可刪的只有 63 條、約 611 行,其餘 331 條要嘛是框架約定(Flask 的 HTTP 動詞、 marshmallow 的 Meta 內部類),要嘛是被 jedi 套件端呼叫的介面實作。 工具的誤報率是 84%——這正是這一棒要人工核對而非直接照單刪的理由。

§2

這一棒怎麼做的

範圍與工具

項目 值
掃描工具 vulture 2.16(裝在 scratchpad,未進 pyproject.toml)
掃描指令 vulture api app domain infra common config di_containers core --min-confidence 60
原始輸出 1,386 行,其中 method 217/class 147/function 30(另有 attribute 127/variable 847 依卡片指示不看)
掃描對象 394 條(method + class + function)
排除 95 條落在第 1~3 棒要整檔刪的檔案(dead-files.txt 73 檔 + subtask_status_history 模組 19 檔)
實際逐條核對 299 條
branch feature/FR-075(BE)
日期 2026-09-12

核對方法:五路查證

卡片要求三個 grep,實作時發現三路不夠,補成五路——漏掉任何一路都會把活的當成死的:

路 查什麼 為什麼需要
① BE 直接呼叫 全 BE 原始碼(含 scripts/、test/、main.py)的識別字比對 基本款
② BE 字串引用 同上,但看被引號包住的出現 DI 容器 method="xxx"、AI 儀表板申報表
③ jedi 套件端 ~/Projects/Jedicogy/module/jedi-python-package/ 全樹 主專案寫的是套件 port 實作,呼叫者在套件裡
④ 前端(卡片未列,自行補) compliance-manager-fe/src/ 的 js/vue/ts SocketIO 事件名由前端 emit 觸發
⑤ 同檔內字串引用(卡片未列,自行補) 定義檔自己的其他行 marshmallow 的 fields.Method("get_xxx")/fields.Nested("XxxSchema") 幾乎都寫在同一個檔

另加兩個機械判準:

  • 框架 decorator:@post_load、@validates_schema、@app.before_request、 @app.errorhandler、@event.listens_for 等,函式由框架呼叫,永遠沒有 caller。 (注意 vulture 報的行號是 decorator 那行、不是 def 那行,判斷時要往下找。)
  • 框架保留名:get/post/put/delete 等 HTTP 動詞是 Flask-RESTful 依請求方法分派的; Meta 是 marshmallow 的內部設定類。

判定結果

判定 條數 意思
確定 63 五路全 0,也不是框架回呼——可刪
活-直接呼叫 86 BE 內有人直接呼叫
活-套件端 38 jedi 套件呼叫的 port 實作
活-字串引用 2 DI 申報表/SocketIO 註冊表用字串指名
FP-框架 92 框架回呼或保留名
FP-同檔字串 13 同檔 fields.Method/fields.Nested 字串引用
疑似-跨棒 1 現在活的,但第 4 棒刪完就變死的
排除-平行棒 4 檔案已在工作樹被刪(FR-091 license 搬移,尚未 commit)
合計 299

誤報的組成(共 105 條 FP):Flask HTTP 動詞 45、marshmallow Meta 30、 同檔字串引用 13、各式框架 decorator 16、stdlib 覆寫 1。

活的 126 條中,套件端 38 條按套件分佈:jedi-iam 9、jedi-compliance-audit 9、 jedi-task-platform 5、jedi-survey 4,其餘八個套件各 1~2。

§3

確定可刪清單(63 條 / 約 611 行)

行數=含 decorator 與 docstring 的完整定義區塊。 全部 63 條在 test/ 零引用,刪除不會弄壞任何既有測試。

app/flow_engine/service/workflow_execution_service.py — 6 條 / 約 231 行

行 類型 成員 行數
468 方法 _is_all_project_jobs_completed 43
1033 方法 add_job_comment 33
1123 方法 get_ext_main_workflow_executions_and_pager 15
1139 方法 get_main_workflow_executions_and_pager 16
1375 方法 notify_control_reviewers_on_task_complete 89
1520 方法 get_ext_workflow_execution_by_uid 35

api/oscal/serializers/framework/oscal_framework.py — 5 條 / 約 52 行

行 類型 成員 行數
5 類別 CatalogSchema 11
18 類別 ProfileSchema 11
31 類別 SystemSecurityPlanSchema 11
44 類別 AssessmentPlanSchema 11
181 類別 OscalFrameworkVersionImportRequestSchema 8

common/util/system_asset_snapshot.py — 1 條 / 約 50 行

行 類型 成員 行數
54 函式 resolve_system_asset_fields 50

api/project/serializers/project.py — 6 條 / 約 38 行

行 類型 成員 行數
18 類別 AuditSystemInputSchema 8
38 類別 DeviceInputSchema 8
91 類別 ProjectPageQueryRequest 4
123 類別 ProjectPageQueryResponse 4
129 類別 ProjectCreateRequestSchema 11
157 類別 ProjectSystemInfoQuerySchema 3

app/module_frame/service/ssp_import_template_app_service.py — 3 條 / 約 32 行

行 類型 成員 行數
1404 方法 _fetch_devices_by_ids 10
1415 方法 _fetch_info_systems_by_ids 15
1431 方法 _party_by_uid 7

domain/oscal/service/reconciliation/_normalizers.py — 3 條 / 約 31 行

行 類型 成員 行數
53 函式 parse_device_lookup_label 12
67 函式 parse_info_system_lookup_label 11
80 函式 _normalize_control_id 8

app/module_frame/service/module_frame_service.py — 2 條 / 約 22 行

行 類型 成員 行數
56 方法 _create_template_ssp 15
317 方法 update_assessment_workflow_job_link 7

api/oscal/serializers/ssp/ssp_docx_import.py — 2 條 / 約 18 行

行 類型 成員 行數
5 類別 SspDocxImportParseRequestSchema 6
114 類別 ObjectiveDiffSchema 12

common/exception_error/common_exception.py — 4 條 / 約 16 行

行 類型 成員 行數
13 類別 ValueMismatchError 4
19 類別 DataIsRevokeError 4
37 類別 AuthError 4
43 類別 MailError 4

domain/module_frame/service/module_frame_domain_service.py — 4 條 / 約 16 行

行 類型 成員 行數
38 方法 get_module_frames_by_ids 3
76 方法 delete_module_frames 3
87 方法 verify_module_frame_exists_by_id 6
94 方法 verify_module_frame_exists_by_name 4

api/system_config/serializers/system_config.py — 3 條 / 約 12 行

行 類型 成員 行數
17 類別 SystemConfigGroupResponse 3
21 類別 SystemConfigValueResponse 2
24 類別 SmtpConfigRequest 7

api/oscal/serializers/ssp/ssp_excel_import.py — 1 條 / 約 11 行

行 類型 成員 行數
5 類別 SspExcelImportParseRequestSchema 11

domain/feedback/service/feedback_issue_domain_service.py — 3 條 / 約 10 行

行 類型 成員 行數
33 方法 get_feedback_issue 3
37 方法 get_feedback_issue_by_id 2
52 方法 verify_feedback_issue_is_exist 5

api/project/serializers/job_execution_device.py — 1 條 / 約 9 行

行 類型 成員 行數
61 類別 JobExecutionDeviceViewQueryResponse 9

api/flow_engine/serializers/flow_engine/workflow_execution.py — 3 條 / 約 8 行

行 類型 成員 行數
28 類別 WorkflowExecutionUpdateRequest 4
33 類別 WorkflowExecutionPageQueryRequest 2
67 類別 WorkflowExecutionPageQueryResponse 2

common/authz/license.py — 1 條 / 約 8 行

行 類型 成員 行數
200 函式 resolve_license_tenant_id 8

api/flow_engine/serializers/flow_engine/element_variable.py — 1 條 / 約 7 行

行 類型 成員 行數
4 類別 ElementVariableRequest 7

app/associations/service/project_system_characteristic_mapping_service.py — 1 條 / 約 7 行

行 類型 成員 行數
63 方法 get_info_by_project_uid 7

domain/flow_engine/service/stage_completion_registry.py — 3 條 / 約 6 行

行 類型 成員 行數
179 方法 has_handler 2
182 方法 has_precondition 2
185 方法 has_rollback_handler 2

infra/system_config/runtime_config.py — 1 條 / 約 6 行

行 類型 成員 行數
77 函式 get_runtime_value 6

domain/project_summary_report/service/project_summary_report_history_domain_service.py — 2 條 / 約 5 行

行 類型 成員 行數
17 方法 get_project_summary_report_history 3
21 方法 get_project_summary_report_history_by_id 2

api/cloud_integration/serializers/google_drive_integration.py — 1 條 / 約 3 行

行 類型 成員 行數
22 類別 GoogleDriveCallbackQuerySchema 3

app/module_frame/service/module_frame_import_service.py — 1 條 / 約 3 行

行 類型 成員 行數
262 方法 module_frame_dto_to_ap_inputs 3

app/oscal/service/excel_parser/types.py — 1 條 / 約 2 行

行 類型 成員 行數
90 方法 has_blocking_errors 2

app/oscal/service/ssp_resources_context_service.py — 1 條 / 約 2 行

行 類型 成員 行數
116 方法 resolve_sys_impl_main_id 2

config/config_util.py — 1 條 / 約 2 行

行 類型 成員 行數
18 方法 get_current_config 2

domain/flow_engine/service/ext_workflow_execution_domain_service.py — 1 條 / 約 2 行

行 類型 成員 行數
28 方法 get_workflow_execution_by_ids 2

domain/oscal/adapter/adapter_registry.py — 1 條 / 約 2 行

行 類型 成員 行數
23 方法 known_frameworks 2
§4

值得先看的三條

① notify_control_reviewers_on_task_complete(89 行,最大一塊) 檔案裡自己寫明「目前無呼叫點(CM-1504)」,且說「刻意保留不刪」——理由是 FR-038 2B 要重接「任務完成通知」時會用到,而且它已登記在系統設計文件產生器 docs/system-design/scripts/generate_business_logic_v2.py:1992 的功能表裡。 這條要決策者裁:是照原註解繼續留(那就在修正卡標明「刻意保留」,不要下次又被掃出來), 還是判定 2B 會另寫、現在就刪。刪的話文件產生器那行要同步移除。

② common/util/system_asset_snapshot.py 的 resolve_system_asset_fields(50 行,單檔最大) 整個檔就這一支公開函式,零呼叫。刪掉這支等於整檔可刪,屬「第 1 棒沒抓到的漏網之魚」 ——建議併進第 1 棒的整檔刪除清單,不要拆成成員刪除。

③ common/authz/license.py 的 resolve_license_tenant_id(8 行) docstring 自稱是「整條讀照鏈的統一入口⋯⋯共用同一份解析,不各自沿 parent 鏈自兜第二套」, 但實際上零 caller,而且沒有在 common/authz/__init__.py export。 真正在用的是同檔 LicenseEnforcer 內部的 self._license_tenant_resolver.resolve()。 這是「說一套做一套」的文件陷阱——後人讀 docstring 會以為該走這支。 要嘛刪掉、要嘛把 docstring 改成實話。注意 license 模組正被 FR-091 搬進套件, 這條要等那邊落定再動(工作樹已有 5 個 license 檔被刪但未 commit)。

§5

兩條要留意的邊界情況

on_reload(app/notification/handler/notification_socketio_handler.py:43)——現在活的,第 4 棒之後會變死的。 它是 SocketIO 事件回呼,前端 emit reload 才會觸發。全前端只有一處在 emit, 就在 views/_archived/projects/oscal/ProjectTaskManage.vue:174——那正是第 4 棒(CM-1707) 要刪的目錄。另外兩處 socket.on('reload') 只是「監聽」不是「觸發」,不算 caller。 第 4 棒做完後這支就成孤兒,屆時可一併清掉。

四條 license 相關成員暫時排除。 infra/license/notifier.py、tenant_directory_adapter.py、tenant_admin_directory_adapter.py 在掃描當下已被 git rm 但尚未 commit(FR-091 把 license 搬進套件)。 這四條不列入本次盤點,等那邊 commit 後若還在再說。

§6

建議拆棒

63 條分佈在 28 個檔,按模組切成四張修正卡,彼此無相依可平行:

卡 範圍 條數 行數 檔數 備註
A flow_engine 14 254 5 含最大的 notify_control_reviewers_on_task_complete,要先裁那條的去留
B OSCAL 14 118 7 CatalogSchema 等四支是 v1 殘骸,與第 1 棒同性質;_normalizers.py 三支 parser 未接線
C module_frame + 其他 app/domain 16 95 7 domain service 的 verify_*/get_*_by_ids 系列,純孤兒
D api serializers + common/infra/config 19 144 9 未掛 route 的 request/response schema;system_asset_snapshot.py 整檔可刪

每張卡的共通紀律:

  • 刪成員時同步檢查該檔是否因此整個空掉(system_asset_snapshot.py、 element_variable.py 可能會),空掉就走整檔刪並更新 dead-files.txt。
  • 有 21 個名字在 docs/ 被提及(多為 docs/api/*/api-spec.md 的 schema 描述與 generate_docx.py 的產生器),那些是描述性引用不是執行性引用,不影響刪除, 但刪完要順手更新文件,免得文件描述不存在的東西。
  • test/ 對這 63 條零引用,刪除後跑既有測試即可,不必補新測試。
§7

附帶發現

首腦初掃的四個熱點,核完後三個是誤報。

  • api/project/serializers/audit_round.py 的「18 個未使用 class」全部是 marshmallow 的 class Meta 內部類,一條都不能刪。
  • api/flow_control/serializers/job.py 的 15 條中,get_id 六條是 fields.Method("get_id") 同檔字串引用,put 是 HTTP 動詞。
  • infra/*/adapters.py 三檔(survey/detection_tools/evidence_classification) 如卡片預判,全是套件 port 實作,開 jedi_survey/domain/ports.py 核過,零可刪。
  • 唯一命中的是 workflow_execution_service.py——10 條裡 6 條確定死、4 條活 (update_job_comment/add_workflow_execution 是 jedi-flow-engine 的覆寫; notify_users_batch_assigned 被 jedi-task-platform 注入呼叫; get_sub_workflow_executions 被 AI 儀表板申報表用字串指名)。

core/plugins/*.py 的 27 條全是活的。 那是 FR-069 插件契約的 port 實作,CM-1700 剛收進去,由各套件端呼叫。 vulture 對這類「宿主實作、套件呼叫」的結構完全無能為力,是本次最大的誤報來源之一。

§8

本次核對的工具限制(給下一棒參考)

  1. vulture 報的行號是 decorator 那行,不是 def 那行。用行號去比對定義時要往下找。
  2. --min-confidence 60 的誤報率 84%;卡片提到 80% 信心下只剩 16 import + 10 variable, 等於把所有 method/class 都濾掉了——信心值不是可靠的篩選軸,該靠結構判準(框架名、 decorator、port 介面)分流。
  3. jedi 套件目錄含各套件自己的 .venv,全樹 grep 會撈到第三方套件的同名符號。 核對時要把 /.venv/ 與 site-packages 的命中排掉。
  4. docs/ 目錄 1.6 GB(含 HTML 站產物),全掃會跑很久。只掃 docs/system-design/scripts、docs/api、docs/spec-site/current、docs/claude 即足夠。