rag-memory-epf-mcp 5.3.1 → 6.0.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
@@ -153,6 +153,30 @@ storeDocument(id, content, metadata)
153
153
 
154
154
  ## Changelog
155
155
 
156
+ ### v6.0.0
157
+
158
+ - **Breaking — observation-derived alias links are now gated.** `autoLinkEntities` used to take every
159
+ `stem.ext` token appearing anywhere in an entity's observations and link that entity to every chunk
160
+ containing the token as a substring. Measured on a live 2,891-chunk / 604-entity corpus: 65,388 of
161
+ 66,841 `chunk_entities` rows (97.8%) existed only because of such a hit, and one token — `agents.md`,
162
+ held by 88 entities — accounted for 39,248 of the 80,096 distinct (chunk, entity) pairs any alias
163
+ could reach (49.0%). The regex also accepted things that are not filenames at all (`v3.3`,
164
+ `gpt-5.6`, `1.7mb`, `github.com`, `os.path`).
165
+ A token now has to (a) look like a filename — extension whitelist, stem ≥ 3 chars, non-numeric
166
+ stem — and (b) be held by at most 3 entities, and it must match on token boundaries so `foo.py` no
167
+ longer matches inside `notfoo.pyc`. Effect on the same corpus: alias links 100% → 4.0%. Filtering by
168
+ extension alone leaves 92.3%, so the owner cap is what does the work. The cap is a judgement, not a
169
+ discovered boundary — the sweep is smooth (`owners<=1` 1.6% … `<=10` 19.9%) — and it is a cap, not a
170
+ ban: a filename named by one to three records still links, which is what the alias path was for.
171
+ - **What this means for existing databases.** Nothing is rewritten on upgrade: rows already in
172
+ `chunk_entities` stay, and new ingests simply link far less. Links have always been a function of
173
+ when a document was last processed (nothing re-links older documents when entities are added), and
174
+ `chunk_entities` has no provenance column, so old alias rows cannot be told apart from name matches
175
+ after the fact. If you want the old noise gone you have to clean it offline, before or after
176
+ upgrading. Callers that assumed dense `chunk_entities` coverage will see sparser graphs.
177
+ - Regression: `test/alias-link-gate.test.mjs` (registered in `verify:engine`), verified to fail
178
+ without the gate.
179
+
156
180
  ### v5.3.0
157
181
  - (Published first as `5.3.0-rc.1` on the `next` dist-tag; promoted to `latest` after a canary run of the published artifact against a real project database: default call carries no `graph_boost` and equals explicit `useGraph:false`, the known-item probe from the 2026-08-17 measurement returns the correct gotcha at rank 1, opt-in `true` still exposes `graph_boost`, schema/MCP defaults read `false`.)
158
182
  - **Behavior change — `hybridSearch` graph re-ranking is now opt-in** (`useGraph` default `true` → `false`; tool schema, MCP exposure and the manager signature agree). Omitting the argument now means "no graph re-ranking" — a behavior change for callers that relied on the old default, hence a release-candidate first (`next` dist-tag, fleet canary) before stable. Measured 2026-08-17 on three real corpora (self-retrieval, usable samples 120/117/120, summaries off): with the additive graph boost on, the known-item chunk got worse in 46/49/52 samples and better in 3/2/0 (sign test p < 7e-11 per corpus), 106 targets left the top-10 entirely; reproduced on the summaries-on product path (HAL, 20 paired samples: hit@1 10→7, hit@5 18→13). Mechanism: only query-matched/connected entities score, but the per-entity boost saturates the cap quickly, so heavily-linked chunks can outrank the exact chunk even at `vector_similarity` 0. This is a harm-reduced default, not a validated graph improvement: the boost path is unchanged for `useGraph: true` (legacy/experimental re-ranker for back-compat and evaluation; the graph does not generate candidates — for relationship exploration use `openNodes` → `getNeighbors`). Regression lock: `test/search-graph-default.test.mjs`.
package/dist/index.d.ts CHANGED
@@ -279,6 +279,8 @@ export declare class RAGKnowledgeGraphManager {
279
279
  errors?: string[];
280
280
  }>;
281
281
  private hasCJK;
282
+ private static readonly ALIAS_FILE_EXT;
283
+ private looksLikeFilename;
282
284
  private buildEntityRangeFinder;
283
285
  private buildEntityMatcher;
284
286
  private autoLinkEntities;
package/dist/index.js CHANGED
@@ -2241,6 +2241,32 @@ export class RAGKnowledgeGraphManager {
2241
2241
  hasCJK(text) {
2242
2242
  return /[\u3000-\u9fff\uac00-\ud7af\uff00-\uffef]/.test(text);
2243
2243
  }
2244
+ // The alias regex in autoLinkEntities matches anything shaped like "stem.ext", which
2245
+ // includes version strings ("v3.3", "gpt-5.6"), measurements ("1.7mb", "0.465") and
2246
+ // domains/module paths ("github.com", "os.path"). Those are not filenames and linking
2247
+ // on them is pure noise. Whitelist the extensions we actually ship and store.
2248
+ static ALIAS_FILE_EXT = new Set([
2249
+ 'md', 'py', 'js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx', 'json',
2250
+ 'sh', 'bash', 'zsh', 'toml', 'yaml', 'yml', 'ini', 'cfg', 'conf',
2251
+ 'txt', 'log', 'csv', 'tsv', 'sql', 'db', 'zip', 'gz', 'tar',
2252
+ 'css', 'scss', 'html', 'htm', 'svg', 'png', 'jpg', 'jpeg', 'gif',
2253
+ 'pdf', 'docx', 'pptx', 'xlsx', 'hwp', 'hwpx', 'lock', 'bak',
2254
+ ]);
2255
+ // Deliberately absent: any 5+ character extension (jsonl, ipynb, scss is 4 so it stays).
2256
+ // The extractor regex is `\w{1,4}`, so a longer extension never reaches this set — listing
2257
+ // one would be a dead entry that reads as support. Extend the regex first if that changes.
2258
+ looksLikeFilename(token) {
2259
+ const i = token.lastIndexOf('.');
2260
+ if (i < 1)
2261
+ return false;
2262
+ const stem = token.slice(0, i);
2263
+ const ext = token.slice(i + 1);
2264
+ if (stem.length < 3)
2265
+ return false; // "d.ts" is a fragment, not a file
2266
+ if (/^\d+$/.test(stem))
2267
+ return false; // "2026.md" style numeric stems
2268
+ return RAGKnowledgeGraphManager.ALIAS_FILE_EXT.has(ext);
2269
+ }
2244
2270
  // spec §5.4 (r7-2·r8-1·r9): primary name 의 본문 occurrence range [sCp, eCp).
2245
2271
  // 의미 = buildEntityMatcher 와 동일 (CJK substring / Latin word-boundary) — 여기서
2246
2272
  // 어긋나면 'Data' 가 'Database' 에 새로 링크되는 식으로 의미가 확장된다.
@@ -2342,6 +2368,51 @@ export class RAGKnowledgeGraphManager {
2342
2368
  // Minimum name length: 2 for CJK (e.g. "할랄"), 4 for Latin (avoid "API", "Bug")
2343
2369
  const MIN_LEN_CJK = 2;
2344
2370
  const MIN_LEN_LATIN = 4;
2371
+ // Observation-derived aliases are only useful when the token identifies few entities.
2372
+ // Measured on a 2,891-chunk / 604-entity corpus (2026-08-22). Denominators matter here,
2373
+ // so both are stated: 66,841 rows in chunk_entities, of which 65,388 (97.8%) exist only
2374
+ // because of an alias hit; counting distinct (chunk, entity) pairs reachable by any alias
2375
+ // gives 80,096. Against that 80,096, "agents.md" alone accounts for 39,248 (49.0%), and
2376
+ // 35,099 (43.8%) have no other alias reason at all. It is held by 88 entities. A filename
2377
+ // that dozens of entities mention is a stopword, not an identifier.
2378
+ //
2379
+ // The owner cap is what does the work: the extension whitelist alone still leaves 92.3%
2380
+ // of alias links. But the cap value is a judgement call, not a discovered boundary — the
2381
+ // sweep is smooth, with no natural knee (share of the 80,096 that survives):
2382
+ // owners<=1 1.6% · <=2 3.7% · <=3 5.2% · <=4 7.0% · <=5 8.3% · <=8 14.7% · <=10 19.9%
2383
+ // 3 was chosen to keep the intended behaviour (a file named by one or two records, plus
2384
+ // some slack) while cutting the stopword tail. Raising it is cheap and reversible.
2385
+ //
2386
+ // A chunk-frequency cap was measured (a further 1.4pp) and rejected on COST: it needs a
2387
+ // full-corpus scan on every ingest. Note the honest caveat — it was first rejected for
2388
+ // depending on ingest order, but this owner cap has that same property, and so does the
2389
+ // engine as a whole: autoLinkEntities only ever sees the entities that exist at ingest
2390
+ // time, and nothing re-links older documents when entities are added (see
2391
+ // createEntities / addObservations — neither calls this). Links are a function of when a
2392
+ // document was last processed. That predates this gate; it is not introduced by it.
2393
+ const MAX_ALIAS_OWNERS = 3;
2394
+ const aliasOwners = new Map();
2395
+ for (const e of entities) {
2396
+ if (!e.observations)
2397
+ continue;
2398
+ let obs;
2399
+ try {
2400
+ obs = JSON.parse(e.observations);
2401
+ }
2402
+ catch {
2403
+ continue;
2404
+ }
2405
+ const seen = new Set();
2406
+ for (const ob of obs) {
2407
+ const pm = String(ob).match(/[\w\-]+\.\w{1,4}\b/g);
2408
+ if (pm)
2409
+ for (const p of pm)
2410
+ if (p.length >= 4)
2411
+ seen.add(p.toLowerCase());
2412
+ }
2413
+ for (const t of seen)
2414
+ aliasOwners.set(t, (aliasOwners.get(t) ?? 0) + 1);
2415
+ }
2345
2416
  const insertStmt = this.db.prepare(`
2346
2417
  INSERT OR IGNORE INTO chunk_entities (chunk_rowid, entity_id) VALUES (?, ?)
2347
2418
  `);
@@ -2351,7 +2422,9 @@ export class RAGKnowledgeGraphManager {
2351
2422
  if (entity.name.length < minLen)
2352
2423
  continue;
2353
2424
  const nameMatcher = this.buildEntityMatcher(entity.name);
2354
- // Also collect observation-derived aliases (short keywords from observations)
2425
+ // Also collect observation-derived aliases (short keywords from observations).
2426
+ // Gated: the token must look like a filename AND identify at most MAX_ALIAS_OWNERS
2427
+ // entities. Ungated, "agents.md" linked 88 entities to every chunk that mentioned it.
2355
2428
  const aliases = [];
2356
2429
  if (entity.observations) {
2357
2430
  let obs;
@@ -2366,9 +2439,30 @@ export class RAGKnowledgeGraphManager {
2366
2439
  const pathMatch = ob.match(/[\w\-]+\.\w{1,4}\b/g);
2367
2440
  if (pathMatch) {
2368
2441
  for (const p of pathMatch) {
2369
- if (p.length >= 4) {
2370
- aliases.push((text) => text.toLowerCase().includes(p.toLowerCase()));
2371
- }
2442
+ if (p.length < 4)
2443
+ continue;
2444
+ const tok = p.toLowerCase();
2445
+ if (!this.looksLikeFilename(tok))
2446
+ continue; // "v3.3", "gpt-5.6", "0.465"
2447
+ if ((aliasOwners.get(tok) ?? 0) > MAX_ALIAS_OWNERS)
2448
+ continue; // shared = identifies nothing
2449
+ // Bare substring matching links "foo.py" to a chunk saying "notfoo.pyc".
2450
+ // Require the token to stand alone: no filename character on either side.
2451
+ const boundary = /[\w\-.]/;
2452
+ aliases.push((text) => {
2453
+ const hay = text.toLowerCase();
2454
+ let from = 0;
2455
+ for (;;) {
2456
+ const i = hay.indexOf(tok, from);
2457
+ if (i < 0)
2458
+ return false;
2459
+ const before = i === 0 ? '' : hay[i - 1];
2460
+ const after = hay[i + tok.length] ?? '';
2461
+ if (!boundary.test(before) && !boundary.test(after))
2462
+ return true;
2463
+ from = i + 1;
2464
+ }
2465
+ });
2372
2466
  }
2373
2467
  }
2374
2468
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rag-memory-epf-mcp",
3
- "version": "5.3.1",
3
+ "version": "6.0.0",
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/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/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
  },