fix(ingest): receiver Zod 追上 contract — 補向量化打標欄位 (issue #1 補對齊) (#3)

契約漂移修補:T3 的 strict Zod 鏡射舊 contract,ingest 照新 contract(ingest#1
升格)送向量化打標欄位會被 .strict() 擋成 422。方向 A:顯式加合法新欄位、保留
strict。

- 同步 contracts/ingest-candidate.json 副本到頂層單一真相源(mira-dissolve)。
- NodeSchema 加 id?/aliases?/embed?;EdgeSchema 加 predicate_embed?。strict() 保留
  → bridge_score/clusters 等 graph 領域禁送欄位仍 422。
- 落地:predicate_embed 透傳進 triplet slot;node 打標(embed/gloss/aliases)存進
  entity slot,供 base/KBDB embed 模組讀標執行(graph 不算向量,鐵律一致)。
- id 作 node 去重鍵:同卡多邊指到只存一筆 entity。
- persistNodes 拆成獨立 action(triplet-ingest.ts 回到 95 行,守樂高 100 行限制)。
- 測試 +4:帶向量化欄位通過、bridge_score/clusters 仍 422、同 id 去重。
  vitest 23 passed。零 SQL / 無 D1·Vectorize·AI 綁定 / dry-run 乾淨。

Co-authored-by: richblack <leo21c@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
uncle6
2026-06-26 20:28:13 +08:00
committed by GitHub
parent 7a29dee357
commit 13db97bb54
6 changed files with 168 additions and 7 deletions
+50
View File
@@ -0,0 +1,50 @@
// node 層打標落地 — 把 envelope nodes[] 的向量化打標(embed/gloss/aliases)存進 entity slot。
// 向量化分工(ingest#1 升格,2026-06-26):ingest 打標、base/KBDB embed 模組讀標執行;graph 不算向量。
// 鐵律:走 base APIAPI-as-Wall)、零 SQL。
import type { KbdbClient } from '../lib/kbdb-client';
import { TPL_ENTITY, ensurePluginTemplates } from '../lib/templates';
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,
): Promise<void> {
if (!nodes || nodes.length === 0) return;
await ensurePluginTemplates(client);
const seen = new Set<string>();
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,
);
}
}