@yolk_vat-y/dsh-project-memory 0.1.6 → 0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 (2026-08-27)
4
+
5
+ ### CJK 检索增强
6
+ - 链接侧:非拉丁符号名改用 CJK 边界正则 `(?<![CJK])名(?![CJK])`,解决 `用户服务` 误链 `用户服务管理器`;英文分支零改动
7
+ - 查询侧:BM25 增加精确短语乘法加分 —— 3+ 字 CJK 短语命中 `title`/`keywords` 时 `score *= 1.5` 并重排,自适应不压过高相关结果
8
+ - 查询侧:同义词表(`数据库连接池` ↔ `连接池` ↔ `DB pool`),查询展开后再走短语加分
9
+
10
+ ### 经验笔记 supersede 阈值收紧
11
+ - 双向重叠判定:`overlap / query_tokens ≥ 0.7` 且 `overlap / item_tokens ≥ 0.7`(原单向 0.6),减少短问题误吞长笔记
12
+
3
13
  ## 0.1.6 (2026-08-25)
4
14
 
5
15
  ### 存储
package/README.md CHANGED
@@ -149,7 +149,7 @@ These commands are for **maintaining the plugin code** — regular users do not
149
149
 
150
150
  ```bash
151
151
  npm install
152
- npm test # 101 checks: chunker / symbols / store / tools / BM25 / links / watch / lazy / config / dump / concurrency / restore / size limit
152
+ npm test # chunker / symbols / store / tools / BM25 / links / watch / lazy / config / dump / concurrency / restore / size limit
153
153
  ```
154
154
 
155
155
  ## License
package/README.zh-CN.md CHANGED
@@ -149,7 +149,7 @@ dsh web --patch ./config.yml
149
149
 
150
150
  ```bash
151
151
  npm install
152
- npm test # 101 项检查:chunker / symbols / store / tools / BM25 / links / watch / lazy / config / dump / concurrency / restore / size limit
152
+ npm test # 检查项:chunker / symbols / store / tools / BM25 / links / watch / lazy / config / dump / concurrency / restore / size limit
153
153
  ```
154
154
 
155
155
  ## 许可证
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolk_vat-y/dsh-project-memory",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Persistent project memory for dsh agents: index docs (PDF/Markdown/text) and code symbols into a searchable per-workspace store, recall them with cited sources, and keep experience entries (problems -> solutions) searchable on demand.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/link.js CHANGED
@@ -1,9 +1,13 @@
1
1
  const LATIN_NAME = /^[A-Za-z0-9_]+$/
2
+ const CJK_CHAR = /[\u3400-\u9fff\uf900-\ufaff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/
2
3
 
3
4
  function buildMatcher(name) {
4
5
  const lower = name.toLowerCase()
5
- if (!LATIN_NAME.test(name)) return { lower, re: null }
6
- return { lower, re: new RegExp(`(?<![a-z0-9_$])${lower}(?![a-z0-9_$])`) }
6
+ if (LATIN_NAME.test(name)) {
7
+ return { lower, re: new RegExp(`(?<![a-z0-9_$])${lower}(?![a-z0-9_$])`) }
8
+ }
9
+ const escaped = lower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
10
+ return { lower, re: new RegExp(`(?<!${CJK_CHAR.source})${escaped}(?!${CJK_CHAR.source})`) }
7
11
  }
8
12
 
9
13
  export function linkEntries(store) {
package/src/store.js CHANGED
@@ -303,8 +303,10 @@ export class ProjectMemoryStore {
303
303
  for (const item of this.experience) {
304
304
  const itemTokens = tokenize(item.problem)
305
305
  const overlap = itemTokens.filter((t) => tokens.includes(t)).length
306
- const base = Math.min(tokens.length, itemTokens.length)
307
- if (base && overlap / base >= 0.6 && overlap > bestOverlap) {
306
+ if (overlap === 0) continue
307
+ const ratioQuery = overlap / tokens.length
308
+ const ratioItem = overlap / itemTokens.length
309
+ if (ratioQuery >= 0.7 && ratioItem >= 0.7 && overlap > bestOverlap) {
308
310
  best = item
309
311
  bestOverlap = overlap
310
312
  }
@@ -1,6 +1,42 @@
1
1
  export const CJK_RANGE =
2
2
  /[\u3400-\u9fff\uf900-\ufaff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/
3
3
 
4
+ const SYNONYMS = new Map([
5
+ ['数据库连接池', ['连接池', 'DB pool', 'db pool']],
6
+ ['连接池', ['数据库连接池', 'DB pool', 'db pool']],
7
+ ['DB pool', ['数据库连接池', '连接池']],
8
+ ['db pool', ['数据库连接池', '连接池']],
9
+ ])
10
+
11
+ function extractCjkPhrases(text) {
12
+ const phrases = []
13
+ let run = ''
14
+ for (const ch of text) {
15
+ if (CJK_RANGE.test(ch)) {
16
+ run += ch
17
+ } else if (run.length >= 3) {
18
+ phrases.push(run)
19
+ run = ''
20
+ } else {
21
+ run = ''
22
+ }
23
+ }
24
+ if (run.length >= 3) phrases.push(run)
25
+ return phrases
26
+ }
27
+
28
+ function expandQuery(query) {
29
+ const lower = query.toLowerCase()
30
+ const expanded = new Set([lower])
31
+ for (const [key, vals] of SYNONYMS) {
32
+ if (lower.includes(key.toLowerCase())) {
33
+ for (const v of vals) expanded.add(v.toLowerCase())
34
+ }
35
+ }
36
+ const cjkPhrases = extractCjkPhrases(query)
37
+ return { original: lower, expanded: [...expanded], cjkPhrases }
38
+ }
39
+
4
40
  export function tokenizeRaw(text) {
5
41
  if (!text) return []
6
42
  const lower = text.toLowerCase()
@@ -43,7 +79,9 @@ export function buildBm25(docs, getFieldText) {
43
79
  const terms = tokenizeRaw(text)
44
80
  const tf = {}
45
81
  for (const t of terms) tf[t] = (tf[t] || 0) + 1
46
- return { doc, length: terms.length, tf }
82
+ const title = (doc.title || '').toLowerCase()
83
+ const keywords = (doc.keywords || []).join(' ').toLowerCase()
84
+ return { doc, length: terms.length, tf, title, keywords }
47
85
  })
48
86
  const df = {}
49
87
  for (const d of documents) {
@@ -55,7 +93,12 @@ export function buildBm25(docs, getFieldText) {
55
93
  return {
56
94
  idf,
57
95
  score(query) {
58
- const q = tokenize(query)
96
+ const { expanded, cjkPhrases } = expandQuery(query)
97
+ const qTokens = new Set()
98
+ for (const term of expanded) {
99
+ for (const tok of tokenizeRaw(term)) qTokens.add(tok)
100
+ }
101
+ const q = [...qTokens]
59
102
  if (!q.length) return []
60
103
  const avgLen = avgdl
61
104
  return documents
@@ -67,6 +110,12 @@ export function buildBm25(docs, getFieldText) {
67
110
  if (!tf) continue
68
111
  score += idf(t) * ((tf * (K1 + 1)) / (tf + K1 * (1 - B + (B * len) / avgLen)))
69
112
  }
113
+ for (const phrase of cjkPhrases) {
114
+ const lowerPhrase = phrase.toLowerCase()
115
+ if (d.title.includes(lowerPhrase) || d.keywords.includes(lowerPhrase)) {
116
+ score *= 1.5
117
+ }
118
+ }
70
119
  return { doc: d.doc, score }
71
120
  })
72
121
  .filter((r) => r.score > 0)