FR-093 · 套件 migration 進出貨線+能力點與選單自動補 row — 需求討論稿 · 2026-09-13(已拍板,定案見 design.md)
FR-089/FR-091 把 21 支 jedi-* 套件整成標準形狀之後,每支套件都會自己宣告「我需要哪些表(migration)」與「我需要哪些權限開關(能力點)」。但今天這兩份宣告都只是寫在那裡,出貨線完全不讀:套件的 SQL 從來沒有被任何環境執行過、新的能力點沒有機制寫進客戶的資料庫、新頁面沒有機制長出選單。結果是——新版套件裝上去,功能存在、資料表可能不存在、管理員點進去 403、選單找不到入口。本案要把這條「套件宣告 → 出貨 bundle → 客戶升級 → 資料庫到位」的鏈補齊。
| 日期 | 變更 | 依據 |
|---|---|---|
| 2026-09-13 | 討論稿 v0:三個缺口的問題陳述+現況盤點+D1–D8 待決策+三階段拆分草案 | 探脈 A/B 報告(2026-09-13 唯讀實查) |
本案的前提是 FR-089/FR-091 完成後的世界:每支 jedi-* 套件都有標準形狀,其中兩樣東西跟資料庫有關——
CAPABILITIES 常數裡宣告「我的端點要用這幾個權限名稱守門」。這兩樣東西現在都只是宣告,沒有任何一條路把它們送進客戶的資料庫。以下三段各一個缺口,每段附探脈報告查到的檔案與行號當證據。
現況一句話:11 支套件共 24 支 SQL 躺在 wheel(Python 套件包)裡,從 DEV 到客戶端沒有任何環境跑過它們;同一批表的「真正 DDL」在主專案 scripts/sql/ 另有一份,兩份已經對不上。
三處斷點(沿著出貨線從頭數):
| 斷點 | 在哪 | 證據 |
|---|---|---|
| A. BE image 不帶 migration 檔 | Nuitka 編譯的 BE image 只複製白名單資料目錄,白名單沒有 */migrations |
scripts/build/build_release.sh:661-669 PKG_DATA_DIRS 只列 jedi_auth/translations、jedi_evidence_classification/resources |
| B. init image 只 COPY 主專案 SQL | 建庫/升級用的 init image 是 alpine+psql,沒有 Python,只拷 scripts/init/ 與 scripts/sql/,不掃 site-packages |
docker/init/Dockerfile:33,35;scripts/init/migrate.sh:46-47 只讀 /opt/guidant/sql/manifest.tsv |
| C. manifest 沒有任何一列指向套件檔 | 升級差集的唯一真相是 scripts/sql/manifest.tsv(269 行、active 且 envs=* 共 150 支),沒有套件檔 |
探脈 A §1;migrate.sh:99-106 差集只算 manifest 內的檔 |
套件那頭的形狀:iter_migrations() 回 [(檔名, SQL)](jedi-asset/jedi_asset/plugin/migrations.py:15-18),docstring 自己寫「套件只提供、不執行」(:13),而 BE 端零生產呼叫者。FR-080 review 已記「客戶端實際建表走 02-schema.sql,migrate.sh 完全不碰套件腳本」(docs/features/FR-080-*/review/2026-09-10-four-merges-review.md:215)。
雙份 DDL 已漂移:主專案 scripts/sql/ 有至少 41 支其實是「套件表」的 migration(detection/license/integrity/remote-agent/upload-file/participant/bulletin 整串),全已收進 02-schema.sql(179 個 CREATE TABLE)。套件裡那份是抽套件時回頭補寫的第二份;inventory/batch-D.md:836 記 detection 隨包 migration 缺 7 條 DEV 實存的 FK。
enum bug 活例(CM-1678):jedi-asset/jedi_asset/migrations/002-asset-rls-grants.sql:120-146 用 ALTER TYPE ... RENAME VALUE 把 compliance.system_status_enum、security_sensitivity_level_enum 收斂成 OSCAL 小寫(commit 3d9772f),001:23,31 的 CREATE TYPE 同步改。但出貨基線 02-schema.sql:116-120 仍是 'LOW','MODERATE','HIGH'、:127-133 大小寫混雜;主專案 scripts/sql/ 與 manifest grep CM-1678 零命中,三環境都沒套。這個修正對客戶端等於不存在——它只寫在一條沒人走的路上。
🔴 「有 migration」是錯覺
wheel 確實帶著 SQL(jedi-asset/pyproject.toml:42-45 顯式 include,抽查 1.0.1 wheel 都有),但出貨 image 不帶、init image 不讀、manifest 不列。三處任一斷掉都等於沒有,現在是三處全斷。往後任何人在套件裡改表、修 enum,都會像 CM-1678 一樣默默消失。
現況一句話:權限判定是拿「名稱」比對(require_capability("system_config.manage")),但名稱要先存在於 capabilities 表、又要被綁到角色,管理員才拿得到;今天沒有任何自動機制做這兩步。
證據:
jedi-iam/jedi_iam/authz/capability.py:134-160,比對取 name 集合交集(:120-127);底層 user_role_repo_impl.py:81-87 靠 Capability.name join RoleCapability.capability_id。key 是 name、join 靠 id——所以補 row 時 id 隨機無妨,只要 role_capabilities 用 name 反查 id。core/app_factory.py:294-301);Plugin.capabilities 只是宣告引用(_host.py:103);migrate.sh grep capabilit|seed 零命中;init.sh:124-126 依序跑 04 核心字典,05/06 留空。jedi-iam/.../tenant_provisioning_service.py:103-150 _add_tenant_with_defaults 取 capability 全集 → 三道扣除(不販售的 resource_type/is_platform/未授權模組,:127-133)→ 建 System Manager 帶 capability_ids。之後新增的能力點不回補既有租戶——DEV 印證 tenant 級角色 43/44=85、49=86、55/56/57=98 筆,都少於全集 134。roles.is_admin=1 不繞過 capability 守門(authz/admin.py 檔頭),唯一繞過是帳號層 users.is_super_admin(break-glass)。所以升級後:新能力點 row 不存在 → 判定失敗 → 403,且沒有錯誤訊息告訴你是缺 row。
ui_routes 就沒選單,補了卻漏綁權限就全員可見現況一句話:選單來自 ui_routes 表,頁面能不能看由 route_capabilities 決定;一張新頁面要同時補這兩處,而套件契約沒有這一段,fail-open(沒綁權限=放行)讓漏綁變成靜默的越權。
證據:
jedi-iam/.../ui_route_repo_impl.py:70-100:撈 user 持有的 capability_id 集合,ANY 持任一即可見、ALL 需全部;沒有任何 route_capabilities 的 route 視為可見(:77)。route_capabilities.capability_id 無 FK、role_capabilities.capability_id 也無 FK——指錯 id 資料庫不會擋(目前孤兒 0,但這是運氣不是機制)。ui_routes 寫死 id,pid(分組節點)指 42~47 的 group-*。現成樣板已經存在:scripts/sql/2026-08-29-cm1423-security-policy-page.sql 不寫 id、WHERE NOT EXISTS (name=)、route_capabilities 用 WITH r AS (SELECT id FROM ui_routes WHERE name=) 反查——只差沒有變成規則。
整案一句話:讓套件宣告的「需要哪些表」與「需要哪些權限、哪些選單」在客戶升級時自動到位——出貨線讀套件 migration、升級時補能力點與角色授權、每支新頁面必綁權限。
| 階段 | 做什麼 | 產出 | 完成怎麼判定(決策者檢查法) |
|---|---|---|---|
| 階段 1 出貨線接套件 migration | build 期把 site-packages 裡的套件 SQL 攤平進出貨 SQL 目錄並自動產子清單;init image 帶著它;migrate.sh 算差集時把套件檔一起算;stamp gate 守住「出貨包的 SQL 清單跟 wheel 一致」;DEV/STG/POC/已出貨客戶庫做一次性補登 |
build 腳本+init image+migrate.sh 改動、既有庫補登 SQL | 在乾淨機器裝新版,SELECT filename FROM schema_migrations WHERE filename LIKE 'packages/%' 看得到 24 支;在 DEV 補登後重跑 migrate.sh 顯示「待套 0 支」;CM-1678 的 enum 修正在 DEV \dT+ compliance.system_status_enum 看到小寫值 |
| 階段 2 能力點自動到位 | 出貨 seed 四段改成用名稱反查(不再寫死 id);升級時把 21 支套件+主專案宣告的能力點 upsert 進 capabilities;root Administrator 自動補齊全部、各租戶管理員依基準角色回補(扣掉平台級與未授權模組) |
name-based seed、宣告對照 JSON/SQL、migrate 模式新步驟 | 升級 DEV 後用租戶管理員(非 root)登入,點新套件的頁面不再 403;SELECT count(*) FROM role_capabilities WHERE role_id=2 等於 capabilities 總數;隨便刪一筆 capabilities 再跑升級,row 回來且 id 不同也不影響登入 |
| 階段 3 選單守衛+套件 SQL 冪等核對 | 每支套件的 migration 必含「補 ui_routes+綁 route_capabilities」;加守衛「有 ui_routes 的套件頁面必有 route_capabilities」;逐支核對 24 支套件 SQL 與 DEV 實況的差(已知 detection 差 7 FK)修到可重複執行 | 守衛測試、24 支套件 SQL 修正(併 jedi 1.1.0) | 故意寫一支只補 ui_routes 不綁權限的 SQL,守衛測試變紅;把 24 支套件 SQL 對著 DEV 連跑兩次不報錯(冪等) |
依賴一句:階段 1 是地基(沒有它階段 2/3 的 SQL 出不了貨);階段 2 不動套件源碼可與階段 3 平行;階段 3 的套件 SQL 修正要動 jedi-*,併 1.1.0 發版。決策者功課:本稿 D1–D8 逐項拍板,其中 D3(往後套件表只在套件改)是長期規則,拍了以後所有人開卡都照它走。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
subgraph PKG["套件 wheel(Nexus 1.0.1)"]
M["jedi_xxx/migrations/NNN-*.sql<br/>11 支套件 · 24 支 SQL"]
C["CAPABILITIES 宣告<br/>21 支 · 80 筆"]
end
subgraph BUILD["build_release.sh"]
PD["PKG_DATA_DIRS 白名單<br/>只有 translations/resources"]
end
subgraph INIT["init image(alpine+psql)"]
DF["Dockerfile COPY<br/>scripts/init/scripts/sql"]
MF["manifest.tsv<br/>150 支主專案檔"]
MS["migrate.sh 差集"]
end
subgraph DB["客戶 DB"]
SM["schema_migrations"]
CAP["capabilities/role_capabilities"]
end
M -. "斷點 A:不在白名單" .-x PD
M -. "斷點 B:不掃 site-packages" .-x DF
M -. "斷點 C:manifest 無此列" .-x MF
MF --> MS --> SM
C -. "無任何寫入路徑(啟動只 mount)" .-x CAP
SQL41["scripts/sql/ 41 支「其實是套件表」<br/>+02-schema.sql 179 張表"] --> MF
SQL41 -. "雙份 DDL 已漂移<br/>(detection 缺 7 FK、enum 大小寫)" .- M
| 元件 | 現況 | 本案動作 |
|---|---|---|
scripts/sql/manifest.tsv |
四欄 TSV,行序即套用順序;只列主專案檔,無套件列(探脈 A §1) | 不改格式;主 manifest 之後追加「套件子清單」段(D1/D2) |
scripts/init/migrate.sh |
讀單一 manifest 算差集(:99-106),grep '\.sql$' 過濾(:86),__init_baseline_* 守門(:92-95) |
加第二輪:主線套完後依套件子清單套 packages/<pkg>/;補登用相對路徑檔名(D2) |
public.schema_migrations |
filename text PK / applied_at / note,無 version/checksum/source(02-schema.sql:16056-16061) |
不動表結構;用 filename 內含相對路徑當命名空間(D2) |
scripts/build/build_release.sh copy_pkg_data_dirs() |
依 PKG_DATA_DIRS 從 site-packages 複製套件資料目錄(:671-683) |
加 */migrations 攤平到 /opt/guidant/sql/packages/<pkg>/ 並產子清單(D1) |
docker/init/Dockerfile |
COPY scripts/sql/ /opt/guidant/sql/(:35),無 Python |
改為 COPY build 期已攤平的目錄(含 packages/);不加 Python 層(D1) |
scripts/build/build_init_image.sh stamp gate |
99-stamp.sql 補登清單須與 manifest active/* 逐字一致(:70-83) |
擴到套件子清單:wheel 內檔案集合 vs 攤平集合 vs stamp 清單三者一致(D1) |
scripts/init/gen_stamp_sql.sh |
把 manifest active/* 補登、標 converged into __init_baseline_v<版>__(:66-70) |
補登清單同步納入套件檔(階段 1 既有庫補登也用同手法,D3) |
scripts/build/assert_db_current.sh |
前置斷言只比主 manifest(:68,174-199) |
同步比套件子清單 |
套件 migrations/*.sql(24 支) |
三位數序號檔名、無版號、無 manifest;已知 detection 缺 7 FK、asset 002 有 RENAME VALUE |
階段 3 逐支核對冪等;往後套件表只在這裡改(D3) |
主專案 scripts/sql/ 41 支套件表 migration |
已收進 02-schema.sql;歷史真相 |
保留不動;往後不再新增套件表 migration(D3) |
scripts/init/04-seed-core.sql capabilities/role_capabilities/route_capabilities/ui_routes 四段 |
pg_dump 產、id 寫死、ON CONFLICT DO NOTHING 無 target(L216/L476 起,探脈 B §2) |
改成 INSERT...SELECT name 反查,仿 labels 特案後處理 gen_seed_sql.sh(D4) |
scripts/init/gen_seed_sql.sh |
--inserts --column-inserts --on-conflict-do-nothing,無法指定 conflict target(L43-46) |
加四段後處理(D4) |
套件 CAPABILITIES 宣告(21 支 80 筆)+主專案 36 筆(CM-1748) |
只是引用,無寫入路徑 | build 期攤平成宣告對照,migrate 模式 upsert by name(D5) |
_add_tenant_with_defaults(jedi-iam) |
建租戶時一次性快照,三道扣除(:127-133) |
不改;回補邏輯複用同三道扣除(D6) |
trg_role_capabilities_platform_guard |
is_platform=true 只能給 root 租戶的角色,否則 raise |
回補段必 NOT is_platform(D6) |
ui_route_repo_impl.py fail-open(:77) |
無 route_capabilities 的 route 全員可見 | 不改邏輯;加守衛擋「有頁無綁」(D7) |
2026-08-29-cm1423-security-policy-page.sql |
name-based 補 ui_routes+route_capabilities 的現成寫法 | 升格為套件 migration 的樣板(D7) |
2026-08-01-fr059-2-detection-profile-capability.sql |
name-based capabilities+「參照角色法」補 role_capabilities 的現成寫法 | 升格為回補樣板(D6) |
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
participant W as 套件 wheel(site-packages)
participant B as build_release.sh/build_init_image.sh
participant I as init image(migrate 模式)
participant D as 客戶 DB
B->>W: 掃 <pkg>/migrations/*.sql + CAPABILITIES 宣告
B->>B: 攤平到 /opt/guidant/sql/packages/<pkg>/ + 產套件子清單 + 宣告對照 capabilities.json
B->>B: stamp gate:wheel 檔案集合 = 攤平集合 = 99-stamp 清單,不一致 build fail
B->>I: COPY 整個 /opt/guidant/sql(主線+packages/)
Note over I,D: 客戶執行 install.sh --upgrade
I->>D: SELECT filename FROM schema_migrations
I->>I: 差集①:主 manifest active/*(既有行為)
I->>D: 套主線待套檔(psql --single-transaction)+ INSERT schema_migrations
I->>I: 差集②:packages/<pkg>/NNN-*.sql 依套件名字母序、套件內序號序
I->>D: 套套件待套檔(每支 SQL 冪等)+ INSERT 'packages/<pkg>/NNN-*.sql'
I->>D: upsert capabilities by name(宣告對照 → INSERT ... ON CONFLICT (name) DO UPDATE description/is_platform)
I->>D: 回補 role_capabilities:root Administrator 補齊全部;租戶管理員照基準角色補,NOT is_platform + licensed_modules 扣除
D-->>I: 完成,印出「主線 N 支/套件 M 支/能力點新增 K 筆/回補角色 R 個」
D1 — 套件 migration 進出貨線的形狀:靜態併入 vs 動態掃描
兩個方向:
<pkg>/migrations/*.sql 攤平進 /opt/guidant/sql/packages/<pkg>/,自動產一份套件子清單,stamp gate 守「wheel 裡有的=攤平的=stamp 清單有的」三者一致。init image 照舊只是 alpine+psql,多 COPY 一個目錄。iter_migrations() 掃 site-packages。建議:靜態併入。理由:既有 copy_pkg_data_dirs()(build_release.sh:671-683)與 build_init_image.sh 的 stamp gate(:70-83)形狀就是「build 期產物+build 期驗證」,加一條白名單、擴一個 gate 就接上;動態掃描要把 init image 從 alpine 變成帶 Python 的 image,升級路徑多一個執行環境就多一種現場才會炸的失敗。另一個好處:攤平後的 SQL 是出貨包裡看得見的檔案,客戶端出事時工程師開包就能對。
D2 — 命名空間與排序:schema_migrations.filename 是裸檔名 PK,11 支套件全從 001- 起算必撞
schema_migrations 只有 filename text PK(02-schema.sql:16056-16061),套件檔名是 001-asset-tables.sql、001-participant-tables.sql……直接登記第二支就撞 PK。
建議:filename 記成 packages/<pkg>/NNN-xxx.sql——相對路徑本身就是命名空間,不動表結構、不加欄位。migrate.sh:86 的 grep '\.sql$' 與哨兵過濾照樣過得去。排序:主線 manifest 先跑完,再依套件名字母序、套件內序號序跑 001..NNN。跨套件依賴(如 task-platform 002 要先有 projects、002-*-rls-grants 要先有 cm_app 與租戶判定函式,jedi-asset/README.md:249-250)以「主線已把宿主生態建好」為前提,不做拓撲宣告——套件之間如果真的互相依賴表,那是套件疆界劃錯,應該回到 D3 的規則處理,不是加一層依賴解析器。配套硬規則:套件 SQL 一律冪等(CREATE ... IF NOT EXISTS、ON CONFLICT、DO $$ IF NOT EXISTS),這是階段 3 的核對目標。
D3 — 雙份 DDL 收斂責任(本案最重要的長期規則)
今天同一張套件表有兩份 DDL:主專案 scripts/sql/(41 支,已收進 02-schema.sql,是實際建表的那份)與套件 migrations/(24 支,從沒跑過,已漂移)。不定規則,「兩份會漂移」在本案接線後會變成「兩份都會被套」。
建議:往後套件疆界的表只在套件 migrations/ 改,主專案 scripts/sql/ 不再新增套件表的 migration;既有 41 支歷史 migration 保留不動(它們是已出貨基線的一部分)。FR-089 README:45 短期建議的「主專案 scripts/sql 另放一支」不採——那正是加深雙份。代價要說清楚:① 第一次上線要做既有庫一次性補登:DEV/STG/POC/已出貨客戶庫的表都在了但 schema_migrations 沒有套件檔名,新機制上線差集會算成 24 支全待套 → 大面積重放失敗;補登用 gen_stamp_sql.sh:66-70 的 converged 手法(標 converged into __init_baseline_*),先補登再上線。② 補登前要逐支核對 24 支套件 SQL 與 DEV 實況的差修到冪等(detection 已知差 7 FK;asset 002 的 RENAME VALUE 在已是小寫的庫上會炸,見風險段),這是階段 3。③ CM-1678 的 enum 修法就用這條線第一次真的套出去——它是活例也是驗收案。
D4 — 出貨 seed 的四段改成用名稱反查(不再寫死 id)
04-seed-core.sql 的 capabilities/role_capabilities/route_capabilities/ui_routes 四段是 pg_dump 產的:INSERT INTO capabilities (id, name, ...) VALUES (1, 'bulletin.create', ...) ON CONFLICT DO NOTHING(L216 起),role_capabilities 是寫死的整數對(L476 起)。gen_seed_sql.sh 用 --on-conflict-do-nothing 產不出 conflict target(L43-46)。
地雷組合:id 寫死 + ON CONFLICT 無 target + capability_id 無 FK。一旦 D5 的自動補 row 先跑、佔走某個 id,seed 那筆因 name unique 撞而靜默跳過,接著 role_capabilities (2, 135) 指向錯的能力點或孤兒——資料庫不擋、沒有錯誤、權限就是錯的。
建議:做。四段改 INSERT ... SELECT 以 name 反查 id(capabilities 用 ON CONFLICT (name);role_capabilities/route_capabilities 反查 capabilities.name+ui_routes.name;ui_routes 的 pid 分組反查 group-* 的 name),作法比照 labels 那段——在 gen_seed_sql.sh 加特案後處理,不改 pg_dump 參數。現成樣板:2026-08-01-fr059-2-detection-profile-capability.sql(name-based + 參照角色法)與 2026-08-29-cm1423-security-policy-page.sql(ui_routes 反查)。附帶效果:seed 不再依賴基線庫的 id 跳號(1,2,3,4,12,13,15…)。
D5 — 能力點自動補 row 的執行點與範圍
三個可能的執行點:BE 啟動期、migrate 模式、seed 檔。
建議:在 migrate 模式加一支「宣告對照」步驟,不在 BE 啟動期寫 DB。作法:init image build 期把 21 支套件的 CAPABILITIES(80 筆)+ CM-1748 主專案 36 筆攤平成一份 capabilities.json(或直接產 SQL),migrate 時 INSERT ... ON CONFLICT (name) DO UPDATE SET description, is_platform。理由:① 啟動期寫 DB 是多 worker 競態(gunicorn 多個 worker 同時起,誰先誰後、誰重試)+ RLS 身分問題(BE 用 cm_app 受 RLS,系統層寫入會 silent fail——CLAUDE.md 已載明 migration 一律 cmmgr);② migrate 模式本來就是「升級時用 cmmgr 改 DB」的唯一合法時點,能力點只是多一種要改的東西;③ 宣告對照是 build 期產物,跟 D1 的攤平同一道工序,stamp gate 一起守。範圍:只 upsert capabilities 表本體;角色授權另見 D6。
D6 — Administrator 與租戶管理員回補(升級等於自動擴權)
新能力點進了 capabilities 表還不夠,要綁到角色管理員才拿得到。兩類角色:
建議:① root Administrator 維持「持有全部」並自動補齊——改成繞過會跟設計哲學衝突(角色層 is_admin 不該有能力語意)且 UI 看不見它有什麼;持有全部的代價只是每次補 capability 同步補 role 2,這在 migrate 步驟裡是一條 INSERT ... SELECT。② 租戶管理員用**「參照角色法」(比照 fr059-2:凡持有某基準能力點的角色都補這批新能力點),不採 roles.is_admin=1 條件**——它會誤中客戶自訂的管理角色。③ 回補段必 AND NOT is_platform,否則 trg_role_capabilities_platform_guard 對非 root 租戶整批 raise。④ 比照 _add_tenant_with_defaults 三道扣除(不販售 resource_type/is_platform/licensed_modules 未授權模組,tenant_provisioning_service.py:127-133)——沒買的模組升級後不該憑空出現權限。基準能力點怎麼選(哪一筆代表「這是管理員」)是開卡時要定的細節,建議選各套件既有的核心 *.manage。
D7 — ui_routes/route_capabilities:套件契約要不要加 UI_ROUTES 宣告
選單來自 ui_routes,可見性來自 route_capabilities;缺前者沒入口、缺後者 fail-open 全員可見(ui_route_repo_impl.py:77)。要不要像 CAPABILITIES 一樣讓套件用 Python 宣告 UI_ROUTES,由本案的機制自動寫入?
建議:本案不擴契約。理由:ui_routes 有 pid 分組、sort、icon、url,這些是 FE 版面知識,用 Python 常數表達會把選單樹的一半塞進套件;而 SQL 已經有現成的 name-based 寫法(cm1423 那支)。作法:把「補 ui_routes + 綁 route_capabilities」列為每支有頁面的套件 migration 的必做項,以 cm1423 為樣板(不寫 id、WHERE NOT EXISTS (name=)、pid 用 group-* 的 name 反查而非硬編 47)。並加守衛:「有 ui_routes 的套件頁面必有 route_capabilities」——擋 fail-open,做法可以是 migrate 尾端一條斷言 SQL,或主專案守衛測試對套件 SQL 靜態掃描。契約擴充留給日後有第二個消費者時再議。
D8 — 排程與 jedi 1.1.0 的關係
建議:本案主體(階段 1、2)純主專案+build 腳本,不動套件源碼——套件 migrations/ 已在 wheel 裡、CAPABILITIES 已宣告,讀它們不需要改它們,所以不綁 1.1.0,可以在 21 支 1.0.x 上直接做。例外是 D3 的「24 支套件 SQL 修到冪等」+ D7 的「每支套件補 ui_routes/route_capabilities」要動套件,那部分併 1.1.0 發版(走 jedi-package-dev 的 path dependency dev loop,發版等令)。順序上:階段 1 先在主專案側接線並用現有 24 支驗通(DEV 補登後差集為 0 即可驗),套件 SQL 的修正隨 1.1.0 進來時再走同一條線第一次真的套。
只給形狀,不開卡;D 項拍板後轉 design.md 時細拆。
| 階段 | 內容 | 完成怎麼判定 |
|---|---|---|
| FR-093.1 出貨線接套件 migration | build_release.sh 白名單加 */migrations 並攤平到 packages/<pkg>/+產套件子清單(D1);docker/init/Dockerfile COPY 該目錄;migrate.sh 第二輪差集(相對路徑命名空間+字母序/序號序,D2);build_init_image.sh stamp gate 擴到套件;gen_stamp_sql.sh/assert_db_current.sh 同步;既有庫一次性補登 SQL(DEV→STG→POC→客戶,照環境鐵律逐級放行);CM-1678 enum 修正走這條線套進 DEV 當驗收案 |
乾淨機新裝:schema_migrations 有 24 支 packages/%;DEV 補登後 migrate.sh 待套 0;故意刪一支攤平檔 → build_init_image.sh fail;DEV \dT+ 看到小寫 enum |
| FR-093.2 能力點自動到位 | gen_seed_sql.sh 四段 name-based 後處理(D4);build 期宣告對照 capabilities.json(21 支 80 筆+主專案 36 筆,D5);migrate 尾端 upsert capabilities + root 補齊 + 租戶管理員參照角色法回補(NOT is_platform + 三道扣除,D6) |
租戶管理員登入點新頁不 403;role 2 筆數=capabilities 總數;刪一筆 capabilities 重跑升級回來且權限不變;非 root 租戶沒拿到任何 is_platform 能力點;未授權模組的能力點沒回補 |
| FR-093.3 選單守衛+套件 SQL 冪等核對(併 jedi 1.1.0) | 守衛「有 ui_routes 必有 route_capabilities」(D7);cm1423 寫法升格為套件 migration 樣板;24 支套件 SQL 逐支對 DEV 實況核對(detection 7 FK、asset 002 RENAME VALUE 防呆)修到冪等;每支有頁面的套件補 ui_routes/route_capabilities;發 1.1.0 | 守衛測試對「只補 ui_routes」的 SQL 變紅;24 支 SQL 對 DEV 連跑兩次零錯;1.1.0 裝上 DEV 後新頁面有選單且非管理員看不到 |
依賴:.1 → .2(.2 的 SQL 要走 .1 的線出貨);.3 與 .2 可平行但發版在 .1 驗通之後。
既有客戶升級失敗面:差集算成 24 支全待套
已出貨客戶庫的表都由 02-schema.sql 建好了,但 schema_migrations 沒有任何 packages/% 列。新機制上線那一版若沒先補登,migrate.sh 會把 24 支全算成待套並逐支重放——CREATE TABLE 撞表名、CREATE TYPE 撞型別名,--single-transaction 一炸整段回滾,升級卡死。
對策:① 補登 SQL 隨該版主線 manifest 出貨,排在套件差集之前執行(主線先跑完再算套件差集正是 D2 的排序理由之一);② 24 支全部冪等化是硬前置(階段 3),補登只是保險不是替代;③ 出包前用「帶舊資料升級」驗(既有規範:每版必須可直接升級不影響舊資料)。
自動擴權的產品面:新版一裝,租戶管理員自動多出權限
D6 的回補等於「升級=擴權」。對客戶是好事(新功能裝上就能用)也是風險(客戶可能刻意收掉某些權限,升級後又長回來)。
對策:① 只回補新增的能力點,不碰既有 role_capabilities(客戶刪掉的不會長回來);② 三道扣除保證不越過 licensed_modules;③ migrate 輸出明列「回補了哪些角色、哪幾筆」,客戶端工程師看得到;④ 手冊註明此行為。要不要提供「升級時不回補」的開關,開卡時再議。
🔴 ALTER TYPE ... RENAME VALUE 在有資料的庫上的行為
asset 002 的 enum 修正用 RENAME VALUE(002-asset-rls-grants.sql:120-146)。它在 PG 上會直接改型別定義、既有資料列跟著變(不需要 rewrite 表),這是好消息;壞消息有三:① 目標值已存在時炸(DEV 若已是小寫,再跑一次 RENAME VALUE 'LOW' TO 'low' 報 enum label "low" already exists)——與 D2「套件 SQL 一律冪等」直接衝突,要包 DO $$ IF EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel='LOW' ...);② RENAME VALUE 不能在同一 transaction 內接著使用新值(PG 的 enum 安全限制)——--single-transaction 下同一支 SQL 後段若 INSERT 新值會炸;③ 出貨基線 02-schema.sql:116-133 仍是大小寫混雜——新裝機走 02 建出舊型別、再由套件 002 改成小寫,新裝與升級兩條路要收斂到同一終態,這要在階段 1 拿 CM-1678 當驗收案時親眼驗。
stamp gate 變嚴後,套件發版與 BE 出包的耦合
擴大後的 gate 要求「wheel 裡的 migration 檔案集合=攤平集合=stamp 清單」。任何一支套件發新版帶新 SQL,BE 出包就要重跑 gen_stamp 才過 gate。這是刻意的(generate-then-commit 產物天生漂移,只驗烙印會出貨壞包),但要寫進 scripts/build/README.md 與 version-bump skill,否則第一次撞到的人會以為是 build 壞了。
scripts/sql/manifest.tsv、scripts/init/migrate.sh、scripts/init/02-schema.sql、scripts/build/build_release.sh、scripts/build/build_init_image.sh、docker/init/Dockerfile、11 支套件 migrations/、jedi 1.0.1 wheel 抽查。capabilities/role_capabilities/roles/ui_routes/route_capabilities schema 與筆數、04-seed-core.sql、gen_seed_sql.sh、jedi-iam authz 與 repo 實作、tenant_provisioning_service.py。assets 與 tamper_events 在 02-schema.sql 查不到同名表(可能表名不同或被排除);② 10 支沒有 migrations 的套件(ai-bot/ai-dashboard/common/compliance-audit/flow-engine/iam/issue/notification/oscal-v2/survey)是真的沒有表,還是表都在主專案 41 支裡——若是後者,D3 生效後它們第一次改表就要開 migrations/;③ D6 各套件的「基準能力點」清單。
public.devices/compliance.information_systems(001-asset-tables.sql:46,97),integrity 建 public.integrity_tamper_events(02-schema.sql:15104 已在基線)。D3「表都已存在」前提成立。