---
title: FR-092 第 5 棒盤點報告——活檔裡的死方法與死類別
status: 🟢 **盤點完成**（2026-09-12，CM-1708）。vulture 報 394 條，核完剩 **63 條確定可刪（約 611 行）**，其餘 331 條全是誤報或活的。只盤不修，修正卡待首腦開。
notion:
  parent: {id: CM-1703, title: "FR-092 主專案廢碼清理", url: "https://app.notion.com/p/FR-092-73-12-15-3d9346da4cd081479cfeed7e07e3488a"}
  self: {id: CM-1708, url: "https://app.notion.com/p/FR-092-5-class-vulture-364-3d9346da4cd0818796e6ea0e23a17626"}
relates: [FR-092, FR-038, FR-069, FR-091]
---

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

## 一句話結論

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

## 這一棒怎麼做的

### 範圍與工具

| 項目 | 值 |
|---|---|
| 掃描工具 | 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。

## 確定可刪清單（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 |

## 值得先看的三條

**① `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）。

## 兩條要留意的邊界情況

**`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 後若還在再說。

## 建議拆棒

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 條零引用，刪除後跑既有測試即可，不必補新測試。

## 附帶發現

**首腦初掃的四個熱點，核完後三個是誤報。**
- `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 對這類「宿主實作、套件呼叫」的結構完全無能為力，是本次最大的誤報來源之一。

## 本次核對的工具限制（給下一棒參考）

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` 即足夠。
