Files
kbdb-graph-plugin/src/actions/triplet-crud.ts
T
uncle6 13db97bb54 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>
2026-06-26 20:28:13 +08:00

104 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 三元組 CRUD — 走基本盤 APIAPI-as-Wall,零 SQL
// 寫 triplet = 確保 template='triplet' + POST /records 填 slot。
// 查 triplet = GET /records/by-template/triplet → 插件層 filter/組裝。
import type { Triplet } from '../types';
import type { KbdbClient } from '../lib/kbdb-client';
import { TPL_TRIPLET, ensurePluginTemplates, recordToTriplet } from '../lib/templates';
import { classifyClusters } from './triplet-cluster';
export type CreateTripletData = {
subject: string;
predicate: string;
object: string;
source_block_id?: string;
confidence?: number;
owner_id?: string;
clusters?: string[];
bridge_score?: number;
subject_entity_type?: string;
object_entity_type?: string;
source_uri?: string;
content_hash?: string;
source_anchor?: string;
predicate_embed?: boolean; // 謂詞向量化打標(ingest 打標、base 讀標執行);graph 不算向量
};
/** 建立三元組 → POST /recordstemplate=triplet)。 */
export async function createTriplet(
client: KbdbClient,
data: CreateTripletData,
): Promise<{ id: string; subject: string; predicate: string; object: string }> {
await ensurePluginTemplates(client);
const clusters = data.clusters ?? [];
const bridgeScore = data.bridge_score ?? Math.max(0, clusters.length - 1);
const values: Record<string, string> = {
subject: data.subject,
predicate: data.predicate,
object: data.object,
confidence: String(data.confidence ?? 1.0),
clusters_json: JSON.stringify(clusters),
bridge_score: String(bridgeScore),
status: 'active',
};
if (data.source_block_id) values.source_block_id = data.source_block_id;
if (data.subject_entity_type) values.subject_entity_type = data.subject_entity_type;
if (data.object_entity_type) values.object_entity_type = data.object_entity_type;
if (data.source_uri) values.source_uri = data.source_uri;
if (data.content_hash) values.content_hash = data.content_hash;
if (data.source_anchor) values.source_anchor = data.source_anchor;
if (data.predicate_embed === false) values.predicate_embed = 'false'; // 謂詞向量化打標透傳,base 讀標執行
const id = await client.createRecord(TPL_TRIPLET, values, data.owner_id);
return { id, subject: data.subject, predicate: data.predicate, object: data.object };
}
export type TripletFilters = {
subject?: string;
predicate?: string;
object?: string;
limit?: number;
offset?: number;
owner_id?: string;
entity_type?: string;
includeDeprecated?: boolean; // 預設只回 activerollback/考古才開(T3.5
};
/** 查三元組 → 取 template 全部 record,插件層 filterbase 無複合 slot 查詢)。 */
export async function queryTriplets(
client: KbdbClient,
filters: TripletFilters,
): Promise<{ triplets: Triplet[]; count: number }> {
const records = await client.listRecordsByTemplate(TPL_TRIPLET, filters.owner_id);
let triplets = records.map(recordToTriplet);
// active-onlydeprecated 不進圖遍歷/查詢(缺省 status 視為 active,相容舊資料)。
if (!filters.includeDeprecated) triplets = triplets.filter((t) => t.status === 'active');
if (filters.subject) triplets = triplets.filter((t) => t.subject === filters.subject);
if (filters.predicate) triplets = triplets.filter((t) => t.predicate === filters.predicate);
if (filters.object) triplets = triplets.filter((t) => t.object === filters.object);
if (filters.entity_type) {
triplets = triplets.filter(
(t) => t.subject_entity_type === filters.entity_type || t.object_entity_type === filters.entity_type,
);
}
const offset = filters.offset ?? 0;
const limit = Math.min(filters.limit ?? 50, 2000);
const page = triplets.slice(offset, offset + limit);
return { triplets: page, count: page.length };
}
/** 取單一三元組 → GET /records/:id。 */
export async function getTriplet(client: KbdbClient, id: string): Promise<Triplet | null> {
const rec = await client.getRecord(id);
if (!rec) return null;
return recordToTriplet({ ...rec, template: TPL_TRIPLET });
}
// re-export clusters helperAI 分群,純函式 + 走 client 無關)
export { classifyClusters };