feat(kbdb): 藏書地圖 M1+M2 — library_map template+/map 端點(聚合 SQL 住基本盤)(#39)

M1:library_map template(migration 0003 seed+runtime ensure,D6 零建表零 ALTER);
核實 triplet 按庫定位=不足(prod triplet template 無 library slot、entries
metadata.library 未標記)→ 走 SDD 預案:recompute 冪等補 optional library slot
(改 template 不動表),slot 值由 ingest 端(M3)補寫,核實結果記入 design §1。

M2:kbdb base 三端點(design §2 歸屬裁定:聚合 SQL 只准住基本盤):
- POST /map/recompute?library=X:degree top-N/predicate 分布/跨庫 bridges/
  triplet_count 一段 SQL 聚合;narrative 本輪收 body 傳入(wiki 抽取屬 M3);
  寫入順序安全(先建新 active map block 再標舊 superseded,讀端取最新 active 自癒);
  過渡 source_prefix fallback 讓無 library 值的舊 triplet 靠 source_uri 前綴歸庫。
- GET /map:全館地圖每庫一行(library+narrative+top 3 entities+triplet_count),
  MCP instructions/GUI 共用(數百 token 內)。
- GET /map/:library:該庫詳圖(完整 slots+可嵌人話 content,design §5 M6 直用)。

測試:node:sqlite(零新依賴)當真 SQLite 實跑 migrations+聚合 SQL,
Hono route 行為同款覆蓋;45/45 過、tsc --noEmit 乾淨。

關聯 #39

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUmjwkHLVBHM3ydhT1WSW3
This commit is contained in:
Claude
2026-07-19 08:40:22 +00:00
parent bad68ef149
commit 15fe012006
9 changed files with 731 additions and 2 deletions
+48
View File
@@ -0,0 +1,48 @@
// Map route — 藏書地圖(library-map SDD M2;源頭 Arcrun#39)。
// 聚合 SQL 只准住基本盤(D6 推論,design §2 關鍵歸屬裁定)——本 route 是薄殼,SQL 全在
// actions/library-map.ts。auth 照 base route 既有慣例:raw worker 不驗 keyowner 隔離由
// cypher proxyX-Arcrun-API-Key → owner_id 注入)/caller 帶 owner_id 參數完成。
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { getLibraryMapDetail, listLibraryMaps, recomputeLibraryMap } from '../actions/library-map';
export const mapRoutes = new Hono<{ Bindings: Bindings }>();
// POST /map/recompute?library=X — 對該庫重算地圖(ingest 尾端/backfill 呼叫;R2 增量重算)。
// body(皆選填):{ narrative, commit_hash, owner_id, source_prefix, triplet_template, top_n }
// - narrative:本輪由 caller 傳入(wiki 首段抽取屬 ingest 端 M3base 不抽取)。
// - source_prefix:舊 triplet(無 library slot 值)的 source_uri 前綴 fallbackM3 backfill 前過渡)。
// 寫入順序安全:先建新 active map block,再把舊 block 標 superseded(見 action 註解)。
mapRoutes.post('/recompute', async (c) => {
const body = (await c.req.json().catch(() => ({}))) as Record<string, unknown>;
const library = c.req.query('library') || (typeof body.library === 'string' ? body.library : '');
if (!library || !library.trim()) return c.json({ success: false, error: 'library required' }, 400);
try {
const result = await recomputeLibraryMap(c.env.DB, {
library,
narrative: typeof body.narrative === 'string' ? body.narrative : undefined,
commit_hash: typeof body.commit_hash === 'string' ? body.commit_hash : undefined,
owner_id: typeof body.owner_id === 'string' ? body.owner_id : c.req.query('owner_id') || undefined,
source_prefix: typeof body.source_prefix === 'string' ? body.source_prefix : undefined,
triplet_template: typeof body.triplet_template === 'string' ? body.triplet_template : undefined,
top_n: typeof body.top_n === 'number' ? body.top_n : undefined,
});
return c.json({ success: true, ...result });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
// GET /map — 全館地圖:每庫一行(librarynarrativetop 3 entitiestriplet_count)。
// 形狀給 MCP instructionsGUI 首頁共用(R3/R4),設計在數百 token 內。
mapRoutes.get('/', async (c) => {
const libraries = await listLibraryMaps(c.env.DB, c.req.query('owner_id') || undefined);
return c.json({ success: true, libraries, count: libraries.length });
});
// GET /map/:library — 該庫詳圖(完整 slots+可嵌人話 content)。
mapRoutes.get('/:library', async (c) => {
const map = await getLibraryMapDetail(c.env.DB, c.req.param('library'), c.req.query('owner_id') || undefined);
if (!map) return c.json({ success: false, error: 'not found' }, 404);
return c.json({ success: true, map });
});