rag-memory-epf-mcp 5.0.0 → 5.2.0

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 CHANGED
@@ -64,7 +64,7 @@ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each
64
64
  ### Document Pipeline (9)
65
65
  | Tool | Description | Annotation |
66
66
  |------|------------|------------|
67
- | `storeDocument` | Store documents with metadata | idempotent |
67
+ | `storeDocument` | Store documents with metadata. Replacing an existing document reports what it destroyed: `{ replaced, deletedChunks }` | idempotent |
68
68
  | `chunkDocument` | Create text chunks with configurable parameters | — |
69
69
  | `embedChunks` | Generate 1024-dim embeddings + auto-link entities | idempotent |
70
70
  | `embedAllEntities` | Batch embed all entities (32 parallel) | idempotent |
@@ -72,7 +72,7 @@ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each
72
72
  | `linkEntitiesToDocument` | Link entities to chunks where they actually appear (text-matched) | idempotent |
73
73
  | `deleteDocuments` | Remove documents and associated data | destructive |
74
74
  | `listDocuments` | View all stored documents | readOnly |
75
- | `syncDocumentFromFile` | One-call server-side sync: reads file + delete/store/chunk/embed/link, content stays off model context. Atomic (embed-first transaction swap) + `content_hash` dedup (skips unchanged files) | idempotent |
75
+ | `syncDocumentFromFile` | One-call server-side sync: reads file + delete/store/chunk/embed/link, content stays off model context. Atomic (embed-first transaction swap) + `content_hash` dedup (skips unchanged files). `excludePattern` strips regions before indexing, and the hash follows the stripped text so changing the pattern re-indexes | idempotent |
76
76
 
77
77
  ### Search & Retrieval (9)
78
78
  | Tool | Description | Annotation |
package/dist/index.d.ts CHANGED
@@ -70,6 +70,7 @@ export declare class RAGKnowledgeGraphManager {
70
70
  currentProfileId: number;
71
71
  grandfatherAllowed: boolean;
72
72
  coordinator: BackfillCoordinator | null;
73
+ readonly calendarTimeZone: string;
73
74
  private embeddingCache;
74
75
  private readonly EMBEDDING_CACHE_MAX;
75
76
  private dictionaryCache;
@@ -227,6 +228,7 @@ export declare class RAGKnowledgeGraphManager {
227
228
  syncDocumentFromFile(filePath: string, documentId: string, options?: {
228
229
  metadata?: Record<string, any>;
229
230
  content?: string;
231
+ excludePattern?: string | string[];
230
232
  entityNames?: string[];
231
233
  chunkParams?: {
232
234
  maxTokens?: number;
@@ -252,6 +254,8 @@ export declare class RAGKnowledgeGraphManager {
252
254
  storeDocument(id: string, content: string, metadata?: Record<string, any>): Promise<{
253
255
  id: string;
254
256
  stored: boolean;
257
+ replaced: boolean;
258
+ deletedChunks: number;
255
259
  }>;
256
260
  chunkDocument(documentId: string, options?: {
257
261
  maxTokens?: number;
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ import { migrations } from './src/migrations/migrations.js';
32
32
  import { EmbeddingGate, GateNotReadyError, GateDisabledError, TerminalConfigError } from './src/embeddingGate.js';
33
33
  import { resolveModelCacheDir, preflightCacheDir, artifactKey, ModelDownloadLock, handleLoaderFailure } from './src/modelCache.js';
34
34
  import { BackfillCoordinator } from './src/backfillCoordinator.js';
35
+ import { calendarDate, resolveCalendarTimeZone, stampDatePrefix, stripDatePrefix } from './src/observations/date-prefix.js';
35
36
  import os from 'node:os';
36
37
  import { createHash } from 'crypto';
37
38
  import { createRequire } from 'module';
@@ -146,6 +147,28 @@ function safeRowid(value) {
146
147
  }
147
148
  return n;
148
149
  }
150
+ // Remove regions the caller does not want indexed. Compiled with `s` because the intended use is
151
+ // spanning a marked block (`<!-- SECRET -->…<!-- /SECRET -->`) and JS has no inline (?s) flag —
152
+ // without it every such pattern would silently match nothing.
153
+ // A malformed pattern throws rather than degrading to "no exclusion": indexing is a disclosure
154
+ // path, so believing you excluded something you did not is worse than a failed sync.
155
+ function applyExcludePatterns(text, pattern) {
156
+ if (pattern === undefined)
157
+ return text;
158
+ const patterns = Array.isArray(pattern) ? pattern : [pattern];
159
+ let out = text;
160
+ for (const p of patterns) {
161
+ let re;
162
+ try {
163
+ re = new RegExp(p, 'gs');
164
+ }
165
+ catch (e) {
166
+ throw new Error(`excludePattern is not a valid regular expression: ${JSON.stringify(p)} (${e.message})`);
167
+ }
168
+ out = out.replace(re, '');
169
+ }
170
+ return out;
171
+ }
149
172
  // Enhanced RAG-enabled Knowledge Graph Manager
150
173
  export class RAGKnowledgeGraphManager {
151
174
  db = null;
@@ -157,6 +180,9 @@ export class RAGKnowledgeGraphManager {
157
180
  // default model config, or with the explicit trust opt-in (spec §6b guard).
158
181
  grandfatherAllowed = IS_DEFAULT_MODEL_CONFIG || process.env.RAG_MEMORY_TRUST_LEGACY_VECTORS === '1';
159
182
  coordinator = null;
183
+ // The calendar that date-only human labels are written in. Resolved once, here, so an invalid
184
+ // zone fails at construction instead of quietly writing wrong days for weeks.
185
+ calendarTimeZone = resolveCalendarTimeZone(process.env.RAG_MEMORY_CALENDAR_TZ);
160
186
  embeddingCache = new Map();
161
187
  EMBEDDING_CACHE_MAX = 500;
162
188
  dictionaryCache = null;
@@ -552,11 +578,12 @@ export class RAGKnowledgeGraphManager {
552
578
  }
553
579
  // === ORIGINAL MCP FUNCTIONALITY ===
554
580
  _timestampObservation(obs) {
555
- // If observation already has a date prefix like [2026-03-21], skip
556
- if (/^\[\d{4}-\d{2}-\d{2}\]/.test(obs))
557
- return obs;
558
- const today = new Date().toISOString().slice(0, 10);
559
- return `[${today}] ${obs}`;
581
+ // Stamp and dedup share one parser (src/observations/date-prefix.ts). They used to carry
582
+ // separate regexes and disagreed about "[2026-08-11 session16] ...": stamping treated it as
583
+ // undated and prepended a second date, while dedup could not strip it — so the same sentence
584
+ // written in two sessions became two observations. Measured on a live database: 82 rows with
585
+ // two dates, 29 with a day earlier than the day they were written.
586
+ return stampDatePrefix(obs, this.calendarTimeZone);
560
587
  }
561
588
  async createEntities(entities) {
562
589
  if (!this.db)
@@ -568,7 +595,7 @@ export class RAGKnowledgeGraphManager {
568
595
  INSERT OR IGNORE INTO entities (id, name, entityType, observations, metadata)
569
596
  VALUES (?, ?, ?, '[]', ?)
570
597
  `);
571
- const stripDate = (s2) => s2.replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, '');
598
+ const stripDate = stripDatePrefix; // shared with the stamp path — see date-prefix.ts
572
599
  for (const entity of entities) {
573
600
  const entityId = `entity_${entity.name.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '_')}`;
574
601
  const ts = new Date().toISOString();
@@ -678,7 +705,7 @@ export class RAGKnowledgeGraphManager {
678
705
  // dedup 기준은 v3.6 과 같다: 날짜 prefix 를 뗀 본문이 active 에 이미 있으면
679
706
  // 새 revision 을 만들지 않는다. 다만 v13 에서는 같은 사실이 다른 출처에서 다시
680
707
  // 온 것이므로 그 revision 에 source link 를 더한다(spec §8.3 T13).
681
- const stripDate = (s2) => s2.replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, '');
708
+ const stripDate = stripDatePrefix; // shared with the stamp path — see date-prefix.ts
682
709
  const activeRows = this.db.prepare(`SELECT observation_id, content FROM entity_observations
683
710
  WHERE entity_id = ? AND status = 'active'`).all(entityId);
684
711
  const activeByBare = new Map(activeRows.map(r => [stripDate(r.content), r.observation_id]));
@@ -1935,9 +1962,16 @@ export class RAGKnowledgeGraphManager {
1935
1962
  const zero = { reusedChunks: 0, newlyEmbeddedChunks: 0, queuedChunks: 0, deletedChunks: 0, chunkerTransitioned: false };
1936
1963
  for (let attempt = 1; attempt <= 3; attempt++) {
1937
1964
  // r6-3: CAS 재시작 = 처음부터 — 파일 읽기·hash·metadata 도 attempt 안에서 재계산한다.
1938
- const content = options.content !== undefined ? options.content : fsSync.readFileSync(filePath, 'utf-8');
1965
+ // Strip excluded regions before anything else looks at the text. Everything downstream —
1966
+ // content_hash, bytes, chunking — then describes what was actually indexed, so changing the
1967
+ // pattern alone still invalidates the dedup gate below. Hashing the raw file instead would
1968
+ // report `unchanged` for a different exclusion, which is the silent-wrong case.
1969
+ const content = applyExcludePatterns(options.content !== undefined ? options.content : fsSync.readFileSync(filePath, 'utf-8'), options.excludePattern);
1939
1970
  const bytes = Buffer.byteLength(content, 'utf-8');
1940
- const today = new Date().toISOString().slice(0, 10);
1971
+ // Same calendar as the observation prefix. Deciding this explicitly rather than leaving it
1972
+ // on UTC: both are date-only labels a person reads, and "observations in Seoul days but
1973
+ // documents in UTC days" is not a distinction anyone could explain later.
1974
+ const today = calendarDate(new Date(), this.calendarTimeZone);
1941
1975
  const contentHash = shaHex(content);
1942
1976
  // spec §5.1: content_hash 는 system-owned — user metadata 뒤에 쓴다 (r1: spread 가 덮어쓸 수 있었다).
1943
1977
  const metadata = { source: filePath, updated: today, ...(options.metadata || {}), content_hash: contentHash };
@@ -2104,15 +2138,18 @@ export class RAGKnowledgeGraphManager {
2104
2138
  if (!this.db)
2105
2139
  throw new Error('Database not initialized');
2106
2140
  console.error(`📄 Storing document: ${id}`);
2141
+ // Decide `replaced` from the document row, not from the chunk count: a document stored but
2142
+ // never chunked still gets overwritten here, and reporting that as a fresh write would be a lie.
2143
+ const existed = this.db.prepare(`SELECT 1 FROM documents WHERE id = ?`).get(id) !== undefined;
2107
2144
  // Clean up existing document
2108
- await this.cleanupDocument(id);
2145
+ const cleaned = await this.cleanupDocument(id);
2109
2146
  // Store document
2110
2147
  this.db.prepare(`
2111
2148
  INSERT OR REPLACE INTO documents (id, content, metadata)
2112
2149
  VALUES (?, ?, ?)
2113
2150
  `).run(id, content, JSON.stringify(metadata));
2114
2151
  console.error(`✅ Document stored: ${id}`);
2115
- return { id, stored: true };
2152
+ return { id, stored: true, replaced: existed, deletedChunks: cleaned.deletedChunks };
2116
2153
  }
2117
2154
  async chunkDocument(documentId, options = {}) {
2118
2155
  if (!this.db)
@@ -2430,9 +2467,13 @@ export class RAGKnowledgeGraphManager {
2430
2467
  console.error(`✅ Entities linked: ${linkedCount} entities linked to document`);
2431
2468
  return { documentId, linkedEntities: linkedCount };
2432
2469
  }
2470
+ // Report what was destroyed. The counts were already computed here and thrown away, so a caller
2471
+ // that replaces a document could not tell from the return value that anything was deleted
2472
+ // (2026-08-05 field report from a deployed project: "{stored:true} came back and I did not know
2473
+ // what I had just wiped"). Silent destruction is the defect; the numbers are free.
2433
2474
  async cleanupDocument(documentId) {
2434
2475
  if (!this.db)
2435
- return;
2476
+ return { deletedChunks: 0, deletedAssociations: 0, deletedVectors: 0 };
2436
2477
  console.error(`🧹 Cleaning up document: ${documentId}`);
2437
2478
  // Get existing chunks
2438
2479
  const existingChunks = this.db.prepare(`
@@ -2460,6 +2501,7 @@ export class RAGKnowledgeGraphManager {
2460
2501
  console.error(` ├─ Deleted ${deletedVectors} vector embeddings`);
2461
2502
  console.error(` └─ Deleted ${metadata.changes} chunk metadata records`);
2462
2503
  }
2504
+ return { deletedChunks: existingChunks.length, deletedAssociations, deletedVectors };
2463
2505
  }
2464
2506
  async deleteDocument(documentId) {
2465
2507
  if (!this.db)
@@ -3418,6 +3460,9 @@ export class RAGKnowledgeGraphManager {
3418
3460
  server: {
3419
3461
  version: PKG_VERSION,
3420
3462
  node: process.versions.node,
3463
+ // Which calendar produced the date-only labels in this database. Surfaced because a
3464
+ // wrong value is otherwise invisible: the labels look plausible either way.
3465
+ calendar_timezone: this.calendarTimeZone,
3421
3466
  embeddings_mode: this.embeddingsMode,
3422
3467
  model: `${EMBEDDING_MODEL}@${MODEL_REVISION}`,
3423
3468
  model_state: gs.state,
@@ -3839,6 +3884,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3839
3884
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.syncDocumentFromFile(validatedArgs.path, validatedArgs.documentId, {
3840
3885
  metadata: validatedArgs.metadata,
3841
3886
  content: validatedArgs.content,
3887
+ excludePattern: validatedArgs.excludePattern,
3842
3888
  entityNames: validatedArgs.entityNames,
3843
3889
  chunkParams: validatedArgs.chunkParams,
3844
3890
  }), null, 2) }] };
@@ -0,0 +1,26 @@
1
+ /** The calendar day of `instant` in `timeZone`, as YYYY-MM-DD. */
2
+ export declare function calendarDate(instant: Date, timeZone: string): string;
3
+ /**
4
+ * Resolve RAG_MEMORY_CALENDAR_TZ. Unset or blank means UTC. An unrecognised zone throws at
5
+ * boot rather than silently falling back — a wrong-but-quiet calendar writes wrong labels for
6
+ * as long as nobody looks, which is the failure this whole module exists to end.
7
+ */
8
+ export declare function resolveCalendarTimeZone(raw: string | undefined | null): string;
9
+ export interface DatePrefix {
10
+ /** YYYY-MM-DD, already validated as a real calendar day. */
11
+ date: string;
12
+ /** Text between the date and the closing bracket, e.g. a session marker. */
13
+ annotation: string | null;
14
+ /** The matched prefix including the brackets, without trailing whitespace. */
15
+ matched: string;
16
+ }
17
+ /** Parse a leading date prefix, or null when the text does not start with one. */
18
+ export declare function parseDatePrefix(content: string): DatePrefix | null;
19
+ /**
20
+ * The dedup key: the text with its date prefix removed. The annotation comes off with the date
21
+ * because it records *when and in which session the line was written*, not what the line claims
22
+ * — the same sentence written in two sessions is one fact, and dedup has to see that.
23
+ */
24
+ export declare function stripDatePrefix(content: string): string;
25
+ /** Prepend today's calendar day unless the text already carries a valid date prefix. */
26
+ export declare function stampDatePrefix(content: string, timeZone: string, now?: Date): string;
@@ -0,0 +1,89 @@
1
+ // One place that decides what a leading "[date]" on an observation means.
2
+ //
3
+ // Why this is a module and not two regexes inline: the stamp path and the dedup path each had
4
+ // their own copy of the pattern, and they disagreed. Stamping asked "does this already start
5
+ // with a date?" while dedup asked "what is this text without its date?", so widening one without
6
+ // the other let the same sentence be stored twice under two session markers. Both now call in
7
+ // here.
8
+ //
9
+ // Why an explicit timezone instead of the process one: a date-only label has no meaning until
10
+ // you say which calendar produced it. The same database is reached from a laptop, from CI and
11
+ // from another country; deriving the day from the ambient TZ makes the stored string mean
12
+ // something different on each. The default stays UTC so the product does not inherit whichever
13
+ // zone its first author happened to sit in — a deployment that wants local days says so.
14
+ /** The calendar day of `instant` in `timeZone`, as YYYY-MM-DD. */
15
+ export function calendarDate(instant, timeZone) {
16
+ const parts = new Intl.DateTimeFormat('en-US', {
17
+ timeZone,
18
+ year: 'numeric',
19
+ month: '2-digit',
20
+ day: '2-digit',
21
+ }).formatToParts(instant);
22
+ const part = (type) => {
23
+ const found = parts.find(p => p.type === type);
24
+ if (!found)
25
+ throw new Error(`calendarDate: missing ${type} for timeZone ${timeZone}`);
26
+ return found.value;
27
+ };
28
+ return `${part('year')}-${part('month')}-${part('day')}`;
29
+ }
30
+ /**
31
+ * Resolve RAG_MEMORY_CALENDAR_TZ. Unset or blank means UTC. An unrecognised zone throws at
32
+ * boot rather than silently falling back — a wrong-but-quiet calendar writes wrong labels for
33
+ * as long as nobody looks, which is the failure this whole module exists to end.
34
+ */
35
+ export function resolveCalendarTimeZone(raw) {
36
+ const value = (raw ?? '').trim();
37
+ if (!value)
38
+ return 'UTC';
39
+ try {
40
+ new Intl.DateTimeFormat('en-US', { timeZone: value });
41
+ }
42
+ catch {
43
+ throw new Error(`RAG_MEMORY_CALENDAR_TZ is not a recognised IANA time zone: ${JSON.stringify(value)}. ` +
44
+ `Use a name like "Asia/Seoul", or leave it unset for UTC.`);
45
+ }
46
+ return value;
47
+ }
48
+ // The annotation may not contain a newline or a bracket: an unclosed or multi-line "[2026-…"
49
+ // is prose that happens to start with a digit, not a prefix. Requiring the closing bracket is
50
+ // what keeps "[2026-08-11 unclosed" and "[2026-08-111]" out.
51
+ const PREFIX_RE = /^\[(\d{4})-(\d{2})-(\d{2})(?:[ \t]([^\]\n]*))?\]/;
52
+ function isRealCalendarDay(year, month, day) {
53
+ const asUtc = new Date(Date.UTC(year, month - 1, day));
54
+ return asUtc.getUTCFullYear() === year
55
+ && asUtc.getUTCMonth() === month - 1
56
+ && asUtc.getUTCDate() === day;
57
+ }
58
+ /** Parse a leading date prefix, or null when the text does not start with one. */
59
+ export function parseDatePrefix(content) {
60
+ const m = PREFIX_RE.exec(content);
61
+ if (!m)
62
+ return null;
63
+ const [matched, year, month, day, annotation] = m;
64
+ if (!isRealCalendarDay(Number(year), Number(month), Number(day)))
65
+ return null;
66
+ const trimmed = annotation === undefined ? null : annotation.trim();
67
+ return {
68
+ date: `${year}-${month}-${day}`,
69
+ annotation: trimmed === '' ? null : trimmed,
70
+ matched,
71
+ };
72
+ }
73
+ /**
74
+ * The dedup key: the text with its date prefix removed. The annotation comes off with the date
75
+ * because it records *when and in which session the line was written*, not what the line claims
76
+ * — the same sentence written in two sessions is one fact, and dedup has to see that.
77
+ */
78
+ export function stripDatePrefix(content) {
79
+ const prefix = parseDatePrefix(content);
80
+ if (!prefix)
81
+ return content;
82
+ return content.slice(prefix.matched.length).replace(/^\s+/, '');
83
+ }
84
+ /** Prepend today's calendar day unless the text already carries a valid date prefix. */
85
+ export function stampDatePrefix(content, timeZone, now = new Date()) {
86
+ if (parseDatePrefix(content))
87
+ return content;
88
+ return `[${calendarDate(now, timeZone)}] ${content}`;
89
+ }
@@ -200,6 +200,11 @@ Observations provide the factual foundation that supports entity existence and p
200
200
  - (!important!) **Entity must exist** - this tool only adds to existing entities
201
201
  - (!important!) Duplicates are filtered - repeating existing text with a **new** sources entry
202
202
  adds evidence to the existing revision and returns null for that position
203
+ - (!important!) A \`[YYYY-MM-DD]\` prefix is prepended unless the text already starts with a valid
204
+ one. The day comes from \`RAG_MEMORY_CALENDAR_TZ\` (default UTC) - see \`calendar_timezone\` in
205
+ getKnowledgeGraphStats. Text may open with \`[YYYY-MM-DD note]\`; the note is treated as recording
206
+ metadata and is ignored when deduplicating, so the same sentence filed under two session markers
207
+ stays one observation
203
208
  - (!important!) Observations are cumulative. **This tool never replaces anything** - use
204
209
  correctObservation to supersede and retractObservation to withdraw
205
210
  - (!important!) **Be specific and factual** - observations should be verifiable statements
@@ -594,6 +594,7 @@ const syncDocumentFromFileSchema = {
594
594
  documentId: z.string().describe('RAG document ID to (re)create from the file'),
595
595
  metadata: z.record(z.any()).optional().describe('Metadata merged into the stored document'),
596
596
  content: z.string().optional().describe('Optional content override; stored instead of reading the file'),
597
+ excludePattern: z.union([z.string(), z.array(z.string())]).optional().describe('Regular expression(s) whose matches are stripped before indexing. Applied to the file (or to `content`) first, so hash, byte count and chunking all describe what was actually indexed. Compiled with the dotAll flag, so a pattern may span lines to drop a marked block. An invalid expression fails the call rather than indexing the whole file.'),
597
598
  entityNames: z.array(z.string()).optional().describe('Optional entities to explicitly link'),
598
599
  chunkParams: z.record(z.any()).optional().describe('Optional chunking parameters { maxTokens }. overlap: omit or 0 only (rejected otherwise since v5.0.0)'),
599
600
  };
package/docs/UPDATING.md CHANGED
@@ -108,6 +108,41 @@ 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
+ ## v5.1.0 (schema v14, unchanged): destructive-replace reporting + `excludePattern`
112
+
113
+ **What changes on upgrade**: nothing you have to do. No migration, no schema
114
+ change, no re-embedding. Both changes are additive — existing calls keep their
115
+ arguments and keep working, and the new response fields are extra keys.
116
+
117
+ **`storeDocument` now says what it destroyed.** It has always deleted the
118
+ previous document's chunks, vectors and entity links before writing, but the
119
+ response was `{ id, stored: true }`, so a caller replacing a document could not
120
+ tell from the return value that anything was removed. It now returns
121
+ `{ id, stored, replaced, deletedChunks }`, matching what `syncDocumentFromFile`
122
+ already reported. `replaced` is decided by the document row, not the chunk
123
+ count — a document that was stored but never chunked still gets overwritten,
124
+ and reporting that as a fresh write would be wrong.
125
+
126
+ **`syncDocumentFromFile` accepts `excludePattern`** (string or array of
127
+ strings): regions matching these regular expressions are stripped before
128
+ indexing. Previously the only way to leave part of a file out was to read it
129
+ yourself and pass the whole edited text through `content`, which defeats the
130
+ point of a tool that reads server-side to keep content off the model context.
131
+
132
+ Three properties worth knowing:
133
+
134
+ 1. **The exclusion happens first**, before hashing and chunking, so
135
+ `content_hash`, the reported `bytes` and the chunk boundaries all describe
136
+ what was actually indexed. Changing only the pattern therefore invalidates
137
+ the dedup gate and re-indexes; it does not silently return `unchanged`.
138
+ 2. **Patterns are compiled with the dotAll flag**, so one pattern can span
139
+ lines to drop a marked block (`<!-- SECRET -->[\s\S]*?<!-- /SECRET -->`).
140
+ JavaScript has no inline `(?s)`, so without this every such pattern would
141
+ quietly match nothing.
142
+ 3. **An invalid expression fails the call.** Degrading to "no exclusion" would
143
+ index the whole file while the caller believes it was filtered, and an index
144
+ is a disclosure path — a failed sync is the safer error.
145
+
111
146
  ## v5.0.0 (schema v14): chunker c1 + vector reuse
112
147
 
113
148
  **Breaking**: `chunkParams.overlap` is rejected on BOTH public paths
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "rag-memory-epf-mcp",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "engines": {
5
5
  "node": ">=24"
6
6
  },
7
- "description": "Project-local RAG memory MCP server \u2014 knowledge graph + multilingual vector + FTS5 in a single SQLite file. Per-project isolation, 38 MCP tools, codepoint-safe chunking (Korean/CJK/emoji).",
7
+ "description": "Project-local RAG memory MCP server knowledge graph + multilingual vector + FTS5 in a single SQLite file. Per-project isolation, 38 MCP tools, codepoint-safe chunking (Korean/CJK/emoji).",
8
8
  "keywords": [
9
9
  "mcp",
10
10
  "model-context-protocol",
@@ -45,7 +45,7 @@
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/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/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.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/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/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/document-return-contracts.test.mjs && node test/observation-date-prefix.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
  },