cntx-ui 2.0.12 → 2.0.15
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/bin/cntx-ui.js +137 -55
- package/lib/agent-runtime.js +1480 -0
- package/lib/agent-tools.js +368 -0
- package/lib/api-router.js +978 -0
- package/lib/bundle-manager.js +471 -0
- package/lib/configuration-manager.js +725 -0
- package/lib/file-system-manager.js +472 -0
- package/lib/function-level-chunker.js +406 -0
- package/lib/heuristics-manager.js +425 -0
- package/lib/mcp-server.js +1054 -1
- package/lib/semantic-splitter.js +588 -0
- package/lib/simple-vector-store.js +329 -0
- package/lib/treesitter-semantic-chunker.js +1485 -0
- package/lib/websocket-manager.js +470 -0
- package/package.json +14 -3
- package/server.js +673 -1704
- package/templates/activities/README.md +67 -0
- package/templates/activities/activities/create-project-bundles/README.md +83 -0
- package/templates/activities/activities/create-project-bundles/notes.md +102 -0
- package/templates/activities/activities/create-project-bundles/progress.md +63 -0
- package/templates/activities/activities/create-project-bundles/tasks.md +39 -0
- package/templates/activities/activities.json +219 -0
- package/templates/activities/lib/.markdownlint.jsonc +18 -0
- package/templates/activities/lib/create-activity.mdc +63 -0
- package/templates/activities/lib/generate-tasks.mdc +64 -0
- package/templates/activities/lib/process-task-list.mdc +52 -0
- package/templates/agent-config.yaml +78 -0
- package/templates/agent-instructions.md +218 -0
- package/templates/agent-rules/capabilities/activities-system.md +147 -0
- package/templates/agent-rules/capabilities/bundle-system.md +131 -0
- package/templates/agent-rules/capabilities/vector-search.md +135 -0
- package/templates/agent-rules/core/codebase-navigation.md +91 -0
- package/templates/agent-rules/core/performance-hierarchy.md +48 -0
- package/templates/agent-rules/core/response-formatting.md +120 -0
- package/templates/agent-rules/project-specific/architecture.md +145 -0
- package/templates/config.json +76 -0
- package/templates/hidden-files.json +14 -0
- package/web/dist/assets/heuristics-manager-browser-DfonOP5I.js +1 -0
- package/web/dist/assets/index-dF3qg-y_.js +2486 -0
- package/web/dist/assets/index-h5FGSg_P.css +1 -0
- package/web/dist/cntx-ui.svg +18 -0
- package/web/dist/index.html +25 -8
- package/web/dist/assets/index-8Kli5657.js +0 -541
- package/web/dist/assets/index-C-Ldi33E.css +0 -1
- package/web/dist/vite.svg +0 -1
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True Semantic Splitting - Function-level code chunks with context
|
|
3
|
+
* Creates surgical, self-contained chunks for AI consumption
|
|
4
|
+
* Operates parallel to file-level bundle system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'fs'
|
|
8
|
+
import { extname, basename, dirname, join } from 'path'
|
|
9
|
+
import glob from 'glob'
|
|
10
|
+
import HeuristicsManager from './heuristics-manager.js'
|
|
11
|
+
|
|
12
|
+
export default class SemanticSplitter {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = {
|
|
15
|
+
maxChunkSize: 2000, // Max chars per chunk
|
|
16
|
+
includeContext: true, // Include imports/types needed
|
|
17
|
+
groupRelated: true, // Group related functions
|
|
18
|
+
minFunctionSize: 50, // Skip tiny functions
|
|
19
|
+
...options
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Initialize heuristics manager
|
|
23
|
+
this.heuristicsManager = new HeuristicsManager()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract semantic chunks from project
|
|
28
|
+
*/
|
|
29
|
+
async extractSemanticChunks(projectPath, patterns = ['**/*.{js,jsx,ts,tsx,mjs}'], bundleConfig = null) {
|
|
30
|
+
console.log('🔪 Starting semantic splitting...')
|
|
31
|
+
|
|
32
|
+
const files = this.findFiles(projectPath, patterns)
|
|
33
|
+
console.log(`📁 Found ${files.length} files to split`)
|
|
34
|
+
|
|
35
|
+
// Load bundle configuration if provided
|
|
36
|
+
this.bundleConfig = bundleConfig
|
|
37
|
+
|
|
38
|
+
const allFunctions = []
|
|
39
|
+
const allTypes = []
|
|
40
|
+
const allImports = []
|
|
41
|
+
|
|
42
|
+
// Extract all code elements
|
|
43
|
+
for (const filePath of files) {
|
|
44
|
+
try {
|
|
45
|
+
const elements = this.extractCodeElements(filePath, projectPath)
|
|
46
|
+
allFunctions.push(...elements.functions)
|
|
47
|
+
allTypes.push(...elements.types)
|
|
48
|
+
allImports.push(...elements.imports)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.warn(`Failed to extract from ${filePath}: ${error.message}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`⚡ Extracted ${allFunctions.length} functions, ${allTypes.length} types`)
|
|
55
|
+
|
|
56
|
+
// Create semantic chunks
|
|
57
|
+
const chunks = this.createSemanticChunks(allFunctions, allTypes, allImports)
|
|
58
|
+
console.log(`🧩 Created ${chunks.length} semantic chunks`)
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
summary: {
|
|
62
|
+
totalFiles: files.length,
|
|
63
|
+
totalFunctions: allFunctions.length,
|
|
64
|
+
totalChunks: chunks.length,
|
|
65
|
+
averageChunkSize: chunks.reduce((sum, c) => sum + c.code.length, 0) / chunks.length
|
|
66
|
+
},
|
|
67
|
+
chunks: chunks
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Find files to analyze (same logic as bundles)
|
|
73
|
+
*/
|
|
74
|
+
findFiles(projectPath, patterns) {
|
|
75
|
+
const files = []
|
|
76
|
+
|
|
77
|
+
for (const pattern of patterns) {
|
|
78
|
+
const matches = glob.sync(pattern, {
|
|
79
|
+
cwd: projectPath,
|
|
80
|
+
ignore: [
|
|
81
|
+
'node_modules/**', 'dist/**', 'build/**', '.git/**',
|
|
82
|
+
'*.test.*', '*.spec.*', '**/test/**', '**/tests/**',
|
|
83
|
+
'**/*.min.js', '**/*.bundle.js'
|
|
84
|
+
]
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
files.push(...matches.filter(file =>
|
|
88
|
+
!file.includes('node_modules') &&
|
|
89
|
+
!file.includes('dist/') &&
|
|
90
|
+
!file.includes('.min.')
|
|
91
|
+
))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [...new Set(files)]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract functions, types, and imports from a file
|
|
99
|
+
*/
|
|
100
|
+
extractCodeElements(relativePath, projectPath) {
|
|
101
|
+
const fullPath = join(projectPath, relativePath)
|
|
102
|
+
if (!existsSync(fullPath)) return { functions: [], types: [], imports: [] }
|
|
103
|
+
|
|
104
|
+
const content = readFileSync(fullPath, 'utf8')
|
|
105
|
+
const lines = content.split('\n')
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
functions: this.extractFunctions(content, lines, relativePath),
|
|
109
|
+
types: this.extractTypes(content, lines, relativePath),
|
|
110
|
+
imports: this.extractImports(content, relativePath)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract functions with robust regex patterns
|
|
116
|
+
*/
|
|
117
|
+
extractFunctions(content, lines, filePath) {
|
|
118
|
+
const functions = []
|
|
119
|
+
|
|
120
|
+
// Pattern 1: Regular function declarations
|
|
121
|
+
const functionRegex = /^(\s*)(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm
|
|
122
|
+
|
|
123
|
+
// Pattern 2: Arrow functions assigned to const/let
|
|
124
|
+
const arrowRegex = /^(\s*)(?:export\s+)?const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>\s*[\{]/gm
|
|
125
|
+
|
|
126
|
+
// Pattern 3: Class methods
|
|
127
|
+
const methodRegex = /^(\s+)(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm
|
|
128
|
+
|
|
129
|
+
// Pattern 4: React components (function components)
|
|
130
|
+
const componentRegex = /^(\s*)(?:export\s+(?:default\s+)?)?function\s+([A-Z][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm
|
|
131
|
+
|
|
132
|
+
const patterns = [
|
|
133
|
+
{ regex: functionRegex, type: 'function' },
|
|
134
|
+
{ regex: arrowRegex, type: 'arrow_function' },
|
|
135
|
+
{ regex: methodRegex, type: 'method' },
|
|
136
|
+
{ regex: componentRegex, type: 'react_component' }
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for (const { regex, type } of patterns) {
|
|
140
|
+
let match
|
|
141
|
+
while ((match = regex.exec(content)) !== null) {
|
|
142
|
+
const functionName = match[2]
|
|
143
|
+
const indentation = match[1]
|
|
144
|
+
const startIndex = match.index
|
|
145
|
+
|
|
146
|
+
// Skip if it's a keyword or common false positive
|
|
147
|
+
if (['if', 'for', 'while', 'switch', 'catch'].includes(functionName)) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const startLine = content.substring(0, startIndex).split('\n').length
|
|
152
|
+
const functionBody = this.extractFunctionBody(content, startIndex)
|
|
153
|
+
|
|
154
|
+
if (functionBody && functionBody.length > this.options.minFunctionSize) {
|
|
155
|
+
functions.push({
|
|
156
|
+
name: functionName,
|
|
157
|
+
type: type,
|
|
158
|
+
filePath: filePath,
|
|
159
|
+
startLine: startLine,
|
|
160
|
+
code: functionBody,
|
|
161
|
+
indentation: indentation.length,
|
|
162
|
+
isExported: match[0].includes('export'),
|
|
163
|
+
isAsync: match[0].includes('async'),
|
|
164
|
+
size: functionBody.length
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return functions
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Extract function body using brace matching
|
|
175
|
+
*/
|
|
176
|
+
extractFunctionBody(content, startIndex) {
|
|
177
|
+
const openBraceIndex = content.indexOf('{', startIndex)
|
|
178
|
+
if (openBraceIndex === -1) return null
|
|
179
|
+
|
|
180
|
+
let braceCount = 0
|
|
181
|
+
let currentIndex = openBraceIndex
|
|
182
|
+
let inString = false
|
|
183
|
+
let stringChar = null
|
|
184
|
+
|
|
185
|
+
while (currentIndex < content.length) {
|
|
186
|
+
const char = content[currentIndex]
|
|
187
|
+
const prevChar = content[currentIndex - 1] || ''
|
|
188
|
+
|
|
189
|
+
// Handle string literals
|
|
190
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
191
|
+
if (!inString) {
|
|
192
|
+
inString = true
|
|
193
|
+
stringChar = char
|
|
194
|
+
} else if (char === stringChar) {
|
|
195
|
+
inString = false
|
|
196
|
+
stringChar = null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Count braces outside strings
|
|
201
|
+
if (!inString) {
|
|
202
|
+
if (char === '{') braceCount++
|
|
203
|
+
else if (char === '}') braceCount--
|
|
204
|
+
|
|
205
|
+
if (braceCount === 0) {
|
|
206
|
+
// Found the closing brace
|
|
207
|
+
return content.substring(startIndex, currentIndex + 1).trim()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
currentIndex++
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null // Unmatched braces
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Extract type definitions and interfaces
|
|
219
|
+
*/
|
|
220
|
+
extractTypes(content, lines, filePath) {
|
|
221
|
+
const types = []
|
|
222
|
+
|
|
223
|
+
// TypeScript interfaces
|
|
224
|
+
const interfaceRegex = /^(\s*)(?:export\s+)?interface\s+([A-Z][a-zA-Z0-9_$]*)\s*\{/gm
|
|
225
|
+
|
|
226
|
+
// Type aliases
|
|
227
|
+
const typeRegex = /^(\s*)(?:export\s+)?type\s+([A-Z][a-zA-Z0-9_$]*)\s*=/gm
|
|
228
|
+
|
|
229
|
+
const patterns = [
|
|
230
|
+
{ regex: interfaceRegex, type: 'interface' },
|
|
231
|
+
{ regex: typeRegex, type: 'type_alias' }
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
for (const { regex, type } of patterns) {
|
|
235
|
+
let match
|
|
236
|
+
while ((match = regex.exec(content)) !== null) {
|
|
237
|
+
const typeName = match[2]
|
|
238
|
+
const startIndex = match.index
|
|
239
|
+
const startLine = content.substring(0, startIndex).split('\n').length
|
|
240
|
+
|
|
241
|
+
let typeBody
|
|
242
|
+
if (type === 'interface') {
|
|
243
|
+
typeBody = this.extractTypeBody(content, startIndex)
|
|
244
|
+
} else {
|
|
245
|
+
// For type aliases, extract until semicolon or newline
|
|
246
|
+
const endIndex = content.indexOf(';', startIndex)
|
|
247
|
+
typeBody = content.substring(startIndex, endIndex + 1).trim()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (typeBody) {
|
|
251
|
+
types.push({
|
|
252
|
+
name: typeName,
|
|
253
|
+
type: type,
|
|
254
|
+
filePath: filePath,
|
|
255
|
+
startLine: startLine,
|
|
256
|
+
code: typeBody,
|
|
257
|
+
isExported: match[0].includes('export')
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return types
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Extract type body (similar to function body)
|
|
268
|
+
*/
|
|
269
|
+
extractTypeBody(content, startIndex) {
|
|
270
|
+
const openBraceIndex = content.indexOf('{', startIndex)
|
|
271
|
+
if (openBraceIndex === -1) return null
|
|
272
|
+
|
|
273
|
+
let braceCount = 0
|
|
274
|
+
let currentIndex = openBraceIndex
|
|
275
|
+
|
|
276
|
+
while (currentIndex < content.length) {
|
|
277
|
+
const char = content[currentIndex]
|
|
278
|
+
|
|
279
|
+
if (char === '{') braceCount++
|
|
280
|
+
else if (char === '}') braceCount--
|
|
281
|
+
|
|
282
|
+
if (braceCount === 0) {
|
|
283
|
+
return content.substring(startIndex, currentIndex + 1).trim()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
currentIndex++
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Extract import statements
|
|
294
|
+
*/
|
|
295
|
+
extractImports(content, filePath) {
|
|
296
|
+
const imports = []
|
|
297
|
+
const importRegex = /^(\s*)import\s+(.+?)\s+from\s+['"`]([^'"`]+)['"`]/gm
|
|
298
|
+
|
|
299
|
+
let match
|
|
300
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
301
|
+
const importStatement = match[0].trim()
|
|
302
|
+
const importPath = match[3]
|
|
303
|
+
|
|
304
|
+
imports.push({
|
|
305
|
+
statement: importStatement,
|
|
306
|
+
path: importPath,
|
|
307
|
+
filePath: filePath,
|
|
308
|
+
isRelative: importPath.startsWith('.'),
|
|
309
|
+
isExternal: !importPath.startsWith('.')
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return imports
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Create semantic chunks from extracted elements
|
|
318
|
+
*/
|
|
319
|
+
createSemanticChunks(functions, types, imports) {
|
|
320
|
+
const chunks = []
|
|
321
|
+
|
|
322
|
+
// Create function-level chunks
|
|
323
|
+
for (const func of functions) {
|
|
324
|
+
const chunk = this.createFunctionChunk(func, types, imports)
|
|
325
|
+
if (chunk) {
|
|
326
|
+
chunks.push(chunk)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Create type-only chunks for standalone types
|
|
331
|
+
for (const type of types) {
|
|
332
|
+
if (!this.isTypeUsedInFunctions(type, functions)) {
|
|
333
|
+
chunks.push(this.createTypeChunk(type, imports))
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return chunks
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create a semantic chunk for a function with its context
|
|
342
|
+
*/
|
|
343
|
+
createFunctionChunk(func, allTypes, allImports) {
|
|
344
|
+
let chunkCode = ''
|
|
345
|
+
const includedImports = new Set()
|
|
346
|
+
const includedTypes = new Set()
|
|
347
|
+
|
|
348
|
+
// Find relevant imports for this function
|
|
349
|
+
const fileImports = allImports.filter(imp => imp.filePath === func.filePath)
|
|
350
|
+
|
|
351
|
+
// Find types referenced in the function
|
|
352
|
+
const referencedTypes = this.findReferencedTypes(func.code, allTypes)
|
|
353
|
+
|
|
354
|
+
// Add necessary imports
|
|
355
|
+
for (const imp of fileImports) {
|
|
356
|
+
if (this.isImportRelevant(imp, func.code)) {
|
|
357
|
+
chunkCode += imp.statement + '\n'
|
|
358
|
+
includedImports.add(imp.path)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Add referenced types
|
|
363
|
+
for (const type of referencedTypes) {
|
|
364
|
+
chunkCode += '\n' + type.code + '\n'
|
|
365
|
+
includedTypes.add(type.name)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Add the function itself
|
|
369
|
+
chunkCode += '\n' + func.code
|
|
370
|
+
|
|
371
|
+
// Create chunk with adaptive sizing - never lose functions
|
|
372
|
+
let finalCode = chunkCode.trim()
|
|
373
|
+
let contextLevel = 'full'
|
|
374
|
+
|
|
375
|
+
// If too large, try with reduced context
|
|
376
|
+
if (chunkCode.length > this.options.maxChunkSize) {
|
|
377
|
+
// Fallback 1: Function + essential imports only (no types)
|
|
378
|
+
finalCode = ''
|
|
379
|
+
for (const imp of fileImports.slice(0, 3)) { // Limit to 3 imports
|
|
380
|
+
if (this.isImportRelevant(imp, func.code)) {
|
|
381
|
+
finalCode += imp.statement + '\n'
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
finalCode += '\n' + func.code
|
|
385
|
+
contextLevel = 'reduced'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// If still too large, function only
|
|
389
|
+
if (finalCode.length > this.options.maxChunkSize) {
|
|
390
|
+
finalCode = func.code
|
|
391
|
+
contextLevel = 'minimal'
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Always create a chunk - never lose functions
|
|
395
|
+
return {
|
|
396
|
+
name: func.name,
|
|
397
|
+
type: 'function_chunk',
|
|
398
|
+
subtype: func.type,
|
|
399
|
+
code: finalCode,
|
|
400
|
+
size: finalCode.length,
|
|
401
|
+
filePath: func.filePath,
|
|
402
|
+
startLine: func.startLine,
|
|
403
|
+
isExported: func.isExported,
|
|
404
|
+
isAsync: func.isAsync,
|
|
405
|
+
complexity: this.calculateComplexity(func.code),
|
|
406
|
+
includes: {
|
|
407
|
+
imports: contextLevel === 'minimal' ? [] : Array.from(includedImports),
|
|
408
|
+
types: contextLevel === 'full' ? Array.from(includedTypes) : []
|
|
409
|
+
},
|
|
410
|
+
purpose: this.determinePurpose(func),
|
|
411
|
+
tags: [...this.generateTags(func), contextLevel === 'full' ? 'full-context' : contextLevel === 'reduced' ? 'reduced-context' : 'minimal-context'],
|
|
412
|
+
bundles: this.getFileBundles(func.filePath)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Create a chunk for standalone types
|
|
418
|
+
*/
|
|
419
|
+
createTypeChunk(type, allImports) {
|
|
420
|
+
let chunkCode = ''
|
|
421
|
+
const includedImports = new Set()
|
|
422
|
+
|
|
423
|
+
// Add relevant imports if any
|
|
424
|
+
const fileImports = allImports.filter(imp => imp.filePath === type.filePath)
|
|
425
|
+
for (const imp of fileImports.slice(0, 3)) { // Limit imports
|
|
426
|
+
chunkCode += imp.statement + '\n'
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
chunkCode += '\n' + type.code
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
name: type.name,
|
|
433
|
+
type: 'type_chunk',
|
|
434
|
+
subtype: type.type,
|
|
435
|
+
code: chunkCode.trim(),
|
|
436
|
+
size: chunkCode.length,
|
|
437
|
+
filePath: type.filePath,
|
|
438
|
+
startLine: type.startLine,
|
|
439
|
+
isExported: type.isExported,
|
|
440
|
+
purpose: 'Type definition',
|
|
441
|
+
tags: ['type', type.type],
|
|
442
|
+
bundles: this.getFileBundles(type.filePath)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Find types referenced in function code
|
|
448
|
+
*/
|
|
449
|
+
findReferencedTypes(functionCode, allTypes) {
|
|
450
|
+
const referenced = []
|
|
451
|
+
|
|
452
|
+
for (const type of allTypes) {
|
|
453
|
+
// Check if type name appears in function code
|
|
454
|
+
const typeRegex = new RegExp(`\\b${type.name}\\b`, 'g')
|
|
455
|
+
if (typeRegex.test(functionCode)) {
|
|
456
|
+
referenced.push(type)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return referenced
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Check if import is relevant to function
|
|
465
|
+
*/
|
|
466
|
+
isImportRelevant(importStatement, functionCode) {
|
|
467
|
+
// Simple heuristic: check if any imported identifiers appear in function
|
|
468
|
+
const importMatch = importStatement.statement.match(/import\s+(.+?)\s+from/)
|
|
469
|
+
if (!importMatch) return false
|
|
470
|
+
|
|
471
|
+
const imported = importMatch[1]
|
|
472
|
+
|
|
473
|
+
// Handle different import styles
|
|
474
|
+
if (imported.includes('{')) {
|
|
475
|
+
// Named imports: import { foo, bar } from 'module'
|
|
476
|
+
const namedImports = imported.match(/\{([^}]+)\}/)?.[1]
|
|
477
|
+
if (namedImports) {
|
|
478
|
+
const names = namedImports.split(',').map(name => name.trim())
|
|
479
|
+
return names.some(name => functionCode.includes(name))
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
// Default import: import foo from 'module'
|
|
483
|
+
const defaultImport = imported.trim()
|
|
484
|
+
return functionCode.includes(defaultImport)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return false
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Check if type is used in any function
|
|
492
|
+
*/
|
|
493
|
+
isTypeUsedInFunctions(type, functions) {
|
|
494
|
+
const typeRegex = new RegExp(`\\b${type.name}\\b`, 'g')
|
|
495
|
+
return functions.some(func => typeRegex.test(func.code))
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Calculate function complexity (cyclomatic complexity)
|
|
500
|
+
*/
|
|
501
|
+
calculateComplexity(code) {
|
|
502
|
+
let complexity = 1 // Base complexity
|
|
503
|
+
|
|
504
|
+
// Simple complexity indicators - just count control flow structures
|
|
505
|
+
const indicators = {
|
|
506
|
+
'if': (code.match(/\bif\s*\(/g) || []).length,
|
|
507
|
+
'else if': (code.match(/\belse\s+if\b/g) || []).length,
|
|
508
|
+
'for': (code.match(/\bfor\s*\(/g) || []).length,
|
|
509
|
+
'while': (code.match(/\bwhile\s*\(/g) || []).length,
|
|
510
|
+
'switch': (code.match(/\bswitch\s*\(/g) || []).length,
|
|
511
|
+
'case': (code.match(/\bcase\s+/g) || []).length,
|
|
512
|
+
'catch': (code.match(/\bcatch\s*\(/g) || []).length,
|
|
513
|
+
'ternary': (code.match(/\?\s*[^?\.\s]/g) || []).length,
|
|
514
|
+
'logical_and': (code.match(/&&\s*[^&=]/g) || []).length,
|
|
515
|
+
'logical_or': (code.match(/\|\|\s*[^|=]/g) || []).length
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Sum all complexity indicators
|
|
519
|
+
for (const count of Object.values(indicators)) {
|
|
520
|
+
complexity += count
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Return complexity with reasonable thresholds
|
|
524
|
+
return {
|
|
525
|
+
score: complexity,
|
|
526
|
+
level: complexity <= 3 ? 'low' : complexity <= 8 ? 'medium' : 'high'
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Determine function purpose using heuristics configuration
|
|
532
|
+
*/
|
|
533
|
+
determinePurpose(func) {
|
|
534
|
+
return this.heuristicsManager.determinePurpose(func)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Generate tags for function
|
|
539
|
+
*/
|
|
540
|
+
generateTags(func) {
|
|
541
|
+
const tags = [func.type]
|
|
542
|
+
|
|
543
|
+
if (func.isExported) tags.push('exported')
|
|
544
|
+
if (func.isAsync) tags.push('async')
|
|
545
|
+
if (func.size > 1000) tags.push('large')
|
|
546
|
+
if (func.code.includes('console.log')) tags.push('has-logging')
|
|
547
|
+
if (func.code.includes('throw')) tags.push('can-throw')
|
|
548
|
+
if (func.code.includes('return')) tags.push('returns-value')
|
|
549
|
+
|
|
550
|
+
return tags
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Determine which bundles a file belongs to
|
|
555
|
+
*/
|
|
556
|
+
getFileBundles(filePath) {
|
|
557
|
+
if (!this.bundleConfig?.bundles) return []
|
|
558
|
+
|
|
559
|
+
const bundles = []
|
|
560
|
+
for (const [bundleName, patterns] of Object.entries(this.bundleConfig.bundles)) {
|
|
561
|
+
// Skip master bundle as requested
|
|
562
|
+
if (bundleName === 'master') continue
|
|
563
|
+
|
|
564
|
+
// Check if file matches any pattern in this bundle
|
|
565
|
+
for (const pattern of patterns) {
|
|
566
|
+
if (this.matchesPattern(filePath, pattern)) {
|
|
567
|
+
bundles.push(bundleName)
|
|
568
|
+
break // Don't add the same bundle multiple times
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return bundles
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Simple pattern matching (basic glob support)
|
|
578
|
+
*/
|
|
579
|
+
matchesPattern(filePath, pattern) {
|
|
580
|
+
// Convert glob pattern to regex
|
|
581
|
+
const regex = pattern
|
|
582
|
+
.replace(/\*\*/g, '.*') // ** matches any directories
|
|
583
|
+
.replace(/\*/g, '[^/]*') // * matches any characters except /
|
|
584
|
+
.replace(/\./g, '\\.') // Escape dots
|
|
585
|
+
|
|
586
|
+
return new RegExp(`^${regex}$`).test(filePath)
|
|
587
|
+
}
|
|
588
|
+
}
|