// node 層打標落地 — 把 envelope nodes[] 的向量化打標(embed/gloss/aliases)存進 entity slot。 // 向量化分工(ingest#1 升格,2026-06-26):ingest 打標、base/KBDB embed 模組讀標執行;graph 不算向量。 // 鐵律:走 base API(API-as-Wall)、零 SQL。 import type { KbdbClient } from '../lib/kbdb-client'; import { TPL_ENTITY, ensurePluginTemplates } from '../lib/templates'; import { upsertGlossEntry } from './gloss-entry'; export type IngestNode = { name: string; id?: string; aliases?: string[]; gloss?: string; embed?: boolean; entity_type?: string; }; /** * 把 node 層打標存進 entity record,供 base embed 模組讀標執行 embedding。 * 去重:以 id(無則 name)為鍵,同鍵在這批內只存一筆——wikilink 卡被多條邊指到仍是一個 node。 * graph 不做 embedding,只負責透傳/落地打標。 */ export async function persistNodes( client: KbdbClient, nodes: IngestNode[], owner_id?: string, source?: string, // envelope source.uri,帶進 gloss entry 的 metadata.source(供 base backfill 依 source 過濾) ): Promise { if (!nodes || nodes.length === 0) return; await ensurePluginTemplates(client); const seen = new Set(); for (const n of nodes) { const key = (n.id ?? n.name).toLowerCase().trim(); if (seen.has(key)) continue; // 同卡多邊指到 → 只存一次 seen.add(key); await client.createRecord( TPL_ENTITY, { canonical: n.name, node_id: n.id ?? '', aliases_json: JSON.stringify(n.aliases ?? []), entity_type: n.entity_type ?? '', gloss: n.gloss ?? '', // contract 預設 true;只在明確 false 時存標(base 看 'false' 跳過 embed)。 embed: n.embed === false ? 'false' : 'true', owner: owner_id ?? '', }, owner_id, ); // 另落一筆 embeddable base entry(metadata.embed=true)——record 的 gloss 標 base embed 讀不到, // 必須也落成 entry base embed 模組才掃得到(打標≠讀標的修補)。只在要 embed(embed !== false)時落; // 空 gloss 由 upsertGlossEntry 自行跳過。冪等:同 node 重複 ingest 不造重複 entry。 if (n.embed !== false) { await upsertGlossEntry(client, { canonical: n.name, node_id: n.id, gloss: n.gloss, source }, owner_id); } } }