@yolk_vat-y/dsh-project-memory 0.5.4 → 0.5.6
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 +128 -0
- package/README.md +98 -42
- package/README.zh-CN.md +98 -42
- package/client/client.js +111 -92
- package/client/client.js.map +1 -1
- package/package.json +7 -2
- package/scripts/bench-store.mjs +113 -0
- package/scripts/bench-synthetic.mjs +364 -0
- package/scripts/bench.mjs +396 -0
- package/src/audit.js +87 -0
- package/src/auto-inject.js +367 -51
- package/src/client/MemoryView.tsx +4 -2
- package/src/client/TaskComponents.tsx +7 -6
- package/src/client/TaskPanel.module.css +7 -0
- package/src/client/TaskPanel.tsx +12 -8
- package/src/client/task-ui-store.ts +11 -1
- package/src/index.js +19 -2
- package/src/insight-store.js +26 -4
- package/src/ops.js +210 -0
- package/src/parsers/pdfjs-parser.js +3 -0
- package/src/readiness.js +468 -0
- package/src/recall.js +217 -0
- package/src/store.js +20 -1
- package/src/tools/index-repo.js +7 -1
- package/src/tools/lesson-tools.js +31 -4
- package/src/tools/query-memory.js +61 -23
- package/src/tools/watch-repo.js +10 -1
- package/src/util/fs.js +41 -0
- package/src/watch.js +28 -4
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolk_vat-y/dsh-project-memory",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
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",
|
|
7
7
|
"files": [
|
|
8
8
|
"src",
|
|
9
9
|
"client",
|
|
10
|
+
"scripts",
|
|
10
11
|
"cordis.patch.yml",
|
|
11
12
|
"README.md",
|
|
12
13
|
"README.zh-CN.md",
|
|
@@ -17,7 +18,11 @@
|
|
|
17
18
|
"url": "https://github.com/00080000/dsh-project-memory.git"
|
|
18
19
|
},
|
|
19
20
|
"scripts": {
|
|
20
|
-
"test": "node test/run-test.mjs && node test/taskbridge.test.mjs && node test/insight-store.test.mjs && node test/reflection-pipeline.test.mjs && node test/auto-inject.test.mjs && node test/insight-actions.test.mjs && node test/host-contract.test.mjs && node test/llm-route.test.mjs && node test/doc-index.test.mjs",
|
|
21
|
+
"test": "node test/run-test.mjs && node test/taskbridge.test.mjs && node test/insight-store.test.mjs && node test/reflection-pipeline.test.mjs && node test/auto-inject.test.mjs && node test/insight-actions.test.mjs && node test/host-contract.test.mjs && node test/llm-route.test.mjs && node test/doc-index.test.mjs && node test/client-hints.test.mjs && node test/recall.test.mjs && node test/readiness.test.mjs && node test/insight-derive.test.mjs && node test/readiness-eval.test.mjs && node test/ops.test.mjs && node test/injection-audit.test.mjs && node test/injection-budget.test.mjs && node test/injection-scenarios.test.mjs",
|
|
22
|
+
"eval:injection": "node test/injection-scenarios.test.mjs",
|
|
23
|
+
"selfcheck:triggers": "node test/injection-scenarios.test.mjs --selfcheck",
|
|
24
|
+
"bench": "node scripts/bench.mjs",
|
|
25
|
+
"bench:synthetic": "node scripts/bench-synthetic.mjs",
|
|
21
26
|
"build:client": "tsdown"
|
|
22
27
|
},
|
|
23
28
|
"keywords": [
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Benchmark the sharded store: cold-load time and hot-path (single lazy re-index) blocking time
|
|
3
|
+
// at a synthetic project scale. Usage: node scripts/bench-store.mjs [files=2000] [--keep] [--cold <dir>]
|
|
4
|
+
import { readdirSync, readFileSync, mkdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { performance } from 'node:perf_hooks'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { ProjectMemoryStore } from '../src/store.js'
|
|
10
|
+
import { scanSymbols } from '../src/symbols.js'
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2)
|
|
13
|
+
const mode = args.includes('--cold') ? 'cold' : 'full'
|
|
14
|
+
const keep = args.includes('--keep')
|
|
15
|
+
const fileCount = Number(args.find((a) => /^\d+$/.test(a))) || 2000
|
|
16
|
+
|
|
17
|
+
function synthFile(i) {
|
|
18
|
+
return [
|
|
19
|
+
`// module ${i}`,
|
|
20
|
+
`export const id${i} = ${i}`,
|
|
21
|
+
`export function compute${i}(a, b) {`,
|
|
22
|
+
` const mid = a * ${i} + b`,
|
|
23
|
+
` if (mid > 0) return mid - 1`,
|
|
24
|
+
` return fallback${i}(mid)`,
|
|
25
|
+
`}`,
|
|
26
|
+
`function fallback${i}(v) {`,
|
|
27
|
+
` return v ?? ${i}`,
|
|
28
|
+
`}`,
|
|
29
|
+
`export class Service${i} {`,
|
|
30
|
+
` handle(input) {`,
|
|
31
|
+
` return input + id${i}`,
|
|
32
|
+
` }`,
|
|
33
|
+
`}`,
|
|
34
|
+
].join('\n')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildTree(root, n) {
|
|
38
|
+
const files = []
|
|
39
|
+
const perDir = 50
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
const dir = path.join(root, 'src', 'mod' + Math.floor(i / perDir))
|
|
42
|
+
mkdirSync(dir, { recursive: true })
|
|
43
|
+
const p = path.join(dir, `file${i}.js`)
|
|
44
|
+
writeFileSync(p, synthFile(i))
|
|
45
|
+
files.push(p)
|
|
46
|
+
}
|
|
47
|
+
return files
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function median(xs) {
|
|
51
|
+
const s = [...xs].sort((a, b) => a - b)
|
|
52
|
+
return s[Math.floor(s.length / 2)]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (mode === 'cold') {
|
|
56
|
+
const dir = args[args.indexOf('--cold') + 1]
|
|
57
|
+
const t0 = performance.now()
|
|
58
|
+
const store = new ProjectMemoryStore(path.join(dir, '.dsh-project-memory')).load()
|
|
59
|
+
const stats = store.stats()
|
|
60
|
+
const ms = performance.now() - t0
|
|
61
|
+
console.log(JSON.stringify({ coldLoadMs: Number(ms.toFixed(1)), ...stats }))
|
|
62
|
+
process.exit(0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const root = mkdtempSync(path.join(tmpdir(), 'pm-bench-'))
|
|
66
|
+
process.stdout.write(`generating ${fileCount} files… `)
|
|
67
|
+
const tGen = performance.now()
|
|
68
|
+
const files = buildTree(root, fileCount)
|
|
69
|
+
console.log(`${(performance.now() - tGen).toFixed(0)}ms`)
|
|
70
|
+
|
|
71
|
+
const memDir = path.join(root, '.dsh-project-memory')
|
|
72
|
+
const store = new ProjectMemoryStore(memDir)
|
|
73
|
+
|
|
74
|
+
process.stdout.write('bulk cold index (scan + insert all, one save)… ')
|
|
75
|
+
const tBulk = performance.now()
|
|
76
|
+
for (const p of files) {
|
|
77
|
+
const entries = scanSymbols(p, readFileSync(p, 'utf8'))
|
|
78
|
+
const rel = path.relative(root, p)
|
|
79
|
+
store.markFile(rel, { sha256: `hash-${rel}`, size: 1, type: 'code', indexedAt: new Date().toISOString() })
|
|
80
|
+
store.setEntries(rel, entries)
|
|
81
|
+
}
|
|
82
|
+
store.save()
|
|
83
|
+
console.log(`${(performance.now() - tBulk).toFixed(0)}ms`)
|
|
84
|
+
|
|
85
|
+
let shardCount = 0
|
|
86
|
+
try {
|
|
87
|
+
shardCount = readdirSync(path.join(memDir, 'shards')).length
|
|
88
|
+
} catch {}
|
|
89
|
+
|
|
90
|
+
// 热路径:模拟单次懒索引(改内容 → 重扫描 → 脏分片落盘),全程同步阻塞
|
|
91
|
+
const samples = []
|
|
92
|
+
for (let round = 0; round < 30; round++) {
|
|
93
|
+
const p = files[round % files.length]
|
|
94
|
+
writeFileSync(p, synthFile(round % files.length) + `\n// touch ${round}\n`)
|
|
95
|
+
const rel = path.relative(root, p)
|
|
96
|
+
const t = performance.now()
|
|
97
|
+
const entries = scanSymbols(p, readFileSync(p, 'utf8'))
|
|
98
|
+
store.markFile(rel, { sha256: `hash-${rel}-${round}`, size: 1, type: 'code', indexedAt: new Date().toISOString() })
|
|
99
|
+
store.setEntries(rel, entries)
|
|
100
|
+
store.save()
|
|
101
|
+
samples.push(performance.now() - t)
|
|
102
|
+
}
|
|
103
|
+
console.log(`files indexed : ${store.stats().files}`)
|
|
104
|
+
console.log(`shard files : ${shardCount}`)
|
|
105
|
+
console.log(`hot lazy-index block: median ${median(samples).toFixed(2)}ms | max ${Math.max(...samples).toFixed(2)}ms (${samples.length} runs)`)
|
|
106
|
+
|
|
107
|
+
const { spawnSync } = await import('node:child_process')
|
|
108
|
+
const selfPath = fileURLToPath(import.meta.url)
|
|
109
|
+
const cold = spawnSync(process.execPath, [selfPath, '--cold', root], { encoding: 'utf8' })
|
|
110
|
+
console.log(`cold load : ${(cold.stdout.trim().split('\n').pop() || '').trim()}`)
|
|
111
|
+
|
|
112
|
+
if (!keep) rmSync(root, { recursive: true, force: true })
|
|
113
|
+
else console.log(`kept: ${root}`)
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-project-memory — 微基准测试(micro-benchmark)
|
|
3
|
+
*
|
|
4
|
+
* 直接驱动内核热路径(不经过 defineTool / HTTP / LLM):
|
|
5
|
+
* - store.js : load / commit / addExperience(findSupersede) / removeExperience
|
|
6
|
+
* - util/search.js : buildBm25 / rankEntriesStreaming / rankExperienceScored
|
|
7
|
+
* - symbols.js : scanSymbols(零 token 符号抽取,无 LLM)
|
|
8
|
+
* - link.js : linkEntries(doc↔symbol 交叉链接)
|
|
9
|
+
* - insight-store.js: GlobalStore read/write + saveInsight(归一化去重)
|
|
10
|
+
* - similarity.js : normalizedTokenOverlap(去重阈值判定)
|
|
11
|
+
*
|
|
12
|
+
* 用法: node scripts/bench-synthetic.mjs [files]
|
|
13
|
+
* files 可选,默认 5000(生成的代码文件数;每个文件约 4~5 条符号)。
|
|
14
|
+
* 数据全部在 OS 临时目录生成,不会写入项目本身。
|
|
15
|
+
*/
|
|
16
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readdirSync, readFileSync } from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
import { performance } from 'node:perf_hooks'
|
|
20
|
+
|
|
21
|
+
import { ProjectMemoryStore } from '../src/store.js'
|
|
22
|
+
import { walkDir, readFileForIndex, relativePath, storeKey, isSupportedCode } from '../src/util/fs.js'
|
|
23
|
+
import { scanSymbols } from '../src/symbols.js'
|
|
24
|
+
import { linkEntries } from '../src/link.js'
|
|
25
|
+
import { rankEntriesStreaming, rankExperienceScored, makeSearchText } from '../src/util/search.js'
|
|
26
|
+
import { GlobalStore, saveInsight, normalizeInsight } from '../src/insight-store.js'
|
|
27
|
+
import { normalizedTokenOverlap } from '../src/similarity.js'
|
|
28
|
+
|
|
29
|
+
const FILES = Number(process.argv[2] || 5000)
|
|
30
|
+
|
|
31
|
+
// 记录所有临时目录,结束时统一清理
|
|
32
|
+
const createdDirs = []
|
|
33
|
+
function tmpd(prefix) {
|
|
34
|
+
const d = mkdtempSync(path.join(tmpdir(), prefix))
|
|
35
|
+
createdDirs.push(d)
|
|
36
|
+
return d
|
|
37
|
+
}
|
|
38
|
+
function cleanup() {
|
|
39
|
+
for (const d of createdDirs) {
|
|
40
|
+
try { rmSync(d, { recursive: true, force: true }) } catch { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- 统计 ----
|
|
45
|
+
function stats(name, times) {
|
|
46
|
+
times.sort((a, b) => a - b)
|
|
47
|
+
const avg = times.reduce((s, t) => s + t, 0) / times.length
|
|
48
|
+
const p50 = times[Math.floor(times.length * 0.5)]
|
|
49
|
+
const p95 = times[Math.min(times.length - 1, Math.floor(times.length * 0.95))]
|
|
50
|
+
const max = times[times.length - 1]
|
|
51
|
+
return { name, avg, p50, p95, max, n: times.length }
|
|
52
|
+
}
|
|
53
|
+
function show(label, s) {
|
|
54
|
+
const f = (x) => x.toFixed(2)
|
|
55
|
+
console.log(`${label.padEnd(46)} avg=${f(s.avg).padStart(8)}ms p50=${f(s.p50).padStart(8)}ms p95=${f(s.p95).padStart(8)}ms max=${f(s.max).padStart(8)}ms (n=${s.n})`)
|
|
56
|
+
return s
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---- 生成合成代码语料 ----
|
|
60
|
+
function generateCorpus(root, files) {
|
|
61
|
+
const src = path.join(root, 'src')
|
|
62
|
+
mkdirSync(src, { recursive: true })
|
|
63
|
+
const writes = []
|
|
64
|
+
for (let i = 0; i < files; i++) {
|
|
65
|
+
const name = `mod_${i}`
|
|
66
|
+
const body = [
|
|
67
|
+
`import { dep${i} } from './dep'`,
|
|
68
|
+
`/** doc for ${name} */`,
|
|
69
|
+
`export class ${cap(name)} {`,
|
|
70
|
+
` methodA$${i}(a${i}, b${i}) { if (a${i}) return b${i}; return null }`,
|
|
71
|
+
` methodB$${i}(c${i}) { return c${i} ?? dep${i} }`,
|
|
72
|
+
`}`,
|
|
73
|
+
`export function helper$${i}(arg${i}) { return arg${i} * 2 }`,
|
|
74
|
+
`export function util$${i}(p${i}, q${i}) { return p${i} + q${i} - dep${i} }`,
|
|
75
|
+
].join('\n')
|
|
76
|
+
writes.push({ p: path.join(src, `${name}.js`), body })
|
|
77
|
+
}
|
|
78
|
+
// 写盘(大量小文件)
|
|
79
|
+
for (const w of writes) writeFileSync(w.p, w.body)
|
|
80
|
+
return src
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cap(s) {
|
|
84
|
+
return s.replace(/^\w/, (c) => c.toUpperCase())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- 与 indexRepository 相同的内核索引路径(去掉 LLM/doc 分支,纯 code symbol) ----
|
|
88
|
+
async function coldIndex(root, config) {
|
|
89
|
+
const storeDir = path.join(root, config.memoryDir)
|
|
90
|
+
const store = new ProjectMemoryStore(storeDir)
|
|
91
|
+
// 首次 load 会读盘(若已存在),这里传入一个全新实例以贴近真实冷启动
|
|
92
|
+
store.load()
|
|
93
|
+
const files = walkDir(root)
|
|
94
|
+
const seen = new Set()
|
|
95
|
+
let updated = 0
|
|
96
|
+
const fileUpdates = []
|
|
97
|
+
for (const filePath of files) {
|
|
98
|
+
const rel = storeKey(relativePath(root, filePath))
|
|
99
|
+
seen.add(rel)
|
|
100
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
101
|
+
if (!isSupportedCode(ext)) continue
|
|
102
|
+
const existing = store.fileRecord(rel)
|
|
103
|
+
const { hash, size, buffer } = readFileForIndex(filePath)
|
|
104
|
+
const entries = scanSymbols(rel, filePath, buffer.toString('utf8'))
|
|
105
|
+
fileUpdates.push({ rel, expectedHash: existing?.sha256, hash, size, entries, type: 'code' })
|
|
106
|
+
updated++
|
|
107
|
+
}
|
|
108
|
+
const report = store.commit((s) => {
|
|
109
|
+
for (const u of fileUpdates) s.applyFileUpdate(u.rel, u)
|
|
110
|
+
for (const rel of Object.keys(s.files)) if (!seen.has(rel)) s.removeFile(rel)
|
|
111
|
+
linkEntries(s)
|
|
112
|
+
return s.stats()
|
|
113
|
+
})
|
|
114
|
+
return { report, store }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- 热环境查询:读盘一次后用内存 store + IDF 缓存反复检索 ----
|
|
118
|
+
function warmStoreFrom(root, config) {
|
|
119
|
+
const store = new ProjectMemoryStore(path.join(root, config.memoryDir)).load()
|
|
120
|
+
return store
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function main() {
|
|
124
|
+
const config = {
|
|
125
|
+
memoryDir: '.dsh-project-memory',
|
|
126
|
+
chunkChars: 3000,
|
|
127
|
+
maxChunksPerFile: 40,
|
|
128
|
+
maxFileSizeMb: 50,
|
|
129
|
+
maxOutputChars: 8000,
|
|
130
|
+
maxPdfPages: 1000,
|
|
131
|
+
llmQueryExpansion: false,
|
|
132
|
+
expansionCount: 6,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log('='.repeat(78))
|
|
136
|
+
console.log('dsh-project-memory 内核微基准测试')
|
|
137
|
+
console.log(` node ${process.version} · ${process.platform}/${process.arch} · ${process.cwd()}`)
|
|
138
|
+
console.log(` synched corpus: ${FILES} 代码文件(每文件约 4~5 条符号)`)
|
|
139
|
+
console.log('='.repeat(78))
|
|
140
|
+
|
|
141
|
+
// 生成语料
|
|
142
|
+
const targetRoot = tmpd('pm-bench-')
|
|
143
|
+
const genT0 = performance.now()
|
|
144
|
+
generateCorpus(targetRoot, FILES)
|
|
145
|
+
const genMs = performance.now() - genT0
|
|
146
|
+
console.log(`\n[语料生成] ${FILES} 文件写盘耗时 ${genMs.toFixed(0)} ms`)
|
|
147
|
+
|
|
148
|
+
// ---------- 1. 冷索引(完整内核管线,无 LLM)----------
|
|
149
|
+
console.log('\n--- 1. 冷索引 index_repo 内核管线(walk + sha256 + scanSymbols + linkEntries + commit)---')
|
|
150
|
+
const idxTimes = []
|
|
151
|
+
let entriesCount = 0
|
|
152
|
+
for (let r = 0; r < 3; r++) {
|
|
153
|
+
const root = tmpd('pm-idx-')
|
|
154
|
+
generateCorpus(root, FILES)
|
|
155
|
+
const t0 = performance.now()
|
|
156
|
+
const { report } = await coldIndex(root, config)
|
|
157
|
+
const dt = performance.now() - t0
|
|
158
|
+
idxTimes.push(dt)
|
|
159
|
+
entriesCount = report.entries
|
|
160
|
+
}
|
|
161
|
+
show('cold index (full pipeline)', stats('idx', idxTimes))
|
|
162
|
+
console.log(` -> ${FILES} files, ${entriesCount} entries in store`)
|
|
163
|
+
|
|
164
|
+
// ---------- 2. 冷加载(首次 load 读盘 shards)----------
|
|
165
|
+
console.log('\n--- 2. 冷加载 store.load()(首次读盘,sharded 布局)---')
|
|
166
|
+
{
|
|
167
|
+
const root = tmpd('pm-cld-')
|
|
168
|
+
generateCorpus(root, FILES)
|
|
169
|
+
await coldIndex(root, config)
|
|
170
|
+
const times = []
|
|
171
|
+
for (let r = 0; r < 5; r++) {
|
|
172
|
+
// 拷贝到新路径,绕开进程内 storeCache,测真实读盘
|
|
173
|
+
const copy = tmpd('pm-cldcp-')
|
|
174
|
+
const srcDir = path.join(root, config.memoryDir)
|
|
175
|
+
const dstDir = path.join(copy, config.memoryDir)
|
|
176
|
+
mkdirSync(dstDir, { recursive: true })
|
|
177
|
+
for (const f of readdirSync(srcDir)) {
|
|
178
|
+
if (f === 'shards') {
|
|
179
|
+
mkdirSync(path.join(dstDir, 'shards'), { recursive: true })
|
|
180
|
+
for (const sf of readdirSync(path.join(srcDir, 'shards'))) {
|
|
181
|
+
writeFileSync(path.join(dstDir, 'shards', sf), readFileSync(path.join(srcDir, 'shards', sf)))
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
writeFileSync(path.join(dstDir, f), readFileSync(path.join(srcDir, f)))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const t0 = performance.now()
|
|
188
|
+
const s = new ProjectMemoryStore(dstDir).load()
|
|
189
|
+
times.push(performance.now() - t0)
|
|
190
|
+
s.stats()
|
|
191
|
+
}
|
|
192
|
+
show('cold load store (first read)', stats('coldload', times))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- 3. 热环境查询(IDF 缓存 + 单遍流式打分)----------
|
|
196
|
+
console.log('\n--- 3. query_memory 热路径(getIdfCache + rankEntriesStreaming + CJK phrase boost)---')
|
|
197
|
+
{
|
|
198
|
+
const root = tmpd('pm-q-')
|
|
199
|
+
generateCorpus(root, FILES)
|
|
200
|
+
const { store } = await coldIndex(root, config)
|
|
201
|
+
// 预取一次 IDF(首次查询需要重建,后续命中缓存)
|
|
202
|
+
store.getIdfCache()
|
|
203
|
+
const all = store.allEntries()
|
|
204
|
+
const queries = ['methodA return', 'helper util dep', 'export class', 'sum addition', 'null coalesce']
|
|
205
|
+
const times = []
|
|
206
|
+
const n = Math.min(200, FILES)
|
|
207
|
+
for (let r = 0; r < n; r++) {
|
|
208
|
+
const q = queries[r % queries.length]
|
|
209
|
+
const idf = store.getIdfCache()
|
|
210
|
+
const t0 = performance.now()
|
|
211
|
+
rankEntriesStreaming(all, [q], idf, 8)
|
|
212
|
+
times.push(performance.now() - t0)
|
|
213
|
+
}
|
|
214
|
+
show(`query_memory cached (${all.length} entries)`, stats('q', times))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------- 4. ID F 重建(写库后首次查询 / version bump 后失效)----------
|
|
218
|
+
console.log('\n--- 4. query_memory 冷查询(IDF 重建 + 检索,首次命中前)---')
|
|
219
|
+
{
|
|
220
|
+
const root = tmpd('pm-qc-')
|
|
221
|
+
generateCorpus(root, FILES)
|
|
222
|
+
const { store } = await coldIndex(root, config)
|
|
223
|
+
const all = store.allEntries()
|
|
224
|
+
store._idfCache = null // 模拟缓存失效(写库后 version bump)
|
|
225
|
+
const t0 = performance.now()
|
|
226
|
+
const idf = store.getIdfCache()
|
|
227
|
+
rankEntriesStreaming(all, ['methodA'], idf, 8)
|
|
228
|
+
const dt = performance.now() - t0
|
|
229
|
+
console.log(` cold query (IDF rebuild + rank) avg=${dt.toFixed(2)}ms (${all.length} entries)`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------- 5. symbol 抽取吞吐(单一文件)----------
|
|
233
|
+
console.log('\n--- 5. scanSymbols 符号抽取吞吐(零 token,无 LLM)---')
|
|
234
|
+
{
|
|
235
|
+
const root = tmpd('pm-sym-')
|
|
236
|
+
generateCorpus(root, FILES)
|
|
237
|
+
const files = walkDir(root).filter((f) => f.endsWith('.js'))
|
|
238
|
+
const times = []
|
|
239
|
+
const n = Math.min(files.length, 500)
|
|
240
|
+
for (let i = 0; i < n; i++) {
|
|
241
|
+
const f = files[i]
|
|
242
|
+
const content = readFileSync(f, 'utf8')
|
|
243
|
+
const t0 = performance.now()
|
|
244
|
+
scanSymbols(relativePath(root, f), f, content)
|
|
245
|
+
times.push(performance.now() - t0)
|
|
246
|
+
}
|
|
247
|
+
const a = times.reduce((s, t) => s + t, 0) / times.length
|
|
248
|
+
show(`scanSymbols per file (${n} files)`, stats('sym', times))
|
|
249
|
+
console.log(` -> ${(a * 1000).toFixed(1)} µs/file`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---------- 6. 热惰性重索引(改一个文件 → 重新抽取 + commit)----------
|
|
253
|
+
console.log('\n--- 6. 热惰性重索引:单文件变更(lazyIndexing 常规路径,已加载 store)---')
|
|
254
|
+
{
|
|
255
|
+
const root = tmpd('pm-hot-')
|
|
256
|
+
generateCorpus(root, FILES)
|
|
257
|
+
const { store, report } = await coldIndex(root, config)
|
|
258
|
+
void report
|
|
259
|
+
const files = walkDir(root).filter((f) => f.endsWith('.js'))
|
|
260
|
+
const times = []
|
|
261
|
+
const n = Math.min(files.length, 50)
|
|
262
|
+
for (let i = 0; i < n; i++) {
|
|
263
|
+
const f = files[i]
|
|
264
|
+
const rel = storeKey(relativePath(root, f))
|
|
265
|
+
const { hash, size, buffer } = readFileForIndex(f)
|
|
266
|
+
const entries = scanSymbols(rel, f, buffer.toString('utf8'))
|
|
267
|
+
const t0 = performance.now()
|
|
268
|
+
store.commit((s) => s.applyFileUpdate(rel, { expectedHash: hash, hash, size, entries, type: 'code' }))
|
|
269
|
+
times.push(performance.now() - t0)
|
|
270
|
+
}
|
|
271
|
+
show(`hot re-index single file (${n} files)`, stats('hot', times))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------- 7. insight 存储:写入 + 读取 ----------
|
|
275
|
+
console.log('\n--- 7. insight-store:GlobalStore 读写 + saveInsight 去重 ---')
|
|
276
|
+
{
|
|
277
|
+
const root = tmpd('pm-ins-')
|
|
278
|
+
const globalFile = path.join(root, 'global.json')
|
|
279
|
+
const cfg = { insight: { dedupOverlap: 0.7, reinforceBand: 0.65 } }
|
|
280
|
+
|
|
281
|
+
const wTimes = []
|
|
282
|
+
for (let r = 0; r < 5; r++) {
|
|
283
|
+
const gs = new GlobalStore(globalFile).load()
|
|
284
|
+
const t0 = performance.now()
|
|
285
|
+
for (let i = 0; i < 100; i++) {
|
|
286
|
+
const raw = normalizeInsight({ kind: 'lesson', title: `Lesson ${i}`, pattern: `JWT token validation error ${i}`, fix: `Use jose ${i}` }, { scope: 'global' })
|
|
287
|
+
const res = saveInsight({ store: { insightItems: () => [], replaceInsightItems: () => {} }, globalStore: gs, raw, scope: 'global', cfg })
|
|
288
|
+
}
|
|
289
|
+
gs.commit()
|
|
290
|
+
wTimes.push(performance.now() - t0)
|
|
291
|
+
}
|
|
292
|
+
show('saveInsight 100 条 (含去重)', stats('ins-w', wTimes))
|
|
293
|
+
|
|
294
|
+
const rTimes = []
|
|
295
|
+
const gs = new GlobalStore(globalFile).load()
|
|
296
|
+
for (let r = 0; r < 10; r++) {
|
|
297
|
+
const t0 = performance.now()
|
|
298
|
+
const items = gs.items()
|
|
299
|
+
void items.length
|
|
300
|
+
rTimes.push(performance.now() - t0)
|
|
301
|
+
}
|
|
302
|
+
show('globalStore items() 读取', stats('ins-r', rTimes))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------- 7. 去重/相似度(save_lesson 命中判断)----------
|
|
306
|
+
console.log('\n--- 8. normalizedTokenOverlap 去重判定(候选池)---')
|
|
307
|
+
{
|
|
308
|
+
const items = []
|
|
309
|
+
for (let i = 0; i < 1000; i++) {
|
|
310
|
+
items.push({ id: `ins_${i}`, title: `Lesson ${i}`, pattern: `JWT token validation error ${i}`, fix: `Use jose library ${i}`, scope: 'global', kind: 'lesson', archived: false })
|
|
311
|
+
}
|
|
312
|
+
// 镜像 findBestOverlapMatch:全量扫描 1000 候选,找最高 overlap(无早退)
|
|
313
|
+
const times = []
|
|
314
|
+
let bestSeen = 0
|
|
315
|
+
const warmQ = `${items[123].title} ${items[123].pattern} ${items[123].fix}`
|
|
316
|
+
normalizedTokenOverlap(warmQ, `${items[123].pattern} ${items[123].fix}`) // JIT 预热
|
|
317
|
+
for (let r = 0; r < 200; r++) {
|
|
318
|
+
const t0 = performance.now()
|
|
319
|
+
let bestScore = 0
|
|
320
|
+
for (const it of items) {
|
|
321
|
+
const sc = normalizedTokenOverlap(warmQ, `${it.title} ${it.pattern} ${it.fix}`)
|
|
322
|
+
if (sc > bestScore) bestScore = sc
|
|
323
|
+
}
|
|
324
|
+
if (bestScore > bestSeen) bestSeen = bestScore
|
|
325
|
+
times.push(performance.now() - t0)
|
|
326
|
+
}
|
|
327
|
+
show('dedupe full scan over 1000 candidates', stats('dedupe', times))
|
|
328
|
+
console.log(` -> 候选池 1000,最高 overlap ≈ ${Math.round(bestSeen * 100)}%`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------- 8. remember(addExperience + findSupersede)----------
|
|
332
|
+
console.log('\n--- 9. remember / addExperience(problem→solution,双向 0.7 去重)---')
|
|
333
|
+
{
|
|
334
|
+
const root = tmpd('pm-exp-')
|
|
335
|
+
const store = new ProjectMemoryStore(path.join(root, config.memoryDir)).load()
|
|
336
|
+
// 用互异的 problem(含随机 token)让经验池真实增长,避免互相 supersede 合并成一条
|
|
337
|
+
const times = []
|
|
338
|
+
const total = []
|
|
339
|
+
for (let i = 0; i < 1000; i++) {
|
|
340
|
+
// 每个 problem 含两个高熵 token,使公共词只占 <0.7,池真实增长而非互相 supersede
|
|
341
|
+
const uniq = Math.floor(Math.random() * 1e9).toString(36)
|
|
342
|
+
const rand = Math.random().toString(36).slice(2, 10)
|
|
343
|
+
const t0 = performance.now()
|
|
344
|
+
store.addExperience({ problem: `payment gateway timeout ${uniq} ${rand}`, solution: `add exponential backoff ${uniq}` })
|
|
345
|
+
const dt = performance.now() - t0
|
|
346
|
+
times.push(dt)
|
|
347
|
+
total.push(dt)
|
|
348
|
+
}
|
|
349
|
+
show('addExperience 单条 (含去重扫描,池增长)', stats('exp', times))
|
|
350
|
+
console.log(` -> 池增长到 ${store.experience.length} 条;1000 次累计 ${total.reduce((s, t) => s + t, 0).toFixed(1)} ms`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 清理
|
|
354
|
+
cleanup()
|
|
355
|
+
|
|
356
|
+
console.log('\n' + '='.repeat(78))
|
|
357
|
+
console.log('完成。以上均为纯内核路径,不含 LLM 摘要 / PDF 解析 / defineTool 包装。')
|
|
358
|
+
console.log('='.repeat(78))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
main().catch((e) => {
|
|
362
|
+
console.error(e)
|
|
363
|
+
process.exit(1)
|
|
364
|
+
})
|