# FR-092 第 7 棒 盤點報告 — DEV 庫裡沒人讀寫的表

> 卡號 CM-1710（母卡 CM-1703）。**只盤不修**，未執行任何 DROP／DELETE／DDL，DEV 全程只 `SELECT`，STG／POC 未連線。

## 範圍與方法

盤點對象是 DEV 庫 `guidant_ai_dev`（192.168.50.188:25432，帳號 `cmmgr`，密碼查 `.env`）的全部 189 張表與 4 個 view。判斷「有沒有人碰」走四路對照，任何一路命中就不算孤兒：

1. **ORM model**——用 AST 解析主專案與 jedi 套件全部 `.py`，抓 `__tablename__` 與 `__table_args__` 的 `schema`。主專案 22 支、jedi 現役套件 134 支（archive 與 tests 另計）。
2. **原生 SQL 字串**——grep `FROM／JOIN／INTO／UPDATE／DELETE FROM` 後接的 schema 限定表名與裸表名，涵蓋 `infra/readmodel/`、各套件 `*_query.py` 的 `text("...")`。
3. **view 定義**——`pg_views` 的 definition 逐個展開找被引用的表。
4. **動態組表名**——grep f-string 與 `.format()` 組出來的 SQL，把常數解回實際表名。

工具是 `psql 16.10` 與 `python 3.11.9` 手寫 AST 腳本，沒有用 vulture／deptry。每一條「沒人碰」的結論都另外開檔核對過，核對內容寫在下面各組的證據欄。

### 兩個一開始漏掉、後來補回的盲區

機械對照第一輪判了 36 張表「無 ORM」，逐一開檔核對後有兩類是誤判，值得記下來給後面幾棒參考：

- **SQLAlchemy Core `Table()` 不是 `__tablename__`**。`public.bulletin_org_units` 是 jedi-bulletin 的公告↔部門橋表，用 `Table("bulletin_org_units", Base.metadata, ...)` 宣告，AST 抓 `__tablename__` 完全看不到它，但 `bulletin_repo_impl.py` 與 `bulletin_org_unit_repo_impl.py` 對它有 `select`／`insert`／`delete`。**它是活的。**
- **schema 可能是執行期才決定的**。jedi-log 的 `log_forwarding_setting.py` 寫 `SETTINGS_SCHEMA = os.getenv("LOG_FORWARDING_SETTINGS_SCHEMA", "config")`，AST 只看得到變數名。實際落在 `config.log_forwarding_settings`，而且**昨天（9/11）還有寫入**。**它是活的。**

這兩條的共同教訓是：靜態抓 `__tablename__` 會漏，任何「零命中」都必須開檔看過才能下結論。

### 統計

| 項目 | 數量 |
|---|---|
| DEV 表 | 189 |
| DEV view | 4 |
| 有 ORM model 對應 | 155（主專案 21／jedi 134） |
| 無 ORM model | 34 |
| 其中是分區子表（父表有 ORM） | 10 |
| **真正的候選（逐一核對過）** | **24** |
| 分類結果 | A 類 3 條／B 類 4 條／D 類 3 條／E 類（基礎設施，不列入）14 條 |

## 逐表結果

命中欄位：`—` 表示零命中，`✓` 表示有命中且已開檔確認是真的引用。

| schema.table | ORM（主） | ORM（套件） | SQL 字串 | view | 筆數 | 最後寫入 | 建表來源 | 分類 |
|---|---|---|---|---|---|---|---|---|
| oscal.component_definitions | — | — | — | — | 0 | 從未 | `scripts/init/02-schema.sql` | **A** |
| oscal.cd_capabilities | — | — | — | — | 0 | 從未 | 同上 | **A** |
| oscal.cd_components | — | — | — | — | 0 | 從未 | 同上 | **A** |
| oscal.cd_control_implementations | — | — | — | — | 0 | 從未 | 同上 | **A** |
| oscal.cd_implemented_requirements | — | — | — | — | 0 | 從未 | 同上 | **A** |
| oscal.cd_statements | — | — | — | — | 0 | 從未 | 同上 | **A** |
| compliance.hi_workflow_executions | — | — | — | — | 0 | 從未 | 同上 | **A** |
| compliance.hi_element_variables | — | — | — | — | 0 | 從未 | 同上 | **A** |
| compliance.hi_workflow_templates | — | — | — | — | 0 | 從未 | 同上 | **A** |
| public.hi_job_executions | — | — | — | — | 0 | 從未 | 同上 | **A** |
| public.device_monitors | — | — | — | — | 0 | 從未 | 同上 | **A** |
| public.device_monitor_archive | — | — | — | — | 0 | 從未 | 同上 | **A** |
| public.role_members | — | — | — | — | 0 | 從未 | 同上 | **A** |
| public.operations | — | — | — | — | 8 | seed 當下 | 同上＋`04-seed-core.sql` 灌 8 列 | **B** |
| compliance.project_job_execution_device_mapping | — | — | ✓ 只讀 | — | 0 | 從未 | 同上 | **B** |
| public.system_logs_old | — | — | — | — | 104,310 | 2026-06-03 | 分區改造時的舊表 | **B** |
| survey.question_answers_dedup_backup_20260428 | — | — | — | — | 1,736 | 2026-03-19 | `2026-04-28-survey-answer-dedup.sql` | **D** |
| config.detection_tool_profiles_deprecated_20260803 | — | — | — | — | 13 | 2026-08-01 | FR-059 profile 改版備份 | **D** |
| public.alembic_version | — | — | — | — | 1 | — | `02-schema.sql` | **D** |
| public.bulletin_org_units | — | ✓ Core `Table()` | ✓ | — | 7 | — | — | 活 |
| config.log_forwarding_settings | — | ✓ 動態 schema | ✓ | — | 1 | 2026-09-11 | — | 活 |
| config.tenant_license_events | — | ✓ | ✓ | — | 31 | 2026-09-10 | — | 活 |
| public.integrity_tamper_events | — | ✓ | ✓ | — | 7 | 2026-08-24 | — | 活 |
| public.license_clock_watermark | — | ✓ | ✓ | — | 1 | 2026-08-14 | — | 活 |

`public.schema_migrations`、`public.schema_version` 屬版位追蹤基礎設施，依卡片指示不列入候選。10 張 `api_logs_*`／`system_logs_*` 分區子表的父表 `public.api_logs`、`public.system_logs` 都有 ORM，是正常分區不是孤兒。

## 分類與建議

### A 類：零引用零資料，技術上可 drop（13 張，3 組）

三組都是「表建了、功能沒做或已退役、一列資料都沒寫過」。**三組在出貨基線庫 `guidant_ai` 裡也都存在且同樣是 0 列**，意思是每一個客戶裝機都會拿到這 13 張空表。

**① OSCAL Component Definition 全族（6 張）**
`component_definitions` 與五張 `cd_*` 子表。整個 codebase 對 `component_definition`／`cd_*`／`ComponentDefinition` 做過大小寫不敏感全文搜尋，主專案與 jedi 套件（含 archive）**零命中**——沒有 ORM、沒有 entity、沒有 repository、沒有 route。唯一提到它們的地方是兩支清理腳本把它們列進 truncate 清單。OSCAL 標準有 Component Definition 這個文件型別，但本產品從未實作，表是照標準先開好的。

**② flow-engine history 全族（4 張）**
`hi_workflow_executions`、`hi_element_variables`、`hi_workflow_templates`、`hi_job_executions`。這組**不需要我重新舉證**：`docs/analysis/2026-09-01-cm1494-zombie-verification.md` 已經把它判為「確定死」，證據是 STG／POC 兩環境四張表筆數皆 0、五個宣告依賴 jedi-flow-engine 的套件零引用、`plugin.py` 也沒對外暴露。我這一輪在 DEV 複驗，四張表同樣 0 列、程式碼同樣零命中，結論一致。套件側那 34 支 `hi_*` 檔（1,522 行）該分析建議併入 jedi-flow-engine 下次改版處理，**不屬本棒範圍**。

**③ 裝置監控預留（2 張）＋ 舊權限模型殘留（1 張）**
`device_monitors`／`device_monitor_archive` 是預留功能，`docs/api/device/generate_docx.py` 白紙黑字寫「Blueprint 中有註解掉的 DeviceMonitorsRoute／DeviceMonitorRoute⋯⋯目前未啟用」。
`role_members` 是舊角色模型的殘留：jedi-iam 的 `role.py` 留了一行註解說「你若仍保留舊的 Web 權限可在此處維持舊關聯」但**關聯本身是註解掉的**，`RoleEntity` 雖有 `role_members` 屬性、`role_domain_service` 也有兩處讀它，但 `RoleMapper.to_entity()` **從來不填這個欄位**，所以執行期恆為 `None`。現行角色授權走 `user_roles`／`role_capabilities`／`v_user_capabilities`，與這張表無關。

> ⚠️ `role_members` 這條要注意：**表可以 drop，但套件那段讀 `role.role_members` 的程式碼是活的、而且恆讀到 `None`**。`get_user_roles_member()` 裡 `roles = roles + role.role_members` 對 `None` 會直接 `TypeError`。這是既有的潛在雷，與 drop 表無關，但值得另開卡看那個分支到底走不走得到。

### B 類：零引用但有資料或有讀取者，需決策者裁（3 張）

**`public.operations`（8 列）**——沒有任何程式讀它，但 `scripts/init/04-seed-core.sql` 每次裝機都會灌進 VIEW／INSERT／UPDATE／DELETE／EXPORT／IMPORT／SUBMIT／VERIFY 八個動作字典。這是舊權限模型的字典表，現行 capability 模型不用它。**要 drop 就得同時改 seed 腳本**，而 `04-seed-core.sql` 是產生檔（`gen_seed_sql.sh` 從基線庫重產），照 `scripts/init/README.md` 的鐵則必須先改基線庫再 regenerate，不能手改。

**`compliance.project_job_execution_device_mapping`（0 列）**——這張**不能只看筆數就 drop**。jedi-task-platform 的 `DeviceReferenceQuery` 會 `SELECT COUNT(*)` 它，而且該 query 經 `core/plugins/asset.py` 的 `AssetReferenceAdapter` 活接線，供前端刪設備前的確認框用。但全 codebase **沒有任何一處 INSERT 進這張表**——它是只讀不寫的表，永遠回 0，對那個計數毫無貢獻。要退役得同時改套件的 SQL（三表相加改兩表），屬跨套件異動。

**`public.system_logs_old`（104,310 列，56 MB）**——分區改造前的舊系統日誌表，最後寫入 2026-06-03。**只存在於 DEV，出貨基線庫沒有這張表**，所以不影響客戶。`scripts/check_env_scoped_migrations.py` 把它與 `detection_tool_profiles_deprecated_20260803` 一起標為環境專屬殘留。留著只佔 DEV 空間，drop 前確認沒人還要查 6 月前的舊 log。

### D 類：一次性腳本的備份表，可 drop 但要註明來源（3 張）

- **`survey.question_answers_dedup_backup_20260428`**（1,736 列，304 KB）——`scripts/sql/2026-04-28-survey-answer-dedup.sql` 去重前的備份，最後資料停在 2026-03-19。DEV 專屬，基線庫沒有。
- **`config.detection_tool_profiles_deprecated_20260803`**（13 列，168 KB）——FR-059 檢測 Profile 改版時的舊表備份，表名自帶 `_deprecated_` 與日期。DEV 專屬，基線庫沒有。
- **`public.alembic_version`**（1 列 `f20ca741a9bd`）——**主專案根本不用 alembic**：`pyproject.toml` 沒有這個依賴，也沒有 `alembic.ini` 或 `alembic/` 目錄，migration 走 `scripts/sql/` ＋ `schema_migrations`。用 alembic 的是 License Center，那是獨立 repo 獨立 DB。這張表卻**隨出貨基線進了每個客戶的庫**，而且 `04-seed-core.sql` 還會灌那個 version_num 進去。屬歷史殘留，建議連同 seed 一起清。

## 反向：ORM 有、DEV 沒有的表

**現役套件與主專案掛零**——155 個 ORM model 全部在 DEV 有對應的表，沒有「程式碼領先 DB」的情況。

唯一三筆是 jedi-common 的**測試檔內**臨時 model（`_sample_translatable`、`t_person_extras`、`t_plain_extras`），跑測試時動態建表用，本來就不該在 DEV 出現，屬正常。

順帶做的 DEV↔出貨基線對照也乾淨：基線有而 DEV 缺的表 **0 張**；DEV 有而基線沒有的 3 張，正是上面 D 類與 B 類點名的環境專屬殘留（`system_logs_old`、`detection_tool_profiles_deprecated_20260803`、`question_answers_dedup_backup_20260428`）。

## 最值得先處理的三條

1. **OSCAL `cd_*` 全族 6 張**——零引用、零資料、零爭議，唯一要動的是 `scripts/init/02-schema.sql` 的重產與那兩支清理腳本的 truncate 清單。**一次少 6 張出貨表。**
2. **`public.alembic_version`**——主專案不用 alembic 卻隨基線出貨給每個客戶，還帶著一個假的 version stamp。誤導性最強：後來的人看到這張表會以為 migration 走 alembic。
3. **`compliance.project_job_execution_device_mapping`**——不是最髒，但最容易被誤判。它**有活的讀取者**，光看筆數會以為能刪；實際是「只讀不寫、恆回 0」的空轉查詢，套件那句三表相加的 SQL 有一項永遠是 0。

## 交給首腦的待辦

本棒只盤不修，以下都要另開卡：

- **drop 卡**：A 類 13 張＋D 類 3 張，走 `sql-migration` skill。**注意這不是單純寫一支 migration**——`02-schema.sql` 與 `04-seed-core.sql` 是產生檔，必須先改基線庫再跑 `gen_schema_sql.sh`／`gen_seed_sql.sh` 重產，照 `scripts/init/README.md` 的鐵則。
- **決策卡**：B 類 3 張（`operations` 要連 seed 一起清嗎／`project_job_execution_device_mapping` 要不要連套件 SQL 一起改／`system_logs_old` 的舊 log 還要不要）。
- **獨立 bug 卡**：jedi-iam `role_domain_service.get_user_roles_member()` 與 `get_roles_by_uid_list()` 讀 `role.role_members`，而 mapper 從不填該欄位，恆為 `None`，`roles + None` 會 `TypeError`。與本棒的 drop 無關，是查證途中發現的既有問題。
- **與第 3 棒的交接**：卡片預期「第 3 棒退 `subtask_status_history` 後那張表會變成新的孤兒」。本棒盤點時 `compliance.subtask_status_histories` 仍有 ORM model（`infra/subtask_status_history/models/`），故不列入。第 3 棒做完要回頭補進 drop 清單。
- **卡片提到的 `compliance.project_device_mapping`**（2026-05-22 C3 退役）**在 DEV 已經不存在**，出貨基線庫也沒有，該案已清乾淨，不必處理。
