claude-mem-lite 2.9.1 → 2.9.2

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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.9.1",
13
+ "version": "2.9.2",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/dispatch.mjs CHANGED
@@ -216,7 +216,7 @@ const NEGATION_CJK = /(?:不要|别|不用|先别|暂时不|不需要|跳过|停
216
216
 
217
217
  // Test-run vs test-write disambiguation (module-scoped for performance)
218
218
  const _RUN_TEST = /\b(run\w*\s+(?:the\s+)?tests?|npm\s+test|npx\s+(?:vitest|jest|mocha|pytest)|yarn\s+test|pnpm\s+test|make\s+test|cargo\s+test|go\s+test|check\s+(?:if\s+)?tests?\s+pass|execute\s+(?:the\s+)?tests?)\b/i;
219
- const _RUN_TEST_CJK = /(?:运行测试|跑测试|跑一下测试|跑单测|执行测试|测试跑|看测试)/;
219
+ const _RUN_TEST_CJK = /(?:运行测试|跑测试|跑一下测试|跑单测|跑一下单测|执行测试|执行单测|测试跑|看测试|看单测)/;
220
220
  const _WRITE_TEST = /\b(write\s+tests?|add\s+tests?|create\s+tests?|need\s+tests?|missing\s+tests?|tdd|test.?driven|red.?green|increase\s+coverage|improve\s+coverage)\b/i;
221
221
  const _WRITE_TEST_CJK = /(?:写测试|加测试|补测试|补单测|缺测试|测试覆盖)/;
222
222
 
@@ -259,7 +259,7 @@ const _INTENT_PATTERNS = (() => {
259
259
  // ── Chinese patterns ──
260
260
  [/(测试|写测试|单测|单元测试|用例|覆盖率)/, 'test'],
261
261
  [/(修复|修bug|改bug|找bug|有bug|调试|排错|报错|出错|有问题|不工作|跑不起来|不能用|挂了|崩溃)/, 'fix'],
262
- [/(审查|审核|代码审查|评审|代码审核|看看代码|review)/, 'review'],
262
+ [/(审查|审核|审计|代码审查|评审|代码审核|看看代码|review)/, 'review'],
263
263
  [/(提交|推送|上传)/, 'commit'],
264
264
  [/(部署|上线|发布|回滚)/, 'deploy'],
265
265
  [/(规划|架构|方案|设计方案)/, 'plan'],
@@ -273,6 +273,10 @@ const _INTENT_PATTERNS = (() => {
273
273
  [/(优化|性能|卡顿|耗时|太慢|慢死了|好慢|缓存)/, 'fast'],
274
274
  [/(格式化|代码风格|代码规范|类型检查)/, 'lint'],
275
275
  [/(界面|前端|样式|页面|组件|布局)/, 'design'],
276
+ // search: only unambiguous web/info search indicators — NOT code search (grep/find).
277
+ // "搜索" alone is ambiguous (code search vs web search), so require context modifiers.
278
+ [/(联网搜索|网上搜索|在线搜索|上网查|搜索.{0,2}最新|搜一下.{0,2}最新|查.{0,2}最新|查资料|找资料|搜索资料|搜索文档)/, 'search'],
279
+ [/\b(google|search\s+online|web\s+search|look\s+up\s+(?:the\s+)?(?:latest|newest|recent|docs?|documentation))\b/i, 'search'],
276
280
  ];
277
281
  // Pre-compile global variants for matchAll — avoids creating new RegExp on every extractIntent call
278
282
  return raw.map(([p, tag]) => [p, new RegExp(p.source, p.flags.includes('g') ? p.flags : p.flags + 'g'), tag]);
@@ -311,15 +315,19 @@ function extractIntent(prompt) {
311
315
  }
312
316
 
313
317
  const found = [];
318
+ const suppressed = [];
314
319
  for (const tag of tagMatched) {
315
320
  if (tagHasAffirmative.get(tag) && !found.includes(tag)) {
316
321
  found.push(tag);
322
+ } else if (!tagHasAffirmative.get(tag)) {
323
+ // Tag was matched but ALL instances were negated → suppress it.
324
+ // This feeds the text-fallback filter to prevent recommending negated resources.
325
+ suppressed.push(tag);
317
326
  }
318
327
  }
319
328
 
320
329
  // Distinguish test-running from test-writing: "run tests" / "npm test" / "运行测试" should NOT
321
330
  // trigger TDD recommendations. Only keep 'test' intent when the prompt implies *writing* tests.
322
- const suppressed = [];
323
331
  if (found.includes('test')) {
324
332
  const isRunning = _RUN_TEST.test(prompt) || _RUN_TEST_CJK.test(prompt);
325
333
  const isWriting = _WRITE_TEST.test(prompt) || _WRITE_TEST_CJK.test(prompt);
@@ -810,8 +818,13 @@ function applyAdoptionDecay(results, db) {
810
818
  */
811
819
  function passesConfidenceGate(results, signals) {
812
820
  // BM25 absolute minimum: filter weak text matches.
813
- // Stricter threshold for 3+ results (reliable IDF); gentler floor for 1-2 results.
814
- const minThreshold = results.length >= 3 ? BM25_MIN_THRESHOLD : 0.5;
821
+ // Threshold is relative to the top result's score to handle varying corpus sizes:
822
+ // small corpora (< 50 resources) naturally produce lower BM25 IDF values,
823
+ // so an absolute threshold would over-filter genuine matches.
824
+ const baseThreshold = results.length >= 3 ? BM25_MIN_THRESHOLD : 0.5;
825
+ const topScore = results.length > 0 ? Math.abs(results[0].composite_score ?? results[0].relevance ?? 0) : 0;
826
+ // Use the lower of: absolute threshold OR 30% of top score (corpus-size-adaptive floor)
827
+ const minThreshold = topScore > 0 ? Math.min(baseThreshold, topScore * 0.3) : baseThreshold;
815
828
  results = results.filter(r => {
816
829
  const raw = r.composite_score ?? r.relevance;
817
830
  if (raw === null || raw === undefined) return true; // no score → pass (pre-scored or synthetic result)
@@ -821,10 +834,15 @@ function passesConfidenceGate(results, signals) {
821
834
  // Gap check: if top-2 results are too close in score, the query is ambiguous.
822
835
  // This prevents recommending when multiple resources match equally well,
823
836
  // which usually means the match is incidental rather than precise.
837
+ // Skip the gap check when rawKeywords promoted #1 (keyword re-ranking changes order,
838
+ // so the BM25 gap no longer reflects true relevance — the keyword match is extra signal).
824
839
  if (results.length >= 2) {
825
840
  const top1 = Math.abs(results[0].composite_score ?? results[0].relevance ?? 0);
826
841
  const top2 = Math.abs(results[1].composite_score ?? results[1].relevance ?? 0);
827
- if (top1 > 0) {
842
+ // After keyword re-ranking, #1 may have lower raw BM25 than #2.
843
+ // The keyword match provides additional confidence, so skip the gap check.
844
+ const wasReRanked = signals?.rawKeywords?.length > 0 && top1 < top2;
845
+ if (!wasReRanked && top1 > 0) {
828
846
  const gapRatio = (top1 - top2) / top1;
829
847
  if (gapRatio < 0.2) {
830
848
  // Top-1 has no clear lead — ambiguous match, suppress recommendation
@@ -976,7 +994,24 @@ function decideTier(resource, signals) {
976
994
  // Normalize: typical good matches score 5-50, great matches 20+
977
995
  // Sigmoid-like mapping to 0-1 range
978
996
  const normalized = raw / (raw + 5.0); // 5→0.5, 10→0.67, 20→0.8, 50→0.91
979
- const confidence = Math.min(1.0, normalized + patternBoost * 0.3);
997
+
998
+ // Signal-based confidence floor: if the result passed structured intent matching
999
+ // + keyword re-ranking, BM25 score alone shouldn't downgrade to 'silent'.
1000
+ // Small corpora produce low BM25 scores even for strong matches.
1001
+ let signalBoost = 0;
1002
+ if (signals?.primaryIntent) {
1003
+ const tags = (resource.intent_tags || '').toLowerCase().split(/[\s,]+/);
1004
+ // Direct intent match: resource's intent_tags contain the detected primary intent.
1005
+ // Strong boost (0.3) ensures small-corpus matches still reach 'hint' tier.
1006
+ if (tags.includes(signals.primaryIntent)) signalBoost += 0.3;
1007
+ else signalBoost += 0.1;
1008
+ }
1009
+ if (signals?.rawKeywords?.length > 0) {
1010
+ const tags = (resource.intent_tags || '').toLowerCase();
1011
+ if (signals.rawKeywords.some(kw => tags.includes(kw))) signalBoost += 0.2;
1012
+ }
1013
+
1014
+ const confidence = Math.min(1.0, normalized + patternBoost * 0.3 + signalBoost);
980
1015
 
981
1016
  if (confidence >= 0.55) return 'full';
982
1017
  if (confidence >= 0.3) return 'hint';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,6 +23,7 @@ export const DISPATCH_SYNONYMS = {
23
23
  'plan': ['planning', 'architecture', 'spec', 'blueprint', 'rfc', 'proposal', 'roadmap'],
24
24
  'build': ['compile', 'bundle', 'webpack', 'vite', 'typescript', 'tsc', 'esbuild', 'rollup', 'parcel', 'babel', 'swc', 'transpile'],
25
25
  'lint': ['eslint', 'prettier', 'biome', 'stylelint', 'format', 'style'],
26
+ 'search': ['lookup', 'latest', 'best-practices', 'perplexity'],
26
27
  // Chinese intent mappings
27
28
  '清理': ['refactor', 'clean', 'lint', 'format', 'simplify'],
28
29
  '测试': ['test', 'testing', 'tdd', 'qa', 'spec', 'jest', 'vitest', 'pytest'],
@@ -44,6 +45,7 @@ export const DISPATCH_SYNONYMS = {
44
45
  '打包': ['bundle', 'build', 'webpack', 'vite'],
45
46
  '容器': ['docker', 'container', 'kubernetes', 'infrastructure'],
46
47
  '运维': ['devops', 'infrastructure', 'deploy', 'docker'],
48
+ '搜索': ['search', 'lookup', 'latest', 'perplexity'],
47
49
  };
48
50
 
49
51
  // ─── CJK Tokenization ───────────────────────────────────────────────────────
@@ -98,6 +100,9 @@ const CJK_INTENT_MAP = {
98
100
  '接口': 'api', '路由': 'route',
99
101
  // plan
100
102
  '规划': 'planning', '架构': 'architecture', '方案': 'plan', '设计方案': 'architecture',
103
+ // search — only web/info search, NOT code search (grep/find)
104
+ '联网搜索': 'search', '网上搜索': 'search', '查资料': 'search', '找资料': 'search',
105
+ '搜索最新': 'search', '搜索资料': 'search', '搜索文档': 'search',
101
106
  };
102
107
 
103
108
  // Merge all CJK keys from both maps, longest-first to avoid partial matches