rag-memory-epf-mcp 3.3.2 → 3.3.3

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/dist/index.js CHANGED
@@ -1136,11 +1136,18 @@ class RAGKnowledgeGraphManager {
1136
1136
  // Slicing token arrays at arbitrary boundaries can leave incomplete UTF-8
1137
1137
  // prefix/suffix bytes, which TextDecoder replaces with U+FFFD (�). Trim the
1138
1138
  // incomplete sequences at chunk boundaries; overlap covers the removed bytes.
1139
+ //
1140
+ // Each chunk records both token-space offsets (start_token/end_token from the
1141
+ // BPE encoder loop) and char-space offsets (start_pos/end_pos into the original
1142
+ // text). Char offsets are recovered with indexOf using a running cursor at the
1143
+ // previous chunk's start, so substr(text, start_pos, end_pos - start_pos)
1144
+ // equals chunk.text. On a coincidental indexOf miss the char offsets are NULL.
1139
1145
  chunkText(text, maxTokens = 800, overlap = 160) {
1140
1146
  if (!this.encoding)
1141
1147
  throw new Error('Tokenizer not initialized');
1142
1148
  const tokens = this.encoding.encode(text);
1143
1149
  const chunks = [];
1150
+ let charCursor = 0;
1144
1151
  for (let i = 0; i < tokens.length; i += maxTokens - overlap) {
1145
1152
  const chunkTokens = tokens.slice(i, i + maxTokens);
1146
1153
  const decodedBytes = this.encoding.decode(chunkTokens);
@@ -1148,13 +1155,40 @@ class RAGKnowledgeGraphManager {
1148
1155
  const isLast = i + chunkTokens.length >= tokens.length;
1149
1156
  const safeBytes = trimIncompleteUtf8(decodedBytes, !isFirst, !isLast);
1150
1157
  const chunkText = new TextDecoder('utf-8').decode(safeBytes);
1158
+ // Recover char-space offsets. First chunk is anchored at 0 because
1159
+ // trimIncompleteUtf8 leaves the leading bytes intact when isFirst is true.
1160
+ let startPos;
1161
+ let endPos;
1162
+ if (isFirst) {
1163
+ startPos = 0;
1164
+ endPos = chunkText.length;
1165
+ charCursor = 0;
1166
+ }
1167
+ else if (chunkText.length === 0) {
1168
+ startPos = null;
1169
+ endPos = null;
1170
+ }
1171
+ else {
1172
+ const idx = text.indexOf(chunkText, charCursor);
1173
+ if (idx >= 0) {
1174
+ startPos = idx;
1175
+ endPos = idx + chunkText.length;
1176
+ charCursor = idx;
1177
+ }
1178
+ else {
1179
+ startPos = null;
1180
+ endPos = null;
1181
+ }
1182
+ }
1151
1183
  chunks.push({
1152
1184
  id: '',
1153
1185
  document_id: '',
1154
1186
  chunk_index: chunks.length,
1155
1187
  text: chunkText,
1156
- start_pos: i,
1157
- end_pos: i + chunkTokens.length
1188
+ start_pos: startPos,
1189
+ end_pos: endPos,
1190
+ start_token: i,
1191
+ end_token: i + chunkTokens.length
1158
1192
  });
1159
1193
  }
1160
1194
  return chunks;
@@ -1230,14 +1264,16 @@ class RAGKnowledgeGraphManager {
1230
1264
  // Store chunk metadata (no embedding yet)
1231
1265
  this.db.prepare(`
1232
1266
  INSERT INTO chunk_metadata (
1233
- chunk_id, document_id, chunk_index, text, start_pos, end_pos
1234
- ) VALUES (?, ?, ?, ?, ?, ?)
1235
- `).run(chunkId, documentId, chunk.chunk_index, chunk.text, chunk.start_pos, chunk.end_pos);
1267
+ chunk_id, document_id, chunk_index, text, start_pos, end_pos, start_token, end_token
1268
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1269
+ `).run(chunkId, documentId, chunk.chunk_index, chunk.text, chunk.start_pos, chunk.end_pos, chunk.start_token, chunk.end_token);
1236
1270
  resultChunks.push({
1237
1271
  id: chunkId,
1238
1272
  text: chunk.text,
1239
1273
  startPos: chunk.start_pos,
1240
- endPos: chunk.end_pos
1274
+ endPos: chunk.end_pos,
1275
+ startToken: chunk.start_token,
1276
+ endToken: chunk.end_token
1241
1277
  });
1242
1278
  }
1243
1279
  console.error(`✅ Document chunked: ${chunks.length} chunks created`);
@@ -1713,6 +1749,8 @@ class RAGKnowledgeGraphManager {
1713
1749
  m.text,
1714
1750
  m.start_pos,
1715
1751
  m.end_pos,
1752
+ m.start_token,
1753
+ m.end_token,
1716
1754
  COALESCE(m.metadata, '{}') as chunk_metadata,
1717
1755
  c.distance,
1718
1756
  COALESCE(d.metadata, '{}') as doc_metadata
@@ -1793,6 +1831,8 @@ class RAGKnowledgeGraphManager {
1793
1831
  cm.text,
1794
1832
  cm.start_pos,
1795
1833
  cm.end_pos,
1834
+ cm.start_token,
1835
+ cm.end_token,
1796
1836
  COALESCE(cm.metadata, '{}') as chunk_metadata,
1797
1837
  COALESCE(d.metadata, '{}') as doc_metadata
1798
1838
  FROM chunk_metadata cm
@@ -427,5 +427,83 @@ export const migrations = [
427
427
  db.exec(`DROP TABLE IF EXISTS entities_fts`);
428
428
  db.exec(`DROP TABLE IF EXISTS chunks_fts`);
429
429
  }
430
+ },
431
+ // Migration 9: Separate token-space vs char-space chunk offsets.
432
+ // Before this migration, chunk_metadata.start_pos/end_pos held *token* indices
433
+ // for document chunks (a leftover from the BPE tokenizer-based chunkText loop)
434
+ // but already held character lengths (0..text.length) for entity/relationship
435
+ // chunks. Same column, two meanings — and a column name (`*_pos`) that implies
436
+ // char offsets in `documents.content`. This migration adds explicit
437
+ // start_token/end_token columns and reinterprets start_pos/end_pos as character
438
+ // offsets going forward. Existing document chunks: token data is moved to the
439
+ // new columns and char offsets are recomputed from documents.content via
440
+ // indexOf with a running cursor (NULL on miss — caller can re-chunk to fill).
441
+ // Existing entity/relationship chunks: leave start_pos/end_pos as-is
442
+ // (already a valid 0..text.length char range against the chunk text itself);
443
+ // token columns stay NULL since these chunks have no token-space concept.
444
+ {
445
+ version: 9,
446
+ description: 'Add start_token/end_token; reinterpret start_pos/end_pos as char offsets',
447
+ up: (db) => {
448
+ // 1) Add columns
449
+ db.exec(`ALTER TABLE chunk_metadata ADD COLUMN start_token INTEGER`);
450
+ db.exec(`ALTER TABLE chunk_metadata ADD COLUMN end_token INTEGER`);
451
+ // 2) Move token data into new columns for document chunks
452
+ db.exec(`
453
+ UPDATE chunk_metadata
454
+ SET start_token = start_pos,
455
+ end_token = end_pos,
456
+ start_pos = NULL,
457
+ end_pos = NULL
458
+ WHERE chunk_type = 'document'
459
+ `);
460
+ // 3) Recompute char offsets via indexOf with a running cursor per document
461
+ const docRows = db.prepare(`
462
+ SELECT DISTINCT document_id FROM chunk_metadata
463
+ WHERE chunk_type = 'document' AND document_id IS NOT NULL
464
+ `).all();
465
+ const docContentStmt = db.prepare(`SELECT content FROM documents WHERE id = ?`);
466
+ const chunksStmt = db.prepare(`
467
+ SELECT rowid, text FROM chunk_metadata
468
+ WHERE document_id = ? AND chunk_type = 'document'
469
+ ORDER BY chunk_index ASC
470
+ `);
471
+ const updateStmt = db.prepare(`
472
+ UPDATE chunk_metadata SET start_pos = ?, end_pos = ? WHERE rowid = ?
473
+ `);
474
+ for (const { document_id } of docRows) {
475
+ const doc = docContentStmt.get(document_id);
476
+ if (!doc)
477
+ continue;
478
+ const content = doc.content;
479
+ const chunks = chunksStmt.all(document_id);
480
+ // Cursor advances by the previous chunk's *start*, not its end, so we can
481
+ // still locate overlapping chunks. Token-space stride guarantees each
482
+ // chunk's start is strictly forward of the previous chunk's start.
483
+ let cursor = 0;
484
+ for (const c of chunks) {
485
+ if (!c.text)
486
+ continue;
487
+ const idx = content.indexOf(c.text, cursor);
488
+ if (idx >= 0) {
489
+ updateStmt.run(idx, idx + c.text.length, c.rowid);
490
+ cursor = idx;
491
+ }
492
+ // miss: leave NULL — caller can re-chunk to repair
493
+ }
494
+ }
495
+ },
496
+ down: (db) => {
497
+ // SQLite cannot DROP COLUMN cleanly. Best-effort: copy token data back into
498
+ // start_pos/end_pos for document chunks so a downgrade leaves the legacy
499
+ // token-space semantics in place.
500
+ db.exec(`
501
+ UPDATE chunk_metadata
502
+ SET start_pos = start_token, end_pos = end_token
503
+ WHERE chunk_type = 'document' AND start_token IS NOT NULL
504
+ `);
505
+ // start_token/end_token columns remain (no DROP COLUMN); they will be
506
+ // ignored by older code.
507
+ }
430
508
  }
431
509
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rag-memory-epf-mcp",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
4
4
  "description": "MCP server for project-local RAG memory with knowledge graph and multilingual vector search",
5
5
  "license": "MIT",
6
6
  "author": "bripin123",