11 支 jedi-* 套件共 24 支 migrations/*.sql,逐支對本機 DEV(localhost:5432/guidant_ai_dev) 實況核對的結果。核對基準是 DEV 現況,不是 scripts/init/02-schema.sql。
結論:24 支全部通過,未修改任何一支。 差異共七類,逐類判定後都屬「套件正確」或 「DEV 端歷史殘留」,無一需要改套件 SQL。
三個臨時庫,都在本機、跑完即刪:
| 庫 | 怎麼來的 | 用來回答 |
|---|---|---|
guidant_ai_dev_snap |
DEV pg_dump --schema-only restore |
對既有庫連跑兩次會不會炸(冪等) |
| 同上,DROP 36 張套件表後重跑 | 同上 + 只用套件 SQL 重建 | 套件建出來的結構與 DEV 差在哪 |
guidant_ai_before / guidant_ai_after |
兩份相同的 DEV 複本,後者跑過 24 支 | 套件 SQL 對既有庫實際改了什麼 |
36 張套件疆界表由 24 支 SQL 的 CREATE TABLE 目標反推。空庫直跑會失敗 6 支 (缺 app_tenant_allowed_for_session() 與 compliance.projects),那是宿主前置依賴、 不是套件缺陷,故改用上表的「DEV 複本 DROP 後重建」取得對等基準。
| 輪次 | 對象 | 結果 |
|---|---|---|
| 第一輪 | DEV 複本(表已存在) | 24/24 OK |
| 第二輪 | 同一庫再跑一次 | 24/24 OK |
| 重建輪 | DROP 36 張後只用套件 SQL 建 | 24/24 OK |
psql 的 NOTICE: relation ... already exists, skipping 不是錯誤,是 IF NOT EXISTS 正常跳過時的訊息;判定只看 ERROR:。
突變測試:把 jedi-file-upload/001 的 CREATE TABLE IF NOT EXISTS public.upload_files 改成裸 CREATE TABLE,第二輪立刻紅(ERROR: relation "upload_files" already exists), 證明這組斷言真的在驗冪等而非空跑。驗畢已還原,git status 乾淨。
DEV 的 public.api_logs 按 act_time 切月分割(5 個分割區),主鍵 (id, act_time); 套件建的是普通表,主鍵 (id)。
判定套件正確。001-api-log-tables.sql 檔頭已明文說明:分割是維運決定不是資料模型, 取決於 consumer 的量體與保留政策;ORM model 宣告的也是普通表,SQLAlchemy 對「底下是不是 分割表」無感。主專案量大所以分割,小型 consumer 不需要。對主專案全程 no-op。
派工卡引用的 batch-D.md:836 記載 detection 隨包 migration「0 處 REFERENCES、缺 7 條 FK」。 實查已經補上了:001-detection-tables.sql:449-473 七條俱全,DEV 與套件建出來的庫 各查到同樣 7 條。補的是 a35e09d fix(CM-1628),時間晚於 batch-D.md 的盤點。
本棒無事可做。此處記錄以免下一棒再照舊文件找一次。
| 表 | DEV | 套件 |
|---|---|---|
public.upload_files |
pk_upload_files / upload_files_uid_unique |
upload_files_pkey / upload_files_uid_key |
compliance.remote_agents |
file_agents_pkey / file_agents_uid_key |
remote_agents_pkey / remote_agents_uid_key |
判定兩邊都可接受,不改。約束語意完全相同(同樣的 PRIMARY KEY / UNIQUE、同樣的欄位), 只有名字不同。file_agents_* 是 remote_agents 改名前的殘留,DEV 與 02-schema.sql 都還帶著它。
不改的理由:全 codebase grep 不到任何一處引用這些約束名(沒有 ON CONFLICT ON CONSTRAINT、 沒有 DROP CONSTRAINT),名字不構成契約;而套件在既有庫上 IF NOT EXISTS 判斷是看 約束是否存在,既有庫已有 PK 就整段跳過,不會多建一個。改名反而要寫 rename 邏輯、 在客戶庫上動既有約束,風險大於收益。
DEV 的 public.bulletins 沒有 uid 唯一約束,套件 002-bulletin-rls-grants.sql:74 會加 bulletins_uid_key UNIQUE (uid)。這是本次唯一「套件在既有庫上真的動了結構」的一處 (見下節「套件對既有庫的實際影響」)。
判定套件正確,保留。uid 是 UUID 識別碼,本來就該唯一;DEV 現有 3 筆資料 uid 全不重複(count(*)=3, count(distinct uid)=3, null=0),加約束不會失敗。 DEV 缺這條才是漏的。
DEV 有、套件沒有:
tenant_licenses_org_unit_id_fkey / tenant_license_events_org_unit_id_fkey → public.org_units(id)idx_tenant_licenses_license_id_current(license_id WHERE is_current = true)判定套件正確,不補。
FK 指向 public.org_units,那是 jedi-iam 的表,不在 jedi-license-runtime 的疆界內。 套件不能假設 consumer 一定裝了 jedi-iam——001-license-tables.sql 檔頭明寫 tenant_id/org_unit_id 在單租戶宿主的 ORM 根本不會被帶上(TenantScopedMixinModel 只在 ENABLE_MULTI_TENANT=true 時才產生這兩欄)。硬加跨套件 FK 會讓只裝 license-runtime 的 consumer 直接裝不起來。這與 FR-080 D17 軟參照同一原則。
部分唯一索引來自 CM-1580(跨租戶重複用照的 TOCTOU 防護),是主專案的業務規則—— D6 replace 制下「同一 license_id 可有多列歷史、同時只能一列現行」是 Guidant AI 的產品決定, 不是 license 資料模型的普遍約束。它已在主專案 scripts/sql/2026-09-06-cm1580-*.sql 且標 envs=* 隨出貨走,客戶庫拿得到,不需要套件再帶一份。
四條 CHECK(chk_lfs_protocol、chk_lfs_transport、ck_tenant_license_events_type、 ck_tenant_licenses_status、ck_integrity_tamper_events_detected_by)在 DEV 渲染成 ANY (ARRAY[('a')::text, ('b')::text]),套件建出來渲染成 ANY ((ARRAY['a','b'])::text[])。
判定完全等價,不動。同一段 SQL 文字,PostgreSQL 因建立時的型別推導路徑不同而 反序列化成兩種寫法,約束行為一模一樣。
7 張表(information_systems、task_assignees 與 5 張 participants)在 DEV 上 cm_app 拿到 DELETE,INSERT,REFERENCES,SELECT,TRIGGER,TRUNCATE,UPDATE,套件只給 SELECT, INSERT, UPDATE, DELETE。
判定套件正確,不追 DEV。關鍵證據是出貨基線本身:scripts/init/03-grants.sql (客戶裝機實際跑的那支)只給四權,且 02-schema.sql 是用 pg_dump --no-privileges 產的、完全不帶權限資訊。也就是客戶庫從來就只有四權, 套件與出貨基線一致,DEV 才是那個不一樣的。
DEV 上有 ALL 的共 19 張表,其中 4 張(job_evidences、drive_sync_jobs 等) 根本不屬任何套件——足證這是 DEV 早年手動 GRANT ALL 的殘留,不是規範。 其餘 154 張表都是標準四權。
「連跑兩次不炸」不等於「什麼都沒做」。拿兩份相同的 DEV 複本、一份跑過 24 支, pg_dump 對比,套件 SQL 在既有庫上確實會改東西:
bulletins_uid_key(見 ④,正確且必要)。COMMENT 的改寫方向值得注意——套件版去掉了 Guidant 內部追蹤編號,改成通用描述:
| DEV(主專案版) | 套件版 |
|---|---|
FR-056.3 Agent 掃描派工單 + 狀態機 |
Agent 派工單 + 狀態機 |
FR-039 客戶端 agent registry |
遠端代理 registry |
soft-ref → 對應稽核任務,FR-056.4 才填 |
soft-ref → 宿主的執行紀錄 |
判定這是對的方向,不擋。套件要能給任何 consumer 用,DB COMMENT 裡帶 FR-056.3 / CM-952 這種 Guidant 內部編號會隨 migration 落進別人的資料庫 (batch-D.md 第 6 點已點名此事)。套件版去編號化正是在修它。
且有幾條套件版的資訊更新更準,例如 agent_tasks.status 套件版列了 cancelled(DEV 版漏)、information_systems.system_status 套件版列全五個值 (DEV 版只列三個)。
另有 3 條 COMMENT 是純新增(DEV 的 api_logs 分割表上沒有表/欄位註解,套件補上)。
這件事要讓首腦知道:D3「往後套件表只在套件改」生效後,客戶升級跑到這 24 支時, 資料庫裡的表說明會從「主專案措辭」換成「套件措辭」。這不影響任何功能(COMMENT 純文件), 但 02-schema.sql 下次重產時會把套件措辭收進基線,屬預期行為。
design §8 問:ai-bot/ai-dashboard/common/compliance-audit/flow-engine/ iam/issue/notification/oscal-v2/survey 這 10 支沒有 migrations/, 是真的沒有表,還是表在主專案 41 支歷史 migration 裡?
答案:三種情況都有。
| 套件 | 有無 ORM 表宣告 | 表在哪 | D3 生效後第一次改表要開 migrations/? |
|---|---|---|---|
| ai-bot | 無(0 個 model 檔) | 無表,狀態存 Redis KV | 不需要 |
| ai-dashboard | 無 | 無表,完全無狀態 | 不需要 |
| notification | 無 | 無表 | 不需要 |
| common | 有(system_logs) |
主專案 41 支內,DEV/02-schema 皆有 | 要 |
| compliance-audit | 有(9 張,poams/project_audit_rounds 等) |
同上 | 要 |
| flow-engine | 有(5 張,workflow_executions 等) |
同上 | 要 |
| iam | 有(19 個 model 檔,capabilities/org_units 等) |
同上 | 要 |
| issue | 有(labels/issue_assignee_mapping 等) |
同上 | 要 |
| oscal-v2 | 有(45 張,ap_tasks 等) |
同上 | 要 |
| survey | 有(15 個 model 檔,survey_pages 等) |
同上 | 要 |
逐表實查 DEV 與 02-schema.sql 都在(抽驗 system_logs/poams/project_audit_rounds/ workflow_executions/capabilities/org_units/labels/survey_pages/ap_tasks 九張, 兩邊皆命中)。
待辦(等令,屬收尾類):把「7 支有表無 migrations/ 的套件,D3 生效後第一次改表 要先開 migrations/ 目錄」寫進 docs/claude/jedi-packages.md。
BE 的 scripts/sql/packages/ 是 build 期由 flatten_pkg_migrations.sh 從 site-packages 攤平的,不是從源碼。實查三邊一致:
scripts/sql/packages/ 24 支:逐檔 diff 全同.venv/lib/python3.11/site-packages/jedi_*/migrations/:24/24 全同jedi_*.pth / __editable__* 皆無)亦即本棒核對的源碼內容,與出貨會帶出去的內容是同一份。
四個臨時庫(guidant_ai_dev_snap/guidant_ai_pkgonly/guidant_ai_before/guidant_ai_after) 驗畢即刪。DEV 本身全程唯讀,未執行任何寫入。188/STG/POC 未連線。