docs(sdd): SDD 生命週期鐵律遷移 + SDD 位置統一到 system-dev/docs/3-specs/

- 位置統一:舊 docs/3-specs/ 五份 SDD git mv 到 system-dev/docs/3-specs/,舊位置留 README 指針
- 狀態判定:0 份 active(無現行開發,合法);paused×3(ingest-contract/kbdb-graph-extraction/plugin-install,等跨 repo 接通);closed×2 入 archive/(arcrun-key-auth/blocks-edit-api 死件,附封存原因)
- 鋪檔(自 system-dev-template v1.15.0):SDD-LIFECYCLE.md、pending-changes.md、sdd-guard.sh 新版、sdd-check.md、sdd-active-check.sh
- hook 掛載:settings.json PreToolUse Write|Edit 加 sdd-guard.sh
- CLAUDE.md:SDD 鐵律段(濃縮五條+0-active 註明重啟先升 active)+修正遷移後舊路徑
- 驗證:sdd-active-check exit 0;guard pipe-test 0-active 擋 code 寫入(exit 2)/md 放行(exit 0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:03:58 +08:00
parent b53c5c94a9
commit d2618758e2
20 changed files with 279 additions and 21 deletions
+39
View File
@@ -0,0 +1,39 @@
# SDD 生命週期鐵律(不可違反)
> 來源:leo 2026-07-17 拍板。
> 適用:`system-dev/docs/3-specs/` 下的「規格 SDD」(requirements/design/tasks 三件式資料夾)。
> **不適用**:派工表/sprint 檔、journeys/ 卷宗、TEMPLATE-sdd、README、pending-changes.md——它們不是 SDD,不掛 status。
## 狀態標記(機器可查)
每個 SDD 資料夾的 `design.md` 最上方掛 YAML frontmatter
```yaml
---
status: active # active | draft | paused | closed
superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名
---
```
- `active`:現行規格,全 repo 開發任務唯一對應源。**任何時刻整個 repo 最多一份。**
- `draft`:起草中,尚未採納。
- `paused`:動過工、暫停中;恢復=升回 active(先收掉現任 active)或被新 SDD 繼承。
- `closed`:已完成或被取代;被取代者填 `superseded_by` 並移入 `3-specs/archive/`
## 五條鐵律
1. **單一活性**:任何時刻只允許一份 `status: active`。所有開發任務必須對應這份 SDD 的 tasks。找不到對應任務 → 停下來問,不准直接做。
2. **禁止自行建立 SDD**:CC 在任何情況下不得主動建新 SDD。收到使用者意見先分類:澄清問題→回答即可不動文件;任務層變更(不影響核心設計)→更新現行 SDD 的 tasks 區段並標日期與原因;規格層變更(核心設計/方向改變)→走第 3 條,不准直接改 spec。
3. **規格變更只有一條路**:產出 change proposal 寫入 `system-dev/docs/3-specs/pending-changes.md`(變更摘要與觸發原因+影響分析:現行 SDD 哪些任務作廢/修改/不受影響/尚未完成),然後**停止**,等使用者明說「confirm」。沒 confirm 就繼續依現行 SDD 工作。多個 proposal 可並存緩衝區、由人一次裁決——CC 的速度導向影響分析,不是規格增生。
4. **開新 SDD 的唯一時機**:使用者 confirm 一份規格層 proposal 時,依序:
a. 舊 SDD 未完成且仍有效的任務**逐條搬入**新 SDD 的 tasks——**這步做完前不准寫任何程式碼**(強迫顯式盤點,遺漏會在 d 的清單被看到,而不是三天後才發現)。
b. 舊 SDD frontmatter 改 `status: closed, superseded_by: <新SDD>`,資料夾移入 `3-specs/archive/`
c. 新 SDD 的 changelog 首行記錄:繼承自哪份、為何取代。
d. 向使用者列出「已搬移任務清單」與「已作廢任務清單」請求最終確認。
5. **每次 session 開始**:先讀現行 active SDD 與 pending-changes.md,回報三個數字——「現行規格〈名稱〉+未完成任務 N+待裁決 proposal M」——再開始工作。若回報出現兩份 active=規則已被違反,當場糾正。
## 硬約束(不信任單點自律,用結構保證不變量)
- `.claude/hooks/sdd-guard.sh`PreToolUse Write|Edit):active 數 >1 → 任何寫檔一律擋;寫 code 檔需恰好 1 份 active。
- `scripts/sdd-active-check.sh`:獨立檢查,pre-commit / CI 可掛,違反 exit 1。
- 誠實限制:hook 只擋語法層明顯違規,繞道可行但留痕可審;不聲稱不可繞過。
@@ -0,0 +1,124 @@
---
status: closed # active | draft | paused | closed(生命週期鐵律見 ../../SDD-LIFECYCLE.md
superseded_by: ""
---
> **封存(2026-07-17**:舊 KBDB 時代草稿(等 richblack review,帳號已 suspend)。key auth 屬基本盤 arcrun/kbdb 職責,非本插件範圍(2026-06-14 API-as-Wall 改寫後失效)。
# KBDB — Arcrun Key Auth
> 建立:2026-05-05
> 狀態:草稿,待 richblack review
---
## 背景
KBDB 是 Arcrun 平台的子服務(對外統稱 Arcrun)。目前 KBDB 有兩種身份:
- **Internal**:後端機器使用 `KBDB_INTERNAL_TOKEN`hex secret
- **Partner**:外部系統使用 `pk_live_xxx` API Key,需先由 internal 建立 partner 記錄
問題:Arcrun 用戶(人類或 AI agent)登入 arcrun.dev 取得的 `ak_xxx` Key 無法直接存取 KBDB。要存 KBDB 目前沒有自助路徑。
## 目標
讓 Arcrun 用戶的 `ak_xxx` Key **直接可用於 KBDB**,不需要額外申請第二把 Key。
---
## 設計決策
### Key 格式不變
Arcrun Key 格式維持 `ak_` 前綴(32 char hex),KBDB 新增對這個前綴的識別邏輯。
不引入新的 Key 格式,不改 Arcrun 的 Key 產生邏輯。
### KBDB 驗證路徑新增 `ak_` 支援
現有 `index.ts` auth middleware 的 Partner 驗證區塊(line 103)僅接受 `pk_` 前綴。
改為同時接受 `pk_``ak_`,查詢同一張 partner 表(tpl-partner)。
```
if (effectiveToken.startsWith('pk_') || effectiveToken.startsWith('ak_')) {
const partner = await lookupPartner(c.env.DB, tokenHash);
...
}
```
### Arcrun 登入時寫入 KBDB partner 記錄
Arcrun `cypher-executor/src/routes/auth.ts` 的 OAuth callback`/auth/callback`
在建立 UserRecord 後,呼叫 KBDB `POST /partners` 建立對應的 partner 記錄。
寫入時機:
- 新用戶首次登入 → 建立 partner 記錄(`upsert` 語意:若已存在則跳過)
- Key rotate`PUT /me/api-key/rotate`)→ 舊 partner revoke,建新 partner 記錄
- Key revoke`DELETE /me/api-key`)→ KBDB partner 設為 revoked
寫入內容:
```json
{
"name": "arcrun:{email}",
"org_namespace": "arcrun:{email}",
"api_key_hash": "SHA-256(ak_xxx)",
"status": "active"
}
```
`org_namespace``arcrun:{email}` 格式,與 KBDB 原有的 partner namespace 不衝突。
### Namespace 隔離
用戶只能存取自己 `org_namespace` 下的 KBDB 資料,與其他用戶完全隔離。
`org_namespace = 'arcrun:{email}'`,不是 admin,不會被提升為 internal。
### 服務啟用 UIDashboard /keys
初期:Arcrun 用戶登入即自動建立 KBDB partner 記錄(無需勾選),
因為「KBDB 是 Arcrun 的捆綁服務」,就像 n8n 登進去 Data Table 就在那裡。
未來(有計費需求時):Dashboard `/keys` 頁面可加 toggle 控制,切換時
呼叫 `PATCH /me/kbdb` → cypher-executor 再去 KBDB 啟用/停用 partner。
---
## 實作範圍(本 SDD
### KBDB 側(`matrix/kbdb/`
**修改** `src/index.ts`
- auth middleware Partner 驗證區塊:將 `effectiveToken.startsWith('pk_')` 改為同時接受 `ak_`
**不改**
- `src/actions/partner-auth.ts``hashToken` / `lookupPartner` 邏輯不變)
- `src/routes/partners.ts``POST /partners` 介面不變,Arcrun 呼叫此 endpoint 建記錄)
- D1 schema`tpl-partner` template 不變)
### Arcrun 側(`matrix/arcrun/cypher-executor/`
屬於 `frontend-redesign` SDD 範圍內的後端補充,見該 SDD tasks.md。
本 SDD **不**負責 Arcrun 側實作,僅說明預期行為:
1. OAuth callback 成功後,以 `KBDB_INTERNAL_TOKEN` 呼叫 KBDB `POST /partners`
2. Key rotate 時:先 KBDB `DELETE /admin/partners/{id}` (revoke),再建新記錄
3. Key revoke 時:KBDB `DELETE /admin/partners/{id}`
---
## 不做的事
- 不改 Key 格式(`ak_` 保留)
- 不合併 Arcrun USERS_KV 和 KBDB partner 表(兩邊各自維護)
- 不做跨 namespace 的資料共享
- 不做 KBDB 側的 OAuth 驗證(KBDB 永遠只驗 token hash
---
## 風險
| 風險 | 緩解 |
|------|------|
| Arcrun 寫 KBDB 失敗(KBDB 暫時不可用) | 登入仍成功;寫 KBDB 失敗靜默 log,用戶下次 rotate key 時重建 |
| `ak_` Key 被猜測 | 32 char hexentropy 足夠;與 `pk_` 同等安全 |
| email 含特殊字元破壞 namespace | `org_namespace``arcrun:` + raw emailD1 存 TEXT 無問題 |
@@ -0,0 +1,60 @@
# KBDB Arcrun Key Auth — Tasks
> 建立:2026-05-05
> 權威進度來源:本檔。完成一項立刻 `[x]`,不批次。
---
## Phase 0 — SDD 建立
- [x] 撰寫 `design.md`
- [x] 撰寫 `tasks.md`(本檔)
- [ ] richblack review + 認可 → 開 Phase 1
---
## Phase 1 — KBDB auth middleware 接受 `ak_` Key
**修改檔案**`matrix/kbdb/src/index.ts`
- [x] 1.1 將 line 103 的 `effectiveToken.startsWith('pk_')` 改為
`effectiveToken.startsWith('pk_') || effectiveToken.startsWith('ak_')`
- [ ] 1.2 本地跑現有測試確認不 break:`pnpm test`
---
## Phase 2 — Arcrun OAuth callback 寫入 KBDB partner 記錄
**修改檔案**`matrix/arcrun/cypher-executor/src/routes/auth.ts`
> 注意:此 Phase 需要 Arcrun 側有 `KBDB_INTERNAL_TOKEN` 和 `KBDB_BASE_URL` 兩個 env binding。
- [x] 2.1 在 `Bindings` type`types.ts`)加入 `KBDB_INTERNAL_TOKEN?: string``KBDB_BASE_URL?: string`
- [x] 2.2 建立 helper `src/lib/kbdb-partner.ts`
- `ensureKbdbPartner(env, email, apiKey)` → PUT /admin/partners/by-key-hash,失敗靜默 log
- `revokeKbdbPartner(env, oldApiKey)` → DELETE /admin/partners/{id},失敗靜默 log
- [x] 2.3 在 OAuth callbackUserRecord 建立/取得後)呼叫 `ensureKbdbPartner`fire-and-forget
- [x] 2.4 在 `PUT /me/api-key/rotate` 呼叫:`revokeKbdbPartner(oldKey)` + `ensureKbdbPartner(newKey)`
- [x] 2.5 在 `DELETE /me/api-key` 呼叫 `revokeKbdbPartner`
- [ ] 2.6 `wrangler secret put KBDB_INTERNAL_TOKEN`cypher-executor Worker)← 需要人工執行
- [x] 2.7 在 `wrangler.toml``KBDB_BASE_URL = "https://kbdb.finally.click"`
另外:KBDB `admin.ts` 新增 `PUT /admin/partners/by-key-hash` endpointupsert by hash,不產生新 key)。
KBDB `types.ts` 加入 `KBDB_INTERNAL_TOKEN` 到 Bindings。
KBDB `admin.ts` 放寬 `org_namespace` regex(允許 `arcrun:email@domain` 格式)。
---
## Phase 3 — 驗證
- [ ] 3.1 新用戶 OAuth 登入 → 確認 KBDB partner 記錄建立(`GET /admin/partners` 查詢)
- [ ] 3.2 用 `ak_xxx` Key 直接打 KBDB `GET /blocks` → 確認 200(非 401
- [ ] 3.3 Key rotate → 確認舊 Key 401,新 Key 200
- [ ] 3.4 Key revoke → 確認舊 Key 401
---
## 目前狀態
- Phase 0 已完成(等 richblack 認可)
- Phase 13 全部 `[ ]`,等認可後動工
@@ -0,0 +1,234 @@
---
status: closed # active | draft | paused | closed(生命週期鐵律見 ../../SDD-LIFECYCLE.md
superseded_by: ""
---
> **封存(2026-07-17**:基於舊「萬物皆 Block/blocks 表」架構,該架構已判定為違規殘留並刪除(2026-06-14 改寫);base `PATCH /records/:id` 已由 Arcrun #6 實作取代本需求。
# KBDB — Blocks Edit API
> **建立**2026-05-06
> **狀態**:草稿,待 richblack review
> **依賴**`matrix/kbdb/CLAUDE.md`(萬物皆 Block 架構,2026-02-28 鎖定)
> **驅動需求**`polaris/mira/.agents/specs/mira-app/design.md`(前端 inline edit 直寫 KBDB 的需求)
---
## 0. 背景
KBDB v3「萬物皆 Block」架構鎖定後,現役 routes 涵蓋 Block CRUD 多數操作,但**缺少編輯既有 block 的 PATCH endpoints**。具體缺口:
| 操作 | 現役狀態 |
|---|---|
| `POST /blocks/ingest`(建立) | ✅ 已有 |
| `GET /blocks/{id}`(讀取單筆) | ✅ 已有 |
| `GET /blocks/`(列表 / 查詢) | ✅ 已有 |
| `DELETE /blocks/{id}`(刪除) | ✅ 已有 |
| **`PATCH /blocks/{id}`(部分更新)** | ❌ **缺** |
| `POST /triplets/`(建立) | ✅ 已有 |
| **`PATCH /triplets/{id}` / `DELETE /triplets/{id}`** | ❌ **缺** |
| `PUT /templates/{name}` | ✅ 已有 |
| `PATCH /tasks/{id}/status` | ✅ 已有 |
→ 沒有 PATCH/UPDATE block 內容的 API,前端「inline edit 寫回 KBDB」做不了。
> **註**`src/routes/blocks.ts.bak` 有 PUT /:id 的舊實作可參考,但已被 v3 取代並停用。**不直接複用 .bak 檔案**,按 v3 規範重寫。
---
## 1. 範圍
本 SDD 涵蓋兩件事:
1. **補三組 endpoint**`PATCH /blocks/{id}`, `PATCH /triplets/{id}`, `DELETE /triplets/{id}`
2. **建三個 templates**`data-source-config`, `source-skill`, `wiki-page`(為 mira-app 準備)
**不在範圍內**
- 改 schemav3 鎖定,禁止 ALTER TABLE
- 改既有 endpoint 行為(純加新 endpoint
- 多用戶權限細分(partner key 已能做 org 隔離,沿用即可)
- 編輯歷史 / undo(未來 SDD
---
## 2. PATCH /blocks/{id}
### 2.1 用途
部分更新一個既有 block。前端 inline edit 場景:使用者點 edit icon,改 content / refs / tags,按儲存 → 送 PATCH。
### 2.2 規格
```
PATCH /blocks/{id}
Authorization: Bearer <api_key>
Content-Type: application/json
Body (所有欄位皆 optional,至少要有一個):
{
"content": "新的內容",
"tags": ["tag1", "tag2"], // 完整覆寫 tags 陣列
"refs": ["block-id-1", "block-id-2"], // 完整覆寫 refs 陣列
"slots": { "key": "value" }, // 完整覆寫 slots 物件
"source": "...", // 通常不改,但允許
"metadata_json": { ... } // 完整覆寫
}
Response 200:
{
"id": "...",
"content": "...",
"updated_at": "2026-05-06T...",
...完整 block 欄位
}
Response 400:
{ "error": "no fields to update" }
Response 403:
{ "error": "block belongs to different org" }
Response 404:
{ "error": "block not found" }
```
### 2.3 行為
- 只更新 body 內提供的欄位
- 自動更新 `updated_at`
- 自動重新計算 `content_hash`(如 content 變動)
- 自動觸發 embedding 重算(如 content 變動,async
- **權限**partner key 只能改自己 org 內的 block(透過 user_id 對應),internal token 可改任何 block
- **content_hash 衝突**partner key 不可修改 v3「`source``system` 的 admin 標記資料」(沿用既有 admin-preservation 規則)
### 2.4 實作位置
- Action: `src/actions/update-block.ts`< 100 行,按 KBDB CLAUDE.md 樂高法)
- Route: `src/routes/blocks.ts` 加新 OpenAPI route
- Test: `tests/blocks-update.test.ts`
---
## 3. Triplet 編輯:使用既有 `PUT /records/:id` + `DELETE /records/:id`
**設計修正(2026-05-06 實作時發現)**
v3 萬物皆 Block 架構下,triplet 是 `tpl-triplet` template 的 record(用 entry_values 存 subject/predicate/object slots)。**既有 `PUT /records/:id``DELETE /records/:id` 已涵蓋編輯/刪除需求**,無需新增 `PATCH /triplets/:id`
→ 前端編輯異見牆上的 triplet:
- 編輯:`PUT /records/{triplet_record_id}` body `{ values: { subject, predicate, object } }`
- 刪除:`DELETE /records/{triplet_record_id}`
**本 SDD 不再規劃新 triplet endpoint**
---
## 5. 三個新 Templates
按 KBDB v3 規範,新資料類型透過 template 定義,**不動 schema**。
### 5.1 template: `data-source-config`
每個資料源實例對應一個此 template 的 block。Mira 的「來源篩選」、cron workflow 的「每天去抓什麼」都讀這個。
```yaml
template_name: data-source-config
slots:
- name: string # "電子時報"、"我的 Logseq"
- channel: string # rss / telegram / km-writer / voice-stt / ai-comment / ai-canon
- config: object # channel-specific (e.g., rss: {url, schedule})
- skill_id: string? # 連到 source-skill block 的 id
- enabled: bool
- ai_comment: bool # 是否需要 AI 加註解
- ai_comment_style: string? # 提示給 claude_api 的風格
```
### 5.2 template: `source-skill`
每個 source 累積的「分析配方」(prompt + few-shot)。可在前端編輯、版本化。
```yaml
template_name: source-skill
slots:
- name: string # 例 "電子時報科技類分析"
- prompt: text # system_prompt 內容
- examples: text? # few-shot examplesmarkdown
- version: int
- based_on: string? # 上一版的 block id
```
### 5.3 template: `wiki-page`
AI 從河道對話合成的定稿。
```yaml
template_name: wiki-page
slots:
- entity_name: string
- summary: text # markdown
- key_blocks: array<string> # 引用的 source block ids
- conflicts: array<string>? # 標記為矛盾的 block ids
- generated_at: timestamp
- version: int
- based_on: string?
```
### 5.4 建立方式
不寫 SQL migrationv3 規範禁止)。改用 KBDB 既有的 `POST /templates`
```bash
curl -X POST https://kbdb.finally.click/templates \
-H "Authorization: Bearer <internal_token>" \
-d '{"name": "data-source-config", "slots": [...]}'
```
→ tasks.md 列為 P0 任務(用 internal token 一次性建好)。
---
## 6. 實作步驟
### Phase 1:補 endpoints
1.`src/actions/update-block.ts`(純函數,< 100 行,含權限檢查)
2.`tests/blocks-update.test.ts`(含 happy path、403、404、no-fields 三案)
3.`src/routes/blocks.ts` 的 PATCH routeOpenAPI 定義 + 呼叫 action
4.`src/routes/triplets.ts` PATCHwrapper+ DELETEalias
5. 部署 + smoke test
### Phase 2:建 templates
6. 用 internal token 呼叫 `POST /templates` 建三個 template
7. 驗證:用 partner key (mira 用的) 創建一個 `data-source-config` block 看能否寫成功
### Phase 3:補 OpenAPI spec
8. 確認新 routes 自動進 swagger.jsonOpenAPIHono 應該自動,需驗證)
---
## 7. 風險
- **embedding 重算成本**PATCH content 會觸發 vectorize 重算,頻繁 inline edit 可能拖慢。**對策**embedding 改為 async(行為已是 async,需確認)。
- **content_hash 計算遺漏**:忘記重算會讓查重失效。**對策**:在 action 內統一處理,不讓 route 層管。
- **partner key 越權**:必須驗 user_id 對應,不能讓 partner A 改 partner B 的 block。**對策**write tests 涵蓋此案。
- **三個 templates 命名衝突**:若 KBDB 已有同名 template 會 fail。**對策**:建立前先 GET /templates/{name} 檢查。
---
## 8. 不在範圍內
- 編輯歷史 / undo / version diff(未來 SDD
- Block soft deletev3 已有 hard deletesoftdelete 是 enhancement
- Bulk PATCH(一次改多個 block,未來看需求)
- Field-level permissions(特定欄位只能某些 user 改)
- WebSocket 通知 block 改了(即時協作)
---
## 9. 變更紀錄
| 版本 | 日期 | 內容 |
|---|---|---|
| v0 | 2026-05-06 | 初稿。對應 mira-app 的 inline edit 需求。 |
@@ -0,0 +1,68 @@
# KBDB Blocks Edit API — Tasks
> 對應 SDD[design.md](design.md)
> 上次更新:2026-05-06Phase 1 + Phase 2 完成)
---
## Phase 1:補 PATCH endpoint ✅ 完成
### 1. PATCH /blocks/{id}
- [x] 1.1 zod schema 直接放在 route 檔(既有 pattern
- [x] 1.2 寫 `src/actions/block-update.ts`96 行,符合樂高法 < 100
- 取既有 block + getBlock fallbackid 或 logseq_uuid
- 權限檢查:partner key 比對 user_id 前綴
- 自動重算 content_hash(如 content 變)
- 觸發 embedding async 重算(不阻塞 PATCH 回應)
- 寫回 D1
- 回傳更新後的 block
- [x] 1.3 寫 `tests/blocks-update.test.ts`**7 case 全通過**
- happy: content + content_hash 重算
- happy: tags + refs 同改
- 400: 無欄位
- 404: 不存在
- 403: partner 越權
- 200: partner 改自己 namespace
- content_hash 在只改 tags 時不變
- [x] 1.4 在 `src/routes/blocks.ts` 加 PATCH routeOpenAPI
- [x] 1.5 部署到 prodkbdb.finally.click+ smoke test 4 case 通過
### 2. Triplet 編輯:使用既有 `PUT/DELETE /records/:id`
- [x] 2.1 設計修正:v3 萬物皆 Blocktriplet 是 record,既有 endpoints 已涵蓋。本任務組無需新增 endpoint。
- [ ] 2.2 在 mira-app 前端「異見牆」實作呼叫 `PUT /records/:id`(待 mira 階段 3
### 3. OpenAPI spec 同步
- [x] 3.1 OpenAPIHono 自動產 swagger.jsonroute 用 createRoute 已自動納入)
- [ ] 3.2 部署後驗證 swagger UI 顯示新 route(待手動驗證)
---
## Phase 2:建三個 templates ✅ 完成
- [x] 4.1 確認 KBDB 內無同名 template(透過 GET /templates 確認)
- [x] 4.2 用 internal token POST /templates 建 `data-source-config`id: `tpl-data-source-config`
- [x] 4.3 用 internal token POST /templates 建 `source-skill`id: `tpl-source-skill`
- [x] 4.4 用 internal token POST /templates 建 `wiki-page`id: `tpl-wiki-page`
- [x] 4.5 驗證:3/3 templates 在 GET /templates 列表內
---
## 風險追蹤
- ~~風險 1partner key 跨 org 越權~~ — ✅ unit test 已涵蓋(403 partner 越權)
- ~~風險 2embedding 重算造成 D1 寫入 spike~~ — ✅ 改成 fire-and-forget(不 await),不阻塞 PATCH
- ~~風險 3content_hash 不一致~~ — ✅ unit test 驗證 hash 重算對應內容
## Known Issues(不在本 SDD 範圍,待另開)
- **`POST /blocks/ingest` 不寫入 `source` 欄位**input 接受 `source` 參數但僅用於 id slug 生成,未寫進 block 的 source 欄位(block-ingest.ts:84 的 INSERT 缺欄位)。對 mira 影響:所有 source 區分目前無效,需等 KBDB 修復或直接走 `POST /blocks` + slots。建議下一份 KBDB SDD `block-ingest-source-fix` 處理。
---
## 部署紀錄
- 2026-05-06: Worker version `b7df3c38-e138-41fb-a16c-cc9d2dfeebea` 部署上線
- Smoke test 通過:content 改寫 + hash 重算、tags 改寫、400 空 body、404 不存在
@@ -0,0 +1,92 @@
---
status: paused # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md
superseded_by: ""
---
# ingest-contract — 設計
> **藍圖在頂層**:本 SDD 只放 **kbdb-graph-plugin 內部實作細節**。跨專案脈絡(為什麼拆 ingest/graph、mira 蒸發、整體資料流)見 InkStoneCo `docs/3-specs/mira-dissolve/`design + requirements)。
> **對應交辦**[kbdb-graph-plugin #1](https://github.com/uncle6me-web/kbdb-graph-plugin/issues/1)T3)。
> **鐵律**API-as-Wall / 零建表 / 零 SQL / 零 migration。embedding/語義 normalize 屬 base 模組,graph 只調不自算。圖在插件層記憶體組裝。
---
## 1. 範圍
graph 插件補上**寫入端**:收 ingest 送來的 `ingest-candidate` envelope,正規化後以「翻 triplet 的 `status` slot」做 **deprecate-then-append** 取代,並讓查詢面 active-only。查詢面(search/traverse/neighbors)已成熟,本批只補 active 過濾。
不在本批:圖工具併入 KBDB MCP 的**註冊薄殼**(需 arcrun 配合,見 §6);semantic normalize 端到端(依賴 base embed 部署)。
## 2. 邊界契約
唯一入口契約 = `contracts/ingest-candidate.json`(已搬入,T3.1)。要點見 `contracts/README.md`
graph 收件後負責的領域欄位(ingest 禁送):`id` / `clusters` / `bridge_score` / `created_at` / triplet 上的 `*_entity_type``additionalProperties:false` → 違規欄位 422。
## 3. template slot 變更(T3.2 / 3.2b
現況(`src/lib/templates.ts`,已核實):
- `TRIPLET_SLOTS``status` / `superseded_by`
- `ENTITY_SLOTS``gloss`
要加:
- triplet`status``active` | `deprecated`,預設 `active`)、`superseded_by`(指向取代它的新 record id,空=未被取代)。
- entity`gloss`(一句話描述,供「詞+gloss」語義 normalize 的 embedding 對象)。
### ⚠️ ensureTemplate early-return 陷阱(前置警示 1,已核實)
`KbdbClient.ensureTemplate``src/lib/kbdb-client.ts:120`)命中既有 template 即 `return`**不會把新 slot 補進已 seed 的 template**。只改 `TRIPLET_SLOTS` 陣列對既有環境無效。
**對策**`ensureTemplate` 改為「命中既有 → 比對 slot 差集 → 缺的走 base `PATCH /templates/:id` 補上」,而非 early-return。新環境(template 不存在)走原 `POST /templates` 路徑不變。如此既有 + 全新環境都正確收斂,不需另跑一次性遷移腳本。
## 4. KbdbClient.updateRecord(前置警示 2,已核實)
client 現有 `createRecord` / `getRecord` / `listRecordsByTemplate`**無 record 層 PATCH**`updateEntry` 是 entry 層,不是 record)。
新增 `updateRecord(recordId, values)` → 呼叫 base `PATCH /records/:id`base #6 已就緒)。deprecate(翻 status)與 rollback(翻回 status)都靠它。
## 5. POST /triplets/ingest(核心,T3.3
純函式 action`src/actions/triplet-ingest.ts`<100 行,第一參數收 `KbdbClient`),route 只驗證 + 呼叫。流程:
1. **驗證 envelope**Zod schema 鏡射 `ingest-candidate.json``additionalProperties:false` → 禁送欄位(如 `bridge_score`)回 422。
2. **idempotency**:以 `source.uri` 為鍵查該來源現存 active triplet 的 `content_hash`(存進 triplet 的某 slot,見下)。同 hash → no-op 回 `{skipped:true}`
3. **deprecate-then-append**:新 hash → 查該 uri 所有 active triplet,逐筆 `updateRecord(id, {status:'deprecated', superseded_by:<新批暫不知 id,二階段或留空>})`,再 append 新批 active。
4. **回應**`{ ingested: N, deprecated: M, skipped: false }`
### content_hash / source.uri 存哪
triplet record 需記住它來自哪個 snapshot 才能做 idempotency 與 active-only。沿用既有 `source_block_id` 不夠(那是 Logseq block)。設計:triplet template 增記 `source_uri` + `content_hash` slot(屬 §3 同批 slot 變更,非建表)。active-only 與 deprecate 都按 `source_uri` 分組。
> 註:`superseded_by` 指向「取代它的新 record id」。新批 record 是 append 後才有 id → 若要精確回填,deprecate 分兩步(先 append 拿 id,再翻舊批 status + superseded_by)。先 append 後 deprecate 比較安全(中途失敗不會留下「全無 active」的空窗)。
## 6. 查詢 active-onlyT3.5
traverse / search / neighbors 從 records 組鄰接表前,先 `filter(status === 'active')`(預設值缺省視為 active,相容舊資料)。active 集合永遠乾淨,deprecated 仍可查(rollback / 考古)但不進圖遍歷。
## 7. 跨 repo 協調點:圖工具併入 KBDB MCPT3.6,需 arcrun 配合)
圖工具(traverse / neighbors / get_source+ `refresh` 要**併進 arcrun 的 KBDB MCP**`u6u-mcp-server`),**不另起 graph 獨立 MCP**。
- **graph repo 端能備好的**:圖查詢的 HTTP API + 邏輯、`get_source` 端點(T3.7)、`refresh` 代轉 ingest 的端點。
- **需 arcrun 端動的**:MCP 工具註冊薄殼那層 → 標清在 issue #1,由總管協調。本批不擅自開獨立 graph MCP。
- **`refresh` 紅線**(T3.6b):只能人發起的 MCP 調用觸發,**禁掛排程/webhook 自動 refresh**(否則變回 fan-out,踩 flag 紅線)。
- **T3.6d**:整合時移除 `search-query.ts` 代理 base 關鍵字那條(重複,關鍵字歸 KBDB MCP)。
## 7.5 get_source / refresh 落地(C 段,已實作)
- **get_source**`graph-source.ts` + `GET /graph/source/:name`):給節點名 → 回觸及它的 active triplet 的來源指標(`uri` / `anchor` / `block_id` / `content_hash`),按 uri+anchor 去重。為此 triplet template 增 `source_anchor` slotingest 從 `source.anchor` 帶入)。
- **refresh**`graph-refresh.ts` + `POST /graph/refresh`):純被動代轉 ingest 重抓+萃。graph 自己不抓不萃(ingest 純餵食器職責)。
- 🚫 紅線:只人發起 MCP 調用觸發,無排程/webhook。
- ingest 對象 = `KBDB_INGEST_URL`(env,T4 就緒前留空)。未設 → 誠實回 `{forwarded:false}`,不假綠。
- **search keyword 收斂**T3.6d):`POST /search` 移除公開 keyword 模式(重複 KBDB MCP `kbdb_search`),收斂為 suggest-only。`keywordSearch` helper 保留為 suggest 內部建構塊。
## 8. 不做 / 延後
- **graph CLI**T3.7b):延後。人少在命令行 traverse、AI 用不到 → 不做(非省工,是不誤導 AI 以為有這條路)。
- **semantic normalize**T3.2c):本批先 exact-onlysemantic 留接口。base embedArcrun #7)部署後才端到端,屆時標「待 base embed 部署驗」。
## 9. 樂高法遵循
- 每個 action <100 行、一檔一事、無狀態(狀態走 base API 持久化)。
- route 不含業務邏輯,只 `makeKbdbClient(c.env)` + 驗證 + 呼叫 action。
- 測試走 `tests/mock-client.ts`,不打真網路。
@@ -0,0 +1,37 @@
# ingest-contract — tasks
> **唯一進度來源**,不靠對話記憶。對應 [issue #1](https://github.com/uncle6me-web/kbdb-graph-plugin/issues/1)(頂層 mira-dissolve T3)。
> 完成一項即時打勾 + 註記證據。端到端需 leo21c 部署的,標「待部署驗」,不假綠。
## A. 契約 + template slot
- [x] **3.1**`contracts/ingest-candidate.json` 進本 repo + `contracts/README.md` 標明候選≠已存(2026-06-26
- [x] **3.2** `ensureTemplate` 改 slot-diff 補丁(命中既有 → base `PATCH /templates/:id` 補缺 slot,不再 early-return);`TRIPLET_SLOTS``status`+`superseded_by`+`source_uri`+`content_hash`2026-06-26`kbdb-client.ts`+`templates.ts`
- [x] **3.2b** `ENTITY_SLOTS``gloss`(已核實現無)(2026-06-26
- [ ] **3.2c** normalize 分層 fallback 接口:exact-only 先做;semantic 留接口(待 base embedArcrun #7
## B. 寫入端 + 取代(核心)
- [x] **3.3a** `KbdbClient.updateRecord(id, values)` → base `PATCH /records/:id`2026-06-26mock 同步)
- [x] **3.3b** `src/actions/triplet-ingest.ts`Zod strict 驗證 → idempotencyuri+hash)→ **先 append 後 deprecate**。88 行純函式(2026-06-26
- [x] **3.3c** `POST /triplets/ingest` route(驗證失敗 → 422 hook,只驗證+呼叫 action)(2026-06-26
- [x] **3.4** 測試 6 案全綠:正常 / 同 hash no-op / 新 hash deprecate / 污染(bridge_score+頂層 id) 422 / rollback`vitest run` 16 passed)(2026-06-26
- [x] **3.5** 查詢 active-only`queryTriplets` 缺省 filter `status==='active'`traverse/search/neighbors 皆經此;`includeDeprecated` opt-out 供 rollback/考古)(2026-06-26
## C. MCP(⚠️ 跨 repo,需 arcrun 配合 → issue 標清)
- [x] **3.6** 圖查詢 + `refresh` **HTTP API/邏輯備好(graph 端)**`GET /graph/source/:name``POST /graph/refresh`、既有 traverse/neighbors/path/relation。**MCP 註冊薄殼仍待 arcrun 配合**(不另起 graph MCP)(2026-06-26
- [x] **3.6b** `refresh` 紅線:`graph-refresh.ts` 純被動代轉,只人發起調用觸發;無排程/webhook2026-06-26
- [x] **3.6d** 移除 graph **公開** keyword 端點(`POST /search` 收斂為 suggest-onlykeywordSearch helper 留作 suggest 內部建構塊)(2026-06-26
- [x] **3.7** `get_source``graph-source.ts` + `GET /graph/source/:name`(回 uri+anchor+block_id+content_hashactive-only,去重)。連帶加 `source_anchor` slot2026-06-26
- [x] **3.7b** ~~graph CLI~~ 延後不做(人少用、AI 用不到 → 不誤導)
> **跨 repo 待接(總管協調)**:圖工具(traverse/neighbors/source+ refresh 的 **MCP 註冊薄殼**併入 arcrun `u6u-mcp-server`KBDB MCP),待 arcrun #7 落地後兩邊接。graph 端 HTTP API 已就緒。
> **refresh 待部署**`KBDB_INGEST_URL` 未設時 `refresh` 誠實回 `forwarded:false`ingest repo T4 未就緒)。端到端待 ingest 部署驗。
## 完成準則
- 全程 zero SQL / zero migration / 無 D1·Vectorize·AI 綁定(`wrangler deploy --dry-run` bundle 乾淨)
- 所有 action ≤100 行;`vitest run` 全綠(mock client
- 端到端 ingest→graph 走通需 base 上線 + ingest repoT4)就緒 → 標「待部署驗」
- issue #1 留 open,待實證綠燈才結案
@@ -0,0 +1,150 @@
---
status: paused # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md
superseded_by: ""
---
# 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。
@@ -0,0 +1,38 @@
# KBDB-graph 抽出 — Requirements
> 建立:2026-06-14
> 來源:InkStoneCo 頂層 `matrix-rearrange` R2 + 本目錄 `docs/HANDOFF-kbdb-plugin.md`
> 定調:leo 2026-06-13
---
## 背景
本目錄(`matrix/kbdb-graph-plugin`,原 `matrix/kbdb`)原是「整套 KBDB」。leo 2026-06-13 拍板拆分:
- **基本盤** = `arcrun/kbdb`D1 三表(blocks/templates/slots)的基本存儲讀寫,已併進 arcrun。
- **本目錄(KBDB-graph** = 掛在基本盤之上的 triplet 採集 + graph 查詢插件,**類比 Apache AGE 之於 Postgres**。
為何獨立:graph 能力較龐大、非基本存儲、leo 產權較複雜 → 獨立成 repo 不留 arcrun。
## 需求
1. **R-EXT-1 確認邊界**:把現有 `src/actions/``src/routes/``migrations/``contracts/` 逐一分類為「插件(triplet/graph/entity/search)」或「基本盤(block CRUD/template/tag/profile)」。基本盤的歸 arcrun/kbdb,插件的留本目錄。順便裁剪 CLAUDE.md(移除基本盤規範,只留 graph 插件相關)。
2. **R-EXT-2 獨立成 repo**:改名 KBDB-graph`git init` + 設 remote(帳號問 leo+ 推 GitHub。由本 CC 自己推,不經總管。
3. **R-EXT-3 定義掛載介面**KBDB-graph 如何掛在 arcrun/kbdb 基本盤上(AGE-on-Postgres 模式)——插件怎麼讀基本盤的 blocks 表、怎麼宣告自己的 triplet/entity/graph schema。
## 約束(硬性)
- **修改不是重建**:在現有實作上改,不重寫。
- **部署繞開 GitHub**wrangler 直推 Cloudflare,禁跨 repo 同步 Actions(當初害帳號被 flag 的模式)。新 repo 預設不開 Actions。
- **本目錄現無獨立 git**matrix 降級後脫離、被 InkStoneCo 頂層 gitignore)→ R-EXT-2 才 git init。在那之前用普通 mv 不是 git mv。
- **API-as-Wall / 萬物皆 Block** 仍適用於基本盤;插件對 graph 資料同樣經 API。
- **樂高法**`src/actions/` 單檔 < 100 行、一檔一事、無狀態。
## leo 已拍板(2026-06-14
- 獨立 repo = **新 repo `uncle6me-web/kbdb-graph-plugin`**(沿用現目錄名,非「KBDB-graph」字面)。
- 灰色地帶處理方式 = 先 grep 查引用再提建議(已完成,見 design.md)。
## 仍待確認(與 arcrun 對齊,非 leo
- 0005/0007 等基本盤 migration 歸屬與 HANDOFF 清單有出入,移交前對齊 arcrun。
@@ -0,0 +1,72 @@
# KBDB-graph 抽出 — Tasks
> 唯一進度來源,不靠對話記憶。完成即時更新。
> 狀態:[ ] 未開始 [🔄] 進行中 [x] 完成 [⏸] 卡住/待確認
---
## R-EXT-1 確認邊界
- [x] 1.1 inventory:列出現有 actions/routes/migrations/contracts2026-06-14 完成,見 design.md
- [x] 1.2 初步分類:插件 / 基本盤 / 灰色地帶(2026-06-14,見 design.md 邊界分類表)
- [x] 1.3 灰色地帶 grep 調查:證實插件 action 層零耦合、耦合只在 DB 層;逐檔附證據歸屬(2026-06-14,見 design.md 灰色地帶結論)。⚠️ 0005/0007 歸屬與 HANDOFF 有出入,仍需與 arcrun 對齊(屬 1.4
- [x] 1.4a 讀 arcrun 端真身對齊(2026-06-14):**發現 arcrun/kbdb 還是 v2entries,無 blocks/0005/0007/block-crud),且兩 repo 是不同 D1 庫**。v3 基本盤真身其實在本目錄。見 design.md「全局核對發現」
- [x] 1.4b 前置議題**總管已答覆**leo 2026-06-14):→ `InkStoneCo/docs/3-specs/matrix-rearrange/DECISION-kbdb-v3-baseplane.md`。三問消解:基本盤已在 arcrun/kbdb 且設計正確、掛載走 API(非共用 D1)、插件零 migration。**阻擋解除。**
- [x] 1.4c 不需移交/升級 arcrun——基本盤已正確。插件改寫成走 API 即可(見 R-EXT-4
- [x] 1.5 裁剪 CLAUDE.md:移除基本盤 v3 規範,只留 graph 插件 + 鐵律 + 安裝契約(2026-06-14
- [x] 1.6 清掉殘留:`blocks.ts.bak``ruvector.db`×2、`finally.click``.swarm`2026-06-14
## R-EXT-3 定義掛載介面(已定案 2026-06-14
- [x] 3.1 確認基本盤 API 契約(讀 arcrun/kbdb src,見 design.md「基本盤 API 契約」表)
- [x] 3.2 掛載方式定案:**API-as-Wall**HTTP API,非共用 D1、非 VIEW、非附加表)。圖在插件層記憶體組裝
- [x] 3.3 寫進 design.md 定稿(「掛載介面 = 基本盤 API」節)
## R-EXT-4 改寫成走 API(核心,2026-06-14 新增)
> 鐵律:插件零建表、零 migration、零 SQL,只用 API/CLI/MCP。
- [x] 4.1 `src/lib/kbdb-client.ts`:封裝基本盤 HTTP API,指向 `KBDB_BASE_URL`。零 `.prepare`2026-06-14
- [x] 4.2 wrangler.toml:移除 D1/Vectorize/AI 綁定,加 `KBDB_BASE_URL` var(留空,安裝時 AI 填)
- [x] 4.3 改寫 `triplet-crud`(拆 triplet-cluster):create/query/get → API
- [x] 4.4 改寫 `triplet-extract`/`triplet-entities`/`triplet-stats`/`triplet-update`/`triplet-embed` → API/薄殼
- [x] 4.5 改寫 `graph-{nodes,path,traverse}`:取 triplet records → 插件層記憶體組圖
- [x] 4.6 改寫 `entity-{crud,normalize,graph-embed}` + 拆 `entity-pending`template='entity',無 vectorize 降級 exact
- [x] 4.7 改寫 `search-query`(→keywordSearch)/`search-suggest`/`search-embed`(stub)keyword;語意標 `[→arcrun embed]`
- [x] 4.8 刪所有 migrations + 清基本盤 action/routeblock-*/entry-crud/record-crud/tag/profile/admin/partner/convert/tasks/personality
- [x] 4.9 測試改走 `tests/mock-client.ts`10 passed);base 缺口(PUT/DELETE record、vectorize)標 `[→arcrun]`
## R-EXT-2 獨立成 repo2026-06-14 完成)
- [x] 2.2 `.gitignore`(排除 *.db/.env/.dev.vars/node_modules/.wrangler/.bak/.swarm+ `git init`
- [x] 2.3 首次 commit + 推 GitHub**public repo `uncle6me-web/kbdb-graph-plugin`**(無 .github/workflows,符合不開 Actions
- [x] 2.4 部署機制:`scripts/install.sh`(安裝時 AI 查 subdomain 拼 base URL → `wrangler secret put``wrangler deploy`)。**實際部署待基本盤 arcrun-kbdb 上線後跑 install.sh**
## 部署現況(leo 2026-06-14 定)
- `KBDB_BASE_URL` 不寫死 toml、不叫人填 → 安裝時 AI 自動算(`https://arcrun-kbdb.<subdomain>.workers.dev`)。
- 現在不空跑部署(避免上線一個打不到基本盤的殼)。基本盤就緒後跑 `scripts/install.sh` 一次到位。
- build 已驗證(`wrangler deploy --dry-run` 通過,bundle 無 D1/AI/Vectorize 綁定)。
## R-EXT-2 獨立成 repo(最後做,依賴 1.4/1.5 完成)
- [x] 2.1 GitHub 帳號 + repo 名:**leo 拍板 = 新 repo `uncle6me-web/kbdb-graph-plugin`**2026-06-14
- [ ] 2.2 `git init` + `.gitignore`(排除 ruvector.db、.env、node_modules、.wrangler、*.bak
- [ ] 2.3 設 remote、首次 commit、推 GitHub(本 CC 自己推,不經總管)
- [ ] 2.4 部署驗證:wrangler 直推 CF,確認不開 Actions
---
## 阻擋項彙整(更新 2026-06-14
1. ✅ repo 已定:`uncle6me-web/kbdb-graph-plugin`(解除 2.1
2. ✅ 灰色地帶已 grep 調查完,附證據建議(解除 1.3)
3.**前置議題(讀 arcrun 後升級為主阻擋)**arcrun/kbdb 還是 v2、與插件不同 D1 庫。三問待 leo/arcrun 定案:
- (1) v3 基本盤(blocks/0005/0007/block-crud)由誰、怎麼進 arcrun?(arcrun 升 v3 vs 本目錄整理好再交)
- (2) 掛載形態:共用同一 D1(需合庫)還是插件透過基本盤 **API** 取 block(不共用 D1)?
- (3) `0005_universal_table` 歸基本盤(它定義 entry_values)——與 HANDOFF 列為插件矛盾,需 arcrun 確認。
## 注意
- arcrun 端對應交棒:`arcrun docs/HANDOFF-matrix-rearrange.md §2`,移交基本盤前先與其對齊。
- 在 2.2 git init 前,本目錄無版控 → 搬檔用 `mv` 不是 `git mv`
@@ -0,0 +1,15 @@
# Pending Changes(規格變更緩衝區)
> 規則來源:`SDD-LIFECYCLE.md` 第 3、4 條。
> 規格層變更(核心設計/方向改變)**只有這一條路**CC 把 change proposal 寫進「待裁決」——
> 變更摘要與觸發原因+影響分析(現行 SDD 哪些任務作廢/修改/不受影響/尚未完成)——然後**停止**,
> 等使用者明說「confirm」才依第 4 條開新 SDD;沒 confirm 就繼續依現行 SDD 工作。
> 多個 proposal 可並存,由人一次裁決。本檔不是 SDD,不掛 status。
## 待裁決
(無)
## 已裁決
(無——裁決後從「待裁決」移到這裡留底,標 confirmed / rejected 日期。)
@@ -0,0 +1,55 @@
---
status: paused # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md
superseded_by: ""
---
# KBDB-graph 插件安裝 — Design
## 目標
用戶給一個 github 網址,AI 就能把 KBDB-graph 插件裝到用戶自己的 CF 帳號,**全程零填寫**。
類比:像裝 Postgres 的 AGE 擴充——掛在已有的基本盤(arcrun/kbdb)上。
## 核心原則:KBDB_BASE_URL 由 AI 自動填,不是人填
| 反例(不要) | 正解 |
|---|---|
| 叫人填 wrangler.toml | toml 留 `""`AI 安裝時填 |
| 叫人填 .env | 不用 .env;部署用 `wrangler secret`,本地測試用 `.dev.vars` |
| 叫人去查自己 worker URL | AI 用 CF API 自動查 subdomain 拼出來 |
## URL 確定性論證(為何 AI 必然查得到)
- URL = `https://arcrun-kbdb.<subdomain>.workers.dev`
- `arcrun-kbdb`:基本盤 worker name,固定。
- `<subdomain>`:用戶 CF 帳號的 workers.dev 子域,`GET /accounts/{id}/workers/subdomain` 查得到。
- **能 deploy ⟹ 能查 URL**:兩者用同一套 CF 憑證(wrangler 登入)。AI 要 deploy 插件就必然已能操作用戶 CF,故必然查得到 subdomain。不存在「能裝卻查不到」。
- 預設 workers.dev,不要求自訂域名 → 安裝時**不問人**。自訂域名是進階選項,另走 config。
## 安裝流程(`scripts/install.sh`
```
輸入:github 網址(用戶提供)
前提:用戶已 wrangler login(自己的 CF 帳號)
1. git clone <插件網址>
2. wrangler whoami → account_id
3. GET /accounts/{account_id}/workers/subdomain → subdomain
4. BASE = https://arcrun-kbdb.<subdomain>.workers.dev
(或:若基本盤這次一起裝,直接取其 wrangler deploy 輸出的 URL
5. wrangler secret put KBDB_BASE_URL ← 填 BASE(不寫進 git
6. wrangler deploy → 插件上線
輸出:插件 workers.dev URLAI 回報給用戶)
```
參考既有實作:arcrun `docs/3-specs/arcrun/sdk-and-website/self-hosted-init.md`(同套路:CF API 查 subdomain + 注入 config + workers_dev 對外)。
## 測試 base URL(兩層)
- **單元測試**`tests/mock-client.ts`,不打網路,KBDB_BASE_URL 留空。日常主力。
- **整合測試**:本地起基本盤 `cd ../arcrun/kbdb && wrangler dev`(如 localhost:8787),插件 `.dev.vars`gitignore)寫 `KBDB_BASE_URL=http://localhost:8787`
## 不變條件(守 KBDB 鐵律)
- 安裝過程**不建表、不跑 migration**(插件零 migration)。基本盤的表由 arcrun/kbdb 維護。
- 插件只透過 `KBDB_BASE_URL` 的 HTTP API 與基本盤互動,安裝後亦然。
@@ -0,0 +1,25 @@
# KBDB-graph 插件安裝 — Tasks
> 對應 design.md。動手前確認 design 已讀。
## Phase 1:安裝腳本
- [ ] 1.1 `scripts/install.sh`git clone → wrangler whoami → 查 subdomain → 拼 BASE → secret put → deploy
- [ ] 1.2 subdomain 查詢:CF API `GET /accounts/{id}/workers/subdomain`(可抽 arcrun cli/lib/cf-api.ts 既有實作)
- [ ] 1.3 BASE 拼接 + 健康檢查:deploy 後 `GET {BASE}/health` 確認基本盤可達,不可達則明確報錯(不是默默裝壞)
## Phase 2:配置位置
- [ ] 2.1 wrangler.toml `KBDB_BASE_URL = ""` 保持空(占位,AI 安裝時填)
- [ ] 2.2 `.dev.vars` 加入 `.gitignore`(本地測試值不進版控)
- [ ] 2.3 部署值走 `wrangler secret put KBDB_BASE_URL`,不寫 toml/git
## Phase 3:測試
- [ ] 3.1 單元測試走 mock-client(已有),不依賴 KBDB_BASE_URL
- [ ] 3.2 整合測試 doc:本地起 `arcrun/kbdb wrangler dev` + 插件 `.dev.vars` 指 localhost
- [ ] 3.3 整合測試驗證:建 template='triplet' + 填 slot + 查圖,全走 API 無 SQL
## 不變條件(每步都守)
- 零建表、零 migration、零直接 SQLhook 會擋)。
- 用戶零填寫——base URL 由 AI 查,不叫人填。