Files
kbdb-graph-plugin/src/actions/triplet-crud.ts
T
Leo 613071f41d feat(graph): get_source + refresh 端點 + keyword 收斂 (T3.6-3.7)
對應 issue #1 T3 C 段(圖工具 HTTP API 備好,MCP 註冊薄殼待 arcrun)。

- get_source (3.7): graph-source.ts + GET /graph/source/:name —
  回節點的 active triplet 來源指標(uri/anchor/block_id/content_hash),去重。
  連帶加 source_anchor slot,ingest 從 source.anchor 帶入
- refresh (3.6/3.6b): graph-refresh.ts + POST /graph/refresh —
  純被動代轉 ingest(KBDB_INGEST_URL),只人發起、無排程/webhook(fan-out 紅線)。
  未設 URL → 誠實 forwarded:false,不假綠
- 3.6d: POST /search 移除公開 keyword 模式(重複 KBDB MCP),收斂 suggest-only;
  keywordSearch helper 留作 suggest 內部建構塊
- 3 新測試(get_source uri+anchor / active-only / refresh 未就緒誠實回報)

gates: vitest 19 passed / zero SQL / 無新綁定 / dry-run bundle 乾淨
待接:MCP 註冊薄殼併 arcrun u6u-mcp-server;refresh 端到端待 ingest(T4) 部署

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:24:04 +08:00

102 lines
3.9 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;
};
/** 建立三元組 → 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;
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 };