rag-memory-epf-mcp 3.3.2 → 3.3.4

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,98 @@ 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 10: Separate token-space vs char-space chunk offsets.
432
+ // (Slot 9 is intentionally skipped — some user databases from early v3.x
433
+ // experiments have an unrelated migration recorded at version 9 (Ollama
434
+ // dimension swap). Reusing that slot would silently no-op against those
435
+ // databases. Version 10 ensures the migration runs everywhere.)
436
+ // Before this migration, chunk_metadata.start_pos/end_pos held *token* indices
437
+ // for document chunks (a leftover from the BPE tokenizer-based chunkText loop)
438
+ // but already held character lengths (0..text.length) for entity/relationship
439
+ // chunks. Same column, two meanings — and a column name (`*_pos`) that implies
440
+ // char offsets in `documents.content`. This migration adds explicit
441
+ // start_token/end_token columns and reinterprets start_pos/end_pos as character
442
+ // offsets going forward. Existing document chunks: token data is moved to the
443
+ // new columns and char offsets are recomputed from documents.content via
444
+ // indexOf with a running cursor (NULL on miss — caller can re-chunk to fill).
445
+ // Existing entity/relationship chunks: leave start_pos/end_pos as-is
446
+ // (already a valid 0..text.length char range against the chunk text itself);
447
+ // token columns stay NULL since these chunks have no token-space concept.
448
+ {
449
+ version: 10,
450
+ description: 'Add start_token/end_token; reinterpret start_pos/end_pos as char offsets',
451
+ up: (db) => {
452
+ // 1) Add columns (idempotent — some databases may have been touched by a
453
+ // pre-release v9 attempt; tolerate the column already existing).
454
+ const cols = db.prepare(`PRAGMA table_info(chunk_metadata)`).all()
455
+ .map(c => c.name);
456
+ if (!cols.includes('start_token')) {
457
+ db.exec(`ALTER TABLE chunk_metadata ADD COLUMN start_token INTEGER`);
458
+ }
459
+ if (!cols.includes('end_token')) {
460
+ db.exec(`ALTER TABLE chunk_metadata ADD COLUMN end_token INTEGER`);
461
+ }
462
+ // 2) Move token data into new columns for document chunks (only if not
463
+ // already moved — guard against re-running in the rare case a prior
464
+ // partial run already touched some rows).
465
+ db.exec(`
466
+ UPDATE chunk_metadata
467
+ SET start_token = start_pos,
468
+ end_token = end_pos,
469
+ start_pos = NULL,
470
+ end_pos = NULL
471
+ WHERE chunk_type = 'document'
472
+ AND start_token IS NULL
473
+ AND start_pos IS NOT NULL
474
+ `);
475
+ // 3) Recompute char offsets via indexOf with a running cursor per document
476
+ const docRows = db.prepare(`
477
+ SELECT DISTINCT document_id FROM chunk_metadata
478
+ WHERE chunk_type = 'document' AND document_id IS NOT NULL
479
+ `).all();
480
+ const docContentStmt = db.prepare(`SELECT content FROM documents WHERE id = ?`);
481
+ const chunksStmt = db.prepare(`
482
+ SELECT rowid, text FROM chunk_metadata
483
+ WHERE document_id = ? AND chunk_type = 'document'
484
+ ORDER BY chunk_index ASC
485
+ `);
486
+ const updateStmt = db.prepare(`
487
+ UPDATE chunk_metadata SET start_pos = ?, end_pos = ? WHERE rowid = ?
488
+ `);
489
+ for (const { document_id } of docRows) {
490
+ const doc = docContentStmt.get(document_id);
491
+ if (!doc)
492
+ continue;
493
+ const content = doc.content;
494
+ const chunks = chunksStmt.all(document_id);
495
+ // Cursor advances by the previous chunk's *start*, not its end, so we can
496
+ // still locate overlapping chunks. Token-space stride guarantees each
497
+ // chunk's start is strictly forward of the previous chunk's start.
498
+ let cursor = 0;
499
+ for (const c of chunks) {
500
+ if (!c.text)
501
+ continue;
502
+ const idx = content.indexOf(c.text, cursor);
503
+ if (idx >= 0) {
504
+ updateStmt.run(idx, idx + c.text.length, c.rowid);
505
+ cursor = idx;
506
+ }
507
+ // miss: leave NULL — caller can re-chunk to repair
508
+ }
509
+ }
510
+ },
511
+ down: (db) => {
512
+ // SQLite cannot DROP COLUMN cleanly. Best-effort: copy token data back into
513
+ // start_pos/end_pos for document chunks so a downgrade leaves the legacy
514
+ // token-space semantics in place.
515
+ db.exec(`
516
+ UPDATE chunk_metadata
517
+ SET start_pos = start_token, end_pos = end_token
518
+ WHERE chunk_type = 'document' AND start_token IS NOT NULL
519
+ `);
520
+ // start_token/end_token columns remain (no DROP COLUMN); they will be
521
+ // ignored by older code.
522
+ }
430
523
  }
431
524
  ];
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.4",
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",