@yolk_vat-y/dsh-project-memory 0.3.0 → 0.3.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.
- package/CHANGELOG.md +33 -1
- package/README.md +22 -16
- package/README.zh-CN.md +21 -15
- package/package.json +11 -6
- package/src/doc-pipeline.js +14 -5
- package/src/enhancer.js +444 -0
- package/src/index.js +8 -0
- package/src/lazy.js +5 -3
- package/src/llm.js +12 -5
- package/src/symbols.js +241 -30
- package/src/tools/index-doc.js +1 -1
- package/src/tools/index-repo.js +16 -3
- package/src/tools/query-memory.js +9 -4
- package/src/util/fs.js +1 -1
- package/src/watch.js +10 -6
package/src/enhancer.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { linkEntries } from './link.js'
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
let ts = null
|
|
10
|
+
let tsPath = null
|
|
11
|
+
let tsVersion = null
|
|
12
|
+
|
|
13
|
+
function acceptTsVersion(mod) {
|
|
14
|
+
if (mod && mod.version && (mod.version.startsWith('6.') || mod.version.startsWith('7.'))) {
|
|
15
|
+
console.warn(`[dsh-project-memory] TypeScript ${mod.version} 不受支持,回退到 L1 正则。请使用 TS 5.x 获得增强类型。`)
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
return mod
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function initTypeScript(config) {
|
|
22
|
+
if (config?.enableTypeScript === false) return null
|
|
23
|
+
if (ts) return ts
|
|
24
|
+
|
|
25
|
+
// 1. 配置指定路径
|
|
26
|
+
if (config?.tsPath) {
|
|
27
|
+
try {
|
|
28
|
+
const req = createRequire(config.tsPath)
|
|
29
|
+
const mod = acceptTsVersion(req(config.tsPath))
|
|
30
|
+
if (mod) {
|
|
31
|
+
ts = mod
|
|
32
|
+
tsPath = config.tsPath
|
|
33
|
+
tsVersion = mod.version
|
|
34
|
+
return ts
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.warn(`[dsh-project-memory] tsPath 无效: ${config.tsPath}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. 从用户项目 cwd 向上查找 node_modules/typescript
|
|
42
|
+
try {
|
|
43
|
+
const req = createRequire(process.cwd() + '/')
|
|
44
|
+
const mod = acceptTsVersion(req('typescript'))
|
|
45
|
+
if (mod) {
|
|
46
|
+
ts = mod
|
|
47
|
+
tsPath = req.resolve('typescript')
|
|
48
|
+
tsVersion = mod.version
|
|
49
|
+
return ts
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
// 3. 全局(从插件自身位置解析)
|
|
54
|
+
try {
|
|
55
|
+
const mod = acceptTsVersion(require('typescript'))
|
|
56
|
+
if (mod) {
|
|
57
|
+
ts = mod
|
|
58
|
+
tsPath = require.resolve('typescript')
|
|
59
|
+
tsVersion = mod.version
|
|
60
|
+
return ts
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
|
|
64
|
+
ts = null
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getTypeScript() {
|
|
69
|
+
return ts
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function hasTypeScript() {
|
|
73
|
+
return ts !== null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getTsPath() {
|
|
77
|
+
return tsPath
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getTsVersion() {
|
|
81
|
+
return tsVersion
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const PRIORITY = {
|
|
85
|
+
ACTIVE: 0,
|
|
86
|
+
RECENT: 1,
|
|
87
|
+
BATCH: 2,
|
|
88
|
+
BACKLOG: 3
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const enhanceQueue = []
|
|
92
|
+
let processing = false
|
|
93
|
+
|
|
94
|
+
function getCacheDirForRoot(root, config) {
|
|
95
|
+
return join(root, config.memoryDir || '.dsh-project-memory', 'type-cache')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getCacheKey(content) {
|
|
99
|
+
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16)
|
|
100
|
+
return hash
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loadTypeCache(cacheDir, key) {
|
|
104
|
+
const file = join(cacheDir, `${key}.json`)
|
|
105
|
+
if (!existsSync(file)) return null
|
|
106
|
+
try {
|
|
107
|
+
const data = JSON.parse(readFileSync(file, 'utf8'))
|
|
108
|
+
return data
|
|
109
|
+
} catch {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function saveTypeCache(cacheDir, key, data) {
|
|
115
|
+
try {
|
|
116
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true })
|
|
117
|
+
const file = join(cacheDir, `${key}.json`)
|
|
118
|
+
writeFileSync(file, JSON.stringify(data))
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isTypeScriptFile(filePath) {
|
|
123
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase()
|
|
124
|
+
return ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' ||
|
|
125
|
+
ext === '.mjs' || ext === '.cjs' || ext === '.mts' || ext === '.cts'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function deepParseWithTS(filePath, content) {
|
|
129
|
+
if (!ts) return null
|
|
130
|
+
|
|
131
|
+
// Create a compiler host that provides the source file from memory
|
|
132
|
+
const host = {
|
|
133
|
+
getSourceFile: (fileName, languageVersion, onError) => {
|
|
134
|
+
if (fileName === filePath) {
|
|
135
|
+
return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)
|
|
136
|
+
}
|
|
137
|
+
return undefined
|
|
138
|
+
},
|
|
139
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFileName(options),
|
|
140
|
+
getDefaultLibLocation: () => ts.getDefaultLibFilePath({}),
|
|
141
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
142
|
+
getCurrentDirectory: () => process.cwd(),
|
|
143
|
+
getNewLine: () => '\n',
|
|
144
|
+
useCaseSensitiveFileNames: () => true,
|
|
145
|
+
fileExists: (fileName) => fileName === filePath,
|
|
146
|
+
readFile: (fileName) => fileName === filePath ? content : undefined,
|
|
147
|
+
directoryExists: () => true,
|
|
148
|
+
getDirectories: () => [],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const program = ts.createProgram([filePath], {}, host)
|
|
152
|
+
const sourceFile = program.getSourceFile(filePath)
|
|
153
|
+
if (!sourceFile) return []
|
|
154
|
+
const checker = program.getTypeChecker()
|
|
155
|
+
|
|
156
|
+
const symbols = []
|
|
157
|
+
|
|
158
|
+
function getSignature(node) {
|
|
159
|
+
try {
|
|
160
|
+
return checker.getSignatureFromDeclaration(node)
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getReturnType(signature) {
|
|
167
|
+
if (!signature) return null
|
|
168
|
+
try {
|
|
169
|
+
return checker.getReturnTypeOfSignature(signature)
|
|
170
|
+
} catch {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getTypeStr(type) {
|
|
176
|
+
if (!type) return 'void'
|
|
177
|
+
try {
|
|
178
|
+
return checker.typeToString(type)
|
|
179
|
+
} catch {
|
|
180
|
+
return 'any'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getLine(node) {
|
|
185
|
+
const pos = node.getStart()
|
|
186
|
+
if (pos < 0) return 1
|
|
187
|
+
return sourceFile.getLineAndCharacterOfPosition(pos).line + 1
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function visit(node) {
|
|
191
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
192
|
+
const signature = getSignature(node)
|
|
193
|
+
const returnType = getReturnType(signature)
|
|
194
|
+
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
195
|
+
const params = node.parameters.map(p => {
|
|
196
|
+
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
197
|
+
return `${p.name.getText()}: ${type}`
|
|
198
|
+
})
|
|
199
|
+
const returnTypeStr = getTypeStr(returnType)
|
|
200
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
201
|
+
symbols.push({
|
|
202
|
+
name: node.name.getText(),
|
|
203
|
+
kind: 'function',
|
|
204
|
+
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
205
|
+
line: getLine(node)
|
|
206
|
+
})
|
|
207
|
+
} else if (ts.isMethodDeclaration(node) && node.name) {
|
|
208
|
+
const signature = getSignature(node)
|
|
209
|
+
const returnType = getReturnType(signature)
|
|
210
|
+
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
211
|
+
const params = node.parameters.map(p => {
|
|
212
|
+
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
213
|
+
return `${p.name.getText()}: ${type}`
|
|
214
|
+
})
|
|
215
|
+
const returnTypeStr = getTypeStr(returnType)
|
|
216
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
217
|
+
symbols.push({
|
|
218
|
+
name: node.name.getText(),
|
|
219
|
+
kind: 'method',
|
|
220
|
+
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
221
|
+
line: getLine(node)
|
|
222
|
+
})
|
|
223
|
+
} else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
224
|
+
const parent = node.parent
|
|
225
|
+
if (parent && (ts.isVariableDeclaration(parent) || ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent))) {
|
|
226
|
+
const signature = getSignature(node)
|
|
227
|
+
const returnType = getReturnType(signature)
|
|
228
|
+
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
229
|
+
const params = node.parameters.map(p => {
|
|
230
|
+
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
231
|
+
return `${p.name.getText()}: ${type}`
|
|
232
|
+
})
|
|
233
|
+
const returnTypeStr = getTypeStr(returnType)
|
|
234
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
235
|
+
const name = parent.name?.getText() || '(anonymous)'
|
|
236
|
+
const kind = ts.isArrowFunction(node) ? 'arrow' : 'function'
|
|
237
|
+
symbols.push({
|
|
238
|
+
name,
|
|
239
|
+
kind,
|
|
240
|
+
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
241
|
+
line: getLine(node)
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
245
|
+
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
246
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
247
|
+
symbols.push({
|
|
248
|
+
name: node.name.getText(),
|
|
249
|
+
kind: 'class',
|
|
250
|
+
typeSig: generics,
|
|
251
|
+
line: getLine(node)
|
|
252
|
+
})
|
|
253
|
+
ts.forEachChild(node, visit)
|
|
254
|
+
} else if (ts.isInterfaceDeclaration(node)) {
|
|
255
|
+
const members = node.members.map(m => {
|
|
256
|
+
// Skip index signatures (m.name is undefined for index signatures like [key: string]: T)
|
|
257
|
+
if (!m.name) return null
|
|
258
|
+
const type = m.type ? getTypeStr(checker.getTypeAtLocation(m.type)) : 'any'
|
|
259
|
+
return `${m.name.getText()}: ${type}`
|
|
260
|
+
}).filter(Boolean).join('; ')
|
|
261
|
+
symbols.push({
|
|
262
|
+
name: node.name.getText(),
|
|
263
|
+
kind: 'interface',
|
|
264
|
+
typeSig: `{ ${members} }`,
|
|
265
|
+
line: getLine(node)
|
|
266
|
+
})
|
|
267
|
+
} else if (ts.isTypeAliasDeclaration(node)) {
|
|
268
|
+
const typeStr = node.type ? getTypeStr(checker.getTypeAtLocation(node.type)) : 'any'
|
|
269
|
+
symbols.push({
|
|
270
|
+
name: node.name.getText(),
|
|
271
|
+
kind: 'type',
|
|
272
|
+
typeSig: `= ${typeStr}`,
|
|
273
|
+
line: getLine(node)
|
|
274
|
+
})
|
|
275
|
+
} else {
|
|
276
|
+
ts.forEachChild(node, visit)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const sf = program.getSourceFile(filePath)
|
|
281
|
+
if (sf) {
|
|
282
|
+
visit(sf)
|
|
283
|
+
}
|
|
284
|
+
return symbols
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function enqueueEnhance(store, relPath, filePath, priority = PRIORITY.BATCH, config, root) {
|
|
288
|
+
if (!ts) return Promise.resolve()
|
|
289
|
+
|
|
290
|
+
let content
|
|
291
|
+
try {
|
|
292
|
+
content = readFileSync(filePath, 'utf8')
|
|
293
|
+
} catch {
|
|
294
|
+
// File disappeared between detection and enqueue - skip silently
|
|
295
|
+
return Promise.resolve()
|
|
296
|
+
}
|
|
297
|
+
const cacheKey = getCacheKey(content)
|
|
298
|
+
|
|
299
|
+
// Dedupe by (relPath, contentHash) - if same content, skip; if different, replace
|
|
300
|
+
const existingIdx = enhanceQueue.findIndex(q => q.relPath === relPath)
|
|
301
|
+
if (existingIdx >= 0) {
|
|
302
|
+
const existing = enhanceQueue[existingIdx]
|
|
303
|
+
if (existing.cacheKey === cacheKey) {
|
|
304
|
+
// Same content, just update priority if higher
|
|
305
|
+
if (priority < existing.priority) {
|
|
306
|
+
enhanceQueue[existingIdx].priority = priority
|
|
307
|
+
enhanceQueue.sort((a, b) => a.priority - b.priority)
|
|
308
|
+
}
|
|
309
|
+
return existing.promise
|
|
310
|
+
} else {
|
|
311
|
+
// Content changed - replace the task
|
|
312
|
+
enhanceQueue[existingIdx] = { relPath, filePath, priority, cacheKey, promise: null }
|
|
313
|
+
// Will create new promise below
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const p = (async () => {
|
|
318
|
+
try {
|
|
319
|
+
const content = readFileSync(filePath, 'utf8')
|
|
320
|
+
const cacheKey = getCacheKey(content)
|
|
321
|
+
const cacheDir = getCacheDirForRoot(root || process.cwd(), config)
|
|
322
|
+
const cached = await loadTypeCache(cacheDir, cacheKey)
|
|
323
|
+
if (cached) {
|
|
324
|
+
// Cache hit: persist via store.commit
|
|
325
|
+
await store.commit(fn => applyEnhancedSymbols(fn, relPath, cached.symbols))
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const enhanced = deepParseWithTS(filePath, content)
|
|
330
|
+
if (enhanced?.length) {
|
|
331
|
+
await saveTypeCache(cacheDir, cacheKey, { symbols: enhanced })
|
|
332
|
+
await store.commit(fn => applyEnhancedSymbols(fn, relPath, enhanced))
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.warn(`[dsh-project-memory] enhance failed for ${relPath}: ${err.message}`)
|
|
336
|
+
} finally {
|
|
337
|
+
// Remove only this task's own queue entry (not a newer one for the same relPath)
|
|
338
|
+
const idx = enhanceQueue.findIndex(q => q.promise === p)
|
|
339
|
+
if (idx >= 0) enhanceQueue.splice(idx, 1)
|
|
340
|
+
}
|
|
341
|
+
})()
|
|
342
|
+
|
|
343
|
+
// Update or add to queue
|
|
344
|
+
const existingIdx2 = enhanceQueue.findIndex(q => q.relPath === relPath)
|
|
345
|
+
if (existingIdx2 >= 0) {
|
|
346
|
+
enhanceQueue[existingIdx2] = { relPath, filePath, priority, cacheKey, promise: p }
|
|
347
|
+
} else {
|
|
348
|
+
enhanceQueue.push({ relPath, filePath, priority, cacheKey, promise: p })
|
|
349
|
+
}
|
|
350
|
+
enhanceQueue.sort((a, b) => a.priority - b.priority)
|
|
351
|
+
|
|
352
|
+
scheduleProcess()
|
|
353
|
+
return p
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function scheduleProcess() {
|
|
357
|
+
if (processing) return
|
|
358
|
+
processing = true
|
|
359
|
+
|
|
360
|
+
const processNext = async () => {
|
|
361
|
+
while (enhanceQueue.length > 0) {
|
|
362
|
+
const task = enhanceQueue.shift()
|
|
363
|
+
try {
|
|
364
|
+
await task.promise
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.warn(`[dsh-project-memory] enhance task failed: ${err.message}`)
|
|
367
|
+
}
|
|
368
|
+
await new Promise(r => setImmediate(r))
|
|
369
|
+
}
|
|
370
|
+
processing = false
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
processNext()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function applyEnhancedSymbols(fn, relPath, enhanced) {
|
|
377
|
+
// Store entries in fn.entries[relPath], not fn.files[relPath].entries
|
|
378
|
+
const existingEntries = fn.entries[relPath] || []
|
|
379
|
+
// L1 entries carry title "name (kind)" + sourceLine but no name/line fields
|
|
380
|
+
const nameOf = e => (e.title || '').replace(/\s*\([^)]*\)\s*$/, '')
|
|
381
|
+
const enhancedByLine = new Map(enhanced.map(s => [s.line, s]))
|
|
382
|
+
const existingKeys = new Set(existingEntries.map(e => `${nameOf(e)}#${e.sourceLine}`))
|
|
383
|
+
|
|
384
|
+
// Upgrade L1 entries in place only when TS found the same symbol on the same line
|
|
385
|
+
const mergedEntries = existingEntries.map(e => {
|
|
386
|
+
const enh = enhancedByLine.get(e.sourceLine)
|
|
387
|
+
if (!enh || nameOf(e) !== enh.name) return e
|
|
388
|
+
return {
|
|
389
|
+
...e,
|
|
390
|
+
text: `${enh.name}${enh.typeSig} -- ${relPath}:${enh.line}`,
|
|
391
|
+
typeSig: enh.typeSig,
|
|
392
|
+
enhanced: true
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// Add enhanced symbols that L1 missed; keep ids unique even when several
|
|
397
|
+
// symbols share one line (e.g. one-line class + method)
|
|
398
|
+
const usedIds = new Set(existingEntries.map(e => e.id))
|
|
399
|
+
const newEntries = []
|
|
400
|
+
for (const s of enhanced) {
|
|
401
|
+
if (existingKeys.has(`${s.name}#${s.line}`)) continue
|
|
402
|
+
const base = `${relPath.replace(/[\/:\s]/g, '_')}#${s.line}`
|
|
403
|
+
let id = base
|
|
404
|
+
if (usedIds.has(id)) id = `${base}-${s.kind}`
|
|
405
|
+
for (let n = 2; usedIds.has(id); n++) id = `${base}-${s.kind}-${n}`
|
|
406
|
+
usedIds.add(id)
|
|
407
|
+
newEntries.push({
|
|
408
|
+
id,
|
|
409
|
+
sourcePath: relPath,
|
|
410
|
+
sourceLine: s.line,
|
|
411
|
+
type: 'symbol',
|
|
412
|
+
title: `${s.name} (${s.kind})`,
|
|
413
|
+
keywords: [s.name, s.kind],
|
|
414
|
+
text: `${s.name}${s.typeSig} -- ${relPath}:${s.line}`,
|
|
415
|
+
enhanced: true
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Write to store.entries via setEntries so the shard is marked dirty and persisted
|
|
420
|
+
fn.setEntries(relPath, [...mergedEntries, ...newEntries])
|
|
421
|
+
|
|
422
|
+
// Refresh doc<->symbol links for newly added symbols
|
|
423
|
+
if (newEntries.length) linkEntries(fn)
|
|
424
|
+
|
|
425
|
+
// Also update fn.files metadata
|
|
426
|
+
if (fn.files[relPath]) {
|
|
427
|
+
fn.files[relPath].enhanced = true
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function onFileObserved(store, relPath, filePath, config, root) {
|
|
432
|
+
if (!isTypeScriptFile(filePath)) return
|
|
433
|
+
enqueueEnhance(store, relPath, filePath, PRIORITY.ACTIVE, config, root)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function onFileChanged(store, relPath, filePath, config, root) {
|
|
437
|
+
if (!isTypeScriptFile(filePath)) return
|
|
438
|
+
enqueueEnhance(store, relPath, filePath, PRIORITY.RECENT, config, root)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function onFileIndexed(store, relPath, filePath, config, root) {
|
|
442
|
+
if (!isTypeScriptFile(filePath)) return
|
|
443
|
+
enqueueEnhance(store, relPath, filePath, PRIORITY.BATCH, config, root)
|
|
444
|
+
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { watchRepoTool } from './tools/watch-repo.js'
|
|
|
8
8
|
import { statsTool } from './tools/stats.js'
|
|
9
9
|
import { WatchManager } from './watch.js'
|
|
10
10
|
import { setupLazyIndexing } from './lazy.js'
|
|
11
|
+
import { initTypeScript } from './enhancer.js'
|
|
11
12
|
|
|
12
13
|
export const name = 'dsh-project-memory'
|
|
13
14
|
export const inject = ['llm', 'tools']
|
|
@@ -25,9 +26,16 @@ export const Config = Schema.object({
|
|
|
25
26
|
autoIndexOnFirstUse: Schema.boolean().default(false),
|
|
26
27
|
watch: Schema.boolean().default(true),
|
|
27
28
|
watchInterval: Schema.number().default(15),
|
|
29
|
+
tsPath: Schema.string(),
|
|
30
|
+
enableTypeScript: Schema.boolean().default(true),
|
|
28
31
|
})
|
|
29
32
|
|
|
30
33
|
export function apply(ctx, config) {
|
|
34
|
+
// Initialize TypeScript enhancer if enabled
|
|
35
|
+
if (config.enableTypeScript !== false) {
|
|
36
|
+
initTypeScript(config)
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
const watchManager = new WatchManager(ctx, config)
|
|
32
40
|
if (config.watch) {
|
|
33
41
|
watchManager.restorePersisted()
|
package/src/lazy.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildDocEntries } from './doc-pipeline.js'
|
|
|
6
6
|
import { scanSymbols } from './symbols.js'
|
|
7
7
|
import { linkEntries } from './link.js'
|
|
8
8
|
import { ProjectMemoryStore } from './store.js'
|
|
9
|
+
import { onFileObserved } from './enhancer.js'
|
|
9
10
|
|
|
10
11
|
const STRONG_MARKERS = ['.git', '.hg', '.svn']
|
|
11
12
|
|
|
@@ -87,6 +88,7 @@ export async function indexFile(ctx, config, filePath, watchManager = null) {
|
|
|
87
88
|
const existing = store.fileRecord(rel)
|
|
88
89
|
let hash
|
|
89
90
|
let size
|
|
91
|
+
let entries
|
|
90
92
|
try {
|
|
91
93
|
if (isSupportedCode(ext) && config.maxFileSizeMb && statSync(filePath).size > config.maxFileSizeMb * 1024 * 1024) {
|
|
92
94
|
return false
|
|
@@ -102,9 +104,9 @@ export async function indexFile(ctx, config, filePath, watchManager = null) {
|
|
|
102
104
|
store.addWatch(root)
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
let entries
|
|
106
107
|
if (isSupportedCode(ext)) {
|
|
107
|
-
entries = scanSymbols(filePath, readFileSync(filePath, 'utf8'))
|
|
108
|
+
entries = scanSymbols(rel, filePath, readFileSync(filePath, 'utf8'))
|
|
109
|
+
onFileObserved(store, rel, filePath, config, root)
|
|
108
110
|
return store.commit((s) => {
|
|
109
111
|
s.markFile(rel, { sha256: hash, size, type: 'code', indexedAt: new Date().toISOString() })
|
|
110
112
|
s.setEntries(rel, entries)
|
|
@@ -112,7 +114,7 @@ export async function indexFile(ctx, config, filePath, watchManager = null) {
|
|
|
112
114
|
return true
|
|
113
115
|
})
|
|
114
116
|
} else {
|
|
115
|
-
entries = await buildDocEntries(ctx.llm, filePath, {
|
|
117
|
+
entries = await buildDocEntries(ctx.llm, rel, filePath, {
|
|
116
118
|
chunkChars: config.chunkChars,
|
|
117
119
|
maxChunks: config.maxChunksPerFile,
|
|
118
120
|
maxFileSizeMb: config.maxFileSizeMb,
|
package/src/llm.js
CHANGED
|
@@ -96,10 +96,15 @@ export async function expandQuery(llm, query, count = 6) {
|
|
|
96
96
|
export async function extractDocEntry(llm, chunk, sourcePath) {
|
|
97
97
|
const system =
|
|
98
98
|
'You are a project-documentation indexer. Given a chunk of a project document, ' +
|
|
99
|
-
'return a STRICT JSON object with exactly
|
|
100
|
-
'"title" (short section title, string),
|
|
101
|
-
'
|
|
102
|
-
'
|
|
99
|
+
'return a STRICT JSON object with exactly four fields: ' +
|
|
100
|
+
'"title" (short section title, string), ' +
|
|
101
|
+
'"summary" (3-5 sentence dense summary of what this section covers, ' +
|
|
102
|
+
'mentioning concrete names, decisions, constraints, and key technical details), ' +
|
|
103
|
+
'"blindSpots" (string describing what this summary does NOT cover, ' +
|
|
104
|
+
'e.g. "未覆盖:部署细节、性能基准、v0.2 前 API", empty string if none), ' +
|
|
105
|
+
'"keywords" (array of 5-10 searchable strings: ' +
|
|
106
|
+
'cover the document\'s own language AND English equivalents, ' +
|
|
107
|
+
'so a query in either language can match). ' +
|
|
103
108
|
'Do not include markdown fences, do not add commentary, output only the JSON object.'
|
|
104
109
|
|
|
105
110
|
const user =
|
|
@@ -109,6 +114,7 @@ export async function extractDocEntry(llm, chunk, sourcePath) {
|
|
|
109
114
|
const fallback = () => ({
|
|
110
115
|
title: chunk.title || sourcePath,
|
|
111
116
|
summary: summarizeText(chunk.text),
|
|
117
|
+
blindSpots: '',
|
|
112
118
|
keywords: tokenize(chunk.title).slice(0, 5),
|
|
113
119
|
})
|
|
114
120
|
|
|
@@ -121,7 +127,8 @@ export async function extractDocEntry(llm, chunk, sourcePath) {
|
|
|
121
127
|
const kw = Array.isArray(parsed.keywords) ? parsed.keywords.map(String).filter((k) => k).slice(0, 8) : []
|
|
122
128
|
return {
|
|
123
129
|
title: typeof parsed.title === 'string' && parsed.title.trim() ? parsed.title.trim() : chunk.title || sourcePath,
|
|
124
|
-
summary: summarizeText(parsed.summary),
|
|
130
|
+
summary: summarizeText(parsed.summary.trim()),
|
|
131
|
+
blindSpots: typeof parsed.blindSpots === 'string' ? parsed.blindSpots.trim() : '',
|
|
125
132
|
keywords: kw.length ? kw : tokenize(chunk.title).slice(0, 5),
|
|
126
133
|
}
|
|
127
134
|
} catch {
|