rag-memory-epf-mcp 6.4.0 → 6.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/index.d.ts +55 -2
- package/dist/index.js +470 -150
- package/dist/src/chunkerC.d.ts +1 -0
- package/dist/src/chunkerC.js +69 -22
- package/dist/src/kg/relation-evidence.d.ts +156 -0
- package/dist/src/kg/relation-evidence.js +438 -0
- package/dist/src/observations/projection.d.ts +2 -0
- package/dist/src/observations/projection.js +49 -0
- package/dist/src/tools/knowledge-graph-tools.js +7 -3
- package/docs/UPDATING.md +17 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -23,9 +23,10 @@ import { getAllMCPTools, validateToolArgs, getSystemInfo } from './src/tools/too
|
|
|
23
23
|
// Import migration system
|
|
24
24
|
import { MigrationManager } from './src/migrations/migration-manager.js';
|
|
25
25
|
import { backupBeforeMigration } from './src/backup/preflight.js';
|
|
26
|
-
import { rebuildProjection, deleteStaleKgChunks, deleteKgRelationshipChunks } from './src/observations/projection.js';
|
|
26
|
+
import { rebuildProjection, deleteStaleKgChunks, deleteKgRelationshipChunks, deleteKgChunkRows, refreshKgRelationshipConfidence } from './src/observations/projection.js';
|
|
27
27
|
import { addRevision, correctRevision, transitionStatus, linkSources, nextProjectionOrder } from './src/observations/lifecycle.js';
|
|
28
28
|
import { getObservationHistory } from './src/observations/history.js';
|
|
29
|
+
import { kgComparisonValue, captureKgEmbedInput, parseEvidenceMetadata, fingerprintFromMetadata, sha256Utf8, EVIDENCE_FORMAT, resolveEvidenceItem, EvidenceInputError, EvidenceCommitConflictError, resolveEvidenceStatus, collectEvidenceRowidsCitingObservations, collectEvidenceRowidsCitingDocument, collectEvidenceRowidsCitingOwnerEntity } from './src/kg/relation-evidence.js';
|
|
29
30
|
// Import chunk text algorithm (extracted for publish-time invariant testing)
|
|
30
31
|
import { chunkStructured as chunkStructuredText, effectiveSignature, isCurrentFormatSignature, LEGACY_SIGNATURE, DEFAULT_MAX_TOKENS } from './src/chunkerC.js';
|
|
31
32
|
import { migrations } from './src/migrations/migrations.js';
|
|
@@ -94,6 +95,12 @@ export function compileFtsLiteralQuery(q) {
|
|
|
94
95
|
return null;
|
|
95
96
|
return terms.map(t => `"${t}"`).join(' OR ');
|
|
96
97
|
}
|
|
98
|
+
// Test-only seam for the real model loader: replaces `pipeline` so a test can see the exact
|
|
99
|
+
// arguments buildRealLoader passes (same rule as setSyncFaultPoint: an argument, never an env var).
|
|
100
|
+
let __pipelineImpl = pipeline;
|
|
101
|
+
export function setPipelineImpl(fn) {
|
|
102
|
+
__pipelineImpl = (fn ?? pipeline);
|
|
103
|
+
}
|
|
97
104
|
// Configure Hugging Face transformers for better compatibility
|
|
98
105
|
if (env.backends?.onnx?.wasm) {
|
|
99
106
|
env.backends.onnx.wasm.wasmPaths = './node_modules/@huggingface/transformers/dist/';
|
|
@@ -141,6 +148,12 @@ const IS_DEFAULT_MODEL_CONFIG = !process.env.EMBEDDING_MODEL || process.env.EMBE
|
|
|
141
148
|
// on 'main' (their vectors are never auto-grandfathered anyway).
|
|
142
149
|
const MODEL_REVISION = IS_DEFAULT_MODEL_CONFIG ? '4de13258303883538bd53b696b452bf8099f0858' : 'main';
|
|
143
150
|
const MODEL_DTYPE = 'fp16';
|
|
151
|
+
// ONNX session options for the embedding model. The CPU memory arena is off: with it on, one long
|
|
152
|
+
// input (12,000 chars = 5,774 tokens) left the process holding its peak (footprint 1.4 → 8.0 GB,
|
|
153
|
+
// measured 2026-10-31 on transformers 3.8.1 and 4.3.2) and every MCP client runs its own engine.
|
|
154
|
+
// Arena off returned to 1.5-2.4 GB with byte-identical vectors; the cost is per-call allocation
|
|
155
|
+
// (query 29 → 33 ms, 484-token chunk 574 → 639 ms on 4.3.2). Not part of the cache artifact key.
|
|
156
|
+
export const MODEL_SESSION_OPTIONS = Object.freeze({ enableCpuMemArena: false });
|
|
144
157
|
// Entity embedding text builder version — mixed into entity input hashes so a
|
|
145
158
|
// builder change re-backfills entities without touching chunk vectors (spec §6c).
|
|
146
159
|
const TEXT_BUILDER_VERSION = 'tb1';
|
|
@@ -186,6 +199,13 @@ export function setSyncFaultPoint(point, fn) {
|
|
|
186
199
|
__syncFaultHook = point && fn ? (p) => { if (p === point)
|
|
187
200
|
fn(); } : null;
|
|
188
201
|
}
|
|
202
|
+
// Test-only fault hook for the KG writers (kg-relation-evidence D1/D2⑤). Same rule as
|
|
203
|
+
// setSyncFaultPoint: an argument, never an environment variable.
|
|
204
|
+
let __kgFaultHook = null;
|
|
205
|
+
export function setKgFaultPoint(point, fn) {
|
|
206
|
+
__kgFaultHook = point && fn ? (p) => { if (p === point)
|
|
207
|
+
fn(); } : null;
|
|
208
|
+
}
|
|
189
209
|
// Pure vector-reuse decision (spec §5.2 조건 1~5). Returns an OWNED Buffer copy:
|
|
190
210
|
// a Buffer read back from SQLite has no byteOffset-0 guarantee, and inserting
|
|
191
211
|
// `.buffer` of a subarray would write the wrong 4,096 bytes (advisor r5-9).
|
|
@@ -379,7 +399,8 @@ export class RAGKnowledgeGraphManager {
|
|
|
379
399
|
env.allowRemoteModels = true;
|
|
380
400
|
env.allowLocalModels = true;
|
|
381
401
|
console.error(`🤖 Loading embedding model: ${EMBEDDING_MODEL} (1024-dim, cache=${cacheDir})...`);
|
|
382
|
-
const model = await
|
|
402
|
+
const model = await __pipelineImpl('feature-extraction', EMBEDDING_MODEL, { revision: MODEL_REVISION, dtype: MODEL_DTYPE, cache_dir: cacheDir,
|
|
403
|
+
session_options: { ...MODEL_SESSION_OPTIONS } });
|
|
383
404
|
// dims fail-fast (spec §2 / beta B8): probe the ACTUAL output length — a
|
|
384
405
|
// 384/768-dim custom model must fail here with a clear message, not at
|
|
385
406
|
// every subsequent vector write.
|
|
@@ -499,11 +520,20 @@ export class RAGKnowledgeGraphManager {
|
|
|
499
520
|
mutateEntityAndInvalidate(entityId, mutate) {
|
|
500
521
|
let changed = false;
|
|
501
522
|
const tx = this.db.transaction(() => {
|
|
502
|
-
|
|
523
|
+
// D2③ (kg-relation-evidence): the fifth obligation — evidence chunks citing a changed observation
|
|
524
|
+
// go in the same transaction. Collected before the mutation (purge deletes the rows they name).
|
|
525
|
+
const marked = new Set();
|
|
526
|
+
const kg = {
|
|
527
|
+
collect: (ids) => collectEvidenceRowidsCitingObservations(this.db, ids),
|
|
528
|
+
mark: (rowids) => { for (const r of rowids)
|
|
529
|
+
marked.add(r); },
|
|
530
|
+
};
|
|
531
|
+
changed = mutate(kg) !== false;
|
|
503
532
|
if (!changed)
|
|
504
533
|
return;
|
|
505
534
|
rebuildProjection(this.db, entityId);
|
|
506
535
|
this.invalidateDerivedForEntity(entityId);
|
|
536
|
+
deleteKgChunkRows(this.db, [...marked]);
|
|
507
537
|
});
|
|
508
538
|
tx();
|
|
509
539
|
if (changed)
|
|
@@ -543,15 +573,70 @@ export class RAGKnowledgeGraphManager {
|
|
|
543
573
|
tx();
|
|
544
574
|
this.coordinator?.invalidateCoverage();
|
|
545
575
|
}
|
|
576
|
+
// D2⑤ (kg-relation-evidence): the one write-back for KG chunk vectors, shared by the dedicated
|
|
577
|
+
// embedKnowledgeGraphChunks loop and the backfill callback. `captured` was taken immediately before
|
|
578
|
+
// inference; every part must still hold at write time: same rowid -> same chunk_id, same text hash,
|
|
579
|
+
// same comparison value (evidence fingerprint, or for legacy rows source identity + body hash), and
|
|
580
|
+
// the profile that was current when inference started. null comparison values never match. A
|
|
581
|
+
// profile change BEFORE inference is normal: the vector and the evidence fingerprint are recorded for
|
|
582
|
+
// the profile captured here, so the stored fingerprint never stays pinned to an outdated profile.
|
|
583
|
+
writeKgVectorIfCurrent(captured, embedding) {
|
|
584
|
+
const rowid = safeRowid(captured.rowid);
|
|
585
|
+
const tx = this.db.transaction(() => {
|
|
586
|
+
const cur = this.db.prepare(`SELECT rowid, chunk_id, chunk_type, entity_id, relationship_id, text, metadata
|
|
587
|
+
FROM chunk_metadata WHERE rowid = ?`).get(rowid);
|
|
588
|
+
if (!cur || cur.text === null)
|
|
589
|
+
return false; // deleted during inference
|
|
590
|
+
if (cur.chunk_type !== 'entity' && cur.chunk_type !== 'relationship')
|
|
591
|
+
return false;
|
|
592
|
+
if (cur.chunk_id !== captured.chunk_id)
|
|
593
|
+
return false; // rowid reused by another chunk
|
|
594
|
+
if (sha256Utf8(cur.text) !== captured.text_sha256)
|
|
595
|
+
return false; // body changed
|
|
596
|
+
if (captured.cmp === null || kgComparisonValue(cur) !== captured.cmp)
|
|
597
|
+
return false; // inputs changed
|
|
598
|
+
if (this.currentProfileId !== captured.profile_id)
|
|
599
|
+
return false; // profile changed during inference
|
|
600
|
+
this.db.exec(`DELETE FROM chunks WHERE rowid = ${rowid}`);
|
|
601
|
+
this.db.prepare(`INSERT INTO chunks (rowid, embedding) VALUES (${rowid}, ?)`).run(Buffer.from(embedding.buffer));
|
|
602
|
+
this.db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = 'verified' WHERE rowid = ?`)
|
|
603
|
+
.run(captured.text_sha256, captured.profile_id, rowid);
|
|
604
|
+
const meta = parseEvidenceMetadata(cur.metadata);
|
|
605
|
+
if (meta && meta.fingerprint_profile_id !== captured.profile_id) {
|
|
606
|
+
meta.input_fingerprint = fingerprintFromMetadata(meta, captured.profile_id);
|
|
607
|
+
meta.fingerprint_profile_id = captured.profile_id;
|
|
608
|
+
this.db.prepare(`UPDATE chunk_metadata SET metadata = ? WHERE rowid = ?`).run(JSON.stringify(meta), rowid);
|
|
609
|
+
}
|
|
610
|
+
this.db.prepare(`DELETE FROM embedding_backfill_failures WHERE kind = 'chunk' AND target_id = ?`).run(String(rowid));
|
|
611
|
+
return true;
|
|
612
|
+
});
|
|
613
|
+
const written = tx();
|
|
614
|
+
this.coordinator?.invalidateCoverage();
|
|
615
|
+
return written;
|
|
616
|
+
}
|
|
546
617
|
// Backfill callback: re-embed one chunk and commit vector + provenance in a
|
|
547
618
|
// single transaction (§6a-2).
|
|
548
619
|
async reembedChunkByRowid(rowid) {
|
|
549
620
|
if (!this.db)
|
|
550
621
|
return false;
|
|
551
|
-
const row = this.db.prepare(`SELECT
|
|
552
|
-
|
|
622
|
+
const row = this.db.prepare(`SELECT rowid, chunk_id, chunk_type, entity_id, relationship_id, text, metadata
|
|
623
|
+
FROM chunk_metadata WHERE rowid = ?`).get(rowid);
|
|
553
624
|
if (!row || row.text === null)
|
|
554
625
|
return false;
|
|
626
|
+
if (row.chunk_type === 'entity' || row.chunk_type === 'relationship') {
|
|
627
|
+
// D2⑤: the text-only check below cannot tell a regenerated evidence chunk with the same body,
|
|
628
|
+
// or another chunk at a reused rowid with the same text, from the input we embedded.
|
|
629
|
+
const captured = captureKgEmbedInput(row, this.currentProfileId);
|
|
630
|
+
try {
|
|
631
|
+
const embedding = await this.generateEmbedding(row.text, 1024, false, 'backfill', `profile:${captured.profile_id}`);
|
|
632
|
+
return this.writeKgVectorIfCurrent(captured, embedding);
|
|
633
|
+
}
|
|
634
|
+
catch (e) {
|
|
635
|
+
if (e instanceof GateNotReadyError || e instanceof GateDisabledError)
|
|
636
|
+
return false;
|
|
637
|
+
throw e;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
555
640
|
try {
|
|
556
641
|
const embedding = await this.generateEmbedding(row.text, 1024, false, 'backfill');
|
|
557
642
|
const hash = createHash('sha256').update(row.text).digest('hex');
|
|
@@ -955,7 +1040,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
955
1040
|
// entity INSERT 도 같은 트랜잭션 안이다. 밖에 두면 lifecycle INSERT 가
|
|
956
1041
|
// 실패할 때 entity 행만 남는 split state 가 생긴다
|
|
957
1042
|
// (advisor 구현리뷰 r1 발견 2, 실행 재현).
|
|
958
|
-
const changed = this.mutateEntityAndInvalidate(entityId, () => {
|
|
1043
|
+
const changed = this.mutateEntityAndInvalidate(entityId, (kg) => {
|
|
959
1044
|
created = insertStmt.run(entityId, entity.name, entity.entityType, '{}').changes > 0;
|
|
960
1045
|
if (!created && entity.entityType && entity.entityType !== 'CONCEPT') {
|
|
961
1046
|
const cur = this.db.prepare(`SELECT entityType FROM entities WHERE id = ?`)
|
|
@@ -976,8 +1061,15 @@ export class RAGKnowledgeGraphManager {
|
|
|
976
1061
|
const dupId = activeByBare.get(bare);
|
|
977
1062
|
if (dupId) {
|
|
978
1063
|
// 같은 사실이 다른 출처에서 다시 왔다 = evidence 추가, 새 revision 아님.
|
|
979
|
-
|
|
980
|
-
|
|
1064
|
+
// D2③: a new source row changes the cited observation's source list (a fingerprint input):
|
|
1065
|
+
// collect before linking, drop only if a row was actually added.
|
|
1066
|
+
if (entity.sources?.length) {
|
|
1067
|
+
const cited = kg.collect([dupId]);
|
|
1068
|
+
const added = linkSources(this.db, dupId, entity.sources, ts);
|
|
1069
|
+
if (added > 0)
|
|
1070
|
+
kg.mark(cited);
|
|
1071
|
+
sourcesAdded += added;
|
|
1072
|
+
}
|
|
981
1073
|
ids.push(null);
|
|
982
1074
|
continue;
|
|
983
1075
|
}
|
|
@@ -1062,14 +1154,20 @@ export class RAGKnowledgeGraphManager {
|
|
|
1062
1154
|
const ids = [];
|
|
1063
1155
|
const added = [];
|
|
1064
1156
|
let sourcesAdded = 0;
|
|
1065
|
-
const changed = this.mutateEntityAndInvalidate(entityId, () => {
|
|
1157
|
+
const changed = this.mutateEntityAndInvalidate(entityId, (kg) => {
|
|
1066
1158
|
for (const raw of obs.contents) {
|
|
1067
1159
|
const content = this._timestampObservation(raw);
|
|
1068
1160
|
const bare = stripDate(content);
|
|
1069
1161
|
const dupId = activeByBare.get(bare);
|
|
1070
1162
|
if (dupId) {
|
|
1071
|
-
if
|
|
1072
|
-
|
|
1163
|
+
// D2③: see createEntities — collect before linking, drop only if a source row was added.
|
|
1164
|
+
if (obs.sources?.length) {
|
|
1165
|
+
const cited = kg.collect([dupId]);
|
|
1166
|
+
const added = linkSources(this.db, dupId, obs.sources, ts);
|
|
1167
|
+
if (added > 0)
|
|
1168
|
+
kg.mark(cited);
|
|
1169
|
+
sourcesAdded += added;
|
|
1170
|
+
}
|
|
1073
1171
|
ids.push(null);
|
|
1074
1172
|
continue;
|
|
1075
1173
|
}
|
|
@@ -1104,11 +1202,13 @@ export class RAGKnowledgeGraphManager {
|
|
|
1104
1202
|
throw new Error(`observation ${observationId} not found`);
|
|
1105
1203
|
let newId = '';
|
|
1106
1204
|
const ts = new Date().toISOString();
|
|
1107
|
-
this.mutateEntityAndInvalidate(row.entity_id, () => {
|
|
1205
|
+
this.mutateEntityAndInvalidate(row.entity_id, (kg) => {
|
|
1206
|
+
const cited = kg.collect([observationId]);
|
|
1108
1207
|
newId = correctRevision(this.db, {
|
|
1109
1208
|
observationId, content: this._timestampObservation(content),
|
|
1110
1209
|
changeKind, reason: reason ?? null, ts
|
|
1111
1210
|
});
|
|
1211
|
+
kg.mark(cited);
|
|
1112
1212
|
});
|
|
1113
1213
|
await this.tryEmbedEntity(row.entity_id, 'bulk');
|
|
1114
1214
|
return newId;
|
|
@@ -1121,8 +1221,10 @@ export class RAGKnowledgeGraphManager {
|
|
|
1121
1221
|
if (!row)
|
|
1122
1222
|
throw new Error(`observation ${observationId} not found`);
|
|
1123
1223
|
const ts = new Date().toISOString();
|
|
1124
|
-
this.mutateEntityAndInvalidate(row.entity_id, () => {
|
|
1224
|
+
this.mutateEntityAndInvalidate(row.entity_id, (kg) => {
|
|
1225
|
+
const cited = kg.collect([observationId]);
|
|
1125
1226
|
transitionStatus(this.db, { observationId, event, reason: reason ?? null, ts });
|
|
1227
|
+
kg.mark(cited);
|
|
1126
1228
|
});
|
|
1127
1229
|
await this.tryEmbedEntity(row.entity_id, 'bulk');
|
|
1128
1230
|
}
|
|
@@ -1164,17 +1266,20 @@ export class RAGKnowledgeGraphManager {
|
|
|
1164
1266
|
if (!row)
|
|
1165
1267
|
return { purged: 0 };
|
|
1166
1268
|
let purged = 0;
|
|
1167
|
-
this.mutateEntityAndInvalidate(row.entity_id, () => {
|
|
1269
|
+
this.mutateEntityAndInvalidate(row.entity_id, (kg) => {
|
|
1168
1270
|
// newest-first so each DELETE has no successor referencing it
|
|
1169
1271
|
const victims = this.db.prepare(`SELECT observation_id FROM entity_observations
|
|
1170
1272
|
WHERE root_id = ? AND revision_no >= ?
|
|
1171
1273
|
ORDER BY revision_no DESC`).all(row.root_id, row.revision_no);
|
|
1274
|
+
const cited = kg.collect(victims.map(v => v.observation_id));
|
|
1172
1275
|
for (const v of victims) {
|
|
1173
1276
|
this.db.prepare(`DELETE FROM observation_events WHERE from_id = ? OR to_id = ?`)
|
|
1174
1277
|
.run(v.observation_id, v.observation_id);
|
|
1175
1278
|
purged += this.db.prepare(`DELETE FROM entity_observations WHERE observation_id = ?`)
|
|
1176
1279
|
.run(v.observation_id).changes;
|
|
1177
1280
|
}
|
|
1281
|
+
if (purged > 0)
|
|
1282
|
+
kg.mark(cited);
|
|
1178
1283
|
return purged > 0;
|
|
1179
1284
|
});
|
|
1180
1285
|
await this.tryEmbedEntity(row.entity_id, 'bulk');
|
|
@@ -1190,6 +1295,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
1190
1295
|
if (!this.db)
|
|
1191
1296
|
throw new Error('Database not initialized');
|
|
1192
1297
|
console.error(`🗑️ Deleting entities: ${entityNames.join(', ')}`);
|
|
1298
|
+
let anyDeleted = false;
|
|
1193
1299
|
for (const name of entityNames) {
|
|
1194
1300
|
const entityId = `entity_${name.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '_')}`;
|
|
1195
1301
|
// One entity = one transaction. The four deletion steps are a single unit:
|
|
@@ -1205,6 +1311,9 @@ export class RAGKnowledgeGraphManager {
|
|
|
1205
1311
|
`).get(eid);
|
|
1206
1312
|
if (!entityExists)
|
|
1207
1313
|
return false;
|
|
1314
|
+
// D2③: evidence chunks of ANY relationship may cite this entity's observations; collect them
|
|
1315
|
+
// before the FK CASCADE takes the observation rows (step 3).
|
|
1316
|
+
const citedEvidence = collectEvidenceRowidsCitingOwnerEntity(this.db, eid);
|
|
1208
1317
|
// Step 0: Delete entity embeddings
|
|
1209
1318
|
const embeddingMetadata = this.db.prepare(`
|
|
1210
1319
|
SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?
|
|
@@ -1247,6 +1356,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
1247
1356
|
// deletion unless swept here (same fail-closed rationale as deleteStaleKgChunks).
|
|
1248
1357
|
deleteStaleKgChunks(this.db, eid);
|
|
1249
1358
|
deleteKgRelationshipChunks(this.db, relIds.map(r => r.id));
|
|
1359
|
+
deleteKgChunkRows(this.db, citedEvidence);
|
|
1250
1360
|
// Step 3: Finally delete the entity itself (FK CASCADE takes observation lifecycle rows)
|
|
1251
1361
|
const entity = this.db.prepare(`
|
|
1252
1362
|
DELETE FROM entities WHERE id = ?
|
|
@@ -1254,6 +1364,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
1254
1364
|
return entity.changes > 0;
|
|
1255
1365
|
})(entityId);
|
|
1256
1366
|
if (deleted) {
|
|
1367
|
+
anyDeleted = true;
|
|
1257
1368
|
console.error(` └─ Deleted entity '${name}' successfully`);
|
|
1258
1369
|
}
|
|
1259
1370
|
else {
|
|
@@ -1265,6 +1376,10 @@ export class RAGKnowledgeGraphManager {
|
|
|
1265
1376
|
// Continue with other entities instead of failing completely
|
|
1266
1377
|
}
|
|
1267
1378
|
}
|
|
1379
|
+
// D2③ (final review A1): the committed deletes removed entity vectors and KG/evidence chunks, so the
|
|
1380
|
+
// cached coverage snapshot is stale — drop it like every other D2③ writer, once, after the commits.
|
|
1381
|
+
if (anyDeleted)
|
|
1382
|
+
this.coordinator?.invalidateCoverage();
|
|
1268
1383
|
console.error(`✅ Entity deletion process completed`);
|
|
1269
1384
|
}
|
|
1270
1385
|
// v3.6 (spec §5c, breaking): structured per-entity results + re-embedding.
|
|
@@ -1321,6 +1436,8 @@ export class RAGKnowledgeGraphManager {
|
|
|
1321
1436
|
if (touched.length > 0) {
|
|
1322
1437
|
const tx = this.db.transaction(() => {
|
|
1323
1438
|
for (const p of touched) {
|
|
1439
|
+
// D2③: this shim does not go through mutateEntityAndInvalidate, so it owes the evidence step itself.
|
|
1440
|
+
const citedEvidence = collectEvidenceRowidsCitingObservations(this.db, p.ids);
|
|
1324
1441
|
for (const id of p.ids) {
|
|
1325
1442
|
transitionStatus(this.db, { observationId: id, event: 'retract',
|
|
1326
1443
|
reason: 'deleteObservations (deprecated shim)', ts });
|
|
@@ -1334,6 +1451,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
1334
1451
|
.run(p.entityId);
|
|
1335
1452
|
}
|
|
1336
1453
|
deleteStaleKgChunks(this.db, p.entityId);
|
|
1454
|
+
deleteKgChunkRows(this.db, citedEvidence);
|
|
1337
1455
|
}
|
|
1338
1456
|
});
|
|
1339
1457
|
tx();
|
|
@@ -1359,11 +1477,22 @@ export class RAGKnowledgeGraphManager {
|
|
|
1359
1477
|
for (const relation of relations) {
|
|
1360
1478
|
const sourceId = `entity_${relation.from.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '_')}`;
|
|
1361
1479
|
const targetId = `entity_${relation.to.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '_')}`;
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1480
|
+
// D2① (kg-relation-evidence): the KG chunk is keyed by relationship_id, so capture the ids
|
|
1481
|
+
// BEFORE the DELETE and remove chunk, vector and FTS row in the same transaction as the row.
|
|
1482
|
+
// 6.4.0 deleted only the row and the chunk stayed searchable (test/kg-delete-relation.test.mjs).
|
|
1483
|
+
this.db.transaction(() => {
|
|
1484
|
+
const ids = this.db.prepare(`
|
|
1485
|
+
SELECT id FROM relationships
|
|
1486
|
+
WHERE source_entity = ? AND target_entity = ? AND relationType = ?
|
|
1487
|
+
`).all(sourceId, targetId, relation.relationType);
|
|
1488
|
+
deleteKgRelationshipChunks(this.db, ids.map(r => r.id));
|
|
1489
|
+
this.db.prepare(`
|
|
1490
|
+
DELETE FROM relationships
|
|
1491
|
+
WHERE source_entity = ? AND target_entity = ? AND relationType = ?
|
|
1492
|
+
`).run(sourceId, targetId, relation.relationType);
|
|
1493
|
+
})();
|
|
1366
1494
|
}
|
|
1495
|
+
this.coordinator?.invalidateCoverage();
|
|
1367
1496
|
}
|
|
1368
1497
|
async updateRelations(updates) {
|
|
1369
1498
|
if (!this.db)
|
|
@@ -1397,9 +1526,15 @@ export class RAGKnowledgeGraphManager {
|
|
|
1397
1526
|
continue;
|
|
1398
1527
|
}
|
|
1399
1528
|
values.push(relationId);
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1529
|
+
// D2② (kg-relation-evidence): the copied display confidence in the KG chunk is refreshed in the
|
|
1530
|
+
// same transaction; body unchanged, so no re-embed (test/kg-confidence-copy.test.mjs).
|
|
1531
|
+
this.db.transaction(() => {
|
|
1532
|
+
this.db.prepare(`
|
|
1533
|
+
UPDATE relationships SET ${setClauses.join(', ')} WHERE id = ?
|
|
1534
|
+
`).run(...values);
|
|
1535
|
+
if (update.confidence !== undefined)
|
|
1536
|
+
refreshKgRelationshipConfidence(this.db, relationId);
|
|
1537
|
+
})();
|
|
1403
1538
|
updated++;
|
|
1404
1539
|
}
|
|
1405
1540
|
return { updated, notFound };
|
|
@@ -1970,34 +2105,17 @@ export class RAGKnowledgeGraphManager {
|
|
|
1970
2105
|
};
|
|
1971
2106
|
}
|
|
1972
2107
|
// NEW: Generate knowledge graph chunks for entities and relationships
|
|
2108
|
+
// D2⑤ (kg-relation-evidence): build every row first, then swap old -> new in ONE transaction. 6.4.0
|
|
2109
|
+
// deleted the old chunks and inserted row by row, so a failure in between published a partial KG.
|
|
2110
|
+
// Bodies, chunk IDs, metadata, offsets and counts are byte-for-byte 6.4.0's (C7.3) — including
|
|
2111
|
+
// end_pos = UTF-16 length, a pre-existing quirk kept on purpose (eval/kg-defaults/generate-snapshot.mjs).
|
|
1973
2112
|
async generateKnowledgeGraphChunks() {
|
|
1974
2113
|
if (!this.db)
|
|
1975
2114
|
throw new Error('Database not initialized');
|
|
1976
2115
|
console.error('🧠 Generating knowledge graph chunks...');
|
|
1977
|
-
// Clean up existing knowledge graph chunks
|
|
1978
|
-
await this.cleanupKnowledgeGraphChunks();
|
|
1979
|
-
let entityChunks = 0;
|
|
1980
|
-
let relationshipChunks = 0;
|
|
1981
|
-
// Generate entity chunks
|
|
1982
2116
|
const entities = this.db.prepare(`
|
|
1983
2117
|
SELECT id, name, entityType, observations FROM entities
|
|
1984
2118
|
`).all();
|
|
1985
|
-
for (const entity of entities) {
|
|
1986
|
-
const observations = JSON.parse(entity.observations);
|
|
1987
|
-
const chunkText = this.generateEntityChunkText(entity.name, entity.entityType, observations);
|
|
1988
|
-
const chunkId = `kg_entity_${entity.id}`;
|
|
1989
|
-
// Store chunk metadata
|
|
1990
|
-
this.db.prepare(`
|
|
1991
|
-
INSERT INTO chunk_metadata (
|
|
1992
|
-
chunk_id, chunk_type, entity_id, chunk_index, text, start_pos, end_pos, metadata
|
|
1993
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1994
|
-
`).run(chunkId, 'entity', entity.id, 0, chunkText, 0, chunkText.length, JSON.stringify({
|
|
1995
|
-
entity_name: entity.name,
|
|
1996
|
-
entity_type: entity.entityType
|
|
1997
|
-
}));
|
|
1998
|
-
entityChunks++;
|
|
1999
|
-
}
|
|
2000
|
-
// Generate relationship chunks
|
|
2001
2119
|
const relationships = this.db.prepare(`
|
|
2002
2120
|
SELECT
|
|
2003
2121
|
r.id,
|
|
@@ -2009,65 +2127,147 @@ export class RAGKnowledgeGraphManager {
|
|
|
2009
2127
|
JOIN entities e1 ON r.source_entity = e1.id
|
|
2010
2128
|
JOIN entities e2 ON r.target_entity = e2.id
|
|
2011
2129
|
`).all();
|
|
2130
|
+
const rows = [];
|
|
2131
|
+
for (const entity of entities) {
|
|
2132
|
+
const chunkText = this.generateEntityChunkText(entity.name, entity.entityType, JSON.parse(entity.observations));
|
|
2133
|
+
rows.push({ chunk_id: `kg_entity_${entity.id}`, chunk_type: 'entity', source: entity.id, text: chunkText,
|
|
2134
|
+
metadata: JSON.stringify({ entity_name: entity.name, entity_type: entity.entityType }) });
|
|
2135
|
+
}
|
|
2012
2136
|
for (const rel of relationships) {
|
|
2013
2137
|
const chunkText = this.generateRelationshipChunkText(rel.source_name, rel.target_name, rel.relationType);
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2138
|
+
rows.push({ chunk_id: `kg_relationship_${rel.id}`, chunk_type: 'relationship', source: rel.id, text: chunkText,
|
|
2139
|
+
metadata: JSON.stringify({ source_entity: rel.source_name, target_entity: rel.target_name, relation_type: rel.relationType, confidence: rel.confidence }) });
|
|
2140
|
+
}
|
|
2141
|
+
const insEntity = this.db.prepare(`
|
|
2142
|
+
INSERT INTO chunk_metadata (chunk_id, chunk_type, entity_id, chunk_index, text, start_pos, end_pos, metadata)
|
|
2143
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
2144
|
+
const insRel = this.db.prepare(`
|
|
2145
|
+
INSERT INTO chunk_metadata (chunk_id, chunk_type, relationship_id, chunk_index, text, start_pos, end_pos, metadata)
|
|
2146
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
2147
|
+
let entityChunks = 0;
|
|
2148
|
+
let relationshipChunks = 0;
|
|
2149
|
+
this.db.transaction(() => {
|
|
2150
|
+
this.deleteAllKgChunks();
|
|
2151
|
+
for (const r of rows) {
|
|
2152
|
+
(r.chunk_type === 'entity' ? insEntity : insRel).run(r.chunk_id, r.chunk_type, r.source, 0, r.text, 0, r.text.length, r.metadata);
|
|
2153
|
+
if (r.chunk_type === 'entity')
|
|
2154
|
+
entityChunks++;
|
|
2155
|
+
else
|
|
2156
|
+
relationshipChunks++;
|
|
2157
|
+
if (entityChunks + relationshipChunks === 1)
|
|
2158
|
+
__kgFaultHook?.('kg-generate-mid');
|
|
2159
|
+
}
|
|
2160
|
+
})();
|
|
2161
|
+
this.coordinator?.invalidateCoverage();
|
|
2028
2162
|
console.error(`✅ Knowledge graph chunks generated: ${entityChunks} entities, ${relationshipChunks} relationships`);
|
|
2029
2163
|
return { entityChunks, relationshipChunks };
|
|
2030
2164
|
}
|
|
2031
2165
|
// NEW: Embed knowledge graph chunks
|
|
2166
|
+
// D2⑤: capture before inference, re-check at write (writeKgVectorIfCurrent). 6.4.0 wrote whatever
|
|
2167
|
+
// came back, onto whatever row then held the rowid.
|
|
2032
2168
|
async embedKnowledgeGraphChunks() {
|
|
2033
2169
|
if (!this.db)
|
|
2034
2170
|
throw new Error('Database not initialized');
|
|
2035
2171
|
console.error('🔮 Embedding knowledge graph chunks...');
|
|
2036
|
-
// Get all knowledge graph chunks
|
|
2037
2172
|
const chunks = this.db.prepare(`
|
|
2038
|
-
SELECT rowid, chunk_id, text
|
|
2039
|
-
FROM chunk_metadata
|
|
2173
|
+
SELECT rowid, chunk_id, chunk_type, entity_id, relationship_id, text, metadata
|
|
2174
|
+
FROM chunk_metadata
|
|
2040
2175
|
WHERE chunk_type IN ('entity', 'relationship')
|
|
2041
2176
|
`).all();
|
|
2042
2177
|
let embeddedCount = 0;
|
|
2178
|
+
let discarded = 0;
|
|
2043
2179
|
const errors = [];
|
|
2044
2180
|
for (const chunk of chunks) {
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
const
|
|
2181
|
+
if (chunk.text === null)
|
|
2182
|
+
continue;
|
|
2183
|
+
const captured = captureKgEmbedInput(chunk, this.currentProfileId);
|
|
2184
|
+
const embedding = await this.generateEmbedding(chunk.text, 1024, false, 'bulk', `profile:${captured.profile_id}`);
|
|
2048
2185
|
try {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
this.db.prepare(`
|
|
2054
|
-
INSERT INTO chunks (rowid, embedding) VALUES (${rowid}, ?)
|
|
2055
|
-
`).run(Buffer.from(embedding.buffer));
|
|
2056
|
-
this.db.prepare(`UPDATE chunk_metadata SET input_hash = ?, profile_id = ?, provenance_state = 'verified' WHERE rowid = ?`)
|
|
2057
|
-
.run(createHash('sha256').update(chunk.text).digest('hex'), this.currentProfileId, chunk.rowid);
|
|
2058
|
-
});
|
|
2059
|
-
tx();
|
|
2060
|
-
embeddedCount++;
|
|
2186
|
+
if (this.writeKgVectorIfCurrent(captured, embedding))
|
|
2187
|
+
embeddedCount++;
|
|
2188
|
+
else
|
|
2189
|
+
discarded++;
|
|
2061
2190
|
}
|
|
2062
2191
|
catch (error) {
|
|
2063
|
-
const errMsg = `chunk ${chunk.chunk_id} (rowid=${
|
|
2192
|
+
const errMsg = `chunk ${chunk.chunk_id} (rowid=${chunk.rowid}): ${error instanceof Error ? error.message : String(error)}`;
|
|
2064
2193
|
console.error(`Failed to embed ${errMsg}`);
|
|
2065
2194
|
errors.push(errMsg);
|
|
2066
2195
|
}
|
|
2067
2196
|
}
|
|
2068
2197
|
this.coordinator?.invalidateCoverage();
|
|
2069
|
-
console.error(`✅ Knowledge graph chunks embedded: ${embeddedCount}/${chunks.length}`);
|
|
2070
|
-
return {
|
|
2198
|
+
console.error(`✅ Knowledge graph chunks embedded: ${embeddedCount}/${chunks.length}${discarded > 0 ? ` (${discarded} discarded: input changed during inference)` : ''}`);
|
|
2199
|
+
return {
|
|
2200
|
+
embeddedChunks: embeddedCount, totalChunks: chunks.length,
|
|
2201
|
+
...(discarded > 0 && { discardedChunks: discarded }),
|
|
2202
|
+
...(errors.length > 0 && { errors: errors.slice(0, 5) }),
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
// D1 (kg-relation-evidence): opt-in evidence-backed relationship chunks. Manager method only — not an
|
|
2206
|
+
// MCP tool (Q3). `items` is the COMPLETE set S of relationships that get a relationship chunk: a
|
|
2207
|
+
// successful call atomically replaces ALL relationship KG chunks (triplet or evidence) with the
|
|
2208
|
+
// evidence-v1 chunks of S; entity KG chunks, documents and relationship rows are untouched; [] is
|
|
2209
|
+
// valid and empties relationship KG chunks. Any invalid item rejects the whole call before any
|
|
2210
|
+
// write. Bodies are prepared first; inside the commit transaction every item is re-resolved from
|
|
2211
|
+
// the CURRENT rows (active status and chunk type/parent first, then the fingerprint) and must match
|
|
2212
|
+
// what was prepared; the re-resolved rows are what gets inserted. Vectors come later from
|
|
2213
|
+
// embedKnowledgeGraphChunks or the backfill.
|
|
2214
|
+
async generateRelationEvidenceChunks(items, options) {
|
|
2215
|
+
if (!this.db)
|
|
2216
|
+
throw new Error('Database not initialized');
|
|
2217
|
+
if (options?.formatVersion !== EVIDENCE_FORMAT) {
|
|
2218
|
+
throw new Error(`generateRelationEvidenceChunks: formatVersion must be '${EVIDENCE_FORMAT}'`);
|
|
2219
|
+
}
|
|
2220
|
+
if (!Array.isArray(items))
|
|
2221
|
+
throw new Error('generateRelationEvidenceChunks: items must be an array');
|
|
2222
|
+
const seen = new Set();
|
|
2223
|
+
items.forEach((it, i) => {
|
|
2224
|
+
const id = it?.relationship_id;
|
|
2225
|
+
if (typeof id !== 'string')
|
|
2226
|
+
return;
|
|
2227
|
+
if (seen.has(id))
|
|
2228
|
+
throw new EvidenceInputError('duplicate-relationship', i, id);
|
|
2229
|
+
seen.add(id);
|
|
2230
|
+
});
|
|
2231
|
+
const profileAtPrepare = this.currentProfileId;
|
|
2232
|
+
// Array.from, not map: map skips holes, so a hole in items reached the commit loop unprepared and
|
|
2233
|
+
// surfaced as a commit conflict instead of an input error (D1, same class as REVIEW-T5-2_ I1).
|
|
2234
|
+
const prepared = Array.from(items, (it, i) => resolveEvidenceItem(this.db, it, i, profileAtPrepare));
|
|
2235
|
+
__kgFaultHook?.('evidence-pre-commit');
|
|
2236
|
+
let removed = 0;
|
|
2237
|
+
this.db.transaction(() => {
|
|
2238
|
+
const db = this.db;
|
|
2239
|
+
// R1 (REVIEW-PLAN): what is published is the metadata re-resolved HERE, from the current rows —
|
|
2240
|
+
// not the prepared copy. A display-only change between preparation and commit (confidence is not a
|
|
2241
|
+
// fingerprint input) therefore succeeds with the current value instead of re-publishing the old one;
|
|
2242
|
+
// any fingerprint input change still fails the whole call.
|
|
2243
|
+
const current = [];
|
|
2244
|
+
for (let i = 0; i < items.length; i++) {
|
|
2245
|
+
let now;
|
|
2246
|
+
try {
|
|
2247
|
+
now = resolveEvidenceItem(db, items[i], i, this.currentProfileId);
|
|
2248
|
+
}
|
|
2249
|
+
catch (e) {
|
|
2250
|
+
throw new EvidenceCommitConflictError(i, e instanceof EvidenceInputError ? e.code : String(e));
|
|
2251
|
+
}
|
|
2252
|
+
if (now.metadata.input_fingerprint !== prepared[i].metadata.input_fingerprint) {
|
|
2253
|
+
throw new EvidenceCommitConflictError(i, 'fingerprint-changed');
|
|
2254
|
+
}
|
|
2255
|
+
current.push(now); // same fingerprint => same body (every body input is a fingerprint input)
|
|
2256
|
+
}
|
|
2257
|
+
const old = db.prepare(`SELECT rowid FROM chunk_metadata WHERE chunk_type = 'relationship'`).all();
|
|
2258
|
+
removed = deleteKgChunkRows(db, old.map(r => r.rowid));
|
|
2259
|
+
const ins = db.prepare(`
|
|
2260
|
+
INSERT INTO chunk_metadata (chunk_id, chunk_type, relationship_id, chunk_index, text, start_pos, end_pos, metadata)
|
|
2261
|
+
VALUES (?, 'relationship', ?, 0, ?, 0, ?, ?)`);
|
|
2262
|
+
current.forEach((p, i) => {
|
|
2263
|
+
ins.run(p.chunk_id, p.relationship_id, p.text, p.body_codepoints, JSON.stringify(p.metadata));
|
|
2264
|
+
if (i === 0)
|
|
2265
|
+
__kgFaultHook?.('evidence-mid-commit');
|
|
2266
|
+
});
|
|
2267
|
+
})();
|
|
2268
|
+
this.coordinator?.invalidateCoverage();
|
|
2269
|
+
console.error(`✅ Evidence relationship chunks: ${prepared.length} written, ${removed} relationship chunks replaced`);
|
|
2270
|
+
return { relationshipChunks: prepared.length, removedRelationshipChunks: removed, formatVersion: EVIDENCE_FORMAT };
|
|
2071
2271
|
}
|
|
2072
2272
|
// NEW: Generate textual representation for entity chunks
|
|
2073
2273
|
generateEntityChunkText(name, entityType, observations) {
|
|
@@ -2080,29 +2280,26 @@ export class RAGKnowledgeGraphManager {
|
|
|
2080
2280
|
const relationText = relationType.toLowerCase().replace(/_/g, ' ');
|
|
2081
2281
|
return `${sourceName} ${relationText} ${targetName}`;
|
|
2082
2282
|
}
|
|
2083
|
-
//
|
|
2084
|
-
|
|
2283
|
+
// Delete every KG chunk (vectors, links, metadata). Synchronous so it can run inside the swap
|
|
2284
|
+
// transaction of generateKnowledgeGraphChunks (D2⑤). Body = 6.4.0 cleanupKnowledgeGraphChunks.
|
|
2285
|
+
deleteAllKgChunks() {
|
|
2085
2286
|
if (!this.db)
|
|
2086
2287
|
return;
|
|
2087
2288
|
console.error('🧹 Cleaning up existing knowledge graph chunks...');
|
|
2088
|
-
// Get existing knowledge graph chunks
|
|
2089
2289
|
const existingChunks = this.db.prepare(`
|
|
2090
2290
|
SELECT rowid FROM chunk_metadata WHERE chunk_type IN ('entity', 'relationship')
|
|
2091
2291
|
`).all();
|
|
2092
2292
|
let deletedVectors = 0;
|
|
2093
2293
|
let deletedAssociations = 0;
|
|
2094
|
-
// Delete vectors and associations
|
|
2095
2294
|
for (const chunk of existingChunks) {
|
|
2096
2295
|
// Delete vector embeddings (vec0 needs literal integer, not parameterized)
|
|
2097
2296
|
this.db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(chunk.rowid)}`);
|
|
2098
2297
|
deletedVectors++;
|
|
2099
|
-
// Delete chunk-entity associations
|
|
2100
2298
|
const associations = this.db.prepare(`
|
|
2101
2299
|
DELETE FROM chunk_entities WHERE chunk_rowid = ?
|
|
2102
2300
|
`).run(chunk.rowid);
|
|
2103
2301
|
deletedAssociations += associations.changes;
|
|
2104
2302
|
}
|
|
2105
|
-
// Delete chunk metadata
|
|
2106
2303
|
const metadata = this.db.prepare(`
|
|
2107
2304
|
DELETE FROM chunk_metadata WHERE chunk_type IN ('entity', 'relationship')
|
|
2108
2305
|
`).run();
|
|
@@ -2300,9 +2497,13 @@ export class RAGKnowledgeGraphManager {
|
|
|
2300
2497
|
}
|
|
2301
2498
|
// Generate embeddings using sentence transformers
|
|
2302
2499
|
// isQuery: true for search queries (adds instruction prefix), false for documents/entities
|
|
2303
|
-
async generateEmbedding(text, dimensions = 1024, isQuery = false, priority = 'interactive') {
|
|
2500
|
+
async generateEmbedding(text, dimensions = 1024, isQuery = false, priority = 'interactive', cacheScope) {
|
|
2304
2501
|
// Check cache first (hash-based key to avoid collisions on long texts)
|
|
2305
|
-
|
|
2502
|
+
// kg-relation-evidence R5: the key has no profile, so a vector cached under profile P1 would be
|
|
2503
|
+
// returned — and stamped P2 by the KG CAS — after a switch to P2. The KG write paths pass
|
|
2504
|
+
// cacheScope = 'profile:<captured id>'; without a scope the key is exactly the 6.4.0 key, so
|
|
2505
|
+
// document and query caching is unchanged (their profile policy is not this change's scope).
|
|
2506
|
+
const cacheKey = createHash('md5').update(`${text}_${dimensions}_${isQuery}${cacheScope === undefined ? '' : `_${cacheScope}`}`).digest('hex');
|
|
2306
2507
|
const cached = this.embeddingCache.get(cacheKey);
|
|
2307
2508
|
if (cached)
|
|
2308
2509
|
return cached;
|
|
@@ -2431,12 +2632,16 @@ export class RAGKnowledgeGraphManager {
|
|
|
2431
2632
|
now.metadata === snap.metadata && now.chunking_signature === snap.chunking_signature);
|
|
2432
2633
|
if (!same)
|
|
2433
2634
|
throw new SyncCasConflictError(documentId);
|
|
2635
|
+
// D2③: collect evidence citing this document before its chunks are replaced; delete it below,
|
|
2636
|
+
// in this transaction (no separate deletion moment).
|
|
2637
|
+
const citedEvidence = collectEvidenceRowidsCitingDocument(db, documentId);
|
|
2434
2638
|
const existing = db.prepare(`SELECT rowid FROM chunk_metadata WHERE document_id = ?`).all(documentId);
|
|
2435
2639
|
for (const ch of existing) {
|
|
2436
2640
|
db.prepare(`DELETE FROM chunk_entities WHERE chunk_rowid = ?`).run(ch.rowid);
|
|
2437
2641
|
db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(ch.rowid)}`);
|
|
2438
2642
|
}
|
|
2439
2643
|
db.prepare(`DELETE FROM chunk_metadata WHERE document_id = ?`).run(documentId);
|
|
2644
|
+
deleteKgChunkRows(db, citedEvidence);
|
|
2440
2645
|
db.prepare(`DELETE FROM documents WHERE id = ?`).run(documentId);
|
|
2441
2646
|
db.prepare(`INSERT INTO documents (id, content, metadata, chunking_signature) VALUES (?, ?, ?, ?)`)
|
|
2442
2647
|
.run(documentId, content, JSON.stringify(metadata), signature);
|
|
@@ -2508,13 +2713,22 @@ export class RAGKnowledgeGraphManager {
|
|
|
2508
2713
|
// Decide `replaced` from the document row, not from the chunk count: a document stored but
|
|
2509
2714
|
// never chunked still gets overwritten here, and reporting that as a fresh write would be a lie.
|
|
2510
2715
|
const existed = this.db.prepare(`SELECT 1 FROM documents WHERE id = ?`).get(id) !== undefined;
|
|
2511
|
-
//
|
|
2512
|
-
|
|
2513
|
-
//
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2716
|
+
// D2③ + R-B (2_ RESULT-T7-TXN-2_): serialise first, then ONE synchronous transaction = cleanup
|
|
2717
|
+
// (collect citing evidence, delete chunks and evidence) + the documents REPLACE. With an await
|
|
2718
|
+
// between the two, another call could publish evidence on this document after the collection, and a
|
|
2719
|
+
// failing REPLACE left the chunks and evidence already deleted.
|
|
2720
|
+
const metadataJson = JSON.stringify(metadata);
|
|
2721
|
+
const cleaned = this.db.transaction(() => {
|
|
2722
|
+
const c = this.cleanupDocument(id);
|
|
2723
|
+
this.db.prepare(`
|
|
2724
|
+
INSERT OR REPLACE INTO documents (id, content, metadata)
|
|
2725
|
+
VALUES (?, ?, ?)
|
|
2726
|
+
`).run(id, content, metadataJson);
|
|
2727
|
+
return c;
|
|
2728
|
+
})();
|
|
2729
|
+
// R-B item 6 (review I1): only after the commit succeeded — chunks and evidence were removed, so the
|
|
2730
|
+
// cached coverage snapshot is stale. A rollback throws above and never reaches this.
|
|
2731
|
+
this.coordinator?.invalidateCoverage();
|
|
2518
2732
|
console.error(`✅ Document stored: ${id}`);
|
|
2519
2733
|
return { id, stored: true, replaced: existed, deletedChunks: cleaned.deletedChunks };
|
|
2520
2734
|
}
|
|
@@ -2530,32 +2744,38 @@ export class RAGKnowledgeGraphManager {
|
|
|
2530
2744
|
}
|
|
2531
2745
|
const { maxTokens } = this.validateChunkParams(options);
|
|
2532
2746
|
console.error(`🔪 Chunking document: ${documentId} (maxTokens: ${maxTokens}, chunker: c1)`);
|
|
2533
|
-
//
|
|
2534
|
-
|
|
2535
|
-
// Create chunks
|
|
2747
|
+
// Create chunks. D2③ + R-B (2_ RESULT-T7-TXN-2_): chunking is synchronous and runs BEFORE anything
|
|
2748
|
+
// is deleted, so a chunker failure deletes nothing.
|
|
2536
2749
|
const chunks = this.chunkStructured(document.content, maxTokens);
|
|
2537
2750
|
const resultChunks = [];
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2751
|
+
// ONE synchronous transaction = cleanup (collect evidence citing this document, delete its chunks and
|
|
2752
|
+
// that evidence) + every new chunk INSERT + the signature UPDATE. A re-chunk reuses the ordinal chunk
|
|
2753
|
+
// ids, so with an await between cleanup and the INSERTs another call could cite the new D_chunk_0
|
|
2754
|
+
// after the collection; and a failure after cleanup lost the old chunks for good.
|
|
2755
|
+
this.db.transaction(() => {
|
|
2756
|
+
this.cleanupDocument(documentId);
|
|
2757
|
+
for (const chunk of chunks) {
|
|
2758
|
+
const chunkId = `${documentId}_chunk_${chunk.chunk_index}`;
|
|
2759
|
+
// Store chunk metadata (no embedding yet)
|
|
2760
|
+
this.db.prepare(`
|
|
2761
|
+
INSERT INTO chunk_metadata (
|
|
2762
|
+
chunk_id, document_id, chunk_index, text, start_pos, end_pos, start_token, end_token
|
|
2763
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2764
|
+
`).run(chunkId, documentId, chunk.chunk_index, chunk.text, chunk.start_pos, chunk.end_pos, chunk.start_token, chunk.end_token);
|
|
2765
|
+
resultChunks.push({
|
|
2766
|
+
id: chunkId,
|
|
2767
|
+
text: chunk.text,
|
|
2768
|
+
startPos: chunk.start_pos,
|
|
2769
|
+
endPos: chunk.end_pos,
|
|
2770
|
+
startToken: chunk.start_token,
|
|
2771
|
+
endToken: chunk.end_token
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
// spec §7.1: 두 번째 chunk 생성 경로 — 스탬프를 안 박으면 §5.1 관측이 조용히 샌다.
|
|
2775
|
+
this.db.prepare(`UPDATE documents SET chunking_signature = ? WHERE id = ?`)
|
|
2776
|
+
.run(effectiveSignature(maxTokens), documentId);
|
|
2777
|
+
})();
|
|
2555
2778
|
console.error(`✅ Document chunked: ${chunks.length} chunks created`);
|
|
2556
|
-
// spec §7.1: 두 번째 chunk 생성 경로 — 스탬프를 안 박으면 §5.1 관측이 조용히 샌다.
|
|
2557
|
-
this.db.prepare(`UPDATE documents SET chunking_signature = ? WHERE id = ?`)
|
|
2558
|
-
.run(effectiveSignature(maxTokens), documentId);
|
|
2559
2779
|
// Indirect missing-row producer (spec §5): freshly chunked rows have no
|
|
2560
2780
|
// vectors yet — let the coordinator recover them without a restart.
|
|
2561
2781
|
this.coordinator?.invalidateCoverage();
|
|
@@ -3007,37 +3227,44 @@ export class RAGKnowledgeGraphManager {
|
|
|
3007
3227
|
// that replaces a document could not tell from the return value that anything was deleted
|
|
3008
3228
|
// (2026-08-05 field report from a deployed project: "{stored:true} came back and I did not know
|
|
3009
3229
|
// what I had just wiped"). Silent destruction is the defect; the numbers are free.
|
|
3010
|
-
|
|
3230
|
+
cleanupDocument(documentId) {
|
|
3011
3231
|
if (!this.db)
|
|
3012
3232
|
return { deletedChunks: 0, deletedAssociations: 0, deletedVectors: 0 };
|
|
3013
3233
|
console.error(`🧹 Cleaning up document: ${documentId}`);
|
|
3014
|
-
//
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
const
|
|
3024
|
-
|
|
3025
|
-
`).
|
|
3026
|
-
deletedAssociations
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3234
|
+
// D2③ (kg-relation-evidence): collect -> delete chunks -> invalidate citing evidence in ONE
|
|
3235
|
+
// transaction. 6.4.0 ran these statements one by one in autocommit. Used by storeDocument,
|
|
3236
|
+
// chunkDocument (re-chunk that reuses ordinal ids) and deleteDocument(s).
|
|
3237
|
+
// R-B (2_ RESULT-T7-TXN-2_): SYNCHRONOUS — results and exceptions reach the caller in the same
|
|
3238
|
+
// tick. Every caller runs it inside its own transaction together with the source change, so this
|
|
3239
|
+
// transaction nests as a savepoint and the caller's final SQL is inside the same rollback scope.
|
|
3240
|
+
return this.db.transaction(() => {
|
|
3241
|
+
const db = this.db;
|
|
3242
|
+
const citedEvidence = collectEvidenceRowidsCitingDocument(db, documentId);
|
|
3243
|
+
const existingChunks = db.prepare(`
|
|
3244
|
+
SELECT rowid FROM chunk_metadata WHERE document_id = ?
|
|
3245
|
+
`).all(documentId);
|
|
3246
|
+
let deletedAssociations = 0;
|
|
3247
|
+
let deletedVectors = 0;
|
|
3248
|
+
for (const chunk of existingChunks) {
|
|
3249
|
+
const associations = db.prepare(`
|
|
3250
|
+
DELETE FROM chunk_entities WHERE chunk_rowid = ?
|
|
3251
|
+
`).run(chunk.rowid);
|
|
3252
|
+
deletedAssociations += associations.changes;
|
|
3253
|
+
// Delete vector embeddings (vec0 needs literal integer, not parameterized)
|
|
3254
|
+
db.exec(`DELETE FROM chunks WHERE rowid = ${safeRowid(chunk.rowid)}`);
|
|
3255
|
+
deletedVectors++;
|
|
3256
|
+
}
|
|
3257
|
+
const metadata = db.prepare(`
|
|
3258
|
+
DELETE FROM chunk_metadata WHERE document_id = ?
|
|
3259
|
+
`).run(documentId);
|
|
3260
|
+
deleteKgChunkRows(db, citedEvidence);
|
|
3261
|
+
if (existingChunks.length > 0) {
|
|
3262
|
+
console.error(` ├─ Deleted ${deletedAssociations} entity associations`);
|
|
3263
|
+
console.error(` ├─ Deleted ${deletedVectors} vector embeddings`);
|
|
3264
|
+
console.error(` └─ Deleted ${metadata.changes} chunk metadata records`);
|
|
3265
|
+
}
|
|
3266
|
+
return { deletedChunks: existingChunks.length, deletedAssociations, deletedVectors };
|
|
3267
|
+
})();
|
|
3041
3268
|
}
|
|
3042
3269
|
async deleteDocument(documentId) {
|
|
3043
3270
|
if (!this.db)
|
|
@@ -3052,12 +3279,18 @@ export class RAGKnowledgeGraphManager {
|
|
|
3052
3279
|
console.warn(`⚠️ Document '${documentId}' not found`);
|
|
3053
3280
|
return { documentId, deleted: false };
|
|
3054
3281
|
}
|
|
3055
|
-
//
|
|
3056
|
-
|
|
3057
|
-
//
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3282
|
+
// D2③ + R-B (2_ RESULT-T7-TXN-2_): cleanup (collect evidence citing this document, delete its
|
|
3283
|
+
// chunks and that evidence) and the document DELETE are ONE synchronous transaction — no await
|
|
3284
|
+
// between them, so no evidence can be published on this document after the collection, and a
|
|
3285
|
+
// failing DELETE rolls the cleanup back.
|
|
3286
|
+
const result = this.db.transaction(() => {
|
|
3287
|
+
this.cleanupDocument(documentId);
|
|
3288
|
+
return this.db.prepare(`
|
|
3289
|
+
DELETE FROM documents WHERE id = ?
|
|
3290
|
+
`).run(documentId);
|
|
3291
|
+
})();
|
|
3292
|
+
// R-B item 6 (review I1): after the successful commit only (a rollback throws above).
|
|
3293
|
+
this.coordinator?.invalidateCoverage();
|
|
3061
3294
|
if (result.changes > 0) {
|
|
3062
3295
|
console.error(`✅ Document '${documentId}' deleted successfully`);
|
|
3063
3296
|
return { documentId, deleted: true };
|
|
@@ -3217,6 +3450,26 @@ export class RAGKnowledgeGraphManager {
|
|
|
3217
3450
|
if (!this.db)
|
|
3218
3451
|
throw new Error('Database not initialized');
|
|
3219
3452
|
console.error(`📥 Importing knowledge graph (merge: ${options.merge !== false})...`);
|
|
3453
|
+
// D2③ / C3 (final review I1): the evidence dependency collectors compare string ids strictly, but
|
|
3454
|
+
// the INSERTs below bind whatever the dump holds, and SQLite stores a number bound to a TEXT id
|
|
3455
|
+
// column as its decimal text (7.5 -> '7.5'). A numeric id could therefore replace a document, or add
|
|
3456
|
+
// a source to an observation, that cited evidence names by string while the collector misses it.
|
|
3457
|
+
// One id contract for writing and collecting: these two ids must be strings, checked before any
|
|
3458
|
+
// write, so a violation refuses the whole import with nothing written. No numeric normalisation.
|
|
3459
|
+
if (Array.isArray(data.documents)) {
|
|
3460
|
+
data.documents.forEach((doc, i) => {
|
|
3461
|
+
if (typeof doc?.id !== 'string') {
|
|
3462
|
+
throw new Error(`importGraph: documents[${i}].id must be a string (got ${doc?.id === null ? 'null' : typeof doc?.id}); nothing was imported`);
|
|
3463
|
+
}
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
if (Array.isArray(data.observation_sources)) {
|
|
3467
|
+
data.observation_sources.forEach((so, i) => {
|
|
3468
|
+
if (typeof so?.observation_id !== 'string') {
|
|
3469
|
+
throw new Error(`importGraph: observation_sources[${i}].observation_id must be a string (got ${so?.observation_id === null ? 'null' : typeof so?.observation_id}); nothing was imported`);
|
|
3470
|
+
}
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3220
3473
|
const imported = { entities: 0, relations: 0, documents: 0 };
|
|
3221
3474
|
const skipped = { entities: 0, relations: 0, documents: 0 };
|
|
3222
3475
|
// merge 로 배열 위치가 재배정된 관찰. 조용히 순서를 바꾸면 호출자가 알 수 없으므로
|
|
@@ -3253,6 +3506,10 @@ export class RAGKnowledgeGraphManager {
|
|
|
3253
3506
|
this.db.exec(`DELETE FROM entity_embedding_metadata`);
|
|
3254
3507
|
console.error('🗑️ Cleared existing data for full import');
|
|
3255
3508
|
}
|
|
3509
|
+
// D2③ (kg-relation-evidence): import is an observation AND a document writer. Evidence citing an
|
|
3510
|
+
// observation that gains a source row, or a document this import replaces, is collected before the
|
|
3511
|
+
// row is written and deleted at the end of this transaction.
|
|
3512
|
+
const citedEvidence = new Set();
|
|
3256
3513
|
// Import entities using INSERT OR IGNORE
|
|
3257
3514
|
if (data.entities && Array.isArray(data.entities)) {
|
|
3258
3515
|
const stmt = this.db.prepare(`
|
|
@@ -3295,6 +3552,9 @@ export class RAGKnowledgeGraphManager {
|
|
|
3295
3552
|
VALUES (?, ?, ?, ?)
|
|
3296
3553
|
`);
|
|
3297
3554
|
for (const doc of data.documents) {
|
|
3555
|
+
// doc.id is a string here (checked above, before any write).
|
|
3556
|
+
for (const r of collectEvidenceRowidsCitingDocument(this.db, doc.id))
|
|
3557
|
+
citedEvidence.add(r);
|
|
3298
3558
|
const result = stmt.run(doc.id, doc.content, JSON.stringify(doc.metadata || {}), doc.created_at || new Date().toISOString());
|
|
3299
3559
|
if (result.changes > 0) {
|
|
3300
3560
|
imported.documents++;
|
|
@@ -3387,6 +3647,8 @@ export class RAGKnowledgeGraphManager {
|
|
|
3387
3647
|
`${so.observation_id}/${so.source_kind}/${so.source_ref} differs from the existing row`);
|
|
3388
3648
|
continue;
|
|
3389
3649
|
}
|
|
3650
|
+
for (const r of collectEvidenceRowidsCitingObservations(this.db, [so.observation_id]))
|
|
3651
|
+
citedEvidence.add(r);
|
|
3390
3652
|
this.db.prepare(`INSERT INTO observation_sources
|
|
3391
3653
|
(observation_id, source_kind, source_ref, source_hash, recorded_at) VALUES (?, ?, ?, ?, ?)`)
|
|
3392
3654
|
.run(so.observation_id, so.source_kind, so.source_ref, so.source_hash ?? null, so.recorded_at);
|
|
@@ -3427,6 +3689,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
3427
3689
|
});
|
|
3428
3690
|
tx();
|
|
3429
3691
|
}
|
|
3692
|
+
deleteKgChunkRows(this.db, [...citedEvidence]);
|
|
3430
3693
|
// projection 재합성 + 파생 상태 무효화.
|
|
3431
3694
|
// 무효화가 없으면 이미 있던 entity 를 덮어쓴 뒤에도 옛 벡터·옛 KG chunk 가
|
|
3432
3695
|
// 검색에 남는다. import 는 관찰을 바꾸는 writer 이므로 다른 writer 와 같은
|
|
@@ -3759,6 +4022,11 @@ export class RAGKnowledgeGraphManager {
|
|
|
3759
4022
|
for (const result of vectorResults) {
|
|
3760
4023
|
// Get entities associated with this chunk (for document chunks)
|
|
3761
4024
|
let chunkEntities = [];
|
|
4025
|
+
// D2④ (kg-relation-evidence): full_context_available = "getDetailedContext can resolve the stored
|
|
4026
|
+
// body". Document chunks: unchanged (true — the FK cascade removes a chunk with its document). KG
|
|
4027
|
+
// chunks: only while their entity / relationship row exists. It is NOT a verified-evidence signal;
|
|
4028
|
+
// that is evidence_status in getDetailedContext. 6.4.0 hard-coded true.
|
|
4029
|
+
let sourceResolved = result.chunk_type === 'document';
|
|
3762
4030
|
if (result.chunk_type === 'document') {
|
|
3763
4031
|
chunkEntities = this.db.prepare(`
|
|
3764
4032
|
SELECT e.name
|
|
@@ -3774,6 +4042,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
3774
4042
|
`).get(result.entity_id);
|
|
3775
4043
|
if (entity) {
|
|
3776
4044
|
chunkEntities = [entity.name];
|
|
4045
|
+
sourceResolved = true;
|
|
3777
4046
|
}
|
|
3778
4047
|
}
|
|
3779
4048
|
else if (result.chunk_type === 'relationship' && result.relationship_id) {
|
|
@@ -3787,6 +4056,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
3787
4056
|
`).get(result.relationship_id);
|
|
3788
4057
|
if (relEntities) {
|
|
3789
4058
|
chunkEntities = [relEntities.source_name, relEntities.target_name];
|
|
4059
|
+
sourceResolved = true;
|
|
3790
4060
|
}
|
|
3791
4061
|
}
|
|
3792
4062
|
// Enhanced graph boost calculation with decay + cap
|
|
@@ -3889,7 +4159,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
3889
4159
|
vector_similarity: vectorSimilarity,
|
|
3890
4160
|
graph_boost: (useGraph && !vectorDegraded) ? graphBoost : undefined,
|
|
3891
4161
|
fts_boost: ftsBoost > 0 ? ftsBoost : undefined,
|
|
3892
|
-
full_context_available:
|
|
4162
|
+
full_context_available: sourceResolved,
|
|
3893
4163
|
chunk_type: result.chunk_type,
|
|
3894
4164
|
source_id: sourceId
|
|
3895
4165
|
});
|
|
@@ -3937,6 +4207,13 @@ export class RAGKnowledgeGraphManager {
|
|
|
3937
4207
|
if (!this.db)
|
|
3938
4208
|
throw new Error('Database not initialized');
|
|
3939
4209
|
console.error(`📖 Getting detailed context for chunk: ${chunkId}`);
|
|
4210
|
+
// D2④ (kg-relation-evidence): KG chunks have no documents row, so the INNER JOIN below never found
|
|
4211
|
+
// them (6.4.0: every KG hit failed here). Branch on chunk_type first; the document branch below is
|
|
4212
|
+
// unchanged.
|
|
4213
|
+
const head = this.db.prepare(`SELECT chunk_type FROM chunk_metadata WHERE chunk_id = ?`).get(chunkId);
|
|
4214
|
+
if (head && (head.chunk_type === 'entity' || head.chunk_type === 'relationship')) {
|
|
4215
|
+
return this.getKgDetailedContext(chunkId);
|
|
4216
|
+
}
|
|
3940
4217
|
// Get the main chunk
|
|
3941
4218
|
const chunk = this.db.prepare(`
|
|
3942
4219
|
SELECT
|
|
@@ -4002,6 +4279,49 @@ export class RAGKnowledgeGraphManager {
|
|
|
4002
4279
|
metadata: metadata
|
|
4003
4280
|
};
|
|
4004
4281
|
}
|
|
4282
|
+
// D2④: entity -> the entity and its ACTIVE observations (history stays with getObservationHistory);
|
|
4283
|
+
// relationship -> the relationship row + evidence_status + per-reference evidence_refs. When the
|
|
4284
|
+
// entity / relationship row itself is gone the call fails as an unknown chunk does.
|
|
4285
|
+
getKgDetailedContext(chunkId) {
|
|
4286
|
+
const row = this.db.prepare(`SELECT chunk_id, chunk_type, entity_id, relationship_id, text, metadata FROM chunk_metadata WHERE chunk_id = ?`)
|
|
4287
|
+
.get(chunkId);
|
|
4288
|
+
let meta = {};
|
|
4289
|
+
try {
|
|
4290
|
+
const parsed = JSON.parse(row.metadata ?? '{}');
|
|
4291
|
+
if (parsed && typeof parsed === 'object')
|
|
4292
|
+
meta = parsed;
|
|
4293
|
+
}
|
|
4294
|
+
catch {
|
|
4295
|
+
meta = {};
|
|
4296
|
+
}
|
|
4297
|
+
const notFound = () => new Error(`Chunk with ID ${chunkId} not found`);
|
|
4298
|
+
if (row.chunk_type === 'entity') {
|
|
4299
|
+
const ent = row.entity_id
|
|
4300
|
+
? this.db.prepare(`SELECT id, name, entityType FROM entities WHERE id = ?`).get(row.entity_id)
|
|
4301
|
+
: undefined;
|
|
4302
|
+
if (!ent)
|
|
4303
|
+
throw notFound();
|
|
4304
|
+
const observations = this.db.prepare(`SELECT observation_id, revision_no, content FROM entity_observations
|
|
4305
|
+
WHERE entity_id = ? AND status = 'active' ORDER BY projection_order`).all(ent.id);
|
|
4306
|
+
return { chunk_id: row.chunk_id, document_id: null, document_title: null, source_kind: 'entity', source_id: ent.id,
|
|
4307
|
+
full_text: row.text ?? '', entities: [ent.name], metadata: meta,
|
|
4308
|
+
entity: { name: ent.name, entityType: ent.entityType, observations } };
|
|
4309
|
+
}
|
|
4310
|
+
const rel = row.relationship_id
|
|
4311
|
+
? this.db.prepare(`
|
|
4312
|
+
SELECT r.id, r.relationType, r.confidence, e1.name AS source_name, e2.name AS target_name
|
|
4313
|
+
FROM relationships r JOIN entities e1 ON e1.id = r.source_entity JOIN entities e2 ON e2.id = r.target_entity
|
|
4314
|
+
WHERE r.id = ?`).get(row.relationship_id)
|
|
4315
|
+
: undefined;
|
|
4316
|
+
if (!rel)
|
|
4317
|
+
throw notFound();
|
|
4318
|
+
const ev = resolveEvidenceStatus(this.db, meta);
|
|
4319
|
+
return { chunk_id: row.chunk_id, document_id: null, document_title: null, source_kind: 'relationship', source_id: rel.id,
|
|
4320
|
+
full_text: row.text ?? '', entities: [rel.source_name, rel.target_name], metadata: meta,
|
|
4321
|
+
relationship: { id: rel.id, source: rel.source_name, target: rel.target_name, relationType: rel.relationType, confidence: rel.confidence },
|
|
4322
|
+
evidence_status: ev.status, evidence_refs: ev.refs,
|
|
4323
|
+
support: meta.kg_format === EVIDENCE_FORMAT && meta.support ? meta.support : null };
|
|
4324
|
+
}
|
|
4005
4325
|
async getKnowledgeGraphStats() {
|
|
4006
4326
|
if (!this.db)
|
|
4007
4327
|
throw new Error('Database not initialized');
|