# DEV fail-open 路由基線白名單（CM-1760，2026-09-13 盤點）

查詢（DEV，`localhost:5432/guidant_ai_dev`，唯讀）：

```sql
SELECT u.id, u.name, u.url, u.enable
FROM ui_routes u
WHERE u.pid IS NOT NULL AND u.pid <> 0
  AND NOT EXISTS (SELECT 1 FROM route_capabilities rc WHERE rc.route_id = u.id)
ORDER BY u.name;
```

（`pid IS NULL` 或 `pid = 0` 的節點是選單分組節點／頂層節點，本來就無 url、不是可綁
權限的葉節點，已排除。）

## 結果：恰好 3 筆

| id | name | url | enable | 為什麼可以 fail-open |
|----|------|-----|--------|----------------------|
| 49 | project-dashboard | /project/dashboard | 1 | `scripts/sql/2026-05-26-menu-group-restructure.sql` Step 2 明寫「不掛 capability（所有 user 可見）」——搬遷自 AppMenu.vue 原本寫死的入口，本來就無角色限制。 |
| 50 | my-tasks | /project/task-manage | 1 | 同上（同一支 migration 同一段），待辦任務對任何登入者都可見。 |
| 54 | license-status | /license/status | 1 | `scripts/sql/2026-08-10-fr062-8-license-menu-restructure.sql` 明寫「/license/status（route 54）刻意不綁任何 capability：全租戶可見的自己狀態頁，維持現況 fail-open 可見」——與同群組的 `license-manage`（root 專屬管理頁，已綁 `license.read`）刻意區分。 |

沒有發現「真漏綁」的路由——三筆都能在各自來源 migration 找到明確的白話說明，判定為
既定設計而非疏漏。**本棒不需回報決策者裁「要不要開卡補」**（.3a 卡面待裁事項已解）。

## 一筆歷史個案（不在 DEV 現況清單內，但守衛測試需要處理）

`license-manage` 在 `scripts/sql/2026-08-08-fr062-2-license-menu-route.sql` 首次
新增時**沒有**綁 `route_capabilities`（該檔註解：「不綁 capability：本階段守門走既有
platform-admin 軸（route decorator），未新增 capability」）——這是「用另一套授權軸
守門、DB 層刻意留空」的設計，不是遺漏。兩天後 `scripts/sql/2026-08-10-fr062-8-
license-menu-restructure.sql` 補上了 `license.read` 能力點與綁定，DEV 現況因此已不
在 fail-open 清單內（見上表，`license-manage` 未出現）。

守衛測試的白名單（`test/test_package_ui_routes_have_route_capabilities.py` 的
`_INTENTIONAL_FAIL_OPEN_ROUTES`）以 **(檔案, name)** 為單位登記，而非只用 name——
只豁免 `2026-08-08-fr062-2-...` 這一支舊檔案，`2026-08-10-fr062-8-...`
（已補綁定的那支）與 `04-seed-core.sql`（出貨 seed 攤平現況，`license-manage` 一列
已含綁定）都不在白名單內、必須實際過守衛。這樣設計是為了避免「用 name 當白名單」
把未來任何新增同名路由的檔案也一併豁免掉，形成守衛盲區。

## 對 .3c 的輸入

`.3c`（CM-1762，套件 migration＋jedi pyproject 對齊）盤點「哪些套件有頁面」時，本
白名單是既有出貨路由的基線；套件新增選單路由時仍受本守衛約束，不會被本白名單
豁免（豁免以「檔案」為單位，套件 SQL 檔名與這裡登記的主專案檔名不會撞名）。
