13db97bb54
契約漂移修補: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>
104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
// 三元組 CRUD — 走基本盤 API(API-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 /records(template=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; // 預設只回 active;rollback/考古才開(T3.5)
|
||
};
|
||
|
||
/** 查三元組 → 取 template 全部 record,插件層 filter(base 無複合 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-only:deprecated 不進圖遍歷/查詢(缺省 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 helper(AI 分群,純函式 + 走 client 無關)
|
||
export { classifyClusters };
|