feat: KBDB-graph 插件獨立 — 全面改寫成走基本盤 API(API-as-Wall)

按 leo 鐵律(2026-06-14)把插件從「直接 SQL 操作基本盤表」改寫成
「只透過基本盤 arcrun/kbdb HTTP API 讀寫」。零建表、零 migration、零 SQL。

- 新增 src/lib/kbdb-client.ts:唯一對外通道,封裝 entries/templates/records API
- 新增 src/lib/templates.ts:triplet/entity template 定義(替代建表)
- 改寫 21 個違規 action(triplet/graph/entity/search)→ 走 client,圖在插件層記憶體組裝
- 移除所有 migrations、D1/Vectorize/AI 綁定;embedding/語意搜尋歸基本盤 optional 模組
- index.ts 只掛 triplets/graph/entities/search 路由;基本盤路由歸 arcrun/kbdb
- 測試改走 mock client(純 node);裁剪 CLAUDE.md 只留 graph 插件 + 鐵律
- 修正 SDD design.md「讀現狀推翻鐵律」的錯誤判斷(共用 D1 → API-as-Wall)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:59:41 +08:00
commit efe8e165cf
62 changed files with 7671 additions and 0 deletions
@@ -0,0 +1,57 @@
# FEATURE REQUEST: KBDB 應提供 upsert block endpoint(目前只能 client 端拼 GET+PATCH/POST
> 日期:2026-05-29
> 來源:arcrun Phase 2(降級假零件成 recipe
> 類型:API 缺口 —— upsert 語義目前不存在於 KBDB,被迫由 client 端拼湊
> 關聯:[BUG-2026-05-29-patch-blocks-403-different-org.md](./BUG-2026-05-29-patch-blocks-403-different-org.md)(拼湊路徑的 PATCH 那段還壞著)
---
## 問題
arcrun 原本有一個 `kbdb_upsert_block` 零件,行為是「依 page_name + user_id 查找,有就更新、沒有就新建」。但它**完全是 client 端拼湊**
```
GET /blocks?page_name=X&limit=10 # 查找
→ client 端 filter user_id 找第一筆
→ 找到:PATCH /blocks/:id # 更新
→ 沒找到:POST /blocks # 新建
```
KBDB **沒有** upsert 語義的 endpoint。實測(同一把 key):
| 探測 | HTTP |
|---|---|
| `PUT /blocks` | 404 |
| `POST /blocks/upsert` | 404 |
| `PUT /blocks/upsert` | 404 |
## 為什麼這是 KBDB 該補的、不是 arcrun 該拼的
arcrun 的設計原則是**薄殼 / 薄 API**:arcrun 只幫既有 API 套一層 recipeendpoint + auth),**不無中生有功能**。
「先查再分支寫」這套 upsert 邏輯,是在 client 端**變出 KBDB 沒有的功能**。這有幾個壞處:
1. **競態(race condition**GET 和後續 PATCH/POST 之間,別人可能插入同 page_name 的 block,造成重複或覆寫。只有 KBDB server 端用單一交易(upsert / `ON CONFLICT`)才能正確。
2. **語義碎裂**:每個 clientarcrun / 其他 SDK)各自拼一套 upsert,filter 規則(怎麼算「同一筆」)可能不一致。
3. **拼湊路徑現在還壞著**:它依賴 PATCH /blocks/:id,而那個 endpoint 目前回 403(見關聯 bug)。
## 建議
KBDB 提供一個原生 upsert endpoint,例如:
```
POST /blocks/upsert
Body: { page_name, user_id, content, type, source, tags, ... }
語義:依 (page_name, user_id) 找唯一 block —— 存在則更新、不存在則建立(單一交易,server 端 ON CONFLICT
回應:{ id, action: "created" | "updated" }
```
有了它之後:
- arcrun 端只要建一個 `recipe:kbdb_upsert` 指向 `POST /blocks/upsert`,套殼即可,跟其他 5 個 KBDB recipe 一致。
- 競態由 KBDB server 端交易保證,client 不再拼湊。
## arcrun 端現狀(等 KBDB
- 其餘 5 個 KBDB 操作已降級成 recipe 並驗收:`kbdb_get`(200) / `kbdb_create_block`(201) / `kbdb_ingest`(201) / `kbdb_delete`(200) 綠;`kbdb_patch_block` 因上述 403 bug 待驗。
- `kbdb_upsert_block` **暫不降級、源碼暫留**,等 KBDB 出 `POST /blocks/upsert` 後改建 `recipe:kbdb_upsert` 套殼。
@@ -0,0 +1,68 @@
# BUG: PATCH /blocks/:id 回 403 "block belongs to different org"(同一把 key 能 create/get/delete 卻不能 patch
> 回報日期:2026-05-29
> 回報來源:arcrun Phase 2(把 kbdb_* 零件降級成 recipe,逐個驗收時發現)
> 嚴重度:中高 —— PATCH endpoint 對「自己剛建、且能刪的 block」拒絕更新,等於 update 能力全壞
> 影響:任何「查→改」或 upsert 流程(先 GET 找到 block,再 PATCH 更新)都無法完成
---
## 症狀
用**同一把 API key**、對**同一個 block**,四個操作的結果不一致:
| 操作 | endpoint | HTTP | 結果 |
|---|---|---|---|
| 建立 | `POST /blocks` | **201** | ✅ 建出 block,回 id |
| 讀取 | `GET /blocks/:id` | **200** | ✅ 讀得到 |
| **更新** | **`PATCH /blocks/:id`** | **403** | ❌ `{"error":"block belongs to different org"}` |
| 刪除 | `DELETE /blocks/:id` | **200** | ✅ `{"deleted":true}` |
**矛盾點**:同一把 key 能 create / get / delete 這個 block —— 代表 KBDB 認定我擁有它(org 一致)。但 PATCH 卻說「belongs to different org」。**create 寫進去的 org 判定,和 patch 讀出來比對的 org 判定不一致**,這是 KBDB 內部 org 歸屬邏輯的 bug。
## 重現(裸 curl,不經 arcrun
為排除是 arcrun 注入問題,直接用裸 curl + Bearer token 打 `https://kbdb.finally.click`
```bash
TOKEN="Bearer ak_402d…" # 同一把 key,全程不變
BASE=https://kbdb.finally.click
# 1. 建立 → 201
curl -X POST $BASE/blocks -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
-d '{"content":"...","type":"note","page_name":"kbdb_bug_repro","source":"...","user_id":"arcrun_phase2"}'
# → {"id":"f39ea877-...","action":"created"} HTTP 201
# 2. 讀取 → 200
curl $BASE/blocks/f39ea877-... -H "Authorization: $TOKEN"
# → HTTP 200
# 3. 更新 → 403 ★ BUG
curl -X PATCH $BASE/blocks/f39ea877-... -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
-d '{"content":"patched"}'
# → {"error":"block belongs to different org"} HTTP 403
# 4. 刪除 → 200(證明我擁有此 block)
curl -X DELETE $BASE/blocks/f39ea877-... -H "Authorization: $TOKEN"
# → {"deleted":true} HTTP 200
```
經 arcruncypher-executor → auth_static_key 注入同一把 token → recipe 轉發)也是完全相同結果,所以**確定是 KBDB server 端 PATCH 路徑的問題,不是 client / arcrun 的問題**。
## 推測方向(給 KBDB 排查)
create / get / delete 的 org 判定路徑,和 PATCH 的 org 判定路徑不一致。可能:
1. **PATCH 用了不同的 org 解析來源**:例如 create 用 token → org_id 的某種映射寫入 block,但 PATCH 的 org-check 從另一個欄位 / 另一張表讀,兩邊算出的 org 不同。
2. **block 落地時的 org_id 與 token 的 org_id 不一致**create 時可能用了 default org 或 null org 寫入,PATCH 的 ownership 檢查卻嚴格比對 token org,導致「自己建的卻不是自己 org」。
3. **org-check 是 PATCH 獨有、其他三個 verb 沒做**:所以只有 PATCH 露出這個不一致。
建議從「create 時 block 實際寫入的 org_id」對比「PATCH org-check 讀的 org_id」兩個值下手,它們應該相等卻不等。
## 對 arcrun 的影響(已隔離,不阻擋 arcrun Phase 2
- arcrun 已把 `kbdb_patch_block` 降級成 reciperecipe 的轉發 + auth 注入經驗證**正確無誤**(請求成功打到 KBDB 的 PATCH handler,非 401)。
- 403 屬 KBDB 端行為,依 arcrun 原則「能不能打通由發 key 的服務裁決」,這不是 recipe 的 bug。
- 但 arcrun 的 `kbdb_upsert_block`GET 查找 → 分支 PATCH/POST)會用到 PATCH**此 bug 未解前,upsert 的 PATCH 分支無法驗收 2xx**。arcrun 端會把該分支標「未驗收:阻擋於 KBDB PATCH 403」。
KBDB 修好後請通知 arcrun,重跑 `kbdb_patch_block` recipe 驗收即可。