rag-memory-epf-mcp 6.0.0 → 6.0.1

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
@@ -153,6 +153,40 @@ storeDocument(id, content, metadata)
153
153
 
154
154
  ## Changelog
155
155
 
156
+ ### v6.0.1
157
+
158
+ **Versioning note.** Both changes below stop links from being created that should never have been
159
+ created, and restore links that should have been. No tool signature, return shape or schema
160
+ changes, so this is a patch: **derived-link density is not part of the compatibility contract**.
161
+ Read the entries as "linking got more precise", not as an API break.
162
+
163
+ - **An observation alias must now also appear in the entity's own name.** 6.0.0 capped how many
164
+ entities may share a token. That answers "does this token point at one entity?" and says nothing
165
+ about the other direction, so an entity that mentions a common filename *once* still attached to
166
+ every chunk containing it — and, being the sole owner, sailed through the cap. The cap stopped
167
+ the explosion, not the magnet. Measured after the 6.0.0 cleanup on a live corpus: 2,014
168
+ alias-only links remained and **34 entities held 68.1%** of them; the largest had the entity name
169
+ in **zero** of its chunks. Requiring the token (or its stem) to appear in the name brings that to
170
+ 6 entities / 42.1%, and what remains are entities that really are about that file. Expect far
171
+ fewer alias links on new ingests; existing rows are not rewritten.
172
+ - **A chunk-frequency cap was measured and rejected on the evidence, not on cost.** The full-corpus
173
+ scan is 585ms (731 tokens x 2,913 chunks). It was rejected because the sweep has no knee and every
174
+ cut also removed legitimate links — `log_coverage.py` occurs in 64 chunks and belongs to an entity
175
+ about exactly that file. The name condition is structural: no threshold, same meaning at any
176
+ corpus size.
177
+ - **Fixed — entity names whose own edge is punctuation never linked.** The Latin matcher used
178
+ `\b<name>\b`. `\b` asserts a transition between a word character and a non-word character, so when
179
+ the name itself starts or ends with punctuation — `Widget Review (2026-05-27)`, `--build-flag` —
180
+ there is no transition to assert and the pattern cannot match however the text reads. Measured on
181
+ a live 2,913-chunk / 312-name corpus: **23 standalone occurrences across 13 names** were invisible.
182
+ Both the chunk matcher and the document range finder now accept an occurrence whose neighbours on
183
+ both sides are non-word. **Not a widening**: both sides must still be non-word, so `Data` continues
184
+ not to match inside `Database`. The scan runs on the original text rather than a lowercased copy,
185
+ because folding can change length (`İ` becomes two units) and the range finder converts these
186
+ indices to codepoints.
187
+ - Regressions: `test/alias-link-gate.test.mjs` (magnet case) and `test/entity-name-boundary.test.mjs`,
188
+ both registered in `verify:engine` and both verified to fail without their fix.
189
+
156
190
  ### v6.0.0
157
191
 
158
192
  - **Breaking — observation-derived alias links are now gated.** `autoLinkEntities` used to take every
package/dist/index.d.ts CHANGED
@@ -279,6 +279,17 @@ export declare class RAGKnowledgeGraphManager {
279
279
  errors?: string[];
280
280
  }>;
281
281
  private hasCJK;
282
+ private static readonly WORD_CH;
283
+ /**
284
+ * Index of an occurrence of `name` in `text` with non-word neighbours, or -1.
285
+ *
286
+ * Runs on the ORIGINAL text, not a lowercased copy. Lowercasing can change length —
287
+ * 'İ' folds to two units — so an index taken from the folded string does not address the
288
+ * same character in the original, and buildEntityRangeFinder hands these indices straight
289
+ * to the codepoint table. (r9-1 made the same call for the regex path; a first cut of this
290
+ * helper folded first and the İ coordinate test caught it.)
291
+ */
292
+ private standaloneIndex;
282
293
  private static readonly ALIAS_FILE_EXT;
283
294
  private looksLikeFilename;
284
295
  private buildEntityRangeFinder;
package/dist/index.js CHANGED
@@ -2241,6 +2241,44 @@ export class RAGKnowledgeGraphManager {
2241
2241
  hasCJK(text) {
2242
2242
  return /[\u3000-\u9fff\uac00-\ud7af\uff00-\uffef]/.test(text);
2243
2243
  }
2244
+ // `\b` asserts a transition between a word char and a non-word char. When the name itself
2245
+ // *ends* (or starts) with a non-word char — and ours routinely do, e.g. "… Review (2026-05-27)"
2246
+ // — there is no transition to assert, so `\bname\b` can never match however the text reads.
2247
+ // Measured 2026-08-23 on a live 2,913-chunk corpus: 23 standalone occurrences across 13 names
2248
+ // were invisible to the regex. This is not a widening: we still require both neighbours to be
2249
+ // non-word, so "Data" continues not to match inside "Database". It is what `\b` was reaching
2250
+ // for, stated in a way that survives a name whose own edges are punctuation.
2251
+ static WORD_CH = /[A-Za-z0-9_]/;
2252
+ /**
2253
+ * Index of an occurrence of `name` in `text` with non-word neighbours, or -1.
2254
+ *
2255
+ * Runs on the ORIGINAL text, not a lowercased copy. Lowercasing can change length —
2256
+ * 'İ' folds to two units — so an index taken from the folded string does not address the
2257
+ * same character in the original, and buildEntityRangeFinder hands these indices straight
2258
+ * to the codepoint table. (r9-1 made the same call for the regex path; a first cut of this
2259
+ * helper folded first and the İ coordinate test caught it.)
2260
+ */
2261
+ standaloneIndex(text, name, from = 0) {
2262
+ let re;
2263
+ try {
2264
+ re = new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
2265
+ }
2266
+ catch {
2267
+ return -1;
2268
+ }
2269
+ re.lastIndex = from;
2270
+ let m;
2271
+ while ((m = re.exec(text)) !== null) {
2272
+ const before = m.index === 0 ? '' : text[m.index - 1];
2273
+ const after = text[m.index + m[0].length] ?? '';
2274
+ if (!RAGKnowledgeGraphManager.WORD_CH.test(before)
2275
+ && !RAGKnowledgeGraphManager.WORD_CH.test(after))
2276
+ return m.index;
2277
+ if (re.lastIndex === m.index)
2278
+ re.lastIndex++;
2279
+ }
2280
+ return -1;
2281
+ }
2244
2282
  // The alias regex in autoLinkEntities matches anything shaped like "stem.ext", which
2245
2283
  // includes version strings ("v3.3", "gpt-5.6"), measurements ("1.7mb", "0.465") and
2246
2284
  // domains/module paths ("github.com", "os.path"). Those are not filenames and linking
@@ -2327,6 +2365,18 @@ export class RAGKnowledgeGraphManager {
2327
2365
  if (re.lastIndex === m.index)
2328
2366
  re.lastIndex++;
2329
2367
  }
2368
+ // Same correction as buildEntityMatcher — the two must agree or a name links at chunk
2369
+ // level but not at range level. Union, deduped: a hit found by both must not be pushed
2370
+ // twice or a chunk gets counted once per path.
2371
+ const seen = new Set(out.map(r => `${r.s}:${r.e}`));
2372
+ for (let at = this.standaloneIndex(content, name); at >= 0; at = this.standaloneIndex(content, name, at + 1)) {
2373
+ const s2 = origCpAt(at), e2 = origCpAt(at + name.length); // 대소문자 차이는 길이를 바꾸지 않는다
2374
+ const key = `${s2}:${e2}`;
2375
+ if (!seen.has(key)) {
2376
+ seen.add(key);
2377
+ out.push({ s: s2, e: e2 });
2378
+ }
2379
+ }
2330
2380
  }
2331
2381
  catch {
2332
2382
  pushAllSubstr();
@@ -2345,7 +2395,8 @@ export class RAGKnowledgeGraphManager {
2345
2395
  try {
2346
2396
  const escaped = lower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2347
2397
  const re = new RegExp(`\\b${escaped}\\b`, 'i');
2348
- return (text) => re.test(text);
2398
+ // See standaloneIndex: `\b` cannot fire when the name's own edge is punctuation.
2399
+ return (text) => re.test(text) || this.standaloneIndex(text, name) >= 0;
2349
2400
  }
2350
2401
  catch {
2351
2402
  return (text) => text.toLowerCase().includes(lower);
@@ -2392,6 +2443,28 @@ export class RAGKnowledgeGraphManager {
2392
2443
  // document was last processed. That predates this gate; it is not introduced by it.
2393
2444
  const MAX_ALIAS_OWNERS = 3;
2394
2445
  const aliasOwners = new Map();
2446
+ // Owners is only half the mapping. It asks "does this token point at one entity?" and says
2447
+ // nothing about "does this entity point at one token?" — so an entity that mentions a common
2448
+ // filename ONCE in its observations attaches to every chunk containing that filename, and
2449
+ // because it is the only owner, the cap waves it through. The cap stopped the explosion
2450
+ // (tens of thousands of rows) but not the magnet (one entity on dozens of chunks).
2451
+ // Measured 2026-08-23 after the 6.0.0 cleanup: 2,014 alias-only links remained and 34
2452
+ // entities held 68.1% of them; the biggest had the entity name appearing in ZERO of its
2453
+ // chunks — pulled in entirely by a filename someone mentioned in passing.
2454
+ // So require the mapping in both directions: the token must identify the entity (owners)
2455
+ // AND the entity must identify the token (the token, or its stem, appears in the name).
2456
+ // This is a structural condition, not a threshold — there is no knee to tune and it keeps
2457
+ // meaning the same as the corpus grows. Chunk-frequency caps were measured instead
2458
+ // (585ms full scan, so cost was NOT the objection) and rejected because the sweep is smooth
2459
+ // and every cut also removed legitimate links.
2460
+ const aliasNamesTheEntity = (entityName, token) => {
2461
+ const lower = entityName.toLowerCase();
2462
+ if (lower.includes(token))
2463
+ return true;
2464
+ const dot = token.lastIndexOf('.');
2465
+ const stem = dot > 0 ? token.slice(0, dot) : token;
2466
+ return stem.length >= 4 && lower.includes(stem);
2467
+ };
2395
2468
  for (const e of entities) {
2396
2469
  if (!e.observations)
2397
2470
  continue;
@@ -2446,6 +2519,8 @@ export class RAGKnowledgeGraphManager {
2446
2519
  continue; // "v3.3", "gpt-5.6", "0.465"
2447
2520
  if ((aliasOwners.get(tok) ?? 0) > MAX_ALIAS_OWNERS)
2448
2521
  continue; // shared = identifies nothing
2522
+ if (!aliasNamesTheEntity(entity.name, tok))
2523
+ continue; // mentioned in passing = magnet
2449
2524
  // Bare substring matching links "foo.py" to a chunk saying "notfoo.pyc".
2450
2525
  // Require the token to stand alone: no filename character on either side.
2451
2526
  const boundary = /[\w\-.]/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rag-memory-epf-mcp",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "engines": {
5
5
  "node": ">=24"
6
6
  },
@@ -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/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/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 && node test/delete-entities-cascade.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",
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/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs && node test/delete-entities-cascade.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",
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
  },