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,145 @@
# KBDB-graph 抽出 — Design
> 建立:2026-06-14
> 大改:2026-06-14(leo 拍板鐵律後,推翻原「共用 D1 / 直接 SQL」判斷)
> 對應 requirements.md 的 R-EXT-1/2/3
---
## ⚠️ 修正:原 design 的錯誤判斷(2026-06-14
**本檔原版犯了「讀現狀推翻鐵律」的錯**,必須記下來避免重犯:
- 原版看到插件現狀「直接 SQL 讀 `blocks`/`entry_values`」(28×/31× SQL 引用),**把它當成「AGE-on-Postgres 訊號」當設計依據**,跑去問「要不要共用同一個 D1、直接 SQL 掛在基本盤表上」。
- **這是錯的**:現狀那 21 個直接 SQL 的 action 是**違規的歷史產物**(違反本 repo CLAUDE.md「禁止繞過 API 直接 D1 操作 — API-as-Wall」),不是設計依據。讀違規現狀去推翻規則,正是 leo 點名的反例。
- leo 2026-06-14 拍板 KBDB 鐵律(見 `InkStoneCo/.../DECISION-kbdb-v3-baseplane.md`):**插件絕不碰表,讀寫全走基本盤 API。零建表、零 migration、零 SQL。** 比 AGE-on-Postgres 更嚴——AGE 能讀 Postgres 表,KBDB 插件連表都不許碰。
### 連帶修正的次要誤判
- **「v3 基本盤真身在本目錄」**:原版以為 arcrun/kbdb 是「v2 落後版」、本目錄的 blocks 表才是 v3 真身。讀 `arcrun/kbdb/0001_base.sql` 註釋確認**前提倒反**:arcrun 的 3 表(`entries`/`templates`/`entry_values``entry_type='block'`)**是刻意設計的基本盤**,明寫 plugin modelcore + AGE)、"Table never changes"。本目錄帶獨立 `blocks` 表 + 0001/0005 CREATE TABLE 的那套「v3」**才是長歪的違規殘留,要刪**。
- **「共用 D1 vs 走 API」**:選走 API(鐵律 3)。兩 repo 不同 D1 庫不是問題——插件本來就不該碰基本盤的 D1。
- **「0005 歸屬」**:問題消解。插件不該有任何 migration。`entry_values` 屬基本盤、基本盤已有。
---
## 基本盤 API 契約(arcrun/kbdb,已存在,不動)
`arcrun/kbdb/src/` 確認的真實端點(插件改寫的目標介面):
| 端點 | 用途 | 備註 |
|---|---|---|
| `POST /entries` | 建 atomic entry | body 用 **`entry_type`**block/value/...)、**`owner_id`**;回 `{success, entry}` |
| `GET /entries` | listfilter: entry_type/owner_id/parent_id/page_name/limit/offset | |
| `GET /entries/search?q=&owner_id=` | D1 LIKE keyword search | base 只有 keyword;語意搜尋是 optional embed module |
| `GET/PATCH/DELETE /entries/:id` | 單筆 CRUD | |
| `POST /templates` | 建 templatename+slots[]= **替代建表** | |
| `GET /templates``GET /templates/:name``PUT /templates/:name` | template CRUD | |
| `POST /records` | 建 record`{template, values:{slot:content}, owner_id?}`= 填 slot | 回 `{success, record}` |
| `GET /records/by-template/:template?owner_id=` | 列某 template 的所有 record | |
| `GET /records/:recordId` | 取單筆 record 的 slot values | |
> ⚠️ **與插件本地舊 copy 的差異**(改寫時務必對齊):欄位是 `entry_type`/`owner_id`(不是本地的 `type`/`user_id`);回應包在 `{success, ...}`;基本盤**無** `PUT/DELETE /records/:id`、**無** `entity_type` 欄位、**無** vectorize 綁定(語意搜尋與 embedding 屬 optional 模組,不在 base)。
### 插件專屬狀態怎麼存(leo 2026-06-14 釘正:不准建表)
triplet 的 `clusters`/`bridge_score`/`confidence`/`source_block_id`、entity 正規化的 `canonical`/`alias``entity_type`——**全部是「新資料類型 = 建 template + 填 slot」**,不是建表:
| 插件狀態 | 存法(純 API) |
|---|---|
| triplet | `template='triplet'`slots: subject/predicate/object/source_block_id/confidence/clusters_json/bridge_score → `POST /records` |
| entity 正規化 | `template='entity'`slots: canonical/aliases_json/entity_type/owner → `POST /records`;查重靠 `GET /records/by-template/entity` + (語意比對走 optional embedbase 沒有就降級 exact match |
| entity_type | 不再是 blocks 欄位(基本盤無此欄)→ 收進 entity record 的 slot |
「插件自建獨立 D1triplet_clusters 等)」**不是選項**——那仍是建表,違反鐵律 1。問「狀態存哪」時若想到建表,只准 template/slot。
## 邊界分類(R-EXT-1
依 HANDOFF 資產清單 + 實際 `ls` 比對(2026-06-14)。三類:**插件留** / **基本盤走** / **灰色地帶待確認**
(註:下表「基本盤走」的前提是 arcrun 升 v3——見上「前置議題」未解前不要實際搬。)
### 插件(graph)— 留本目錄
| 類型 | 檔案 |
|---|---|
| actions | `triplet-{crud,embed,entities,extract,stats,syntax,update}` (7)、`graph-{nodes,path,traverse}` (3)、`entity-{crud,graph-embed,normalize}` (3)、`predicate-normalize``search-{embed,query,suggest}` (3) |
| routes | `triplets.ts``graph.ts``entities.ts``search.ts` |
| migrations | `0003_triplet_user_id``0005_universal_table``0006_triplet_clusters``0008_entity_type` |
| contracts | `triplet.json` |
> 註:HANDOFF 同時列 `search-*` 與 `entity-graph-embed` 為插件資產,與此處一致。`0005_universal_table` 雖名「universal」(看似基本盤),但 HANDOFF 明列為 triplet/graph 相關 → 暫歸插件,待 R-EXT-3 釐清(見灰色地帶)。
### 基本盤(block CRUD)— 走 arcrun/kbdb
| 類型 | 檔案 |
|---|---|
| actions | `block-{crud,embed,import,ingest,process,update}` (6)、`tag-crud``profile-crud` |
| routes | `blocks.ts`+ `blocks.ts.bak` 清掉)、`tags.ts``templates.ts``profiles.ts` |
| migrations | `0001_init``0002_block_indexing` |
### 灰色地帶 — grep 調查結論(2026-06-14
調查方法:`src/index.ts` route 掛載、route↔action import、plugin action 的相依、DB 表引用。
**關鍵發現(耦合面)**
- **插件與基本盤在 action 層完全解耦**:所有 plugin actiontriplet/graph/entity/search**不 import 任何**基本盤或灰色地帶 actionplugin route 也只 import plugin action。`src/lib``src/models` 都是空的,無共用程式碼耦合。
- **耦合只在 DB 層**plugin action 直接以 SQL 讀 `blocks`(28×)、`entry_values`(31×)、`triplets`(23×)。→ 這正是 AGE-on-Postgres 訊號:插件靠共用 D1 掛在基本盤表之上。
- `triplets` 演進:0001 是 TABLE → 0005/0006/0007 改成 VIEW(疊在 universal table 上)。`entry_values` 是 0005 定義的 universal 儲存**表**。
**逐檔歸屬建議(附證據)**
| 檔案 | 證據 | 建議歸屬 |
|---|---|---|
| `0005_universal_table` | 定義 `entry_values`(v3 slots 儲存表)= 基本盤核心基礎設施,非純插件 | **基本盤(arcrun**。⚠️ 與 HANDOFF 列為插件相反——需與 arcrun 對齊 |
| `0007_v3_rename_and_cleanup` | 同時 rename 基本盤 `blocks` 表 + 重建 `triplets`/`user_profiles` VIEW | **基本盤(arcrun**做 rename;插件只依賴「基本盤已是 v3 schema + 有 triplets VIEW」 |
| `entry-crud` | **無人 import**route/index 都沒引用)= dead code | **刪**v2 legacy |
| `record-crud` + `records.ts` | 被 admin/templates/records 用,非任何 graph route | **基本盤/arcrun**(非 graph |
| `0004_task_status` + `tasks.ts` | `/tasks` 掛載,與 block 儲存/graph 都無關 | **arcrun 其他子系統**(非本插件) |
| `block-documents``convertPdf` + `convert.ts` | 被 `blocks.ts` 用 / `/convert` 掛載;PDF 轉換 | **基本盤/arcrun**(非 graph |
| `partner-auth` + `partners.ts` | 被 admin/partners/index 用;partner API key 認證 | **基本盤/arcrun 認證層** |
| `admin.ts``personality.ts` | `/admin``/personality` 掛載;與 graph 無關 | **arcrun 其他子系統**(非本插件) |
**結論**graph 插件邊界乾淨(action 層零耦合)。耦合**只在 DB 層**——而那層正是要拆掉的違規。掛載介面不是「DB 掛載」,是 **API 掛載**(見下)。
## 掛載介面(R-EXT-3= 基本盤 APIAPI-as-Wall,非共用 D1
> 推翻原「AGE-on-Postgres 共用 D1 + triplets VIEW」設計。leo 鐵律:插件不碰表。
- **掛載 = HTTP API**:插件不共用 D1、不自建表、不建 VIEW。插件**只能用基本盤 API / CLI / MCP 去建與讀**leo 2026-06-14 釘正),與 AI/人同一條路。連 `triplets` VIEW 都不做——「圖」在**插件層的記憶體裡**從 record 組裝,不靠 DB VIEW。
- **插件依賴的基本盤介面** = 上節「基本盤 API 契約」那張表(已存在於 arcrun/kbdb,不需 arcrun 升級、不需合庫)。
- **base URL** = `KBDB_BASE_URL` env varleo 2026-06-14:做成可設定,先留空)。插件透過 `src/lib/kbdb-client.ts` 打它。本地測試用 mock / 本地 base worker,部署時填真網址。
- **禁止繞道**:不准把 SQL 藏在 helper 裡假裝走 API。client 只發 HTTP,零 `.prepare`。hook 擋語法層,這條是設計層補強。
### 改寫對照(21 個違規 action → API
| 現狀(違規 SQL) | 改寫成(基本盤 API) |
|---|---|
| `triplet-crud` `INSERT INTO entry_values` + value blocks | `POST /templates`(確保 triplet template 存在) + `POST /records`(template=triplet, 填 slot) |
| `triplet-crud` `queryTriplets` 大 JOIN | `GET /records/by-template/triplet?owner_id=` → 插件層 filter/組裝 |
| `triplet-crud` `getTriplet`/`updateTriplet`/`deleteTriplet` | `GET /records/:id`update/delete 受限於 base 無 PUT/DELETE record(見「缺口」) |
| `triplet-stats` 聚合 SQL | `GET /records/by-template/triplet` 後在插件層 reduce 統計 |
| `triplet-extract`/`triplet-entities` 讀 blocks | `GET /entries`/`GET /entries/search` |
| `graph-{nodes,path,traverse}``triplets` 表 | 先 `GET /records/by-template/triplet` 取全部 triplet → 插件層建鄰接表跑圖演算法 |
| `entity-crud`/`entity-normalize` 讀寫 entity 表 | `template='entity'` + `POST/GET records`;語意比對降級 exactbase 無 vectorize |
| `search-query` SQL | `GET /entries/search?q=`keyword);語意搜尋待 optional embed 模組 |
| `predicate-normalize` | 純函式(若有 SQL 一併改 API) |
### 基本盤缺口(改寫時誠實標記,不偷建表補)
base 目前**無** `PUT/DELETE /records/:id`、**無** entity_type 欄位、**無** vectorize。影響:
- triplet/entity 的 **update/delete** → base 缺端點。對策:(a) 標記為 `[→arcrun]` 缺口待基本盤補端點;(b) 暫以「建新 record + 標記舊 record 作廢」soft-delete**仍走 API**。不得為此自建表或直連 D1。
- **語意搜尋 / entity embedding 比對** → 屬 optional embed 模組(不在 base)。base 沒有時降級成 exact match / keyword。embedding 不是插件職責,不在插件建 vectorize。
## 改寫 task(落到 tasks.md R-EXT-4
見 tasks.md 新增的 R-EXT-4「改寫成走 API」區塊。
## 獨立成 repoR-EXT-2
1. 確認 R-EXT-1 邊界、清掉基本盤檔案(移交 arcrun)後,本目錄只剩插件。
2. 改名 KBDB-graph、`git init`、設 remote(帳號問 leo)。
3. 部署繞開 GitHubwrangler 直推 CF;不開 Actions。
4. 推 GitHub(由本 CC 自己推)。
## CLAUDE.md 裁剪
移除整套 KBDB v3 基本盤規範(萬物皆 Block 全文、50 endpoints、Block CRUD 細節),保留:樂高法、graph 插件定位、掛載介面、上游約束、wiki 讀取順序。基本盤規範移交 arcrun/kbdb 的 CLAUDE.md。