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
+78
View File
@@ -0,0 +1,78 @@
// 記憶體版 KbdbClient — 模擬基本盤 API,供插件單元測試(不打真網路、不碰 D1)。
// 只實作插件實際用到的方法,行為對齊 arcrun/kbdb 契約。
import type { KbdbClient, BaseEntry, BaseRecord } from '../src/lib/kbdb-client';
export class MockKbdbClient {
private entries = new Map<string, BaseEntry>();
private templates = new Map<string, string[]>();
private records = new Map<string, { template: string; values: Record<string, string>; owner_id?: string }>();
private seq = 0;
private id(prefix: string): string {
this.seq += 1;
return `${prefix}-${this.seq}`;
}
async createEntry(input: any): Promise<BaseEntry> {
const id = this.id('entry');
const entry: BaseEntry = { id, content: input.content ?? null, entry_type: input.entry_type, owner_id: input.owner_id ?? null };
this.entries.set(id, entry);
return entry;
}
async getEntry(id: string): Promise<BaseEntry | null> {
return this.entries.get(id) ?? null;
}
async listEntries(filters: any = {}): Promise<BaseEntry[]> {
return [...this.entries.values()].filter(
(e) => (!filters.entry_type || e.entry_type === filters.entry_type) && (!filters.owner_id || e.owner_id === filters.owner_id),
);
}
async searchEntries(q: string, owner_id?: string): Promise<BaseEntry[]> {
const needle = q.toLowerCase();
return [...this.entries.values()].filter(
(e) => (e.content ?? '').toLowerCase().includes(needle) && (!owner_id || e.owner_id === owner_id),
);
}
async updateEntry(id: string, patch: any): Promise<BaseEntry | null> {
const e = this.entries.get(id);
if (!e) return null;
Object.assign(e, patch);
return e;
}
async deleteEntry(id: string): Promise<void> {
this.entries.delete(id);
}
async ensureTemplate(name: string, slots: string[]): Promise<void> {
if (!this.templates.has(name)) this.templates.set(name, slots);
}
async createRecord(template: string, values: Record<string, string>, owner_id?: string): Promise<string> {
const id = this.id('rec');
this.records.set(id, { template, values, owner_id });
return id;
}
async getRecord(recordId: string): Promise<BaseRecord | null> {
const r = this.records.get(recordId);
if (!r) return null;
return { record_id: recordId, template: r.template, values: r.values };
}
async listRecordsByTemplate(template: string, owner_id?: string): Promise<BaseRecord[]> {
return [...this.records.entries()]
.filter(([, r]) => r.template === template && (!owner_id || r.owner_id === owner_id))
.map(([record_id, r]) => ({ record_id, template: r.template, values: r.values }));
}
}
/** 轉成 KbdbClient 型別供 action 接受。 */
export function mockClient(): KbdbClient {
return new MockKbdbClient() as unknown as KbdbClient;
}