# ADR-017: 模組化收尾路線圖（Phase 3f）

- **狀態**: Accepted
- **日期**: 2026-04-29
- **觸發**: 12 Agent 全景盤點（debugger / refactor-specialist / critic / db-expert / explorer）
- **相關 ADR**: ADR-008（188 拓撲）、ADR-011（資源隔離）、ADR-016（cache fingerprint）
- **相關 Memory**: `docs/memory/project_phase3f_cleanup_roadmap.md`、`docs/memory/feedback_db_metadata_import.md`、`docs/memory/history_logs.md`

## 背景

Phase 3e（4/28-29）完成 app.py 7,386→6,590 行（-10.8%），但**僅完成「搬檔」，未完成「拆乾淨」**。12 Agent 盤點揭露 6 個面向的殘留問題，且發現新的 critical 風險（DB metadata import 漏洞、孤兒表）。

## 完成度真相（2026-04-29 盤點基線）

| 維度 | 完成度 | 真相 |
|------|--------|------|
| 路由搬檔（41 條 → 28 BP）| 68% | 13 條 app.py 獨有未遷移 |
| 路由「拆乾淨」 | **0%** | app.py 41 條一條未刪，28 條與 BP 同 URL 雙寫，由 first-registered-wins 決定行為 |
| `USE_MODULAR_ROUTES` 開關 | **0%** | `register_blueprints()` 從未被 app.py 呼叫，整套設計死碼 |
| services/ 模組化 | ~95% | 唯一乾淨；3 個孤兒 service（0 引用）|
| 模板統一 | ~50% | 三目錄並存 + 1 空檔 + 3 死檔 + 2 TemplateNotFound 風險 |
| DB schema vs Model | ~60% | manager.py import 漏 3 模組；6 張表有 SQL 無 ORM；realtime_sales_monthly 孤兒 |

## 實作狀態補記（2026-05-13）

上表是 2026-04-29 立案時的盤點基線；後續整改已把多個 HIGH 項目落地並加上回歸守門：

- `app.py` 已收斂為 Flask bootstrap / Blueprint registration / 啟動自檢，active `@app.route` 為 0，並由 `tests/test_phase3f_cleanup_contracts.py::test_app_py_stays_blueprint_only_for_routes` 守住。
- 舊 `USE_MODULAR_ROUTES`、`register_blueprints()`、`MODULAR_ENDPOINTS`、duplicate cleanup shim 已移除；`routes/__init__.py` 僅保留 package docstring。
- DB metadata / migration 覆蓋已由 `tests/test_migration_metadata_coverage.py` 與啟動自檢守住，v5 observability / Market Intel 等表不再只靠手動口述。
- 模板路徑已收斂為 `templates/` 與 `web/templates/vendor_stockout/`；根層 placeholder `templates/list.html` 已刪除並有測試防回歸。
- 大檔治理仍未結案：`routes/openclaw_bot_routes.py`、`routes/admin_observability_routes.py`、`routes/sales_routes.py`、`scheduler.py` 仍超過 800 行，後續只能做 bugfix、安全修補或往外抽模組。

## 決策

執行 **Phase 3f 五階段收尾**，總工期估 12-15 小時（不含驗證），每階段獨立 commit、每階段 critic 審查、每階段先 SSH 驗證 production。

### Phase 3f-0：DB metadata 救急（30 分鐘，最高優先）
1. `database/manager.py` 補完 import：
   - `permission_models`（Permission, UserPermission）
   - `vendor_models` 補 VendorList / VendorEmail / EmailSendLog
   - `ai_models` 顯式 import 4 個 AI history/template class
   - `autoheal_models` 顯式 import 7 個 AIOps class，移除 `ai_models.py` re-export shim
2. 處置 `realtime_sales_monthly` 孤兒表：
   - 選項 A：建 `database/realtime_sales_models.py`（推薦，補 ORM 一致性）
   - 選項 B：移除 `app.py:693` 的 import，metrics 不依賴此 model
3. PostgreSQL 與 SQLite 初始化都執行全域 `Base.metadata.create_all()`；PostgreSQL 路徑以 process-local guard + advisory lock 保護，避免一般流量重複碰 DDL
4. `app.py` 啟動加 metadata self-check，缺表直接 `SystemExit`
5. `docker/postgres/init/01-init.sql` 的 `realtime_sales_monthly` 欄位同步 ORM，避免 fresh volume 先由 init.sql 建出窄表後 create_all 無法補欄位

**驗收**：Base.metadata 含全 34 個 table；create_all 在新環境零漏。

### Phase 3f-1：路由雙註冊徹底解（4-6 小時，P9 切分）

**策略**：保留 Blueprint，刪 app.py 對應 `@app.route`（28 條），13 條 app.py 獨有遷至 BP。實作順序要先處理 API shadow、次要頁面與 `/brand_assets`，首頁 `/` 最後動，因為多處 `url_for('index')` 仍依賴 app endpoint name。

子任務（按 BP 分組獨立 commit）：

| Sprint | 範圍 | 動作 |
|--------|------|------|
| 3f-1-a | dashboard_bp | 刪 app.py:722 `/`；確認 BP 版可用 |
| 3f-1-b | edm_bp | 刪 app.py:1029, 1280 兩條 |
| 3f-1-c | export_bp | 刪 app.py:1431-1905 共 9 條；補 `/api/export/excel/seasonality_detail` 進 BP |
| 3f-1-d | api_bp | 刪 app.py:2219-2451 共 6 條 trigger/run |
| 3f-1-e | import_bp | 刪 app.py:2691, 2980 兩條 |
| 3f-1-f | monthly_bp | 刪 app.py:3076, 3083 兩條 |
| 3f-1-g | sales_bp | 刪 app.py:3592-5809 共 7 條；**同時刪 routes/sales_routes.py 6 處 wrapper（174-348）**，把實作搬進 BP |
| 3f-1-h | system routes 補強 | 現有 `system_bp` 有 `/api/system` prefix；公開 URL（`/health` `/metrics` `/logs` `/settings` 等）需新建無 prefix 的 `system_public_bp` 或拆成 public/internal 兩個 BP，不能直接塞進現有 `system_bp`。`/abc_analysis/detail` 歸 `sales_bp`。 |
| 3f-1-i | 死碼清除 | 刪 `routes/__init__.py:32-177` 整套（register_blueprints / MODULAR_ENDPOINTS / is_endpoint_modular / cleanup_duplicate_routes）+ `config.py:244-254` USE_MODULAR_ROUTES |

**驗收**：
- `app.url_map.iter_rules()` 任一 (rule, methods) 皆唯一
- 啟動加 self-check：duplicate detect 直接 raise SystemExit
- 全 endpoint smoke test（critic 監督）

### Phase 3f-2：Cache 統一（2-3 小時）
1. Dockerfile + docker-compose.yml gunicorn 加 `--preload`，只作為 COW 記憶體優化，不作為一致性保證
2. 新建 `services/cache_manager.py` 套用 ADR-016 fingerprint 模式
3. 移除三處重複 `_SALES_*_CACHE` 定義（app.py:82, routes/sales_routes.py:32, services/cache_service.py:16）→ 統一 import `services.cache_manager`
4. `clear_cache` endpoint 從「N-POST 廣播」改「DB fingerprint pull」（已在 daily_sales bp 完成，擴及 sales）

### Phase 3f-3：穩定性補強（1-2 小時）
1. 先在 `services/event_router.py` 補同步安全 facade（如 `notify_failure()` / `dispatch_sync()`），因現有 `EventRouter` 只有 async `dispatch()`，scheduler 直接呼叫會掉告警
2. scheduler.py 7 處裸 `except: pass`（line 243, 254, 582, 587, 653, 890, 1222）改為 `except Exception as e: logger.exception(...)`，P1/P2 經 EventRouter 同步 facade 強制告警
3. docker-compose.yml 刪 7 條死路徑 mount（vendor_routes.py / vendor_stockout_*.html）
4. routes/vendor_routes.py:28-30 template_folder 改用絕對路徑，移除靠 Flask fallback 的脆弱依賴

### Phase 3f-4：模板統一（2-3 小時）
1. 補 2 個 TemplateNotFound：`trends.html`（trend_routes.py:33）、`login_history.html`（user_routes.py:40）→ 找回或停用對應 endpoint
2. 刪 `web/templates/sales_analysis.html`（0-byte 空檔）
3. 刪 3 個雙寫死檔：`web/templates/brand_assets.html`、`web/templates/growth_analysis.html`、根目錄 `logs.html`
4. 根目錄 11 個 *.html 全搬 `templates/`，確認 docker-compose 舊 mount 已清理後，再移除 `app.py:185-188` ChoiceLoader 的 BASE_DIR fallback
5. `web/templates/` 僅留 `vendor_stockout/` 子目錄（vendor_bp 自帶 template_folder）

### Phase 3f-5：死碼清除（30 分鐘）
1. 確認並刪除 3 個孤兒 service：
   - `services/elephant_alpha_decision_router.py`
   - `services/telegram_ai_integration.py`
   - `services/watcher_agent.py`
2. `.env.example` 補齊 15+ 個程式碼實際讀但 example 缺的變數（AIDER_*, ELEPHANT_ALPHA_*, HEAL_SSH_*, NVIDIA_API_KEY 等）

## 風險與回滾

每階段獨立 commit，CI/CD 失敗自動回滾（依賴 ADR-014 防線）。Phase 3f-1 為高風險區，必須：
1. 先在本機 `python app.py` 啟動驗證 url_map 無重複
2. critic 審 diff 過關
3. 部署後 SSH 健康檢查 `/health` + 抽測 5 條核心 endpoint

## 不做的事

- 不做模組化開關設計回填（USE_MODULAR_ROUTES 已死，直接刪）
- 不做 ORM 全面遷移（25 張無 SQL migration 軌跡的 table 留待 Phase 4）
- 不在 3f-0 修 `docker/postgres/init/01-init.sql` 與 ORM 的 `products` 中文欄位 schema drift；這是 Phase 4 migration 題
- 不做 openclaw_bot_routes.py（5,543 行）拆解（留 Phase 4）
- 不撤換 NGROK Token（統帥 2026-04-29 明示先忽略）

## 完成定義

- app.py < 5,000 行
- url_map 零重複
- create_all 新環境零漏表
- 模板僅存 `templates/` + `web/templates/vendor_stockout/`
- gunicorn `--preload` 啟用、cache 跨 worker 一致
- scheduler 7 處 except 全改、docker-compose mount 全清

## 2026-04-30 補充決策：模組化治理守門

Phase 3f 已陸續完成 DB metadata、路由註冊、cache、scheduler、模板與 orphan cleanup 的多個收斂項目，但 line-count 盤點仍顯示 15 個 Python 檔案超過 800 行。為避免後續功能開發再次把 route / service 寫成巨檔，本 ADR 補充以下守門規則：

1. `app.py` 只保留 Flask app bootstrap、Blueprint registration、啟動自檢、版本與全域設定；不得新增 route 或商業邏輯。
2. `routes/` 必須保持 thin controller，重複邏輯需抽到 `services/` 或 `utils/`。
3. 超過 800 行的 Python 檔案列入 `docs/memory/code_modularization_inventory_20260430.md`，只能做安全修補、bugfix、或往外抽模組；新增功能應先拆分。
4. 新增 Python 檔案超過 600 行需提出拆分理由；超過 800 行需同步更新 inventory 與測試。
5. `tests/test_modularization_governance.py` 是守門測試：新巨檔沒有被盤點會失敗。

詳細操作規則以 `docs/guides/modularization_governance.md` 為準。
