---
status: paused
superseded_by: ""
---

# Design: KBDB Base —— 原子化萬用表（self-hosted 資料底座 + 官方核心）

> 2026-06-07 richblack 拍板。來源：壓測報告（`test_arcrun/docs/壓測報告.md`）暴露 self-hosted 無資料層 →
> richblack 決定把 KBDB 的「基礎儲存」開源進 arcrun，向量/三元組走插件模式（如 PostgreSQL 的 PGVector / Apache AGE）。
> **架構同時影響官方版**：官方也改成「基礎核心 + 可選模組」，故開源與官方共用同一基礎，不維護兩套。

---

## 0. 為什麼（壓測暴露的缺口）

壓測（第一次完整壓測，6 輪回歸）跑通了「表單 → workflow → Google Sheets」，但暴露 **self-hosted 沒有資料層**：

| 缺口 | 現況 | KBDB Base 如何解 |
|---|---|---|
| **無「專案」維度** | KV key 只有 `{namespace}:{type}:{name}`，無 project；recipe 甚至全局共享 | entries 樹狀（entry_type=project/workflow，parent_id）→ 可列「某專案的所有工作流」 |
| **D1 完全空置** | self-hosted init 只建 8 KV，無 D1；`grep d1_databases` 全 repo 零結果 | KBDB Base 用 D1 → 填上閒置的 D1 |
| **recipe 成功無記錄** | push 時 probe 2xx 但不持久化 | 成功記錄存 entries/records → 可監控、可當投稿依據 |
| **資料碎（像 n8n）** | 散落 KV，無結構 | templates 做虛擬表 → 結構化資料形態 |

> recipe 成功記錄 + 投稿（帶記錄免驗證）本次一起做（見 7）。recipe 信譽/市場/排序是後續，不在本次。

---

## 1. 核心設計：原子化萬用表（萬年不動）

> richblack 設計原則：「基礎表是 truth，萬年不動。新技術（embed/triplet/未來）一直增加，
> 它們把衍生結果記在**各自的地方**，不回頭改基礎表。」這正是 PostgreSQL 核心 + PGVector/AGE 插件的模型。

### 三張表（沿用 KBDB v3 `0005_universal_table`，已驗證可用）

| 表 | 作用 |
|---|---|
| `entries` | 原子資料。`entry_type`：`block`/`value`/`template`/`slot`（+ 本 SDD 擴充 `project`/`workflow`）。樹狀 `parent_id`、多租戶 `owner_id` |
| `templates` | 定義虛擬表的 slots（`slots_json`，如 `["display_name","gender"]`）→ 同一份 entries 用不同 template 呈現不同資料形態 |
| `entry_values` | 把多個 entries 按 template 組成一筆結構化記錄（`record_id` + `slot_name` + `entry_id`） |

### 萬年不動原則（本 SDD 要修正 KBDB 現況的地方）

KBDB 現況**大致已符合**，但有違反處要修：
- ✅ embed：`block-embed.ts` 把向量 upsert 到 **Vectorize**（獨立），不動 blocks 表。
- ✅ triplet 主體：`triplet-crud.ts` 主要 `INSERT INTO entry_values`（獨立記錄）。
- 🔴 **違反處**：`triplet-crud.ts:106/110` `UPDATE blocks SET entity_type = ?` —— 抽 triplet 後回頭改基礎表。
  → **修法**：衍生屬性（entity_type 等）移出基礎表，記到衍生層（entry_values 或獨立 template）。基礎表只存 truth。

---

## 2. 插件模式：一套 code，三層 binding 開關（不維護兩套）

> richblack：「像 PGVector/AGE 是插件。基礎模組提供原子化萬用表（這也是賣點）；要向量、要圖資料庫另外裝。
>  我還是希望只維持一套。」

| 層 | 能力 | 依賴 | 開源 / 費用 |
|---|---|---|---|
| **基礎 KBDB Base** | entries/templates/entry_values CRUD + **D1 LIKE 關鍵字搜尋** | D1 only | ✅ 開源，免費，**不綁卡** |
| **+ embed 模組** | 語義搜尋（search 升級） | CF **Vectorize** binding + AI | embed 是 **CF 內建**（程式薄）→ **不拆 repo，給「開/關」**：有 Vectorize binding 就啟用，沒有就降級 LIKE。Vectorize 需用戶自開（自付費） |
| **+ triplet 模組** | 三元組抽取 / 知識圖譜 / 圖查詢 | AI + triplet-*.ts（8 個 action，較重） | triplet 是 **richblack 的 IP、程式量大** → **獨立 repo / 獨立 worker**，基礎不依賴它，要才裝 |

### Q1 拍板（richblack 2026-06-07）：embed 不拆、triplet 拆

- **embed → 不拆 repo，binding 開/關**：因為 embed 靠 CF 內建 Vectorize，程式薄（呼叫 `env.VECTORIZE.upsert`），沒 binding 就降級。一套 code、開關即可。
- **triplet → 獨立 repo/worker**：非 CF 內建、是 richblack IP、action 多（triplet-extract/normalize/embed/crud/stats/syntax/entities/update）。基礎 KBDB **不 import triplet**，要才裝。

### search 分層（壓測者問「search 還是要有，只是沒語義」→ 對）

KBDB `search.ts` 已有兩層骨架：
- 基礎版：D1 `LIKE` 全文搜尋（KBDB 現有 fallback 邏輯，不需 Vectorize）。
- + embed：語義搜尋（Vectorize + AI）。
**API 不變，能力分層**：基礎用 LIKE，裝 embed 升級語義。

---

## 3. 解耦工作（把寫死的觸發改成可選）

KBDB 現況：`blocks.ts` 把 `ingestText`/`createTriplet`/`deleteBlockVector` **寫死 import 進寫入流程**（寫 block 順手 embed+抽 triplet）。

解耦目標：
- 寫 block = 只寫 block（純基礎）。
- embed = 可選 hook（Vectorize binding 在才掛）。
- triplet = 可選（triplet 模組裝了才掛）。
- 基礎 `blocks.ts` / `templates.ts` / `records.ts` **不 import** vectorize/triplet。

> ✅ 已查證 `templates.ts` / `records.ts` 本就乾淨（只 import record-crud）；主要工作在 `blocks.ts`。

---

## 4. 落地形態：import 進 arcrun（如 MCP 的 pattern）

> richblack：「像先前把 MCP import 進本專案一樣，也把 KBDB import 進來。」對，同一 pattern。

- **基礎 KBDB Base 搬進 `arcrun/kbdb/`**（如 `arcrun/mcp/`）。形態：獨立 Worker + D1。納入 deploy 掃描（rule 05：新目錄 + wrangler.toml 自動部署）。
- **init 建 D1**：`cli/src/lib/cf-api.ts` 加 D1 建立（如現有 KV 建立），`deploy.ts` 注入 D1 id。**self-hosted 從只建 KV → 建 KV + D1**。
- **cypher 改用 KBDB Base 存專案/工作流**：webhook/credential/recipe 的「專案歸屬」走 KBDB entries（漸進，不一次搬完）。
- **embed**：隨基礎進 arcrun，Vectorize binding 開關。
- **triplet**：獨立 repo，self-host 用戶要才另裝（不進 arcrun 基礎）。

### 共用同一基礎（官方也改）

richblack 決定官方版也改成「基礎核心 + 可選模組」→ 官方與 self-hosted **共用同一份基礎 KBDB code**，差別只在 binding（官方全開、self-host 基礎免費可選開）。**不維護兩套。**

---

## 5. 邊界（本 SDD 不做）

- **recipe 投稿入口 + 帶成功記錄免驗證**：✅ **本次要做**，見新增 §7（與 KBDB 成功記錄嵌在一起）。
- **recipe 信任 = 市場機制（靠量/星數模型）**：拍板用市場優先、暫不做強防偽（見 §7.3）。本次（5.2）做投稿端點（submit 私庫 / submit-p 公共庫覆蓋同 canonical_id）+ stat 存證；**跨環境「靠量同步回市場」聚合層另排 task**；強防偽（官方重跑/多方回報）只在市場失靈才做。
- **triplet 模組本身**：獨立 repo，不在本 SDD（本 SDD 只確保基礎不依賴它、可選掛上）。
- **vectorize 的進階用法**（suggest/entities-graph-embed）：隨 triplet/進階模組，不在基礎。
- **官方版的搬遷細節**：本 SDD 聚焦「基礎 KBDB 進 arcrun self-hosted」，官方共用基礎是方向，搬遷另排。

## 7. recipe 公庫/私庫機制 + 帶 KBDB 成功記錄（市場信任）—— 本次要做

> §7.1-7.4 原只想「投稿」單向（本次最小已做：成功記錄 5.1 + submit-p 雛形 5.2）。
> §7.5 是 richblack 2026-06-07 指示補的 **CHANGE**：把公庫/私庫的所有互動情境想全（pull/搜尋/投稿/市場同步），
> 待 review。下方 7.1-7.4 是已成立的基礎，7.5 是擴充全貌。

> richblack 2026-06-07：本次只做「可投稿 + 帶成功記錄免驗證」，這兩件嵌在一起。
> 背景：官方初期那批 recipe 是一次性建立、未驗證（只為「Arcrun 推出不能空空的」）。
> 壓測者打通的修正版（如 google_sheets_append method/body）回不了官方 → 官方仍錯，下個用戶再撞。
> 不該由 AI 手改種子 recipe（治標）；正解是投稿機制 + 用真實成功記錄當免驗證依據（治本）。

### 7.1 兩件嵌在一起

1. 成功記錄（KBDB）：判定單位是「**工作流執行**」（對標 n8n execution，非逐節點）。一條 workflow 跑完，
   整體成功（到 Output、無 error）→ 把這次**用到的每個 recipe 節點**各記成功 +1；整體失敗 → 已跑到的 recipe 節點各記失敗 +1。
   歸屬對象是 recipe（因為投稿的是 recipe），**canonical_id 取 auth recipe 的 `service` 欄位**（與投稿身份一致）。
   單節點工作流（cron→gdrive，一個節點一個 credential）是最簡情形。存 D1（entry/record），可累積、可查。
   > 註（術語別混）：**cypher-executor = 工作流引擎**，用 cypher **語法**（節點+邊的圖遍歷語意）把零件串成工作流，
   > 本就在開源核心內，**不是** graph DB。「可選/不在核心」的是兩個插件：① **triplet**（知識三元組的圖**儲存**，
   > 對標 Apache AGE）② **vectorize**（CF 內建但可能多花錢）。recipe 成功記錄是純 D1 KBDB 核心能力，不依賴這兩個插件。
2. 投稿（registry/官方）：投稿 recipe 時帶上它在投稿者環境的成功記錄。官方端看記錄即信，免人工驗證——真實打通比投稿者自寫測試可信（呼應 gherkin-not-safety-sandbox-is）。

### 7.2 本次範圍（最小）

- recipe 成功/失敗計數落地 D1（KBDB 成功記錄）。
- 一個投稿端點：收 recipe + 其成功記錄 → 進官方 recipe 池，**新增一個作者版本**（app-store 模型 §7.5.5，**非**覆蓋同 canonical_id；同 canonical 多作者並存）。
- 投稿者目前就是 richblack，不需誘因；重點是有入口可投 + 帶記錄免驗證。

### 7.3 信任機制：市場優先（靠量，非人力/強防偽）—— richblack 2026-06-07 拍板

> 這個決策之前討論過、那時選「先不做」，導致今天又重講一次 → 故此次**寫進 SDD**，不再丟。

**核心：recipe 信任靠「量」決定，像 GitHub 星數，不靠人力檢核、也不先做強防偽。**

不可能全世界這麼多 recipe 都靠人力檢核。改用市場機制：
- recipe 的成功/失敗由**全體使用者的真實執行**累積（投稿者 A 的 recipe 通過 2000 次；投稿者 B 的失敗 500 次 = 像被按爛）。
- 上傳爛 recipe → 別人用會失敗 → 累積失敗數 → 投稿者信用下降 → 有動機更新自己發佈的 recipe，避免持續被按爛。
- **數據來源 = §7.1 的成功記錄**（每次工作流執行把用到的 recipe 成功/失敗記到 KBDB）。市場機制 = 把這些**跨使用者**的記錄**同步回公共市場**匯總。

**為什麼先市場、暫不做強防偽（誠實，mindset §7）**：
self-hosted 投稿者 = 自己環境 root，能寫任意數字進自己 KBDB → 自報 stat 可造假。
但**先信市場**：實驗一段時間，若市場反應爛（造假猖獗）再考慮強防偽（官方重跑 / 多方獨立回報——證據由投稿者控制不了的一方產生）。
故**本次不拿投稿者自報的數字當自動放行門檻**（那會造債：未來門檻是空的）。stat 先當存證 + 法律歸責軌跡。

**分期**：
- **本次（5.2）**：投稿端點 `POST /recipes/submit`。submit（私庫）vs submit-p（公共庫，需 exposure_consent = 暴露同意，mindset §6）。
  公共庫**新增作者版本**（app-store 模型 §7.5.5，非覆蓋）。投稿者帶的 stat 寫進官方 KBDB 當存證（不當門檻）。
- **後續（市場同步，§7.3 主體）**：跨環境「靠量同步回市場」——self-hosted 各用戶的真實成功/失敗匯總回公共市場，
  形成 recipe 的星數/信用。是 §7.1 數據的跨環境聚合層。本次先留接口（5.1 已在各環境記錄，市場聚合另排 task）。
- **更後續（強防偽，只在市場失靈才做）**：官方 in-process 重跑投稿者工作流拿官方自己的 2xx / 第二投稿者獨立回報。
  （註：app-store 模型下「版本並存」是常態、不刪舊版 §7.5.5；舊版靠市場淘汰，不做下架引導。）

### 7.4 與零件投稿的差異

零件投稿走 GitHub PR（人 merge + CI runtime 驗 wasm，記憶 component-submission-via-pr）。
recipe 是純文字資料（非 wasm），不需 CI 跑沙箱；它的驗證改用真實成功記錄。故 recipe 投稿機制與零件不同，不混用 component-registry-canon SDD。

### 7.5 公庫 / 私庫雙向機制（CHANGE，richblack 2026-06-07 指示「把兩庫所有情境想好」）—— 待 review

> 背景修正：原 §7.1-7.4 只想了「投稿」單向，太窄。richblack 指出：
> **SaaS 版（standard mode）本來就只有公庫；self-hosted 版本來就只有私庫**。
> 兩庫的互動是 overall 架構（不只為投稿）：私庫是公庫的**按需子集**——self-hosted 也許只用 10 個，
> 公庫也許有 10000 個，**不需每次全部拉下來，用到的再複製**。投稿（submit-p）只是其中一條反向流。

#### 7.5.1 兩個庫的物理本質（已確認現況）

| 庫 | 是誰 | 物理 | 規模 | 角色 |
|----|------|------|------|------|
| **公庫** | 官方 SaaS（`cypher.arcrun.dev`）的 `RECIPES` KV | 官方 CF 帳號 | 大（~10000，含社群投稿） | 唯一公共真相、可瀏覽/搜尋/pull |
| **私庫** | 每個 self-hosted（用戶自己帳號）的 `RECIPES` KV | 用戶 CF 帳號 | 小（~10，按需子集） | 自己實際用到的 + 自己改的 |

- recipe key 都是 `recipe:{canonical_id}`（無 owner 前綴）。同一套部署的 KV = 那套的庫。**物理隔離**（不同帳號不同 KV）。
- `mode`（cli config）：`standard` = SaaS 用戶（只連公庫，無私庫）；`self-hosted` = 自己一套（有私庫，可選連公庫 pull/submit）；`local` = 純本地。

#### 7.5.2 三種使用者 × 互動矩陣

| 使用者 | 有哪些庫 | 取 recipe | 改 / 投稿 | 成功記錄 |
|--------|----------|-----------|-----------|----------|
| **SaaS 用戶**（standard） | 只公庫 | 直接用公庫（公庫即他的庫） | 改了直接寫公庫（=投稿，需 consent） | 記在官方 KBDB（即市場數據本身） |
| **self-hosted 用戶** | 私庫（主）+ 公庫（唯讀來源） | **公→私 pull**：搜公庫→複製到私庫 | 私庫改 →（可選）**submit-p** 推回公庫 | 記在自己 KBDB；市場同步另排（§7.3 5.5） |
| **local** | 無遠端庫 | 本地 recipe 檔 | 無 | 本地 |

#### 7.5.3 四條互動流（窮舉）

1. **公→私 pull（核心，之前漏）**：self-hosted `acr recipe pull <canonical_id>` → 從公庫只讀端點抓該 recipe → 寫進自己私庫 KV。
   按需，**不全量同步**（10000 不必拉）。pull 也可帶公庫的成功記錄（市場星數）供用戶判斷要不要用。
2. **公庫瀏覽/搜尋**：`acr recipe search <q>` / `list` → 打公庫只讀端點（`GET /public-recipes?q=`）。
   self-hosted 沒搜尋能力就不知道公庫有什麼可 pull。SaaS 用戶同一端點即用。
3. **私→公 submit-p（已做雛形）**：self-hosted 把私庫某 recipe 推回公庫（`POST /recipes/submit`，需 exposure_consent）。**新增作者版本**（app-store 模型 §7.5.5，非覆蓋；同 canonical 多作者並存）。
4. **成功記錄 → 市場同步（§7.3 5.5，另排）**：self-hosted 各環境真實成功/失敗匯總回公庫的市場星數。pull(流1) 帶的星數就來自這裡。

#### 7.5.4 公庫只讀端點（公→私 + 瀏覽的基礎，待實作）

官方 cypher 開**公開只讀**端點（無需 api_key，因公庫本就公共）：
- `GET /public-recipes?q=&limit=&offset=` → 搜尋/列出公庫 recipe。**同 canonical_id 回多筆（多作者）**，各附作者 + 市場星數 success/failure，供 CC/AI 依數據選（§7.5.5）。
- `GET /public-recipes/:canonical_id?author=` → 取單一 recipe 全文（pull 用）。不指定 author → 回市場最佳版本（成功率最高）。
- **搜尋/取用落空 → 回 `{ found: false, canonical_id, hint }` 創作引導**（§7.5.6），不回空陣列乾等。讓 CC 知道下一步是「自己做一個成為作者」。
> 與現有 `GET /recipes`（已存在，列當前部署 KV）的差別：`/public-recipes` 語意是「**這是公庫**，給外部 self-hosted pull/瀏覽」，且**含作者維度 + 市場數據**。
> 在官方部署上讀同一 KV；命名分開是為了語意清楚 + 公庫的多作者/市場排序不污染內部 `/recipes`。

> **市場星數 per-uuid（§7.5.h 已實作）**：5.1 改收集 API recipe 的 **uuid**（非 auth service），KBDB 市場星數記 per-uuid。
> 故 `GET /public-recipes/:canonical_id` 的「選市場最佳作者版本」**真正能區分 Leo 版/John 版**（各自 uuid 各自累積）。
> 舊資料無 uuid → fallback canonical_id（migration 後自然帶 uuid）。

#### 7.5.5 recipe = 具名作者作品（app-store 模型）+ UUID 身份 —— richblack 2026-06-07 拍板

> 這條推翻了我原本「覆蓋同 canonical_id」的錯誤前提。正確模型如下。

**每個投稿的 recipe 是一個「具名作者作品」，像 app store 上的一個 app，只有作者（+admin）能改。**

- Leo 投稿 `gsheets_upsert` = 在 app store 放了一個 app。**只有 Leo + admin 能動它**。
- John 覺得 Leo 的很爛 → John **另推一個** `gsheets_upsert`（John 的作品）。**只有 John + admin 能動**。
- 公庫裡因此**同 canonical_id 並存多份**（Leo 版、John 版），各帶作者、各帶獨立市場數據。

**所以：**
1. **submit-p ≠ 覆蓋**。投稿是**新增一個作者版本**，不是覆蓋同 canonical_id。（修正原 §7.2/§7.3 的「覆蓋」字眼。）
2. **沒有「公庫改了 Leo 要不要跟」的問題**：Leo 的作品只有 Leo 改；別人不滿意是自己推新作品，不是改 Leo 的。版本關係問題消失。
3. **向後相容、不刪舊 recipe**：Leo 的 5/10 不被刪，只是市場上沒人選 → 自然淘汰（長尾留著，無破壞性刪除）。
4. **選擇由市場數據驅動（CC/AI 操盤手選）**：CC 搜 `gsheets_upsert` → 找到 2 個 → 看數據（Leo 5成功/10失敗、John 100成功/0失敗）→ **選 John 的**。Leo 的逐漸沒人用。

**身份模型（落地）= UUID（richblack 2026-06-07 拍板，比 canonical_id+author 複合鍵更乾淨）**：

**每個 recipe 一誕生就領一個 UUID = 唯一身份。`canonical_id` / `author` / `公私` 全是屬性，不是身份。**
- 身份(UUID) 與 歸屬(author 屬性) 分離 → 沒有「同 canonical_id 撞 key」問題（key 是 UUID，本就唯一）。
- **KV key 設計**：`recipe:{uuid}` 存 recipe 本體；`idx:canonical:{canonical_id}` → UUID 清單（同 canonical 多 UUID）；
  私庫執行：`idx:installed:{canonical_id}` → 該 canonical 在本庫安裝的**唯一** UUID（pull/author 時定一個）→ 執行查找仍由 canonical_id 解析到唯一 recipe，**不破執行**。
- **Leo 改 John 的**：Leo 拿 John 版（UUID-J）改 → 發出去領**新 UUID-L** = Leo 作品。UUID-J/UUID-L 是兩個東西，不冒名、天然不衝突。
- **author 屬性 = 該 UUID 誕生時的投稿者**（誰投誰負責那版市場數據）；可選帶 `derived_from: {uuid}` 溯源（致謝/fork upstream）。

**「Leo 跟 John 一模一樣」怎麼辦**（你問的）：兩個不同 UUID 都通過、各跑各的市場數據。
**重複 = 市場冗餘，靠數據淘汰不靠技術阻止**（先有數據的勝，複製品沒人選自然沉底，§7.3 靠量不靠人力）。

**檢舉功能**：本次**不做**。檢舉 = 人力檢核，與「市場優先、暫不強防偽」（§7.3）矛盾，提前做還用不到 = 造債。
市場失靈才考慮（§7.3 更後續）。**CC 不檢舉**（mindset §7：CC 是操盤手不是審查者、不代人裁決；CC 對爛 recipe 的「投票」= 不選它 = 市場數據）。

> 影響現有 code：5.2 的 `POST /recipes/submit` 改「覆蓋」→「領新 UUID 新增」；recipe key 從 `recipe:{canonical_id}` 轉
> `recipe:{uuid}` + canonical 索引；執行查找改經 `idx:installed:{canonical_id}` 拿唯一 UUID（保證不破 component-loader/auth-dispatcher/credential-injector）。列入 7.5.f task。

#### 7.5.6 搜尋落空 = 創作入口（richblack 2026-06-07 拍板）

> app-store 模型的閉環關鍵：公庫沒有的 recipe，由 CC 現做成為作者，再投回公庫補上。

**CC 搜「gsheets_delete_sheet」公庫沒有 → 端點不能只回空陣列乾等，要回「可創作」訊號 → CC 說「那我做一個」→ CC 成為作者。**

- 公庫只讀端點（§7.5.4）搜尋**落空時的回應 = 創作引導**，不是空結果：
  明確回 `{ found: false, canonical_id, hint: "公庫無此 recipe，可自行建立並 submit-p 投稿成為作者" }`，
  讓 CC（AI 操盤手）知道下一步是「自己做一個」而非卡住。
- CC 現做 → 跑成功累積市場數據（§7.1）→ submit-p 推回公庫 → 成為公庫**第一個** `gsheets_delete_sheet`（CC 是作者）。
- **閉環**：
  - 公庫有 → pull 用（市場數據選最佳作者版本，§7.5.5）。
  - 公庫沒有 → CC 現做成為作者 → 用 → 投稿 → 公庫從此有了。
- 呼應 mindset：**CC 是大腦**（arcrun 是 AI 呼叫的工具），缺能力時 CC 自己補（做 recipe / 工作流），
  不停在「找不到」。工作流是 default、零件是例外（缺能力先想工作流/recipe，不是停手）。

---

## 8. KV list 上限威脅免費承諾 → 高頻 list 遷 D1（CHANGE，2026-06-08 richblack CF 收到 list 超標信）

> 事件：richblack 的 CF 帳號收到「Workers KV 免費等級每日 1000 次 list 上限已超」信，
> list API 回 429 直到隔日重置。**這直接威脅 arcrun「免費可用」核心承諾**（[[deploy-via-local-deploy-not-ci]] 免費優先精神）。
> 方向拍板（richblack 2026-06-08）：**高頻 list 改走 D1**（對齊 §6 Q2 的 KV vs D1 分工——要列舉/搜尋的進 D1）。

### 8.1 根因：哪些 KV list 是高頻（grep cypher-executor/src）

| 位置 | list 操作 | 頻率 | 嚴重度 |
|------|----------|------|--------|
| `scheduled.ts:34` | `WEBHOOKS.list({prefix:'cron-idx:'})` | **每分鐘一次 = 1440/日** | 🔴 **常駐單獨就爆**（>1000 上限） |
| `recipes.ts` public-recipes（§7.5 新加） | `RECIPES.list({prefix:'recipe:'})` | 每次 recipe 搜尋 | 🟠 搜尋頻繁就疊加爆 |
| `recipes.ts` GET /recipes / listAllRecipes | `RECIPES.list` | 每次列 recipe | 🟠 |
| `webhooks-list.ts` / `webhooks-named.ts` | `WEBHOOKS.list` | 每次列 workflow | 🟠 |

> **D1 額度遠寬**：免費 5M 讀/日、100K 寫/日（§6 Q4），對比 KV list 僅 1000/日。要「列舉/搜尋/排序」的天生該 D1。

### 8.2 緊急止血（cron，今天在爆）

`scheduled.ts` 每分鐘 list `cron-idx:` → 改成**不靠 list**：
- 方案：維護**單一固定 key**（如 `cron-idx:_all` 存所有 cron workflow 的 {apiKey,name,cron_expr} JSON 陣列），
  scheduled 只 `get` 一次（取代 list）。acr push 時 upsert 進這個 key。
- 1440 次/日 list → **0 次 list**（改 1 次 get/分鐘 = 1440 get/日，KV get 免費額度 100K/日，遠夠）。

### 8.3 治本（recipe / workflow 查詢遷 D1，分期）

對齊 §6 Q2（recipe + workflow 進 D1）：
- **recipe**：recipe 本體從 RECIPES KV 遷進 KBDB D1（entry_type='recipe' 或專表）。
  public-recipes 搜尋、list 改 D1 query（LIKE/WHERE，含 §7.5 多作者/市場排序天生適合 SQL）。
  執行時 resolveRecipe 也改 D1（但執行是 get-by-key 級，KV get 不爆——可漸進）。
- **workflow**：workflow record 從 WEBHOOKS KV 遷 D1（entry_type='workflow' 掛 project parent，§6 Q1）。
  list workflow 改 D1 query。
- **保留 KV 的**：session / credential / exec context（§6 Q2：短期高頻純 get，不 list）。

### 8.4 分期建議（待 richblack 拍板細節）

1. **P0 止血**（§8.2）：cron 改單 key get。最小改動、今天可止血。
2. **P1**：public-recipes / recipe list 改 D1（我 §7.5 新加的 list 是疊加元兇）。
3. **P2**：workflow list 改 D1。
4. **P3**：recipe/workflow 本體完整遷 D1（resolveRecipe / KV→D1 讀寫）。

> **誠實 trade-off**：遷 D1 要寫 migration + 雙寫過渡（KV 舊資料相容），工程量不小。
> 但不遷 = 免費承諾破功（用戶用一用就 429）。**這是核心承諾必修項，不是優化。**
> 待 review §8 + 拍板分期顆粒度後動 code。**勿在 review 前動。**

---

## 6. 開放問題（待 review）

1. **專案維度怎麼進 entries**：`entry_type='project'` + workflow 用 `parent_id` 掛專案？還是 templates 定義「專案」虛擬表？
   1. Leo：由你決定，我記得先前 KBDB 已經有實作，參考原有做法。
   2. [DECIDED, CC 2026-06-07] use entry_type=project + parent_id tree (NOT templates virtual table). Matches existing KBDB: entry_type is an extensible string, entry-crud.ts already supports WHERE parent_id; no schema change (honors table-never-changes). "list workflows under project" = WHERE parent_id=PROJECT_ID AND entry_type=workflow. templates/slot reserved for structured records like recipe success stats.
2. KV vs D1 分工[拍板 2026-06-07]：KV 不全廢，按特性分。workflow YAML + recipe + 成功記錄進 D1（要層級/列舉/排序）；session、verdict、credential 留 KV（短期高頻純取用）。類比 PostgreSQL + Redis。workflow 變 entry（entry_type=workflow 掛 project parent）。
   1. Leo：同意
3. **triplet 獨立 repo 的接點**：基礎 KBDB 怎麼讓 triplet 模組「掛上」——HTTP hook？還是 triplet worker 自己讀同一個 D1？
   1. Leo：triplet 其實也就是 template 加上一些函式，最簡單就是你給他一段文字抽取出三元組，但如果要自動化，就會提供 API，背後是工作流，例如 block 存入時就叫起 triplet 功能，這個功能如果自動化就是在安裝時同意，但應該也可以有 mcp 讓 AI 易於操作，如同前述，交貨時有 MCP，因為是 AI Friendly 的系統。
4. **init 建 D1 的冪等 + self-hosted 免綁卡確認**：D1 是否如 KV 一樣免費不綁卡（需查證 CF D1 免費額度條件）。
   1. Leo：一樣不綁卡，除非他用量很大，屆時不是我來提醒而是 CF 會提醒他
