出貨升級鏈端到端驗收:乾淨新裝 vs 舊機升級(CM-1778,2026-09-14)

拿 build 好的 guidant-ai-1.20.0b1 安裝包,在兩台乾淨機器上各走一條路,比對終態是否一致。

  • A 路(新裝):空機 → 用新包裝一次
  • B 路(升級):空機 → 先裝 1.19.0 → 造客戶資料 → 用新包 --upgrade

結論:兩路終態實質一致,但發現一處真落差(內建稽核流程範本的 scope),詳見第 5 節。該落差會讓新裝客戶的租戶看不到兩個內建流程範本。依卡片第 ④ 條不自行修復,回寫等首腦裁示。

1. 驗收用的包

項目 值
新版包 guidant-ai-1.20.0b1.tar.gz(933 MB)
SHA256 daa49224cef4ff7f7e5a773956e10986a929b1de1d6be34a6b14bfa4aad52f43(與派工單一致)
來源 commit 4a7be93f(兩路 BE 實跑回報的 commit 也是這串)
舊版包 guidant-ai-1.19.0.tar.gz(2026-09-04 正式版)

取包方式為 scp 唯讀取檔,未對 188 做任何變更。

2. 驗收環境與打折說明

兩台「乾淨主機」都是本機 docker 內自建的 Debian 12 容器(amd64、跑自己的 dockerd),互相獨立、各自空白。

項目 實際狀況 是否打折
主機 OS/架構 Linux x86_64(容器內) ⚠️ 是。實體機是 macOS arm64,amd64 由 Docker Desktop 模擬。安裝包的 check_environment 硬性要求 Linux + x86_64,此模擬讓檢查通過。CPU 指令集層面與真實 x86 主機不完全等價
起始 image 數 0(兩台都是) 否。包自帶的 6 顆 image 全部靠自己載入
外部網路 實際切斷(docker network disconnect,容器內 DNS 解不到任何外部網域) 否。是真封閉網路,不是模擬
記憶體 7 GB(建議值 8 GB) ⚠️ 安裝程式印警告後照常通過。不影響功能,可能影響大檔匯入速度
主機時區 Etc/UTC(部署設 Asia/Taipei) ⚠️ 安裝程式印警告後照常通過
對外 port 80/443,未改埠 否
授權匯入 未驗 ⚠️ 是。本包以 --skip-prod-key-check 產出(無 PROD 授權簽發鑰),依派工單指示跳過此項

其餘各項(安裝流程、DB、權限、選單、檔案)皆為真實執行,無替代或跳過。

3. 兩條路的執行結果

步驟 A 路(新裝) B 路(升級)
環境檢查 通過(2 項警告:記憶體、時區) 同左
安裝/升級 install.sh --config → exit 0 先 1.19.0 → exit 0;再 --upgrade → exit 0
映像載入 6 顆全數載入 6 顆重新載入並通過完整性驗證
migration init 三輪建庫 主線 24 支 + 套件 46 支 = 實際套用 70 支,能力點 upsert 115 筆
系統檔灌入 10 個 補灌 10 個(不重複)
服務健康 6 個容器全 healthy 同左
跑的映像 guidant-ai-be/fe:1.20.0b1 完全相同
/api/1.0/version 1.20.0b1 / commit 4a7be93f 完全相同

升級前 B 路造的客戶資料:1 個租戶、1 個專案、1 筆正常稽核輪次、1 筆孤兒稽核輪次(父專案不存在,用來驗孤兒清理)、1 位專案參與者、1 個使用者上傳檔。

4. 逐項比對(一致的部分)

比對項目 方法 結果
DB 結構 pg_dump --schema-only 全文 diff 16904 行完全相同。唯一差異是 5 條 CHECK 的陣列轉型寫法(ARRAY[(x)::text,…] vs (ARRAY[x,…])::text[])與 pg_dump 每次隨機的 restrict token——逐條取 pg_get_constraintdef 核對,語意等價
表數量 information_schema.tables 兩路皆 169
權限(表+序列+預設 ACL) 全量列出排序 diff 4188 行,0 差異
RLS 開關與 policy pg_class + pg_policies 全量 461 條 policy、165 張表旗標,0 差異
CM-1800 的 13 張表 派工單指定查詢 兩路皆 13 張全部 relrowsecurity=t、每張 4 條 policy
CM-1800 哨兵 schema_migrations 兩路皆有 2026-09-14-fr094-cm1800-rls-project-scoped-tables.sql
migration 清單 全 211 筆檔名排序 diff 0 差異(兩路皆 211 筆)
索引 pg_indexes 全量 0 差異
函式 pg_proc 全量 0 差異(83 個)
enum 型別 全部 12 個型別與標籤 0 差異
能力點 127 筆,名稱/resource_type/action/is_platform 0 差異
路由↔︎能力點、角色↔︎能力點綁定 366 行全量 diff 0 差異
選單路由 48 筆,含層級/url/排序/啟用 0 差異
fail-open 路由 未綁能力點的葉節點 兩路皆 恰好 3 筆(license-status/my-tasks/project-dashboard,即 CM-1760 白名單)
登入後的實際選單 建租戶管理員 → 登入 → /user/web-menu 兩路皆 http 200,10 個節點、結構/網址/能力點名稱完全相同(僅自動編號流水號不同,屬正常)
檢測基準 detection_profiles 兩路皆 10 套;版本 10、控制項 4133、分類 13、工具 8,皆 0 差異
框架 oscal.frameworks 兩路皆 1 個(CMMC_2),版本 2、catalog 2
稽核階段字典 stage_objects 兩路皆 6 筆
storage-seed 檔案 bucket 內容逐檔比對 10 個系統檔,檔名與大小完全相同
框架 PDF 可讀性 從 filer 實際下載 兩路皆 http 200、2272014 bytes、%PDF- 開頭 %%EOF 結尾——不是空白檔
租戶管理員登入 設定精靈建帳號 → 登入 → 打 API 兩路皆成功,/oscal-frameworks/menu 200、/users/menu 200,無 403

升級路專項(新裝路不適用)

項目 結果
孤兒稽核輪次清理 升級前 2 筆(1 正常 + 1 孤兒)→ 升級後孤兒 0 筆、正常那筆完好。清理生效且沒誤刪
舊資料保留 租戶、專案、參與者、上傳檔全部還在
客戶上傳檔 cm1778-user.txt 升級後仍在 bucket 內,未被 storage-seed 補灌覆蓋

5. 🔴 發現的落差

5.1 內建稽核流程範本的 scope 兩路不同(真缺陷,會影響客戶)

新裝路 升級路
完整稽核流程 / 自我評估稽核 的 scope TENANT SYSTEM
非 1 號租戶的使用者看得到幾個內建範本 0 個 2 個

實測方式:以 cm_app(受 RLS 約束的業務帳號)模擬一個新租戶的 session,查 compliance.flow_templates。

為什麼會這樣(追到根因):

  1. scope='SYSTEM' 是 RLS policy 裡讓「原廠公版全租戶可讀」的那條分支(flow_templates_select 第一條件)。
  2. 這兩筆範本由 2026-09-14-fr093-4d-flow-templates-stage-objects.sql 插入,該 INSERT 沒有指定 scope 欄位,吃資料表預設值 TENANT。
  3. 本該把它改正的是 2026-09-14-fr094-cm1790-shared-scope-subtree.sql——它有一句 UPDATE … SET scope='SYSTEM' WHERE is_builtin=true。
  4. 升級路:兩支依序都跑(先插入、後回填)→ 正確變 SYSTEM。
  5. 新裝路:99-stamp.sql 把 CM-1790 蓋章為「已收斂進基線」所以不跑;但範本是蓋章之後才由 4d migration 插入的 → 沒有任何語句把它改成 SYSTEM,停在預設值。

關鍵佐證:188 基線庫本身這兩筆是 SYSTEM(唯讀查證,正確)。所以問題不在基線資料,而在「蓋章跳過的 migration,其回填對象卻在蓋章後才被另一支 migration 產生」這個順序交錯。

客戶端症狀:新安裝的客戶,建了自己的租戶後,流程範本清單是空的,看不到原廠附的兩個內建稽核流程;升級上來的舊客戶則正常看得到。且不會有錯誤訊息——RLS 過濾是靜默的。

未修復(依派工單第 ④ 條)。可能修法有幾種(把 4d 的 INSERT 補上 scope='SYSTEM'/基線重產時連帶處理/99-stamp 不蓋 CM-1790 的章),屬決策者裁示範圍,runner 不自行選。

5.2 public.schema_version 兩路不同(不影響功能,但仍是不一致)

新裝路 升級路
schema_version.version 1.20.0b1 1.19.0b1(停在舊值)
schema_migrations 的基線哨兵 __init_baseline_v1.20.0b1__ __init_baseline_v1.19.0b1__

原因:schema_version 只在新裝時由 99-stamp.sql 寫入,升級路的 migrate.sh 完全不碰這張表。

影響評估:查過全 codebase 與 install.sh/guidant 維運腳本,沒有任何程式或腳本讀 public.schema_version(Python 側同名字串都是 SSP 匯入的欄位,無關)。升級判定實際讀的是 /srv/guidant-ai/.env 的 GUIDANT_VERSION,兩路都正確是 1.20.0b1。所以目前不影響任何功能,但「同一版的兩台機器 DB 自報版號不同」對日後除錯與盤點是個誤導源。

基線哨兵那筆屬同源現象:升級路的庫當初是 1.19.0 裝的,該筆是歷史事實,不宜也不必改寫。

5.3 已排除、不算落差的項目

觀察到的差異 判定
派工單寫「能力點 115」,實測 127 非缺陷。115 是套件宣告數(升級日誌「upsert 115 筆能力點宣告」),127 是總數(含主專案)。出貨 seed 04-seed-core.sql 本身即 127 筆,兩路一致
派工單寫「新裝 enum 應為小寫」,實測有 6 個大寫 非缺陷。大寫的是流程引擎狀態 enum(jobstatus/taskstatus 等,值為 TODO,PROCESSING,…),與 CM-1756 Q2 講的 asset enum 不同組。asset 相關 enum 在此庫中不存在(無對應表)。兩路 12 個 enum 完全相同,不存在兩路不一致問題
5 條 CHECK 約束的 SQL 文字不同 非缺陷,pg_get_constraintdef 核對語意等價,僅 pg_dump 輸出寫法差異
選單能力點的數字 id 不同(如 72 vs 80) 非缺陷,自動編號,兩路產生順序不同所致;名稱層比對 0 差異
升級路多 1 個租戶、多 1 個上傳檔 預期,是驗收時刻意造的客戶資料,正是「舊資料沒弄丟」的證據

6. 待首腦裁示

  1. 5.1 內建流程範本 scope(會影響新裝客戶,建議優先):怎麼修、要不要重產出貨基線。
  2. 5.2 schema_version 升級不更新:要不要讓 migrate.sh 也寫這張表,或確認它已無用途可考慮廢除。