@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/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
- m = line.match(/^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/)
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
- m = line.match(/^(?:export\s+)?(?:async\s+)?function\s*\(/) // anonymous
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, filePath, rawLines) {
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 text = masked[i].trim()
128
- if (!text) continue
229
+ const rawText = rawLines[i].trim()
230
+ const maskedText = masked[i].trim()
231
+ if (!rawText) continue
232
+
129
233
  let matched = null
130
- if (prevOpensBlock) {
131
- const method = text.match(/^([A-Za-z_$][\w$]*)\s*(?:<[^<>]*>)?\s*\(([^()]*)\)\s*(?::\s*[^={]{1,80})?\{/)
132
- if (method && !JS_NON_METHOD.has(method[1])) matched = { name: method[1], kind: 'method' }
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
- if (!matched) matched = matchJsLike(text)
135
- if (!matched && JS_DECL_START.test(text)) {
136
- let joined = text
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
- for (let j = i + 1; j < masked.length && extra < 3; j++) {
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
- matched = matchJsLike(joined)
144
- if (matched) {
145
- i = j
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
- if (matched) symbols.push(buildSymbol(matched, filePath, rawLines[i], i + 1))
299
+
152
300
  prevOpensBlock = masked[i].trim().endsWith('{')
153
301
  }
154
302
  return symbols
155
303
  }
156
304
 
157
- function scanPython(masked, filePath, rawLines) {
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, filePath, rawLines[i], i + 1))
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 buildSymbol(matched, filePath, rawLine, lineNo) {
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(filePath).replace(/[\\/:\s]/g, '_')}#${lineNo}`,
214
- sourcePath: filePath,
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: String(rawLine).trim().slice(0, 200),
429
+ text: identity,
221
430
  }
222
431
  }
223
432
 
224
- export function scanSymbols(filePath, content) {
225
- const ext = filePath.slice(filePath.lastIndexOf('.'))
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), filePath, lines)
228
- if (PYTHON.has(ext)) return scanPython(maskTokens(lines, PY_MASKER), filePath, lines)
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, filePath, raw, i + 1))
461
+ if (matched) symbols.push(buildSymbol(matched, relPath, raw, i + 1))
251
462
  }
252
463
  return symbols
253
464
  }
@@ -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,
@@ -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
- return store.commit((s) => {
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 source = e.sourceLine ? `${e.sourcePath}:${e.sourceLine}` : e.sourcePath
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: ${source}\n- ${e.summary}`)
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
- // Update snapshot for successfully processed files only, using the
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
  }