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
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// kg-relation-evidence (specs/changes/kg-relation-evidence/proposal.md r3, D1·D2).
|
|
2
|
+
// Evidence-backed relationship chunks: everything that must be computed identically in four places —
|
|
3
|
+
// generation, the commit-time re-check, and both KG embedding write-backs (dedicated + backfill).
|
|
4
|
+
// Pure functions first; the resolvers further down take a better-sqlite3 handle and never open a
|
|
5
|
+
// transaction themselves: callers own the transaction (CODE_CONTEXT §2).
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
export const EVIDENCE_FORMAT = 'evidence-v1';
|
|
8
|
+
// Policy limits for this experiment, not a claim of sufficiency (proposal D1). Over-cap input is
|
|
9
|
+
// refused, never truncated, and an item is never dropped from S to fit.
|
|
10
|
+
export const EVIDENCE_CAPS = Object.freeze({ maxEntries: 8, maxQuoteCodepoints: 4000, maxBodyCodepoints: 8000 });
|
|
11
|
+
export const FINGERPRINT_TAG = 'kg-evidence-fp/1';
|
|
12
|
+
export const LEGACY_CMP_TAG = 'kg-legacy-cmp/1';
|
|
13
|
+
// "Evidence-backed" is a claim about references, not about truth (proposal D1, last bullet).
|
|
14
|
+
export const EVIDENCE_SEMANTICS = 'caller-selected evidence; the engine verified the references exist verbatim; not an independent proof of the relation';
|
|
15
|
+
// Codepoints, never UTF-16 units: '🎉'.length === 2 but cpLength('🎉') === 1. Caps and [start, end)
|
|
16
|
+
// are defined in codepoints (D1), like chunk start_pos/end_pos since migration 11.
|
|
17
|
+
export function cpLength(s) {
|
|
18
|
+
let n = 0;
|
|
19
|
+
for (const _ of s)
|
|
20
|
+
n++;
|
|
21
|
+
return n;
|
|
22
|
+
}
|
|
23
|
+
export function cpSlice(s, start, end) {
|
|
24
|
+
return Array.from(s).slice(start, end).join('');
|
|
25
|
+
}
|
|
26
|
+
// SHA-256 of the UTF-8 bytes, lowercase hex. No normalisation, no newline rewriting. Node encodes a
|
|
27
|
+
// lone surrogate as U+FFFD (EF BF BD), the same bytes TextEncoder produces.
|
|
28
|
+
export function sha256Utf8(s) {
|
|
29
|
+
return createHash('sha256').update(s, 'utf8').digest('hex');
|
|
30
|
+
}
|
|
31
|
+
// Fixed order: kind, then ref, then hash with null first; strings compared by code unit. The reader
|
|
32
|
+
// sorts with the same function, so "same sources" means the same sorted list.
|
|
33
|
+
export function compareSourceTuples(a, b) {
|
|
34
|
+
const c = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
|
|
35
|
+
if (a.source_kind !== b.source_kind)
|
|
36
|
+
return c(a.source_kind, b.source_kind);
|
|
37
|
+
if (a.source_ref !== b.source_ref)
|
|
38
|
+
return c(a.source_ref, b.source_ref);
|
|
39
|
+
// An absent hash is null (global constraints): normalise here so undefined vs 'a' is not a tie.
|
|
40
|
+
const ah = a.source_hash ?? null;
|
|
41
|
+
const bh = b.source_hash ?? null;
|
|
42
|
+
if (ah === bh)
|
|
43
|
+
return 0;
|
|
44
|
+
if (ah === null)
|
|
45
|
+
return -1;
|
|
46
|
+
if (bh === null)
|
|
47
|
+
return 1;
|
|
48
|
+
return c(ah, bh);
|
|
49
|
+
}
|
|
50
|
+
function int(v, what) {
|
|
51
|
+
if (typeof v !== 'number' || !Number.isInteger(v))
|
|
52
|
+
throw new Error(`fingerprint: ${what} must be an integer (got ${String(v)})`);
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
// The one serialisation (plan "Fingerprint serialisation"). Positional arrays only, so no key order can
|
|
56
|
+
// drift; evidence in INPUT order — the range is included because the same quote text can sit at two
|
|
57
|
+
// positions of one chunk ([0,13) vs [14,27), REVIEW-ALPHA2 §2.1).
|
|
58
|
+
export function fingerprintSerialization(f) {
|
|
59
|
+
const refs = f.refs.map((r) => r.kind === 'document'
|
|
60
|
+
? ['document', r.document_id, r.chunk_id, int(r.start, 'start'), int(r.end, 'end'), r.chunk_text_sha256, r.quote_sha256]
|
|
61
|
+
: ['observation', r.observation_id, r.entity_id, int(r.revision_no, 'revision_no'), r.content_sha256,
|
|
62
|
+
[...r.sources].sort(compareSourceTuples).map((s) => [s.source_kind, s.source_ref, s.source_hash ?? null])]);
|
|
63
|
+
return JSON.stringify([FINGERPRINT_TAG, f.relationship_id, f.source_entity_id, f.target_entity_id,
|
|
64
|
+
f.relation_type, f.source_name, f.target_name, f.kg_format, refs, int(f.profile_id, 'profile_id')]);
|
|
65
|
+
}
|
|
66
|
+
export function computeInputFingerprint(f) {
|
|
67
|
+
return sha256Utf8(fingerprintSerialization(f));
|
|
68
|
+
}
|
|
69
|
+
export function fingerprintFromMetadata(m, profileId) {
|
|
70
|
+
return computeInputFingerprint({
|
|
71
|
+
relationship_id: m.relationship_id, source_entity_id: m.source_entity_id, target_entity_id: m.target_entity_id,
|
|
72
|
+
relation_type: m.relation_type, source_name: m.source_entity, target_name: m.target_entity,
|
|
73
|
+
kg_format: EVIDENCE_FORMAT, refs: m.evidence_refs, profile_id: profileId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Claim line (raw type, direction kept) + each quote verbatim in input order (D1). No IDs, hashes or
|
|
77
|
+
// internal state in the embedded text.
|
|
78
|
+
export function buildEvidenceBody(sourceName, relationType, targetName, quotes) {
|
|
79
|
+
return [`${sourceName} — ${relationType} — ${targetName}`, ...quotes].join('\n\n');
|
|
80
|
+
}
|
|
81
|
+
// Same deterministic id as the triplet chunk (index.ts:2311): one relationship chunk per relationship.
|
|
82
|
+
export function evidenceChunkId(relationshipId) {
|
|
83
|
+
return `kg_relationship_${relationshipId}`;
|
|
84
|
+
}
|
|
85
|
+
export function parseEvidenceMetadata(raw) {
|
|
86
|
+
if (raw === null)
|
|
87
|
+
return null;
|
|
88
|
+
let m;
|
|
89
|
+
try {
|
|
90
|
+
m = JSON.parse(raw);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (!m || typeof m !== 'object' || m.kg_format !== EVIDENCE_FORMAT)
|
|
96
|
+
return null;
|
|
97
|
+
return m;
|
|
98
|
+
}
|
|
99
|
+
// D2⑤ comparison value. Evidence-v1: the stored fingerprint, but only if it still recomputes from the
|
|
100
|
+
// row's own metadata and belongs to this row (a hand-edited or foreign row is never "equal"). Legacy
|
|
101
|
+
// entity/triplet rows have no fingerprint field, so the value is derived at read time from the source
|
|
102
|
+
// identity + body hash. Anything missing returns null, and null is never equal (proposal D2⑤).
|
|
103
|
+
export function kgComparisonValue(row) {
|
|
104
|
+
if (row.text === null)
|
|
105
|
+
return null;
|
|
106
|
+
let meta;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(row.metadata ?? '{}');
|
|
109
|
+
if (!parsed || typeof parsed !== 'object')
|
|
110
|
+
return null;
|
|
111
|
+
meta = parsed;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (meta.kg_format !== undefined) {
|
|
117
|
+
if (meta.kg_format !== EVIDENCE_FORMAT || row.chunk_type !== 'relationship')
|
|
118
|
+
return null;
|
|
119
|
+
const m = meta;
|
|
120
|
+
if (typeof m.input_fingerprint !== 'string' || typeof m.fingerprint_profile_id !== 'number')
|
|
121
|
+
return null;
|
|
122
|
+
// A row without a source id is never equal (D2⑤), even if its metadata agrees with that null.
|
|
123
|
+
if (typeof row.relationship_id !== 'string' || row.relationship_id === '')
|
|
124
|
+
return null;
|
|
125
|
+
if (m.relationship_id !== row.relationship_id)
|
|
126
|
+
return null;
|
|
127
|
+
// Identifiers must be non-empty; names and the raw type are stored as-is and may legitimately be
|
|
128
|
+
// empty strings (the engine accepts them), so for those only a non-string is "missing".
|
|
129
|
+
for (const k of [m.source_entity_id, m.target_entity_id]) {
|
|
130
|
+
if (typeof k !== 'string' || k === '')
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
for (const k of [m.source_entity, m.target_entity, m.relation_type]) {
|
|
134
|
+
if (typeof k !== 'string')
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// Final review A2: the reader (resolveEvidenceStatus) and every dependency lookup (collectCiting) treat
|
|
138
|
+
// an empty or malformed evidence_refs as ineligible; a row that cannot be resolved must not be
|
|
139
|
+
// comparable either, even when its stored fingerprint recomputes from that metadata.
|
|
140
|
+
const refs = m.evidence_refs;
|
|
141
|
+
if (!Array.isArray(refs) || refs.length === 0 || !refs.every(isWellFormedRef))
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
if (fingerprintFromMetadata(m, m.fingerprint_profile_id) !== m.input_fingerprint)
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return `fp:${m.input_fingerprint}`;
|
|
151
|
+
}
|
|
152
|
+
const sourceId = row.chunk_type === 'entity' ? row.entity_id : row.chunk_type === 'relationship' ? row.relationship_id : null;
|
|
153
|
+
if (!sourceId)
|
|
154
|
+
return null;
|
|
155
|
+
return `legacy:${sha256Utf8(JSON.stringify([LEGACY_CMP_TAG, row.chunk_type, sourceId, sha256Utf8(row.text)]))}`;
|
|
156
|
+
}
|
|
157
|
+
// Taken immediately BEFORE inference; writeKgVectorIfCurrent (index.ts) re-checks all of it at write.
|
|
158
|
+
export function captureKgEmbedInput(row, profileId) {
|
|
159
|
+
return { rowid: row.rowid, chunk_id: row.chunk_id, text_sha256: sha256Utf8(row.text ?? ''), cmp: kgComparisonValue(row), profile_id: profileId };
|
|
160
|
+
}
|
|
161
|
+
// (resolvers are added by Tasks 5, 6 and 7 below this line)
|
|
162
|
+
// ---- D1 resolution (generation AND commit re-check run exactly this) ----
|
|
163
|
+
export class EvidenceInputError extends Error {
|
|
164
|
+
code;
|
|
165
|
+
itemIndex;
|
|
166
|
+
constructor(code, itemIndex, detail) {
|
|
167
|
+
super(`generateRelationEvidenceChunks: item ${itemIndex}: ${code}${detail ? ` (${detail})` : ''}`);
|
|
168
|
+
this.code = code;
|
|
169
|
+
this.itemIndex = itemIndex;
|
|
170
|
+
this.name = 'EvidenceInputError';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export class EvidenceCommitConflictError extends Error {
|
|
174
|
+
itemIndex;
|
|
175
|
+
reason;
|
|
176
|
+
constructor(itemIndex, reason) {
|
|
177
|
+
super(`generateRelationEvidenceChunks: item ${itemIndex} changed between preparation and commit: ${reason}`);
|
|
178
|
+
this.itemIndex = itemIndex;
|
|
179
|
+
this.reason = reason;
|
|
180
|
+
this.name = 'EvidenceCommitConflictError';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const WHY_STATUSES = ['reason-stated', 'relation-only', 'unjudged'];
|
|
184
|
+
// Every stored source of an observation, sorted (D1: the real rows, never a summary label).
|
|
185
|
+
export function readSourceTuples(db, observationId) {
|
|
186
|
+
const rows = db.prepare(`SELECT source_kind, source_ref, source_hash FROM observation_sources WHERE observation_id = ?`)
|
|
187
|
+
.all(observationId);
|
|
188
|
+
return rows.map(r => ({ source_kind: r.source_kind, source_ref: r.source_ref, source_hash: r.source_hash ?? null }))
|
|
189
|
+
.sort(compareSourceTuples);
|
|
190
|
+
}
|
|
191
|
+
function sourceSummary(sources) {
|
|
192
|
+
if (sources.length === 0)
|
|
193
|
+
return 'unrecorded';
|
|
194
|
+
return sources.every(s => s.source_kind === 'import') ? 'import-only' : 'recorded';
|
|
195
|
+
}
|
|
196
|
+
// Validate one item against the CURRENT rows and build its body + metadata. Any failure throws
|
|
197
|
+
// EvidenceInputError; nothing is written here. Order matters for the commit re-check
|
|
198
|
+
// (REVIEW-ALPHA3 §3.1): an observation's ACTIVE status and a document chunk's type/parent are checked
|
|
199
|
+
// before any hash or fingerprint, because a retract keeps id, revision and content and changes only
|
|
200
|
+
// the status (lifecycle.ts:146-147) — the fingerprint alone cannot see it.
|
|
201
|
+
export function resolveEvidenceItem(db, item, itemIndex, profileId) {
|
|
202
|
+
const fail = (code, detail = '') => { throw new EvidenceInputError(code, itemIndex, detail); };
|
|
203
|
+
if (!item || typeof item !== 'object')
|
|
204
|
+
fail('invalid-item');
|
|
205
|
+
if (typeof item.relationship_id !== 'string' || item.relationship_id === '')
|
|
206
|
+
fail('invalid-item', 'relationship_id');
|
|
207
|
+
if (!Array.isArray(item.evidence) || item.evidence.length === 0)
|
|
208
|
+
fail('empty-evidence');
|
|
209
|
+
if (item.evidence.length > EVIDENCE_CAPS.maxEntries)
|
|
210
|
+
fail('too-many-entries', `${item.evidence.length} > ${EVIDENCE_CAPS.maxEntries}`);
|
|
211
|
+
const rel = db.prepare(`
|
|
212
|
+
SELECT r.id, r.source_entity, r.target_entity, r.relationType, r.confidence,
|
|
213
|
+
e1.name AS source_name, e2.name AS target_name
|
|
214
|
+
FROM relationships r
|
|
215
|
+
JOIN entities e1 ON e1.id = r.source_entity
|
|
216
|
+
JOIN entities e2 ON e2.id = r.target_entity
|
|
217
|
+
WHERE r.id = ?`).get(item.relationship_id);
|
|
218
|
+
if (!rel)
|
|
219
|
+
return fail('unknown-relationship', item.relationship_id);
|
|
220
|
+
// kgComparisonValue (D2⑤) never accepts an empty endpoint id, so a chunk published with one could never
|
|
221
|
+
// be embedded by either CAS path. The schema has no non-empty CHECK and importGraph copies ids as given,
|
|
222
|
+
// so the shared preparation/commit resolver checks it before any write. Names and the raw type may
|
|
223
|
+
// legitimately be empty; only the ids are identifiers (REVIEW-T5-2_ I2).
|
|
224
|
+
if (typeof rel.source_entity !== 'string' || rel.source_entity === '' || typeof rel.target_entity !== 'string' || rel.target_entity === '') {
|
|
225
|
+
return fail('invalid-relationship-endpoint', item.relationship_id);
|
|
226
|
+
}
|
|
227
|
+
const refs = [];
|
|
228
|
+
const quotes = [];
|
|
229
|
+
// Array.from visits every index; forEach alone skips holes, so new Array(1) passed the 1–8 count with zero
|
|
230
|
+
// resolved entries and [ref, <hole>] dropped one silently (D1: at least one entry, none dropped;
|
|
231
|
+
// REVIEW-T5-2_ I1). This is a JS manager API, so no JSON layer turns holes into null first. A hole
|
|
232
|
+
// reads as undefined and fails as an invalid entry.
|
|
233
|
+
Array.from(item.evidence).forEach((e, j) => {
|
|
234
|
+
if (!e || typeof e !== 'object')
|
|
235
|
+
return fail('unknown-evidence-kind', `entry ${j}${j in item.evidence ? '' : ': hole'}`);
|
|
236
|
+
if (e.kind === 'document') {
|
|
237
|
+
if (typeof e.chunk_id !== 'string')
|
|
238
|
+
return fail('unknown-chunk', `entry ${j}`);
|
|
239
|
+
const c = db.prepare(`
|
|
240
|
+
SELECT m.chunk_id, m.chunk_type, m.document_id, m.text, d.id AS doc_id
|
|
241
|
+
FROM chunk_metadata m LEFT JOIN documents d ON d.id = m.document_id
|
|
242
|
+
WHERE m.chunk_id = ?`).get(e.chunk_id);
|
|
243
|
+
if (!c)
|
|
244
|
+
return fail('unknown-chunk', `entry ${j}: ${e.chunk_id}`);
|
|
245
|
+
if (c.chunk_type !== 'document' || c.document_id === null || c.doc_id === null || c.text === null) {
|
|
246
|
+
return fail('not-a-document-chunk', `entry ${j}: ${e.chunk_id}`);
|
|
247
|
+
}
|
|
248
|
+
if (typeof e.start !== 'number' || typeof e.end !== 'number' || !Number.isInteger(e.start) || !Number.isInteger(e.end)) {
|
|
249
|
+
return fail('range-not-integer', `entry ${j}`);
|
|
250
|
+
}
|
|
251
|
+
const n = cpLength(c.text);
|
|
252
|
+
if (!(0 <= e.start && e.start < e.end && e.end <= n))
|
|
253
|
+
return fail('range-out-of-bounds', `entry ${j}: [${e.start},${e.end}) of ${n} codepoints`);
|
|
254
|
+
if (e.end - e.start > EVIDENCE_CAPS.maxQuoteCodepoints)
|
|
255
|
+
return fail('quote-over-cap', `entry ${j}: ${e.end - e.start} codepoints`);
|
|
256
|
+
const quote = cpSlice(c.text, e.start, e.end);
|
|
257
|
+
if (sha256Utf8(quote) !== e.quote_sha256)
|
|
258
|
+
return fail('quote-hash-mismatch', `entry ${j}`);
|
|
259
|
+
refs.push({ kind: 'document', document_id: c.document_id, chunk_id: c.chunk_id, start: e.start, end: e.end,
|
|
260
|
+
chunk_text_sha256: sha256Utf8(c.text), quote_sha256: e.quote_sha256 });
|
|
261
|
+
quotes.push(quote);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (e.kind === 'observation') {
|
|
265
|
+
if (typeof e.observation_id !== 'string')
|
|
266
|
+
return fail('unknown-observation', `entry ${j}`);
|
|
267
|
+
const o = db.prepare(`SELECT observation_id, entity_id, revision_no, content, status FROM entity_observations WHERE observation_id = ?`)
|
|
268
|
+
.get(e.observation_id);
|
|
269
|
+
if (!o)
|
|
270
|
+
return fail('unknown-observation', `entry ${j}: ${e.observation_id}`);
|
|
271
|
+
if (o.status !== 'active')
|
|
272
|
+
return fail('observation-not-active', `entry ${j}: status ${o.status}`);
|
|
273
|
+
if (o.revision_no !== e.revision_no)
|
|
274
|
+
return fail('observation-revision-mismatch', `entry ${j}`);
|
|
275
|
+
if (sha256Utf8(o.content) !== e.content_sha256)
|
|
276
|
+
return fail('observation-content-mismatch', `entry ${j}`);
|
|
277
|
+
const len = cpLength(o.content); // the quoted unit is the WHOLE current content; never cut
|
|
278
|
+
if (len > EVIDENCE_CAPS.maxQuoteCodepoints)
|
|
279
|
+
return fail('quote-over-cap', `entry ${j}: ${len} codepoints`);
|
|
280
|
+
const sources = readSourceTuples(db, o.observation_id);
|
|
281
|
+
refs.push({ kind: 'observation', observation_id: o.observation_id, entity_id: o.entity_id, revision_no: o.revision_no,
|
|
282
|
+
content_sha256: e.content_sha256, status_at_generation: 'active', sources, source_summary: sourceSummary(sources) });
|
|
283
|
+
quotes.push(o.content);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
return fail('unknown-evidence-kind', `entry ${j}`);
|
|
287
|
+
});
|
|
288
|
+
const sup = item.support === undefined ? {} : item.support;
|
|
289
|
+
if (sup === null || typeof sup !== 'object')
|
|
290
|
+
fail('invalid-support');
|
|
291
|
+
const why = sup.why_status ?? 'unjudged';
|
|
292
|
+
if (!WHY_STATUSES.includes(why))
|
|
293
|
+
fail('invalid-support', `why_status ${String(why)}`);
|
|
294
|
+
if (sup.valid_time !== undefined && typeof sup.valid_time !== 'string')
|
|
295
|
+
fail('invalid-support', 'valid_time');
|
|
296
|
+
if (sup.scope_note !== undefined && typeof sup.scope_note !== 'string')
|
|
297
|
+
fail('invalid-support', 'scope_note');
|
|
298
|
+
const text = buildEvidenceBody(rel.source_name, rel.relationType, rel.target_name, quotes);
|
|
299
|
+
const bodyCp = cpLength(text);
|
|
300
|
+
if (bodyCp > EVIDENCE_CAPS.maxBodyCodepoints)
|
|
301
|
+
fail('body-over-cap', `${bodyCp} codepoints`);
|
|
302
|
+
const fp = computeInputFingerprint({
|
|
303
|
+
relationship_id: rel.id, source_entity_id: rel.source_entity, target_entity_id: rel.target_entity,
|
|
304
|
+
relation_type: rel.relationType, source_name: rel.source_name, target_name: rel.target_name,
|
|
305
|
+
kg_format: EVIDENCE_FORMAT, refs, profile_id: profileId,
|
|
306
|
+
});
|
|
307
|
+
const metadata = {
|
|
308
|
+
kg_format: EVIDENCE_FORMAT,
|
|
309
|
+
relationship_id: rel.id, source_entity_id: rel.source_entity, target_entity_id: rel.target_entity,
|
|
310
|
+
source_entity: rel.source_name, target_entity: rel.target_name, relation_type: rel.relationType, direction: 'source->target',
|
|
311
|
+
confidence: rel.confidence,
|
|
312
|
+
evidence_refs: refs,
|
|
313
|
+
support: { why_status: why, valid_time: sup.valid_time ?? null, scope_note: sup.scope_note ?? null },
|
|
314
|
+
generated_ref_status: 'refs-resolved',
|
|
315
|
+
input_fingerprint: fp, fingerprint_profile_id: profileId,
|
|
316
|
+
body_codepoints: bodyCp,
|
|
317
|
+
embedded_truncation: 'unchecked',
|
|
318
|
+
evidence_semantics: EVIDENCE_SEMANTICS,
|
|
319
|
+
};
|
|
320
|
+
return { chunk_id: evidenceChunkId(rel.id), relationship_id: rel.id, text, body_codepoints: bodyCp, metadata };
|
|
321
|
+
}
|
|
322
|
+
// legacy triplet -> claim_only, no refs. evidence-v1 -> each reference resolved against the CURRENT
|
|
323
|
+
// rows; all available -> refs_available, anything else (missing, mismatch, malformed or empty list) ->
|
|
324
|
+
// refs_unavailable. An inactive or historical revision is 'mismatch', never 'available'.
|
|
325
|
+
export function resolveEvidenceStatus(db, meta) {
|
|
326
|
+
if (!meta || meta.kg_format === undefined)
|
|
327
|
+
return { status: 'claim_only', refs: [] };
|
|
328
|
+
const list = meta.kg_format === EVIDENCE_FORMAT && Array.isArray(meta.evidence_refs) ? meta.evidence_refs : null;
|
|
329
|
+
if (!list || list.length === 0)
|
|
330
|
+
return { status: 'refs_unavailable', refs: [] };
|
|
331
|
+
const refs = list.map((r, i) => resolveOneRef(db, r, i));
|
|
332
|
+
return { status: refs.every(r => r.status === 'available') ? 'refs_available' : 'refs_unavailable', refs };
|
|
333
|
+
}
|
|
334
|
+
// R4 (REVIEW-PLAN): structure is checked at runtime before any field is used — `as SourceTuple[]` is not
|
|
335
|
+
// a check, and `sources: [null]` used to reach `s.source_kind`. A structurally malformed reference is
|
|
336
|
+
// 'mismatch' (so the chunk is refs_unavailable), never a TypeError. Real DB errors are NOT caught here.
|
|
337
|
+
const isStr = (v) => typeof v === 'string';
|
|
338
|
+
const isInt = (v) => typeof v === 'number' && Number.isInteger(v);
|
|
339
|
+
export function isSourceTuple(v) {
|
|
340
|
+
if (!v || typeof v !== 'object')
|
|
341
|
+
return false;
|
|
342
|
+
const s = v;
|
|
343
|
+
return isStr(s.source_kind) && isStr(s.source_ref) && (s.source_hash === null || isStr(s.source_hash));
|
|
344
|
+
}
|
|
345
|
+
export function isWellFormedRef(v) {
|
|
346
|
+
if (!v || typeof v !== 'object')
|
|
347
|
+
return false;
|
|
348
|
+
const r = v;
|
|
349
|
+
if (r.kind === 'document') {
|
|
350
|
+
return isStr(r.document_id) && isStr(r.chunk_id) && isInt(r.start) && isInt(r.end) && isStr(r.chunk_text_sha256) && isStr(r.quote_sha256);
|
|
351
|
+
}
|
|
352
|
+
if (r.kind === 'observation') {
|
|
353
|
+
return isStr(r.observation_id) && isStr(r.entity_id) && isInt(r.revision_no) && isStr(r.content_sha256)
|
|
354
|
+
&& Array.isArray(r.sources) && r.sources.every(isSourceTuple);
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
function resolveOneRef(db, raw, index) {
|
|
359
|
+
const r = raw && typeof raw === 'object' ? raw : {};
|
|
360
|
+
const out = (status, text = null) => ({ index, kind: typeof r.kind === 'string' ? r.kind : 'unknown', status, ref: raw, text });
|
|
361
|
+
if (!isWellFormedRef(raw))
|
|
362
|
+
return out('mismatch');
|
|
363
|
+
if (r.kind === 'document') {
|
|
364
|
+
const c = db.prepare(`
|
|
365
|
+
SELECT m.chunk_type, m.document_id, m.text, d.id AS doc_id
|
|
366
|
+
FROM chunk_metadata m LEFT JOIN documents d ON d.id = m.document_id WHERE m.chunk_id = ?`)
|
|
367
|
+
.get(r.chunk_id);
|
|
368
|
+
if (!c || c.doc_id === null)
|
|
369
|
+
return out('missing');
|
|
370
|
+
if (c.chunk_type !== 'document' || c.document_id !== r.document_id || c.text === null)
|
|
371
|
+
return out('mismatch');
|
|
372
|
+
if (sha256Utf8(c.text) !== r.chunk_text_sha256)
|
|
373
|
+
return out('mismatch');
|
|
374
|
+
if (typeof r.start !== 'number' || typeof r.end !== 'number' || !Number.isInteger(r.start) || !Number.isInteger(r.end))
|
|
375
|
+
return out('mismatch');
|
|
376
|
+
if (!(0 <= r.start && r.start < r.end && r.end <= cpLength(c.text)))
|
|
377
|
+
return out('mismatch');
|
|
378
|
+
const quote = cpSlice(c.text, r.start, r.end);
|
|
379
|
+
if (sha256Utf8(quote) !== r.quote_sha256)
|
|
380
|
+
return out('mismatch');
|
|
381
|
+
return out('available', quote);
|
|
382
|
+
}
|
|
383
|
+
if (r.kind === 'observation') {
|
|
384
|
+
const o = db.prepare(`SELECT entity_id, revision_no, content, status FROM entity_observations WHERE observation_id = ?`)
|
|
385
|
+
.get(r.observation_id);
|
|
386
|
+
if (!o)
|
|
387
|
+
return out('missing');
|
|
388
|
+
if (o.status !== 'active' || o.entity_id !== r.entity_id || o.revision_no !== r.revision_no || sha256Utf8(o.content) !== r.content_sha256) {
|
|
389
|
+
return out('mismatch');
|
|
390
|
+
}
|
|
391
|
+
const now = readSourceTuples(db, r.observation_id).map(s => [s.source_kind, s.source_ref, s.source_hash]);
|
|
392
|
+
const stored = [...r.sources].sort(compareSourceTuples).map(s => [s.source_kind, s.source_ref, s.source_hash]);
|
|
393
|
+
if (JSON.stringify(now) !== JSON.stringify(stored))
|
|
394
|
+
return out('mismatch');
|
|
395
|
+
return out('available', o.content);
|
|
396
|
+
}
|
|
397
|
+
return out('mismatch');
|
|
398
|
+
}
|
|
399
|
+
function evidenceRowViews(db) {
|
|
400
|
+
const rows = db.prepare(`SELECT rowid, metadata FROM chunk_metadata WHERE chunk_type = 'relationship' AND metadata LIKE '%evidence-v1%'`).all();
|
|
401
|
+
const out = [];
|
|
402
|
+
for (const r of rows) {
|
|
403
|
+
let m;
|
|
404
|
+
try {
|
|
405
|
+
m = JSON.parse(r.metadata ?? 'null');
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
out.push({ rowid: r.rowid, refs: null });
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (!m || typeof m !== 'object') {
|
|
412
|
+
out.push({ rowid: r.rowid, refs: null });
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const meta = m;
|
|
416
|
+
if (meta.kg_format !== EVIDENCE_FORMAT)
|
|
417
|
+
continue; // a legacy row that merely mentions the string
|
|
418
|
+
out.push({ rowid: r.rowid, refs: Array.isArray(meta.evidence_refs) ? meta.evidence_refs : null });
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
function collectCiting(db, hit) {
|
|
423
|
+
return evidenceRowViews(db)
|
|
424
|
+
.filter(v => v.refs === null || v.refs.length === 0 || v.refs.some(r => !isWellFormedRef(r) || hit(r)))
|
|
425
|
+
.map(v => v.rowid);
|
|
426
|
+
}
|
|
427
|
+
export function collectEvidenceRowidsCitingObservations(db, observationIds) {
|
|
428
|
+
if (observationIds.length === 0)
|
|
429
|
+
return [];
|
|
430
|
+
const ids = new Set(observationIds);
|
|
431
|
+
return collectCiting(db, r => r.kind === 'observation' && typeof r.observation_id === 'string' && ids.has(r.observation_id));
|
|
432
|
+
}
|
|
433
|
+
export function collectEvidenceRowidsCitingDocument(db, documentId) {
|
|
434
|
+
return collectCiting(db, r => r.kind === 'document' && r.document_id === documentId);
|
|
435
|
+
}
|
|
436
|
+
export function collectEvidenceRowidsCitingOwnerEntity(db, entityId) {
|
|
437
|
+
return collectCiting(db, r => r.kind === 'observation' && r.entity_id === entityId);
|
|
438
|
+
}
|
|
@@ -2,3 +2,5 @@ import type Database from 'better-sqlite3';
|
|
|
2
2
|
export declare function rebuildProjection(db: Database.Database, entityId: string): void;
|
|
3
3
|
export declare function deleteStaleKgChunks(db: Database.Database, entityId: string): number;
|
|
4
4
|
export declare function deleteKgRelationshipChunks(db: Database.Database, relationshipIds: string[]): number;
|
|
5
|
+
export declare function deleteKgChunkRows(db: Database.Database, rowids: number[]): number;
|
|
6
|
+
export declare function refreshKgRelationshipConfidence(db: Database.Database, relationshipId: string): number;
|
|
@@ -46,3 +46,52 @@ export function deleteKgRelationshipChunks(db, relationshipIds) {
|
|
|
46
46
|
}
|
|
47
47
|
return n;
|
|
48
48
|
}
|
|
49
|
+
// kg-relation-evidence D2: the one place that removes KG chunk rows by rowid. A rowid that is not a
|
|
50
|
+
// KG row (or no longer exists) is skipped BEFORE anything is deleted, so a rowid reused by a document
|
|
51
|
+
// chunk never loses its vector. Order: vector (vec0 needs a literal integer rowid), entity links,
|
|
52
|
+
// metadata (the chunks_fts delete trigger fires on the metadata row). Callers already inside a
|
|
53
|
+
// transaction pass rowids they collected before mutating the source.
|
|
54
|
+
export function deleteKgChunkRows(db, rowids) {
|
|
55
|
+
let n = 0;
|
|
56
|
+
for (const raw of new Set(rowids)) {
|
|
57
|
+
const rowid = Number(raw);
|
|
58
|
+
if (!Number.isInteger(rowid) || rowid < 0)
|
|
59
|
+
throw new Error(`Invalid rowid: ${raw}`);
|
|
60
|
+
const row = db.prepare(`SELECT chunk_type FROM chunk_metadata WHERE rowid = ?`).get(rowid);
|
|
61
|
+
if (!row || (row.chunk_type !== 'entity' && row.chunk_type !== 'relationship'))
|
|
62
|
+
continue;
|
|
63
|
+
db.exec(`DELETE FROM chunks WHERE rowid = ${rowid}`);
|
|
64
|
+
db.prepare(`DELETE FROM chunk_entities WHERE chunk_rowid = ?`).run(rowid);
|
|
65
|
+
n += db.prepare(`DELETE FROM chunk_metadata WHERE rowid = ?`).run(rowid).changes;
|
|
66
|
+
}
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
69
|
+
// kg-relation-evidence D2②: relationship KG chunks (triplet and evidence-v1) carry a DISPLAY copy of
|
|
70
|
+
// `confidence` (index.ts:2319-2324). It is not a search input, so refresh it in place: the body, the
|
|
71
|
+
// input_hash and the vector stay, and kg_format / evidence_refs / input_fingerprint are never touched.
|
|
72
|
+
// The narrowed FTS trigger (migration 12) fires only on text/chunk_id, so this does not reindex.
|
|
73
|
+
export function refreshKgRelationshipConfidence(db, relationshipId) {
|
|
74
|
+
const rel = db.prepare(`SELECT confidence FROM relationships WHERE id = ?`).get(relationshipId);
|
|
75
|
+
if (!rel)
|
|
76
|
+
return 0;
|
|
77
|
+
const rows = db.prepare(`SELECT rowid, metadata FROM chunk_metadata WHERE chunk_type = 'relationship' AND relationship_id = ?`).all(relationshipId);
|
|
78
|
+
let n = 0;
|
|
79
|
+
for (const r of rows) {
|
|
80
|
+
let meta;
|
|
81
|
+
try {
|
|
82
|
+
meta = JSON.parse(r.metadata ?? '{}');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
} // malformed: the reader reports it
|
|
87
|
+
if (!meta || typeof meta !== 'object')
|
|
88
|
+
continue;
|
|
89
|
+
const m = meta;
|
|
90
|
+
if (m.confidence === rel.confidence)
|
|
91
|
+
continue;
|
|
92
|
+
m.confidence = rel.confidence;
|
|
93
|
+
db.prepare(`UPDATE chunk_metadata SET metadata = ? WHERE rowid = ?`).run(JSON.stringify(m), r.rowid);
|
|
94
|
+
n++;
|
|
95
|
+
}
|
|
96
|
+
return n;
|
|
97
|
+
}
|
|
@@ -287,7 +287,7 @@ const hybridSearchCapability = {
|
|
|
287
287
|
},
|
|
288
288
|
limit: {
|
|
289
289
|
type: 'number',
|
|
290
|
-
description: 'Maximum number of results to return',
|
|
290
|
+
description: 'Maximum number of results to return AND the retrieval depth (each query variant requests up to limit x 3 vector and up to limit x 3 FTS candidates, then merged), so hybridSearch(q, 30) cut to 10 can differ from hybridSearch(q, 10)',
|
|
291
291
|
default: 5
|
|
292
292
|
},
|
|
293
293
|
useGraph: {
|
|
@@ -312,6 +312,8 @@ chunk out of the top-5 in ~40% of known-item queries. For relationship explorati
|
|
|
312
312
|
- (!important!) **Graph re-ranking is opt-in (default false since v5.3.0)** — it re-orders candidates by matched/connected entity links, which is measured to hurt known-item retrieval; enable it only for backward compatibility or controlled evaluation
|
|
313
313
|
- (!important!) Graph re-ranking does not generate candidates — for "what is connected to X" use openNodes -> getNeighbors
|
|
314
314
|
- (!important!) relevance_score is a RANK score on the default call: 1/(60 + vector rank) + 1/(60 + FTS rank), at most 2/61; it is not a similarity or probability and is only comparable within one call (vector_similarity and fts_boost are reported alongside)
|
|
315
|
+
- (!important!) limit is both the maximum result count and the retrieval depth (each query variant requests up to limit x 3 vector candidates and up to limit x 3 FTS candidates, which are then merged), so results, even the top-1, can differ between limits; for comparisons fix the call limit and cut k from the returned list
|
|
316
|
+
- (!important!) full_context_available means getDetailedContext can resolve the stored chunk body; it is not a verified-evidence signal (for knowledge-graph relationship chunks read evidence_status there)
|
|
315
317
|
- (!important!) **Best results when knowledge graph is well-populated** with entities and relationships
|
|
316
318
|
</importantNotes>
|
|
317
319
|
|
|
@@ -345,7 +347,7 @@ chunk out of the top-5 in ~40% of known-item queries. For relationship explorati
|
|
|
345
347
|
|
|
346
348
|
<parameters>
|
|
347
349
|
- query: Search query — a natural-language question or an exact term/identifier (string, required)
|
|
348
|
-
- limit: Maximum results to return, default 5 (number, optional)
|
|
350
|
+
- limit: Maximum results to return AND retrieval depth (up to limit x 3 vector and limit x 3 FTS candidates per query variant), default 5 (number, optional). For comparisons fix the call limit and cut k from the returned list
|
|
349
351
|
- useGraph: Enable knowledge graph re-ranking, default false / opt-in (boolean, optional)
|
|
350
352
|
</parameters>
|
|
351
353
|
|
|
@@ -356,7 +358,7 @@ chunk out of the top-5 in ~40% of known-item queries. For relationship explorati
|
|
|
356
358
|
</examples>`;
|
|
357
359
|
const hybridSearchSchema = {
|
|
358
360
|
query: z.string().describe('The search query to find relevant information'),
|
|
359
|
-
limit: z.number().optional().default(5).describe('Maximum number of results to return'),
|
|
361
|
+
limit: z.number().optional().default(5).describe('Maximum number of results to return; also the retrieval depth (up to limit x 3 vector and limit x 3 FTS candidates per query variant)'),
|
|
360
362
|
useGraph: z.boolean().optional().default(false).describe('Opt-in legacy/experimental graph re-ranker (default false since v5.3.0; measured to hurt known-item retrieval; for exploration use openNodes -> getNeighbors)'),
|
|
361
363
|
};
|
|
362
364
|
export const hybridSearchTool = {
|
|
@@ -461,6 +463,8 @@ Perfect drill-down tool for exploring the full content behind search result high
|
|
|
461
463
|
- (!important!) **Returns complete chunk text** - not just summaries or highlights
|
|
462
464
|
- (!important!) **Includes surrounding chunks** - for better context understanding
|
|
463
465
|
- (!important!) **Shows entity associations** - to understand knowledge graph connections
|
|
466
|
+
- (!important!) **Knowledge-graph chunks** (chunk_type entity/relationship) return document_id: null and document_title: null with source_kind and source_id; an entity chunk returns the entity and its active observations, a relationship chunk returns the relationship, evidence_status (claim_only | refs_available | refs_unavailable) and per-reference evidence_refs (available | missing | mismatch)
|
|
467
|
+
- (!important!) evidence_status is reference resolution, not a truth verdict: refs_available means every cited quote or observation still matches verbatim (current chunk range and hashes, or the current active observation revision); whoever generated the chunk selected the evidence
|
|
464
468
|
</importantNotes>
|
|
465
469
|
|
|
466
470
|
<whenToUseThisTool>
|
package/docs/UPDATING.md
CHANGED
|
@@ -108,6 +108,23 @@ path and holder pid (e.g. `.download-<key>.lock`). Verify the holder process
|
|
|
108
108
|
is genuinely gone or hung (`ps -p <pid>`), then remove the lock file manually;
|
|
109
109
|
the next start becomes a clean download owner.
|
|
110
110
|
|
|
111
|
+
## v6.4.2 (schema v14, unchanged): CPU memory arena off, transformers 4.3.2
|
|
112
|
+
|
|
113
|
+
**What changes for an existing database.** Nothing is migrated and nothing is re-embedded (the embedding profile —
|
|
114
|
+
model, revision, dtype, dimensions, pooling, normalize — is unchanged). The model cache is reused as is. What changes
|
|
115
|
+
is the engine process: after a long embedding it returns to about 2-3 GB instead of holding about 8 GB. An engine that
|
|
116
|
+
is already running keeps the old behaviour until it is restarted on 6.4.2. Details: README changelog.
|
|
117
|
+
|
|
118
|
+
## v6.4.1 (schema v14, unchanged): evidence-backed relationship chunks (opt-in), KG lifecycle fixes, faster oversize split
|
|
119
|
+
|
|
120
|
+
**What changes for an existing database.** Nothing is migrated and search defaults are unchanged (the 113-query
|
|
121
|
+
parity run on a frozen real project database returned the same ids, scores and field types as 6.4.0). Documents
|
|
122
|
+
chunked from now on are stamped `fallback=cp-window-N`; documents stamped `cp-exact-N` by 6.4.0 stay current format
|
|
123
|
+
and an unchanged-content sync does not re-chunk them. KG chunks for a deleted relationship or a changed cited source
|
|
124
|
+
now leave search in the same transaction. **`importGraph` now refuses non-string document ids and
|
|
125
|
+
observation-source ids** (the whole import, nothing written) — a dump produced by `exportGraph` uses string ids.
|
|
126
|
+
Details: README changelog.
|
|
127
|
+
|
|
111
128
|
## v6.4.0 (schema v14, unchanged): `hybridSearch` default = rank fusion, summaries opt-in
|
|
112
129
|
|
|
113
130
|
(Published first as `6.4.0-rc.1` on the `next` dist-tag; promoted to `latest` after the published artifact passed a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rag-memory-epf-mcp",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.2",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24"
|
|
6
6
|
},
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"prepare": "npm run build",
|
|
46
46
|
"watch": "tsc --watch",
|
|
47
47
|
"verify:invariants": "node test/chunk-invariants.test.mjs",
|
|
48
|
-
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/search-graph-default.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/alias-link-gate.test.mjs && node test/entity-name-boundary.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/search-fusion-rrf.test.mjs && node test/query-input.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs && node test/delete-entities-cascade.test.mjs && node test/delete-entities-kg-hygiene.test.mjs && node test/backup-publish-portable.test.mjs && node test/graph-context-explain.test.mjs && node test/eval-graph-role-libs.test.mjs && node test/eval-graph-role-t5b.test.mjs && node --test test/eval-graph-role-t8-fix.test.mjs && node --test test/eval-graph-role-t7-upstream.test.mjs && node --test test/eval-graph-role-t11-decision.test.mjs && node --test test/eval-graph-role-prereq-fix.test.mjs && node test/db-path.test.mjs && node test/mmap-option.test.mjs && node test/wal-exit.test.mjs && node test/wal-sidecar-guard.test.mjs",
|
|
48
|
+
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/search-graph-default.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node --no-concurrent-sparkplug test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/alias-link-gate.test.mjs && node test/entity-name-boundary.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/search-fusion-rrf.test.mjs && node test/query-input.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs && node test/delete-entities-cascade.test.mjs && node test/delete-entities-kg-hygiene.test.mjs && node test/backup-publish-portable.test.mjs && node test/graph-context-explain.test.mjs && node test/eval-graph-role-libs.test.mjs && node test/eval-graph-role-t5b.test.mjs && node --test test/eval-graph-role-t8-fix.test.mjs && node --test test/eval-graph-role-t7-upstream.test.mjs && node --test test/eval-graph-role-t11-decision.test.mjs && node --test test/eval-graph-role-prereq-fix.test.mjs && node test/db-path.test.mjs && node test/mmap-option.test.mjs && node test/wal-exit.test.mjs && node test/wal-sidecar-guard.test.mjs && node test/kg-evidence-core.test.mjs && node test/kg-delete-relation.test.mjs && node test/kg-confidence-copy.test.mjs && node test/kg-embed-cas.test.mjs && node test/kg-relation-evidence.test.mjs && node test/kg-detail-context.test.mjs && node test/kg-evidence-invalidation.test.mjs && node test/kg-evidence-cas.test.mjs && node test/kg-defaults.test.mjs && node test/chunker-oversize-cost.test.mjs && node test/chunker-structure-parity.test.mjs && node test/search-limit-depth.test.mjs && node test/kg-import-id-contract.test.mjs && node test/kg-evidence-guards.test.mjs && node test/model-session-options.test.mjs",
|
|
49
49
|
"test": "npm run build && npm run verify:invariants && npm run verify:engine",
|
|
50
50
|
"prepublishOnly": "npm run build && npm run verify:invariants && npm run verify:engine"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@huggingface/transformers": "^3.
|
|
53
|
+
"@huggingface/transformers": "^4.3.2",
|
|
54
54
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
55
55
|
"better-sqlite3": "^12.8.0",
|
|
56
56
|
"graphology": "^0.26.0",
|