---
title: "更新站 D1 資料表 (FR-167)"
brand: "Guidant AI · **FR-167** 線上更新"
eyebrow: "FR-167 · 資料表規格 · 2026-11-01"
h1: "更新站資料表"
subtitle: "update-center D1（SQLite）七張表：欄位、約束、索引、誰寫誰讀、現況筆數，以及 migration 對照"
lede: "更新站的資料庫是 Cloudflare D1（SQLite），庫名 `guidant-update-center`、綁定名 `UPDATE_DB`。七張表裡沒有任何客戶名稱欄位：回報只記授權編號、機器指紋、版號、結果、時間（D9）。本頁欄位以 `update-center/migrations/0001～0004.sql` 為準，寫入與讀取對回 `src/` 的函式，現況筆數為 2026-11-01 唯讀實查。"
---

## 總覽 {#overview nav="總覽"}

| 表 | 用途 | 主鍵 | 寫入者 | 現況筆數 |
|---|---|---|---|---|
| `product_releases` | 發行版：一版一列，含簽好的清單原文 | `version` | CI `register-draft`、CLI `publish`、後台撤回／說明／內部先裝 | 10 |
| `instance_version_reports` | 每台客戶主機現在幾版、最近一次升級結果 | `(license_id, fingerprint)` | `/v1/check`、`/v1/report`（host） | 3 |
| `agent_version_reports` | 每台 Agent 現在幾版、最近一次升級結果（主系統代報） | `(license_id, agent_fingerprint)` | `/v1/report`（agent） | 2 |
| `release_download_log` | 誰在什麼時候拿了哪一版的整包下載網址 | `id` 自增 | `/v1/download-url`（整包） | 15 |
| `license_denylist` | 拒絕提供更新的授權 | `license_id` | 後台 | 0 |
| `signing_keys` | 發布印樣（公鑰）登記表，只給後台顯示 | `kid` | CLI `sync-keys`、後台 | 0 |
| `ci_tokens` | CI 用的 scoped token（只存雜湊） | `id` 自增 | 後台 | 0 |

另有 wrangler 管理的 `d1_migrations`（已套 `0001_init.sql`～`0004_agent_version_reports.sql`）。

**D1 的型別慣例**（`0001_init.sql:1-2`）：

- SQLite 沒有陣列型別，陣列欄位存 **JSON 文字**，以 `CHECK (json_valid(...))` 守住；讀取用 `json_each`。
- 時間一律 **ISO 8601 UTC 字串**，Worker 以 `new Date().toISOString()` 寫（帶毫秒與 `Z`）。
- 布林存 `INTEGER` 0／1，以 `CHECK (x IN (0, 1))` 守住。
- 版號存 `TEXT`。**不可用 SQL 字串比大小**（`"1.10.0" < "1.9.0"`）；排序與比較都在 Worker 用 `src/versions.ts` 做。

## 關聯圖 {#erd nav="關聯圖"}

表之間**沒有宣告外鍵**，都是邏輯關聯：

```{.mermaid cap="圖 1 — 更新站七張表的邏輯關聯（虛線＝無外鍵約束）"}
%%{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'}}}%%
erDiagram
  product_releases {
    TEXT version PK
    INTEGER major
    TEXT min_base_version
    TEXT r2_key UK
    TEXT status
    TEXT internal_fingerprints "JSON 陣列"
    TEXT signed_manifest
    TEXT kid
    TEXT min_agent_version
  }
  instance_version_reports {
    TEXT license_id PK
    TEXT fingerprint PK
    TEXT current_version
    TEXT last_upgrade_to
  }
  agent_version_reports {
    TEXT license_id PK
    TEXT agent_fingerprint PK
    TEXT host_fingerprint
    TEXT current_version
    TEXT last_upgrade_to
  }
  release_download_log {
    INTEGER id PK
    TEXT license_id
    TEXT fingerprint
    TEXT version
  }
  license_denylist {
    TEXT license_id PK
    TEXT reason
  }
  signing_keys {
    TEXT kid PK
    TEXT status
  }
  ci_tokens {
    INTEGER id PK
    TEXT name UK
    TEXT token_hash UK
  }
  product_releases ||..o{ release_download_log : "version"
  product_releases ||..o{ instance_version_reports : "version = current_version／last_upgrade_to"
  product_releases ||..o{ agent_version_reports : "Agent 版號，非主系統版號"
  product_releases }o..o{ instance_version_reports : "internal_fingerprints 含 fingerprint"
  signing_keys |o..o{ product_releases : "kid"
  license_denylist |o..o{ instance_version_reports : "license_id"
  instance_version_reports ||..o{ agent_version_reports : "license_id＋host_fingerprint"
```

注意 `agent_version_reports.current_version` 是 **Agent 自己的版號**（例如 1.4.8），不是主系統版號。後台「Agent（在版／退回／失敗）」那欄拿它和 `product_releases.version` 比對（`releaseAdoption`，`src/releases.ts:370-385`），只有兩者版號剛好相同時才會計到數；這是現有統計的語意，看那欄時要知道它比對的是什麼。

## product_releases {#product-releases nav="發行版"}

**用途**：一版一列。CI 登記成草稿，決策者蓋章後變已發布，撤回是終點。同一版號只會有一列：重登記草稿是覆蓋不是新增。

| 欄位 | 型別 | NOT NULL | 預設 | CHECK／約束 | 說明 | 來源 |
|---|---|---|---|---|---|---|
| `version` | TEXT | 是（PK） | — | PRIMARY KEY | 版號 `X.Y.Z[-後綴]` | 0001:6 |
| `major` | INTEGER | 是 | — | `major >= 1` | 由 `version` 第一段推，不收 CI 自報 | 0001:7 |
| `min_base_version` | TEXT | 是 | — | — | 可直升的最低起始版；`planPath` 由它遞推 | 0001:9 |
| `sha256` | TEXT | 是 | — | — | 整包雜湊（Worker 端驗 64 hex） | 0001:10 |
| `size` | INTEGER | 是 | — | `size > 0` | 整包位元組數 | 0001:11 |
| `r2_key` | TEXT | 是 | — | UNIQUE | 整包的 R2 key，只由 `r2Key()` 產生 | 0001:13 |
| `status` | TEXT | 是 | `'draft'` | `IN ('draft','published','withdrawn')` | | 0001:14 |
| `internal_fingerprints` | TEXT | 是 | `'[]'` | `json_valid` | 草稿先開給哪幾台（JSON 陣列，機器指紋 64 hex，≤20） | 0001:16 |
| `has_security_fix` | INTEGER | 是 | `0` | `IN (0, 1)` | | 0001:17 |
| `signed_manifest` | TEXT | 否 | — | — | 決策者簽好的 v2 信封原文，更新站不解、不改 | 0001:19 |
| `kid` | TEXT | 否 | — | — | 主簽章 kid | 0001:20 |
| `retain_until` | TEXT | 否 | — | — | 登記時寫死 = 現在 + 10 年（D29） | 0001:22 |
| `created_at` | TEXT | 是 | — | — | 登記時間；重登記草稿會更新 | 0001:23 |
| `published_at` | TEXT | 否 | — | — | 最近一次蓋章送回的時間（同版重簽會覆蓋） | 0001:24 |
| `withdrawn_at` | TEXT | 否 | — | — | | 0001:25 |
| `release_notes` | TEXT | 是 | `'[]'` | `json_valid` | 「這一版更新了什麼」字串陣列（≤20 條、每條 ≤500 字） | 0002:4 |
| `impact` | TEXT | 是 | `''` | — | 影響範圍（≤2000 字） | 0002:5 |
| `action` | TEXT | 是 | `''` | — | 建議動作（≤2000 字） | 0002:6 |
| `min_agent_version` | TEXT | 否 | — | — | 最低相容 Agent 版；CI 登記時帶，CLI 蓋進簽章 | 0003:3 |

**表層 CHECK**：`status <> 'published' OR (signed_manifest IS NOT NULL AND kid IS NOT NULL)`（0001:26）——已發布的版一定有簽好的清單與 kid，連直接寫 D1 也繞不過。

**索引**：`idx_product_releases_major_status (major, status)`（0001:28），給 `visibleReleases` 的 `WHERE major = ? AND status …` 用；另有 PK 與 `r2_key` UNIQUE 的自動索引。

**狀態轉換**：

```{.mermaid cap="圖 2 — product_releases.status 狀態機"}
%%{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'}}}%%
stateDiagram-v2
  [*] --> draft: register-draft（CI）
  draft --> draft: register-draft 同版號（覆蓋；包換了清內部先裝結果）
  draft --> published: publish（內部先裝全成功）
  published --> published: publish 同版重簽
  draft --> withdrawn: withdraw
  published --> withdrawn: withdraw
  withdrawn --> [*]
```

| 寫入 | 函式 | 動到的欄位 |
|---|---|---|
| 建／覆蓋草稿 | `registerDraft`（`src/releases.ts:226-250`） | 新增全部；覆蓋時只改 `min_base_version`、`sha256`、`size`、`has_security_fix`、`created_at`、`min_agent_version`，`WHERE status = 'draft'` |
| 發布／重簽 | `publishRelease`（`:331-347`） | `status`、`signed_manifest`、`kid`、`published_at` |
| 撤回 | `withdrawRelease`（`:350-356`） | `status`、`withdrawn_at` |
| 內部先裝 | `setInternalTargets`（`:310-318`） | `internal_fingerprints`，只有 `draft` |
| 版本說明 | `setReleaseNotes`（`:411-421`） | `release_notes`、`impact`、`action`（`COALESCE` 部分更新），`draft`／`published` |

| 讀取 | 函式 | 用途 |
|---|---|---|
| `visibleReleases`（`:62-75`） | 客戶 `check`／`download-url` | 同主版號 `published` ＋ `draft` 且 `json_each(internal_fingerprints)` 含這台 |
| `getRelease`、`listReleases`（`:270-272`、`:423-426`） | 後台、CLI | `SELECT *`；列表在 Worker 依 semver 新到舊排 |
| `releaseAdoption`（`:370-385`） | 後台列表 | 每版關聯回報表算台數 |

**現況**（2026-11-01）：10 列（1.27.1～1.27.10），全部 `published`、`kid` 皆為測試章 `b91819f86eb4b4f8`、`min_base_version` 皆 `1.0.0`、`min_agent_version` 皆 `1.0.0`、`has_security_fix` 皆 0、`internal_fingerprints` 皆 `[]`、最早 `retain_until` 為 2036-10-30。

## instance_version_reports {#instance-reports nav="主機回報"}

**用途**：每台客戶主機一列（同一台換過授權會多一列），記「現在幾版、最後一次來問是什麼時候、最近一次升級結果」。**刻意沒有客戶名稱欄位**：不要順手加 `customer_code`／`issued_to`，那會讓更新站變成客戶名單（0001:30-31）。

| 欄位 | 型別 | NOT NULL | CHECK | 說明 | 來源 |
|---|---|---|---|---|---|
| `license_id` | TEXT | 是（PK） | — | 授權編號 | 0001:33 |
| `fingerprint` | TEXT | 是（PK） | — | 主機機器指紋（64 hex） | 0001:34 |
| `current_version` | TEXT | 是 | — | 主系統現行版號 | 0001:35 |
| `last_seen` | TEXT | 是 | — | 最近一次 check／report 時間 | 0001:36 |
| `last_upgrade_result` | TEXT | 否 | `IN ('succeeded','rolled_back','failed','skipped')` | | 0001:37 |
| `last_upgrade_from` | TEXT | 否 | — | | 0001:38 |
| `last_upgrade_to` | TEXT | 否 | — | | 0001:39 |
| `last_upgrade_at` | TEXT | 否 | — | | 0001:40 |

**主鍵**：`(license_id, fingerprint)`（0001:41）。沒有另建索引；`internalTargetStatus` 依 `fingerprint IN (…)` 查，筆數小，未建索引。

| 寫入 | 函式 | 動到的欄位 |
|---|---|---|
| `/v1/check` | `touchInstance`（`src/instances.ts:8-17`） | upsert `current_version`、`last_seen`；**不碰** `last_upgrade_*` |
| `/v1/report`（host） | `recordUpgrade`（`:21-41`） | upsert 全部欄位；`current_version` 取 body 的 `current_version`，`last_upgrade_at = last_seen = 現在` |
| 重登記草稿且包換了 | `clearInternalResults`（`src/releases.ts:252-261`） | 那幾台內部機器對這版的 `last_upgrade_result` 設 NULL |

讀取：`internalTargetStatus`（發布前核內部先裝）、`releaseAdoption`（`on_version`＝`current_version` 等於該版；`rolled_back`／`failed`＝`last_upgrade_to` 等於該版且結果相符；`major_total`＝`current_version LIKE '<major>.%'`）。

**現況**：3 列——兩台 `current_version = 1.27.8`、一台 `1.27.10`，`last_upgrade_result` 皆 `succeeded`。

## agent_version_reports {#agent-reports nav="Agent 回報"}

**用途**：主系統代 Agent 回報升級結果，一台 Agent 一列。Agent 沒有自己的授權，身分是主機的授權。同樣不存客戶名稱與 Agent 主機名稱（0004:2-3）。

| 欄位 | 型別 | NOT NULL | CHECK | 說明 | 來源 |
|---|---|---|---|---|---|
| `license_id` | TEXT | 是（PK） | — | 主機的授權編號 | 0004:6 |
| `host_fingerprint` | TEXT | 是 | — | 代報的那台主機的指紋 | 0004:7 |
| `agent_fingerprint` | TEXT | 是（PK） | — | Agent 指紋（64 hex） | 0004:8 |
| `current_version` | TEXT | 是 | — | Agent 現行版號：`succeeded` 時 = `to`，其他結果 = `from` | 0004:9 |
| `last_upgrade_result` | TEXT | 否 | `IN ('succeeded','rolled_back','failed','skipped')` | | 0004:10 |
| `last_upgrade_from` | TEXT | 否 | — | | 0004:11 |
| `last_upgrade_to` | TEXT | 否 | — | | 0004:12 |
| `last_upgrade_at` | TEXT | 否 | — | | 0004:13 |
| `last_seen` | TEXT | 是 | — | 最近一次回報時間 | 0004:14 |

**主鍵**：`(license_id, agent_fingerprint)`（0004:15），**不含** `host_fingerprint`：Agent 換掛到同授權另一台主機時覆寫同一列，不是多一台。

寫入：只有 `/v1/report` `target=agent` 的 `recordAgentUpgrade`（`src/instances.ts:47-68`），upsert 全部欄位。`current_version` 只在成功時寫 `to`：退回／失敗／略過時 Agent 還停在 `from`，寫 `to` 會讓後台把沒升上去的 Agent 算進「在這版」。

讀取：`releaseAdoption` 的 `agent_on_version`、`agent_rolled_back`、`agent_failed`。

BE 送出端欄位對應：`agent_rollout_service._unreported`（`app/product_update/service/agent_rollout_service.py:428-438`）送 `agent_fingerprint`、`result`（rollout 列 `status`）、`from_version`、`to_version`（rollout 列 `target_version`）；送達才寫 `reported_at`。

**現況**：2 列——一台 `succeeded`、`current_version = 1.4.8`；一台 `failed`、`current_version = 1.4.7`（失敗時停在 `from`，符合上述規則）。

## release_download_log {#download-log nav="下載紀錄"}

**用途**：稽核「誰在什麼時候拿了哪一版的下載網址」。只記**發出網址**；R2 實際有沒有下載完不在這裡（0001:44）。

| 欄位 | 型別 | NOT NULL | 說明 | 來源 |
|---|---|---|---|---|
| `id` | INTEGER | 是（PK） | AUTOINCREMENT | 0001:46 |
| `license_id` | TEXT | 是 | | 0001:47 |
| `fingerprint` | TEXT | 是 | | 0001:48 |
| `version` | TEXT | 是 | | 0001:49 |
| `at` | TEXT | 是 | | 0001:50 |

**索引**：`idx_release_download_log_version (version, at)`（0001:52）。

寫入：`logDownload`（`src/instances.ts:70-75`），只在 `/v1/download-url` 回**整包**網址時；`artifact=ops` 小檔不記。目前沒有任何端點讀這張表（後台未顯示），供直接查 D1 稽核。

**現況**：15 列；依版號 1.27.8 四筆、1.27.1 三筆，其餘八版各一筆。

## license_denylist {#denylist nav="拒絕名單"}

**用途**：被停止提供更新的授權（D12：由決策者在後台手動加）。在名單上的授權打三個客戶端端點一律 403 `license_denied`，連有沒有新版都不回。

| 欄位 | 型別 | NOT NULL | 說明 | 來源 |
|---|---|---|---|---|
| `license_id` | TEXT | 是（PK） | `^[A-Za-z0-9_-]{1,64}$`（Worker 驗） | 0001:56 |
| `reason` | TEXT | 是 | ≤500 字 | 0001:57 |
| `added_by` | TEXT | 是 | Access 信箱，或 `ci:<名稱>` | 0001:58 |
| `at` | TEXT | 是 | | 0001:59 |

| 動作 | 函式（`src/denylist.ts`） |
|---|---|
| 判定（客戶端三端點） | `isDenied`：`SELECT 1 … WHERE license_id = ?` |
| 列表 | `listDenied`：`ORDER BY at DESC` |
| 加入 | `addDenied`：`ON CONFLICT (license_id) DO UPDATE` 理由、操作者、時間（留最新一次） |
| 移除 | `removeDenied`：實刪 |

**現況**：0 列。

## signing_keys {#signing-keys nav="印樣表"}

**用途**：發布印樣（公鑰）的登記表，只給後台與 `GET /admin/keys` 顯示。**更新站不用它驗任何東西**：客戶端只信自己機器上 `/etc/guidant/release-keys/*.pub`，這張表改錯也騙不了客戶端（`src/keys.ts:1-3`）。私鑰永遠不進這裡。

| 欄位 | 型別 | NOT NULL | CHECK | 說明 | 來源 |
|---|---|---|---|---|---|
| `kid` | TEXT | 是（PK） | — | 公鑰 raw 32 bytes 的 sha256 前 16 hex | 0001:65 |
| `pubkey` | TEXT | 是 | — | base64 的 32 bytes Ed25519 公鑰 | 0001:66 |
| `status` | TEXT | 是 | `IN ('primary','backup','revoked')` | | 0001:67 |
| `created_at` | TEXT | 是 | — | 第一次登記時間（之後 upsert 不改） | 0001:68 |
| `revoked_at` | TEXT | 否 | — | | 0001:69 |

寫入：`upsertKey`（`src/keys.ts:41-52`）。登記前 `kidMatches` 核 kid 與公鑰對得上；**作廢單向**：已 `revoked` 的 kid 再送任何狀態都回 409 `kid_revoked`。CLI `sync-keys` 把本機角色對應成站上狀態：主力 → `primary`、備用 → `backup`、換鑰中新章 → `backup`、作廢 → `revoked`。

讀取：`listKeys`（依 primary、backup、revoked 排）；`publish` 端點查「這個 kid 是不是 `revoked`」，是就 409（只擋作廢、不要求必須登記過）。

**現況**：0 列（尚未跑過 `sync-keys`）。

## ci_tokens {#ci-tokens nav="CI token"}

**用途**：GitLab CI 打 `register-draft` 的 scoped token。只存 sha256 雜湊，明文只在建立當下回一次（0001:72-73）。

| 欄位 | 型別 | NOT NULL | 預設 | CHECK／約束 | 說明 | 來源 |
|---|---|---|---|---|---|---|
| `id` | INTEGER | 是（PK） | AUTOINCREMENT | | | 0001:75 |
| `name` | TEXT | 是 | — | UNIQUE | `^[A-Za-z0-9._-]{1,64}$` | 0001:76 |
| `token_hash` | TEXT | 是 | — | UNIQUE | `guc_<64 hex>` 明文的 sha256 hex | 0001:77 |
| `scope` | TEXT | 是 | `'register-draft'` | `scope = 'register-draft'` | 只能是這個值 | 0001:78 |
| `is_active` | INTEGER | 是 | `1` | `IN (0, 1)` | 撤銷＝0，不刪列 | 0001:79 |
| `created_at` | TEXT | 是 | — | | | 0001:80 |
| `last_used_at` | TEXT | 否 | — | | 每次驗過都更新 | 0001:81 |

`scope` 的 CHECK 讓「開一支能打別的端點的 token」在資料庫層就做不到；Worker 驗 token 時也再帶 `AND scope = 'register-draft'`（`src/access.ts:134`）。

| 動作 | 函式（`src/access.ts`） |
|---|---|
| 驗 token | `ciOperator`：長度 ≤128 → sha256 → 查 `is_active = 1 AND scope = 'register-draft'` → 更新 `last_used_at` |
| 建立 | `createCiToken`：`ON CONFLICT (name) DO NOTHING RETURNING id`，重名回 null（409 `name_taken`） |
| 撤銷 | `revokeCiToken`：`is_active` 1 → 0 |
| 列表 | `listCiTokens`：不含 `token_hash` |

**現況**：0 列（CI 自動登記草稿尚未啟用）。

## migration 對照 {#migrations nav="migration"}

| 檔 | 內容 | 動到的表 |
|---|---|---|
| `0001_init.sql` | 建六張表與兩個索引 | `product_releases`、`instance_version_reports`、`release_download_log`、`license_denylist`、`signing_keys`、`ci_tokens` |
| `0002_release_notes.sql` | 加版本說明三欄 `release_notes`（`'[]'`、`json_valid`）、`impact`、`action`（`''`）。刻意不進簽章：清單一簽就改不了，這三段發布後仍可修字 | `product_releases` |
| `0003_min_agent_version.sql` | 加 `min_agent_version`（可 NULL：舊草稿與不設下限的版本沒有） | `product_releases` |
| `0004_agent_version_reports.sql` | 建 `agent_version_reports` | 新表 |

**套用**：

```bash
cd ~/Projects/Billows/Audit-Manager/update-center
npm run db:migrate:local                                        # 本機 env.dev 模擬庫
npx wrangler d1 migrations apply guidant-update-center --remote # 正式庫（首腦做）
```

wrangler 在 `d1_migrations` 記已套過的檔名，重跑只套新的。測試由 `test/apply-migrations.ts` 每次對測試庫套全部 migration。

**改 schema 的規則**：

- 只加不改：新欄位用 `ALTER TABLE … ADD COLUMN`，NOT NULL 欄位要給預設值（0002 的寫法），讓既有列直接合法。
- 先套 migration、再部署 Worker。
- 不加客戶名稱類欄位：`client.spec.ts`「D1 schema 守衛」會擋 `instance_version_reports` 出現這類欄位，並核六張主表都在。

**唯讀查詢範例**：

```bash
npx wrangler d1 execute guidant-update-center --remote --json \
  --command "SELECT version, status, kid, published_at FROM product_releases"
```

D1 對 compound SELECT 的項數有上限，多張表計數請用子查詢寫在同一個 SELECT 裡，不要用一長串 `UNION ALL`。

## 相關文件 {#refs nav="相關"}

- [更新站規格](update-site-spec.md)：端點、身分、planPath、R2、部署
- [發布簽章規格](release-signing-spec.md)：`signed_manifest` 的格式與驗法
- [更新中心 API 規格](update-center-api-spec.md)
- [升級指令規格](upgrade-runner-spec.md)
- [Agent 跟進規格](agent-rollout-spec.md)
- [時序圖](sequence-diagrams-spec.md)
- [發版操作手冊](release-runbook.md)
- [設計文件](design.md) §5.4
