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

卡號 CM-1710(母卡 CM-1703)。只盤不修,未執行任何 DROP/DELETE/DDL,DEV 全程只 SELECT,STG/POC 未連線。

§1

範圍與方法

盤點對象是 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 條
§2

逐表結果

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

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,是正常分區不是孤兒。

§3

分類與建議

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 一起清。
§4

反向: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)。

§5

最值得先處理的三條

  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。
§6

交給首腦的待辦

本棒只盤不修,以下都要另開卡:

  • 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 已經不存在,出貨基線庫也沒有,該案已清乾淨,不必處理。