@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/symbols.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
|
-
const JS_LIKE = new Set(['.js', '.mjs', '.cjs', '.ts', '.jsx', '.tsx'])
|
|
3
|
+
const JS_LIKE = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx'])
|
|
4
4
|
const PYTHON = new Set(['.py'])
|
|
5
5
|
const GO = new Set(['.go'])
|
|
6
6
|
const RUST = new Set(['.rs'])
|
|
@@ -96,19 +96,121 @@ function balanceDelta(text) {
|
|
|
96
96
|
return delta
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function extractTypeSignature(line) {
|
|
100
|
+
let sig = ''
|
|
101
|
+
|
|
102
|
+
// 1. Generics: <T, U extends Base> - but NOT the return type generics like Promise<T>
|
|
103
|
+
// Match generics that appear BEFORE the first ( (function params) or => (arrow)
|
|
104
|
+
// For: function foo<T>(a: T): R
|
|
105
|
+
// For: const foo = <T>(a: T): R =>
|
|
106
|
+
// For: class Foo<T> { method() }
|
|
107
|
+
const beforeParams = line.split(/\(|=>/)[0]
|
|
108
|
+
const genericMatch = beforeParams.match(/<[^<>]*(?:<[^<>]*>[^<>]*)*>/)
|
|
109
|
+
if (genericMatch) sig += genericMatch[0]
|
|
110
|
+
|
|
111
|
+
// 2. Parameters: (a: string, b: number) - handle nested parentheses
|
|
112
|
+
let paramMatch = null
|
|
113
|
+
const parenIdx = line.indexOf('(')
|
|
114
|
+
if (parenIdx >= 0) {
|
|
115
|
+
let depth = 0
|
|
116
|
+
for (let i = parenIdx; i < line.length; i++) {
|
|
117
|
+
if (line[i] === '(') depth++
|
|
118
|
+
else if (line[i] === ')') {
|
|
119
|
+
depth--
|
|
120
|
+
if (depth === 0) {
|
|
121
|
+
paramMatch = line.slice(parenIdx, i + 1)
|
|
122
|
+
// 3. Return type: ONLY after the closing ) of params
|
|
123
|
+
const afterParams = line.slice(i + 1)
|
|
124
|
+
// Match return type including nested generics, stop at { ; =>
|
|
125
|
+
const retMatch = afterParams.match(/^\s*:\s*([^{;=]+)/)
|
|
126
|
+
if (retMatch) sig += paramMatch + `: ${retMatch[1].trim()}`
|
|
127
|
+
else sig += paramMatch
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3b. Arrow function without parens: foo = (x): R => or foo = x: R =>
|
|
135
|
+
if (!paramMatch) {
|
|
136
|
+
const arrowMatch = line.match(/=>/)
|
|
137
|
+
if (arrowMatch) {
|
|
138
|
+
const afterArrow = line.slice(arrowMatch.index + 2)
|
|
139
|
+
const retMatch = afterArrow.match(/^\s*([^{;=]+)/)
|
|
140
|
+
if (retMatch) sig += `: ${retMatch[1].trim()}`
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return sig
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function extractInterfaceOrType(line) {
|
|
148
|
+
// interface User { name: string; age: number }
|
|
149
|
+
const ifaceMatch = line.match(/^interface\s+(\w+)\s*(?:extends\s+[^{]+)?\s*\{([^}]*)\}/)
|
|
150
|
+
if (ifaceMatch) {
|
|
151
|
+
return `{ ${ifaceMatch[2].trim()} }`
|
|
152
|
+
}
|
|
153
|
+
// type UserMap = Map<string, User>
|
|
154
|
+
const typeMatch = line.match(/^type\s+(\w+)\s*=\s*([^;{]+)/)
|
|
155
|
+
if (typeMatch) {
|
|
156
|
+
return `= ${typeMatch[2].trim()}`
|
|
157
|
+
}
|
|
158
|
+
return ''
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function extractOverloads(masked, startIdx) {
|
|
162
|
+
const overloads = []
|
|
163
|
+
for (let i = startIdx; i < masked.length; i++) {
|
|
164
|
+
const text = masked[i].trim()
|
|
165
|
+
if (!text) continue
|
|
166
|
+
|
|
167
|
+
// Skip comments
|
|
168
|
+
if (text.startsWith('//') || text.startsWith('/*')) continue
|
|
169
|
+
|
|
170
|
+
// Check for function declaration (with or without export/async/default)
|
|
171
|
+
const fnMatch = text.match(/^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+\w+\s*/)
|
|
172
|
+
if (!fnMatch) break
|
|
173
|
+
|
|
174
|
+
// Extract ONLY the signature part (generics + params + return), NOT the function name
|
|
175
|
+
const sig = extractTypeSignature(text)
|
|
176
|
+
if (sig) overloads.push(sig)
|
|
177
|
+
|
|
178
|
+
// If it has a body {, it's the implementation - stop
|
|
179
|
+
if (text.includes('{')) break
|
|
180
|
+
}
|
|
181
|
+
return overloads
|
|
182
|
+
}
|
|
183
|
+
|
|
99
184
|
function matchJsLike(line) {
|
|
185
|
+
// export default async function foo<T>(a: T): Promise<T> { ... }
|
|
100
186
|
let m = line.match(/^export\s+(?:default\s+)?(?:async\s+)?(?:function\s+([A-Za-z_$][\w$]*)|class\s+([A-Za-z_$][\w$]*))/)
|
|
101
187
|
if (m) return { name: m[1] || m[2], kind: m[1] ? 'function' : 'class' }
|
|
102
|
-
|
|
188
|
+
|
|
189
|
+
// export async function foo<T>(a: T): Promise<T> { ... }
|
|
190
|
+
// Handle optional generics: function name<T>(...
|
|
191
|
+
m = line.match(/^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*(?:<[^<>]*>)?\s*\(/)
|
|
103
192
|
if (m) return { name: m[1], kind: 'function' }
|
|
193
|
+
|
|
194
|
+
// export class Foo { ... }
|
|
104
195
|
m = line.match(/^(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/)
|
|
105
196
|
if (m) return { name: m[1], kind: 'class' }
|
|
197
|
+
|
|
198
|
+
// export const foo = (a: T): Promise<T> => { ... }
|
|
106
199
|
m = line.match(/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\(/)
|
|
107
200
|
if (m) return { name: m[1], kind: 'function' }
|
|
201
|
+
|
|
202
|
+
// export const foo = async (a: T): Promise<T> => { ... }
|
|
108
203
|
m = line.match(/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/)
|
|
109
204
|
if (m) return { name: m[1], kind: 'function' }
|
|
110
|
-
|
|
205
|
+
|
|
206
|
+
// export function (multi-line, name on next line)
|
|
207
|
+
m = line.match(/^(?:export\s+)?(?:async\s+)?function\s*$/)
|
|
208
|
+
if (m) return { name: '(pending)', kind: 'function' }
|
|
209
|
+
|
|
210
|
+
// anonymous function
|
|
211
|
+
m = line.match(/^(?:export\s+)?(?:async\s+)?function\s*\(/)
|
|
111
212
|
if (m) return { name: '(anonymous)', kind: 'function' }
|
|
213
|
+
|
|
112
214
|
return null
|
|
113
215
|
}
|
|
114
216
|
|
|
@@ -120,41 +222,87 @@ const JS_NON_METHOD = new Set([
|
|
|
120
222
|
'new', 'delete', 'typeof', 'instanceof', 'void', 'await', 'yield', 'with',
|
|
121
223
|
])
|
|
122
224
|
|
|
123
|
-
function scanJsLike(masked,
|
|
225
|
+
function scanJsLike(masked, relPath, rawLines) {
|
|
124
226
|
const symbols = []
|
|
125
227
|
let prevOpensBlock = false
|
|
126
228
|
for (let i = 0; i < masked.length; i++) {
|
|
127
|
-
const
|
|
128
|
-
|
|
229
|
+
const rawText = rawLines[i].trim()
|
|
230
|
+
const maskedText = masked[i].trim()
|
|
231
|
+
if (!rawText) continue
|
|
232
|
+
|
|
129
233
|
let matched = null
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
234
|
+
let joinedText = null
|
|
235
|
+
|
|
236
|
+
// Check for interface / type alias first (on raw line, not masked)
|
|
237
|
+
const ifaceSig = extractInterfaceOrType(rawText)
|
|
238
|
+
if (ifaceSig) {
|
|
239
|
+
const nameMatch = rawText.match(/^(?:export\s+)?(?:interface|type)\s+(\w+)/)
|
|
240
|
+
if (nameMatch) {
|
|
241
|
+
matched = { name: nameMatch[1], kind: 'interface', typeSig: ifaceSig }
|
|
242
|
+
}
|
|
133
243
|
}
|
|
134
|
-
|
|
135
|
-
if (!matched
|
|
136
|
-
|
|
244
|
+
|
|
245
|
+
if (!matched) {
|
|
246
|
+
// Check for method inside class (prev line ends with {)
|
|
247
|
+
if (prevOpensBlock) {
|
|
248
|
+
const method = maskedText.match(/^([A-Za-z_$][\w$]*)\s*(?:<[^<>]*>)?\s*\(([^()]*)\)\s*(?::\s*[^={]{1,80})?\{/)
|
|
249
|
+
if (method && !JS_NON_METHOD.has(method[1])) matched = { name: method[1], kind: 'method' }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Regular declarations - try initial match
|
|
253
|
+
if (!matched) matched = matchJsLike(maskedText)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Multi-line declaration joining (only when no initial match, or when match is pending)
|
|
257
|
+
if ((!matched || (matched && matched.name === '(pending)')) && JS_DECL_START.test(maskedText)) {
|
|
258
|
+
// No initial match, or pending match - do joining to find/resolve one
|
|
259
|
+
let joined = maskedText
|
|
137
260
|
let extra = 0
|
|
138
|
-
|
|
261
|
+
let declMatched = null
|
|
262
|
+
for (let j = i + 1; j < masked.length && extra < 5; j++) {
|
|
139
263
|
const tail = masked[j].trim()
|
|
140
264
|
if (!tail) continue
|
|
141
265
|
joined += ' ' + tail
|
|
142
266
|
extra++
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
267
|
+
if (!declMatched) declMatched = matchJsLike(joined)
|
|
268
|
+
if (/[{};]/.test(tail)) {
|
|
269
|
+
if (declMatched && declMatched.name !== '(pending)') {
|
|
270
|
+
matched = declMatched
|
|
271
|
+
i = j
|
|
272
|
+
joinedText = joined
|
|
273
|
+
}
|
|
146
274
|
break
|
|
147
275
|
}
|
|
148
|
-
if (/[{};]/.test(tail)) break
|
|
149
276
|
}
|
|
277
|
+
if (!matched && declMatched && declMatched.name !== '(pending)') {
|
|
278
|
+
matched = declMatched
|
|
279
|
+
joinedText = joined
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (matched) {
|
|
284
|
+
// Extract type signature: use joined text if multi-line, else raw line
|
|
285
|
+
if (matched.kind !== 'interface') {
|
|
286
|
+
const sourceForType = joinedText || rawText
|
|
287
|
+
const typeSig = extractTypeSignature(sourceForType)
|
|
288
|
+
if (typeSig) matched.typeSig = typeSig
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Extract overloads for function declarations
|
|
292
|
+
if (matched.kind === 'function' && (joinedText || rawText).startsWith('function')) {
|
|
293
|
+
const overloads = extractOverloads(masked, i)
|
|
294
|
+
if (overloads.length > 1) matched.overloads = overloads
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
symbols.push(buildSymbol(matched, relPath, rawLines[i], i + 1))
|
|
150
298
|
}
|
|
151
|
-
|
|
299
|
+
|
|
152
300
|
prevOpensBlock = masked[i].trim().endsWith('{')
|
|
153
301
|
}
|
|
154
302
|
return symbols
|
|
155
303
|
}
|
|
156
304
|
|
|
157
|
-
function scanPython(masked,
|
|
305
|
+
function scanPython(masked, relPath, rawLines) {
|
|
158
306
|
const symbols = []
|
|
159
307
|
let depth = 0
|
|
160
308
|
for (let i = 0; i < masked.length; i++) {
|
|
@@ -169,7 +317,7 @@ function scanPython(masked, filePath, rawLines) {
|
|
|
169
317
|
const cls = fn ? null : text.match(/^class\s+(\w+)\s*[(:]/)
|
|
170
318
|
if (fn) matched = { name: fn[1], kind: 'function' }
|
|
171
319
|
else if (cls) matched = { name: cls[1], kind: 'class' }
|
|
172
|
-
if (matched) symbols.push(buildSymbol(matched,
|
|
320
|
+
if (matched) symbols.push(buildSymbol(matched, relPath, rawLines[i], i + 1))
|
|
173
321
|
depth += delta
|
|
174
322
|
}
|
|
175
323
|
return symbols
|
|
@@ -208,24 +356,87 @@ function matchShell(line) {
|
|
|
208
356
|
return m ? { name: m[1], kind: 'function' } : null
|
|
209
357
|
}
|
|
210
358
|
|
|
211
|
-
function
|
|
359
|
+
function extractParamsAndReturn(line) {
|
|
360
|
+
const trimmed = line.trim()
|
|
361
|
+
|
|
362
|
+
// Try to extract params and return type from function declaration
|
|
363
|
+
// Matches: function name(params): returnType { or => returnType {
|
|
364
|
+
const funcMatch = line.match(/function\s+([A-Za-z_$][\w$]*)\s*(\([^)]*\))\s*(?::\s*([^{]+))?\s*\{/)
|
|
365
|
+
if (funcMatch) {
|
|
366
|
+
const params = funcMatch[2] || '()'
|
|
367
|
+
const returnType = funcMatch[3] ? `: ${funcMatch[3].trim()}` : ''
|
|
368
|
+
return `${funcMatch[1]}${params}${returnType}`
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Arrow function: const name = (params): returnType => {
|
|
372
|
+
const arrowMatch = line.match(/const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*:\s*([^=]+)\s*=>/)
|
|
373
|
+
if (arrowMatch) {
|
|
374
|
+
return `${arrowMatch[1]}(${arrowMatch[2]}): ${arrowMatch[3].trim()}`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Arrow function without explicit return type: const name = (params) => {
|
|
378
|
+
const arrowSimple = line.match(/const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/)
|
|
379
|
+
if (arrowMatch) {
|
|
380
|
+
return `${arrowMatch[1]}(${arrowMatch[2]})`
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Class method: methodName(params): returnType {
|
|
384
|
+
const methodMatch = line.match(/^\s*([A-Za-z_$][\w$]*)\s*(?:<[^<>]*>)?\s*\(([^()]*)\)\s*(?::\s*([^{]+))?\s*\{/)
|
|
385
|
+
if (methodMatch) {
|
|
386
|
+
const returnType = methodMatch[3] ? `: ${methodMatch[3].trim()}` : ''
|
|
387
|
+
return `${methodMatch[1]}(${methodMatch[2] || ''})${returnType}`
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Constructor
|
|
391
|
+
const constructorMatch = line.match(/constructor\s*\(([^)]*)\)/)
|
|
392
|
+
if (constructorMatch) {
|
|
393
|
+
return `constructor(${constructorMatch[1]})`
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Fallback: try to extract from the raw line
|
|
397
|
+
const declMatch = line.match(/(function|const|let|var)\s+([A-Za-z_$][\w$]*)/)
|
|
398
|
+
if (declMatch) {
|
|
399
|
+
return declMatch[0]
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Fallback: return the raw line trimmed
|
|
403
|
+
return ''
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildSymbol(matched, relPath, rawLine, lineNo) {
|
|
407
|
+
// Use pre-extracted type signature if available, otherwise fall back to extractParamsAndReturn
|
|
408
|
+
let typeSig = matched.typeSig || extractParamsAndReturn(rawLine)
|
|
409
|
+
|
|
410
|
+
// Add overloads if present
|
|
411
|
+
const overloadPart = matched.overloads?.length
|
|
412
|
+
? ` | overloads: ${matched.overloads.join('; ')}`
|
|
413
|
+
: ''
|
|
414
|
+
|
|
415
|
+
// For interface/type, typeSig already includes the full signature without name
|
|
416
|
+
// For others, typeSig is just the generics+params+return
|
|
417
|
+
const prefix = matched.kind === 'interface' ? '' : matched.name
|
|
418
|
+
|
|
419
|
+
// Build one-line identity: name<Generics>(params): ReturnType — file.ts:lineNo
|
|
420
|
+
const identity = `${prefix}${typeSig}${overloadPart} — ${relPath}:${lineNo}`
|
|
421
|
+
|
|
212
422
|
return {
|
|
213
|
-
id: `${String(
|
|
214
|
-
sourcePath:
|
|
423
|
+
id: `${String(relPath).replace(/[\\/:\s]/g, '_')}#${lineNo}`,
|
|
424
|
+
sourcePath: relPath,
|
|
215
425
|
sourceLine: lineNo,
|
|
216
426
|
type: 'symbol',
|
|
217
427
|
title: `${matched.name} (${matched.kind})`,
|
|
218
|
-
summary: `${matched.kind} "${matched.name}" declared at ${filePath}:${lineNo}`,
|
|
219
428
|
keywords: [matched.name, matched.kind],
|
|
220
|
-
text:
|
|
429
|
+
text: identity,
|
|
221
430
|
}
|
|
222
431
|
}
|
|
223
432
|
|
|
224
|
-
export function scanSymbols(
|
|
225
|
-
|
|
433
|
+
export function scanSymbols(a, b, c) {
|
|
434
|
+
// Backward compatible: old signature (filePath, content) or new (relPath, filePath, content)
|
|
435
|
+
const [relPath, filePath, content] = c === undefined ? [a, a, b] : [a, b, c]
|
|
436
|
+
const ext = relPath.slice(relPath.lastIndexOf('.'))
|
|
226
437
|
const lines = content.split(/\r?\n/)
|
|
227
|
-
if (JS_LIKE.has(ext)) return scanJsLike(maskTokens(lines, JS_MASKER),
|
|
228
|
-
if (PYTHON.has(ext)) return scanPython(maskTokens(lines, PY_MASKER),
|
|
438
|
+
if (JS_LIKE.has(ext)) return scanJsLike(maskTokens(lines, JS_MASKER), relPath, lines)
|
|
439
|
+
if (PYTHON.has(ext)) return scanPython(maskTokens(lines, PY_MASKER), relPath, lines)
|
|
229
440
|
|
|
230
441
|
let masker = null
|
|
231
442
|
if (GO.has(ext)) masker = GO_MASKER
|
|
@@ -247,7 +458,7 @@ export function scanSymbols(filePath, content) {
|
|
|
247
458
|
else if (/^(?:def|func|fn|function)\s+(\w+)/.test(line)) {
|
|
248
459
|
matched = { name: line.match(/^(?:def|func|fn|function)\s+(\w+)/)[1], kind: 'function' }
|
|
249
460
|
}
|
|
250
|
-
if (matched) symbols.push(buildSymbol(matched,
|
|
461
|
+
if (matched) symbols.push(buildSymbol(matched, relPath, raw, i + 1))
|
|
251
462
|
}
|
|
252
463
|
return symbols
|
|
253
464
|
}
|
package/src/tools/index-doc.js
CHANGED
|
@@ -42,7 +42,7 @@ export function indexDocTool(ctx, config) {
|
|
|
42
42
|
return `Skipped (unchanged): ${rel}\nAlready indexed with ${(store.entries[rel] || []).length} entry/entries.`
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const entries = await buildDocEntries(ctx.llm, filePath, {
|
|
45
|
+
const entries = await buildDocEntries(ctx.llm, rel, filePath, {
|
|
46
46
|
chunkChars: config.chunkChars,
|
|
47
47
|
maxChunks: config.maxChunksPerFile,
|
|
48
48
|
maxFileSizeMb: config.maxFileSizeMb,
|
package/src/tools/index-repo.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 { onFileIndexed, isTypeScriptFile } from '../enhancer.js'
|
|
9
10
|
|
|
10
11
|
export async function indexRepository(ctx, config, root, { reindex = false } = {}) {
|
|
11
12
|
const memoryDir = memoryRootFor(root, config.memoryDir)
|
|
@@ -47,7 +48,7 @@ export async function indexRepository(ctx, config, root, { reindex = false } = {
|
|
|
47
48
|
let entries
|
|
48
49
|
if (isSupportedCode(ext)) {
|
|
49
50
|
const content = readFileSync(filePath, 'utf8')
|
|
50
|
-
entries = scanSymbols(filePath, content)
|
|
51
|
+
entries = scanSymbols(rel, filePath, content)
|
|
51
52
|
fileUpdates.push({ rel, expectedHash: existing?.sha256, hash, size, entries, type: 'code' })
|
|
52
53
|
updated++
|
|
53
54
|
} else {
|
|
@@ -57,7 +58,7 @@ export async function indexRepository(ctx, config, root, { reindex = false } = {
|
|
|
57
58
|
skipped++
|
|
58
59
|
continue
|
|
59
60
|
}
|
|
60
|
-
entries = await buildDocEntries(ctx.llm, filePath, {
|
|
61
|
+
entries = await buildDocEntries(ctx.llm, rel, filePath, {
|
|
61
62
|
chunkChars: config.chunkChars,
|
|
62
63
|
maxChunks: config.maxChunksPerFile,
|
|
63
64
|
maxFileSizeMb: config.maxFileSizeMb,
|
|
@@ -77,7 +78,7 @@ export async function indexRepository(ctx, config, root, { reindex = false } = {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// Second pass: single commit with all updates
|
|
80
|
-
|
|
81
|
+
const report = store.commit((s) => {
|
|
81
82
|
for (const update of fileUpdates) {
|
|
82
83
|
const result = s.applyFileUpdate(update.rel, update)
|
|
83
84
|
if (result.skipped) {
|
|
@@ -105,6 +106,18 @@ export async function indexRepository(ctx, config, root, { reindex = false } = {
|
|
|
105
106
|
}
|
|
106
107
|
return report
|
|
107
108
|
})
|
|
109
|
+
|
|
110
|
+
// Trigger TS enhancement for code files (TS/JS only)
|
|
111
|
+
for (const update of fileUpdates) {
|
|
112
|
+
if (update.type === 'code') {
|
|
113
|
+
const filePath = path.join(root, update.rel)
|
|
114
|
+
if (isTypeScriptFile(filePath)) {
|
|
115
|
+
onFileIndexed(store, update.rel, filePath, config, root)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return report
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
export function indexRepoTool(ctx, config) {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
+
import path from 'node:path'
|
|
2
3
|
import { memoryRootFor, resolveIndexRoot } from '../util/fs.js'
|
|
3
4
|
import { ProjectMemoryStore, storeOverview } from '../store.js'
|
|
4
5
|
import { expandQuery } from '../llm.js'
|
|
5
6
|
import { rankEntriesMergedScored, rankExperienceScored } from '../util/search.js'
|
|
6
7
|
import { truncate } from '../util/text.js'
|
|
7
8
|
|
|
9
|
+
function toAbs(root, rel) {
|
|
10
|
+
return path.isAbsolute(rel) ? rel : path.join(root, rel)
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export function queryMemoryTool(ctx, config) {
|
|
9
14
|
return defineTool({
|
|
10
15
|
name: 'query_memory',
|
|
@@ -59,13 +64,13 @@ export function queryMemoryTool(ctx, config) {
|
|
|
59
64
|
const top = scored[0].score || 1
|
|
60
65
|
lines.push(`## Memory (${type === 'all' ? 'docs + symbols' : type})`)
|
|
61
66
|
for (const { entry: e, score } of scored) {
|
|
62
|
-
const
|
|
67
|
+
const absSource = e.sourceLine ? `${toAbs(root, e.sourcePath)}:${e.sourceLine}` : toAbs(root, e.sourcePath)
|
|
63
68
|
const rel = Math.round((score / top) * 100)
|
|
64
|
-
lines.push(`### ${e.title} (score: ${rel})\n- source: ${
|
|
69
|
+
lines.push(`### ${e.title} (score: ${rel})\n- source: ${absSource}\n- ${e.summary}`)
|
|
65
70
|
if (e.type === 'doc' && Array.isArray(e.linkedSymbols) && e.linkedSymbols.length) {
|
|
66
71
|
const refs = e.linkedSymbols.slice(0, 5).map((id) => {
|
|
67
72
|
const s = symbolById.get(id)
|
|
68
|
-
return s ? `${s.title} @ ${s.sourcePath}:${s.sourceLine}` : id
|
|
73
|
+
return s ? `${s.title} @ ${toAbs(root, s.sourcePath)}:${s.sourceLine}` : id
|
|
69
74
|
})
|
|
70
75
|
lines.push(`- references: ${refs.join('; ')}`)
|
|
71
76
|
}
|
|
@@ -78,7 +83,7 @@ export function queryMemoryTool(ctx, config) {
|
|
|
78
83
|
const expTop = scoredExp[0].score || 1
|
|
79
84
|
lines.push(`## Experience (past problems -> solutions)`)
|
|
80
85
|
for (const { item: e, score } of scoredExp) {
|
|
81
|
-
const source = e.sourceFile ? ` (source: ${e.sourceFile})` : ''
|
|
86
|
+
const source = e.sourceFile ? ` (source: ${toAbs(root, e.sourceFile)})` : ''
|
|
82
87
|
lines.push(
|
|
83
88
|
`### Problem: ${e.problem} (score: ${Math.round((score / expTop) * 100)}, id: ${e.id})\n- solution: ${e.solution}${source}\n- updated: ${e.updatedAt}`,
|
|
84
89
|
)
|
package/src/util/fs.js
CHANGED
|
@@ -85,7 +85,7 @@ export function looksLikeDump(text, maxBytes = 4096) {
|
|
|
85
85
|
|
|
86
86
|
export function isSupportedCode(ext) {
|
|
87
87
|
return [
|
|
88
|
-
'.js', '.mjs', '.cjs', '.ts', '.jsx', '.tsx', '.py', '.java', '.go',
|
|
88
|
+
'.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx', '.py', '.java', '.go',
|
|
89
89
|
'.rs', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.php', '.rb',
|
|
90
90
|
'.swift', '.kt', '.kts', '.sh', '.zsh',
|
|
91
91
|
].includes(ext)
|
package/src/watch.js
CHANGED
|
@@ -5,6 +5,7 @@ import { buildDocEntries } from './doc-pipeline.js'
|
|
|
5
5
|
import { scanSymbols } from './symbols.js'
|
|
6
6
|
import { linkEntries } from './link.js'
|
|
7
7
|
import { ProjectMemoryStore } from './store.js'
|
|
8
|
+
import { onFileChanged, isTypeScriptFile } from './enhancer.js'
|
|
8
9
|
|
|
9
10
|
export class WatchManager {
|
|
10
11
|
constructor(ctx, config) {
|
|
@@ -95,9 +96,9 @@ export class WatchManager {
|
|
|
95
96
|
try {
|
|
96
97
|
let entries
|
|
97
98
|
if (isSupportedCode(ext)) {
|
|
98
|
-
entries = scanSymbols(filePath, readFileSync(filePath, 'utf8'))
|
|
99
|
+
entries = scanSymbols(rel, filePath, readFileSync(filePath, 'utf8'))
|
|
99
100
|
} else {
|
|
100
|
-
entries = await buildDocEntries(this.ctx.llm, filePath, {
|
|
101
|
+
entries = await buildDocEntries(this.ctx.llm, rel, filePath, {
|
|
101
102
|
chunkChars: this.config.chunkChars,
|
|
102
103
|
maxChunks: this.config.maxChunksPerFile,
|
|
103
104
|
maxFileSizeMb: this.config.maxFileSizeMb,
|
|
@@ -144,12 +145,15 @@ export class WatchManager {
|
|
|
144
145
|
}
|
|
145
146
|
})
|
|
146
147
|
|
|
147
|
-
//
|
|
148
|
-
// first-pass signature: if the file changed during the compute window
|
|
149
|
-
// (hash / scanSymbols / LLM summary), the next poll's signature differs
|
|
150
|
-
// and the file is re-indexed — restoring the pre-refactor self-healing.
|
|
148
|
+
// Trigger TS enhancement for code files (TS/JS only)
|
|
151
149
|
for (const update of fileUpdates) {
|
|
152
150
|
if (update._skipSnapshot) continue
|
|
151
|
+
if (update.type === 'code') {
|
|
152
|
+
const filePath = path.join(root, update.rel)
|
|
153
|
+
if (isTypeScriptFile(filePath)) {
|
|
154
|
+
onFileChanged(state.store, update.rel, filePath, this.config, root)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
153
157
|
state.snapshot[update.rel] = update._sig
|
|
154
158
|
}
|
|
155
159
|
}
|