// library-map(藏書地圖)— 聚合 SQL 的家(SDD system-dev/docs/3-specs/library-map/design.md §2)。 // D6 鐵律推論:degree 排序/predicate 統計/跨庫 join 是聚合 SQL,插件與 workflow 全程禁 SQL, // 所以重算只能住 kbdb base 本體(本檔)。三表不變量不破:地圖=library_map template 的 record //(每庫一個 map block)+slots,零建表零 ALTER。 // // triplet 按庫定位現況(2026-07-19 對 prod 核實,design §1 的「先核實」): // - triplet template(prod 名 'triplet',kbdb-graph 建)有 source_uri slot、無 library slot; // - entries 的 metadata.library 機制在(portal-auth P1)但既有資料未標記(?library=kb → 0 筆)。 // → 依 SDD 預案:在 triplet template schema 加 optional `library` slot(改 template 不動表, // 見 ensureTripletLibrarySlot);ingest 端補寫值屬 M3。過渡期(舊 triplet 沒有 library slot 值) // recompute 可帶 source_prefix 參數用 source_uri 前綴當 fallback 過濾——由 caller 提供前綴, // base 不寫死任何 URI 格式語意(base 對內容語意無知的既有原則)。 import { createEntry, getEntry } from './entry-crud'; import { createRecord, createTemplate, getTemplate, updateRecord, updateTemplate } from './record-crud'; export const LIBRARY_MAP_TEMPLATE_ID = 'tpl-library-map'; export const LIBRARY_MAP_TEMPLATE_NAME = 'library_map'; // 與 migrations/0003_library_map.sql 的 seed 同一份定義(兩邊必須一致)。 export const LIBRARY_MAP_SLOTS = [ 'library', 'narrative', 'top_entities', 'relation_profile', 'bridges', 'triplet_count', 'commit_hash', 'status', ]; // prod 實際部署的 triplet template 名(kbdb_list_templates 核實);caller 可用參數覆蓋。 export const DEFAULT_TRIPLET_TEMPLATE = 'triplet'; export interface TopEntity { name: string; degree: number } export interface RelationStat { predicate: string; count: number } export interface Bridge { entity: string; libraries: string[] } export interface LibraryMapRow { library: string; narrative: string | null; top_entities: string[]; // 全館視圖只回 top 3 名字(數百 token 內,design §4 MCP instructions 用) triplet_count: number; // Arcrun#87 三次收尾(2026-08-13,「藏書地圖說實話」):triplet_count=0 不等於「這庫沒有知識」—— // entries(ingest 進來的原始卡片/skill 內容)與 triplet(從 entries 萃取出的三元組)是兩件事, // 一個庫可能有大量 entries、但三元組萃取從沒對它跑過(實測:kb 以外 7 庫皆此況,entries 存在 // 且 library 標記正確,triplet 一筆都沒有)。entry_count 讓讀端(AI/MCP 渲染層)分得清 // 「真的沒東西」與「有東西但還沒被萃取成三元組」,不再把後者誤讀成前者、誤判庫是空的而放棄查詢 // (見本檔 liveEntryCountsByLibrary 的計數口徑)。 entry_count: number; updated_at: number; } export interface LibraryMapDetail { record_id: string; library: string; narrative: string | null; content: string | null; // map block 的可嵌人話(design §5,M6 semantic 路由直接用) top_entities: TopEntity[]; relation_profile: RelationStat[]; bridges: Bridge[]; triplet_count: number; entry_count: number; // 同 LibraryMapRow.entry_count(見該欄位註解) commit_hash: string | null; status: string; updated_at: number; } export interface RecomputeInput { library: string; narrative?: string; // wiki 首段抽取屬 ingest 端(M3)——base 只收值不抽取 commit_hash?: string; owner_id?: string; source_prefix?: string; // 過渡 fallback:library slot 缺值的舊 triplet 以 source_uri LIKE 前綴歸庫 triplet_template?: string; top_n?: number; // top_entities 取幾個(預設 10、上限 50) } export interface RecomputeResult { map: LibraryMapDetail; superseded: string[]; // 被標 superseded 的舊 map record ids triplet_template: string; triplet_library_slot_added: boolean; // 本次是否幫 triplet template 補上 optional library slot } // ---- template ensure(M1) ---- // library_map template 若不存在就走既有 createTemplate 路徑補建(migration 0003 的 runtime 保險)。 // UNIQUE(name) 撞到(並發/半套 seed)→ 重讀即可,冪等。 export async function ensureLibraryMapTemplate(db: D1Database): Promise { const existing = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); if (existing) return; try { await createTemplate(db, { id: LIBRARY_MAP_TEMPLATE_ID, name: LIBRARY_MAP_TEMPLATE_NAME, description: 'per-library map block(藏書地圖:graph 機械導出,零 LLM 生成;Arcrun#39)', slots: LIBRARY_MAP_SLOTS, created_by: 'system', }); } catch { if (!(await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME))) throw new Error('ensureLibraryMapTemplate failed'); } } // triplet template schema 加 optional `library` slot(design §1 預案:改 template 不動表)。 // 只增不減、冪等;ingest 端(M3)開始寫值後,recompute 就能按 slot 精準歸庫,不再靠 source_prefix。 export async function ensureTripletLibrarySlot(db: D1Database, tripletTemplate: string): Promise { const tpl = await getTemplate(db, tripletTemplate); if (!tpl) throw new Error(`triplet template not found: ${tripletTemplate}`); const slots: string[] = JSON.parse(tpl.slots_json); if (slots.includes('library')) return false; await updateTemplate(db, tpl.id, { slots: [...slots, 'library'] }); return true; } // ---- 聚合 SQL(M2 recompute) ---- // record → 一列一 triplet 的 pivot(0007 樹狀 record 模型:格子=關係列)。 // b=歸屬關係列(rel=sys_belongs, dst=template 的 sheet entry)=record 成員名單; // r=該 record 的格子關係列;v=格子指到的內容 entry。欄位謂詞 id 決定性衍生 // (fld_