cntx-ui 2.0.15 → 3.0.1
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/README.md +40 -344
- package/bin/cntx-ui-mcp.sh +3 -0
- package/bin/cntx-ui.js +2 -1
- package/lib/agent-runtime.js +161 -1340
- package/lib/agent-tools.js +9 -7
- package/lib/api-router.js +262 -79
- package/lib/bundle-manager.js +172 -407
- package/lib/configuration-manager.js +94 -59
- package/lib/database-manager.js +397 -0
- package/lib/file-system-manager.js +17 -0
- package/lib/heuristics-manager.js +119 -17
- package/lib/mcp-server.js +125 -55
- package/lib/semantic-splitter.js +222 -481
- package/lib/simple-vector-store.js +69 -300
- package/package.json +18 -31
- package/server.js +151 -73
- package/templates/TOOLS.md +41 -0
- package/templates/activities/activities/create-project-bundles/README.md +4 -3
- package/templates/activities/activities/create-project-bundles/notes.md +15 -19
- package/templates/activities/activities/create-project-bundles/tasks.md +4 -4
- package/templates/activities/activities.json +1 -1
- package/templates/agent-config.yaml +0 -13
- package/templates/agent-instructions.md +22 -6
- package/templates/agent-rules/capabilities/bundle-system.md +1 -1
- package/templates/agent-rules/project-specific/architecture.md +1 -1
- package/web/dist/assets/index-B2OdTzzI.css +1 -0
- package/web/dist/assets/index-D0tBsKiR.js +2016 -0
- package/web/dist/index.html +2 -2
- package/mcp-config-example.json +0 -9
- package/web/dist/assets/heuristics-manager-browser-DfonOP5I.js +0 -1
- package/web/dist/assets/index-dF3qg-y_.js +0 -2486
- package/web/dist/assets/index-h5FGSg_P.css +0 -1
package/lib/semantic-splitter.js
CHANGED
|
@@ -1,588 +1,329 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Semantic Splitter - High-Performance AST-based Chunker
|
|
3
|
+
* Uses tree-sitter for surgical, function-level code extraction
|
|
4
|
+
* Integrated with HeuristicsManager for intelligent categorization
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFileSync, existsSync } from 'fs'
|
|
8
|
-
import {
|
|
8
|
+
import { join, extname } from 'path'
|
|
9
9
|
import glob from 'glob'
|
|
10
|
+
import Parser from 'tree-sitter'
|
|
11
|
+
import JavaScript from 'tree-sitter-javascript'
|
|
12
|
+
import TypeScript from 'tree-sitter-typescript'
|
|
10
13
|
import HeuristicsManager from './heuristics-manager.js'
|
|
11
14
|
|
|
12
15
|
export default class SemanticSplitter {
|
|
13
16
|
constructor(options = {}) {
|
|
14
17
|
this.options = {
|
|
15
|
-
maxChunkSize:
|
|
18
|
+
maxChunkSize: 3000, // Max chars per chunk
|
|
16
19
|
includeContext: true, // Include imports/types needed
|
|
17
|
-
|
|
18
|
-
minFunctionSize: 50, // Skip tiny functions
|
|
20
|
+
minFunctionSize: 40, // Skip tiny functions
|
|
19
21
|
...options
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
// Initialize
|
|
24
|
+
// Initialize tree-sitter parsers
|
|
25
|
+
this.parsers = {
|
|
26
|
+
javascript: new Parser(),
|
|
27
|
+
typescript: new Parser(),
|
|
28
|
+
tsx: new Parser()
|
|
29
|
+
}
|
|
30
|
+
this.parsers.javascript.setLanguage(JavaScript)
|
|
31
|
+
this.parsers.typescript.setLanguage(TypeScript.typescript)
|
|
32
|
+
this.parsers.tsx.setLanguage(TypeScript.tsx)
|
|
33
|
+
|
|
23
34
|
this.heuristicsManager = new HeuristicsManager()
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
getParser(filePath) {
|
|
38
|
+
const ext = extname(filePath)
|
|
39
|
+
switch (ext) {
|
|
40
|
+
case '.ts': return this.parsers.typescript
|
|
41
|
+
case '.tsx': return this.parsers.tsx
|
|
42
|
+
default: return this.parsers.javascript
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
/**
|
|
27
|
-
*
|
|
47
|
+
* Main entry point - extract semantic chunks from project
|
|
28
48
|
*/
|
|
29
49
|
async extractSemanticChunks(projectPath, patterns = ['**/*.{js,jsx,ts,tsx,mjs}'], bundleConfig = null) {
|
|
30
|
-
console.log('🔪 Starting semantic splitting...')
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
console.log('🔪 Starting surgical semantic splitting via tree-sitter...');
|
|
51
|
+
console.log(`📂 Project path: ${projectPath}`);
|
|
52
|
+
console.log(`📋 Patterns: ${patterns}`);
|
|
53
|
+
|
|
54
|
+
this.bundleConfig = bundleConfig;
|
|
55
|
+
const files = this.findFiles(projectPath, patterns);
|
|
56
|
+
console.log(`📁 Found ${files.length} files to split`);
|
|
57
|
+
if (files.length > 0) {
|
|
58
|
+
console.log('📄 Sample files:', files.slice(0, 5));
|
|
59
|
+
}
|
|
37
60
|
|
|
38
|
-
const
|
|
39
|
-
const allTypes = []
|
|
40
|
-
const allImports = []
|
|
61
|
+
const allChunks = []
|
|
41
62
|
|
|
42
|
-
// Extract all code elements
|
|
43
63
|
for (const filePath of files) {
|
|
44
64
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
allTypes.push(...elements.types)
|
|
48
|
-
allImports.push(...elements.imports)
|
|
65
|
+
const fileChunks = this.processFile(filePath, projectPath)
|
|
66
|
+
allChunks.push(...fileChunks)
|
|
49
67
|
} catch (error) {
|
|
50
|
-
console.warn(`Failed to
|
|
68
|
+
console.warn(`Failed to process ${filePath}: ${error.message}`)
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
console.log(
|
|
55
|
-
|
|
56
|
-
// Create semantic chunks
|
|
57
|
-
const chunks = this.createSemanticChunks(allFunctions, allTypes, allImports)
|
|
58
|
-
console.log(`🧩 Created ${chunks.length} semantic chunks`)
|
|
59
|
-
|
|
72
|
+
console.log(`🧩 Created ${allChunks.length} semantic chunks across project`)
|
|
60
73
|
return {
|
|
61
74
|
summary: {
|
|
62
75
|
totalFiles: files.length,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
averageChunkSize: chunks.reduce((sum, c) => sum + c.code.length, 0) / chunks.length
|
|
76
|
+
totalChunks: allChunks.length,
|
|
77
|
+
averageSize: allChunks.reduce((sum, c) => sum + c.code.length, 0) / allChunks.length
|
|
66
78
|
},
|
|
67
|
-
chunks:
|
|
79
|
+
chunks: allChunks
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
/**
|
|
72
|
-
* Find files to analyze (same logic as bundles)
|
|
73
|
-
*/
|
|
74
83
|
findFiles(projectPath, patterns) {
|
|
75
84
|
const files = []
|
|
76
|
-
|
|
77
85
|
for (const pattern of patterns) {
|
|
78
86
|
const matches = glob.sync(pattern, {
|
|
79
87
|
cwd: projectPath,
|
|
80
|
-
ignore: [
|
|
81
|
-
'node_modules/**', 'dist/**', 'build/**', '.git/**',
|
|
82
|
-
'*.test.*', '*.spec.*', '**/test/**', '**/tests/**',
|
|
83
|
-
'**/*.min.js', '**/*.bundle.js'
|
|
84
|
-
]
|
|
88
|
+
ignore: ['node_modules/**', 'dist/**', '.git/**', '*.test.*', '*.spec.*']
|
|
85
89
|
})
|
|
86
|
-
|
|
87
|
-
files.push(...matches.filter(file =>
|
|
88
|
-
!file.includes('node_modules') &&
|
|
89
|
-
!file.includes('dist/') &&
|
|
90
|
-
!file.includes('.min.')
|
|
91
|
-
))
|
|
90
|
+
files.push(...matches)
|
|
92
91
|
}
|
|
93
|
-
|
|
94
92
|
return [...new Set(files)]
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
functions:
|
|
109
|
-
types:
|
|
110
|
-
imports: this.extractImports(content, relativePath)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
95
|
+
processFile(relativePath, projectPath) {
|
|
96
|
+
const fullPath = join(projectPath, relativePath);
|
|
97
|
+
if (!existsSync(fullPath)) return [];
|
|
98
|
+
|
|
99
|
+
// console.log(` 📄 Processing: ${relativePath}`);
|
|
100
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
101
|
+
const parser = this.getParser(relativePath);
|
|
102
|
+
const tree = parser.parse(content);
|
|
103
|
+
const root = tree.rootNode;
|
|
104
|
+
|
|
105
|
+
const elements = {
|
|
106
|
+
functions: [],
|
|
107
|
+
types: [],
|
|
108
|
+
imports: this.extractImports(root, content, relativePath)
|
|
109
|
+
};
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
}
|
|
111
|
+
// Traverse AST for functions and types
|
|
112
|
+
this.traverse(root, content, relativePath, elements);
|
|
172
113
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
114
|
+
const chunks = this.createChunks(elements, content, relativePath);
|
|
115
|
+
// if (chunks.length > 0) {
|
|
116
|
+
// console.log(` ✅ Found ${chunks.length} chunks in ${relativePath}`);
|
|
117
|
+
// }
|
|
118
|
+
return chunks;
|
|
215
119
|
}
|
|
216
120
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
}
|
|
121
|
+
traverse(node, content, filePath, elements) {
|
|
122
|
+
// Detect Function Declarations
|
|
123
|
+
if (node.type === 'function_declaration' || node.type === 'method_definition' || node.type === 'arrow_function') {
|
|
124
|
+
const func = this.mapFunctionNode(node, content, filePath)
|
|
125
|
+
if (func && func.code.length > this.options.minFunctionSize) {
|
|
126
|
+
elements.functions.push(func)
|
|
260
127
|
}
|
|
261
128
|
}
|
|
262
|
-
|
|
263
|
-
return types
|
|
264
|
-
}
|
|
265
129
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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++
|
|
130
|
+
// Detect Type Definitions (TS)
|
|
131
|
+
if (node.type === 'interface_declaration' || node.type === 'type_alias_declaration') {
|
|
132
|
+
const typeDef = this.mapTypeNode(node, content, filePath)
|
|
133
|
+
if (typeDef) elements.types.push(typeDef)
|
|
287
134
|
}
|
|
288
|
-
|
|
289
|
-
return null
|
|
290
|
-
}
|
|
291
135
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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)
|
|
136
|
+
// Recurse unless we've already captured the block (like a function body)
|
|
137
|
+
if (node.type !== 'function_declaration' && node.type !== 'method_definition') {
|
|
138
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
139
|
+
this.traverse(node.namedChild(i), content, filePath, elements)
|
|
327
140
|
}
|
|
328
141
|
}
|
|
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
142
|
}
|
|
339
143
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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)
|
|
144
|
+
mapFunctionNode(node, content, filePath) {
|
|
145
|
+
let name = 'anonymous';
|
|
146
|
+
|
|
147
|
+
// Find name identifier based on node type
|
|
148
|
+
if (node.type === 'function_declaration' || node.type === 'method_definition') {
|
|
149
|
+
const nameNode = node.childForFieldName('name');
|
|
150
|
+
if (nameNode) name = content.slice(nameNode.startIndex, nameNode.endIndex);
|
|
151
|
+
} else if (node.type === 'arrow_function') {
|
|
152
|
+
// 1. Check if assigned to a variable: const foo = () => {}
|
|
153
|
+
const parent = node.parent;
|
|
154
|
+
if (parent && parent.type === 'variable_declarator') {
|
|
155
|
+
const nameNode = parent.childForFieldName('name');
|
|
156
|
+
if (nameNode) name = content.slice(nameNode.startIndex, nameNode.endIndex);
|
|
359
157
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
}
|
|
158
|
+
// 2. Check if part of an object property: { foo: () => {} }
|
|
159
|
+
else if (parent && parent.type === 'pair') {
|
|
160
|
+
const keyNode = parent.childForFieldName('key');
|
|
161
|
+
if (keyNode) name = content.slice(keyNode.startIndex, keyNode.endIndex);
|
|
162
|
+
}
|
|
163
|
+
// 3. Check if part of an assignment: this.foo = () => {}
|
|
164
|
+
else if (parent && parent.type === 'assignment_expression') {
|
|
165
|
+
const leftNode = parent.childForFieldName('left');
|
|
166
|
+
if (leftNode) name = content.slice(leftNode.startIndex, leftNode.endIndex);
|
|
383
167
|
}
|
|
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
168
|
}
|
|
169
|
+
|
|
170
|
+
const code = content.slice(node.startIndex, node.endIndex)
|
|
393
171
|
|
|
394
|
-
// Always create a chunk - never lose functions
|
|
395
172
|
return {
|
|
396
|
-
name
|
|
397
|
-
type:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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)
|
|
173
|
+
name,
|
|
174
|
+
type: node.type,
|
|
175
|
+
filePath,
|
|
176
|
+
startLine: node.startPosition.row + 1,
|
|
177
|
+
code,
|
|
178
|
+
isExported: this.isExported(node),
|
|
179
|
+
isAsync: code.includes('async')
|
|
413
180
|
}
|
|
414
181
|
}
|
|
415
182
|
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
183
|
+
mapTypeNode(node, content, filePath) {
|
|
184
|
+
const nameNode = node.childForFieldName('name')
|
|
185
|
+
if (!nameNode) return null
|
|
430
186
|
|
|
431
187
|
return {
|
|
432
|
-
name:
|
|
433
|
-
type:
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
startLine: type.startLine,
|
|
439
|
-
isExported: type.isExported,
|
|
440
|
-
purpose: 'Type definition',
|
|
441
|
-
tags: ['type', type.type],
|
|
442
|
-
bundles: this.getFileBundles(type.filePath)
|
|
188
|
+
name: content.slice(nameNode.startIndex, nameNode.endIndex),
|
|
189
|
+
type: node.type,
|
|
190
|
+
filePath,
|
|
191
|
+
startLine: node.startPosition.row + 1,
|
|
192
|
+
code: content.slice(node.startIndex, node.endIndex),
|
|
193
|
+
isExported: this.isExported(node)
|
|
443
194
|
}
|
|
444
195
|
}
|
|
445
196
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
referenced.push(type)
|
|
197
|
+
extractImports(root, content, filePath) {
|
|
198
|
+
const imports = []
|
|
199
|
+
// Simple traversal for import statements
|
|
200
|
+
for (let i = 0; i < root.namedChildCount; i++) {
|
|
201
|
+
const node = root.namedChild(i)
|
|
202
|
+
if (node.type === 'import_statement') {
|
|
203
|
+
imports.push({
|
|
204
|
+
statement: content.slice(node.startIndex, node.endIndex),
|
|
205
|
+
filePath
|
|
206
|
+
})
|
|
457
207
|
}
|
|
458
208
|
}
|
|
459
|
-
|
|
460
|
-
return referenced
|
|
209
|
+
return imports
|
|
461
210
|
}
|
|
462
211
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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)
|
|
212
|
+
isExported(node) {
|
|
213
|
+
let current = node
|
|
214
|
+
while (current) {
|
|
215
|
+
if (current.type === 'export_statement') return true
|
|
216
|
+
current = current.parent
|
|
485
217
|
}
|
|
486
|
-
|
|
487
218
|
return false
|
|
488
219
|
}
|
|
489
220
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
221
|
+
createChunks(elements, content, filePath) {
|
|
222
|
+
const chunks = [];
|
|
223
|
+
const pathParts = filePath.toLowerCase().split(/[\\\/]/);
|
|
224
|
+
|
|
225
|
+
for (const func of elements.functions) {
|
|
226
|
+
// Pass full context to heuristics
|
|
227
|
+
const heuristicContext = {
|
|
228
|
+
...func,
|
|
229
|
+
includes: elements,
|
|
230
|
+
pathParts
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const purpose = this.heuristicsManager.determinePurpose(heuristicContext);
|
|
234
|
+
const businessDomain = this.heuristicsManager.inferBusinessDomains(heuristicContext);
|
|
235
|
+
const technicalPatterns = this.heuristicsManager.inferTechnicalPatterns(heuristicContext);
|
|
236
|
+
const tags = this.generateTags(func);
|
|
497
237
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
238
|
+
// if (businessDomain.length > 0) {
|
|
239
|
+
// console.log(` 💎 ${func.name}: Domains: [${businessDomain}], Patterns: [${technicalPatterns}]`);
|
|
240
|
+
// }
|
|
241
|
+
|
|
242
|
+
let chunkCode = '';
|
|
243
|
+
if (this.options.includeContext) {
|
|
244
|
+
const relevantImports = elements.imports
|
|
245
|
+
.filter(imp => this.isImportRelevant(imp.statement, func.code))
|
|
246
|
+
.map(imp => imp.statement)
|
|
247
|
+
.join('\n');
|
|
248
|
+
|
|
249
|
+
if (relevantImports) chunkCode += relevantImports + '\n\n';
|
|
250
|
+
}
|
|
251
|
+
chunkCode += func.code;
|
|
252
|
+
|
|
253
|
+
chunks.push({
|
|
254
|
+
id: `${filePath}:${func.name}:${func.startLine}`,
|
|
255
|
+
name: func.name,
|
|
256
|
+
filePath,
|
|
257
|
+
type: 'function',
|
|
258
|
+
subtype: func.type,
|
|
259
|
+
code: chunkCode,
|
|
260
|
+
startLine: func.startLine,
|
|
261
|
+
complexity: this.calculateComplexity(func.code),
|
|
262
|
+
purpose,
|
|
263
|
+
tags,
|
|
264
|
+
businessDomain,
|
|
265
|
+
technicalPatterns,
|
|
266
|
+
includes: {
|
|
267
|
+
imports: elements.imports.map(i => i.statement),
|
|
268
|
+
types: elements.types.map(t => t.name)
|
|
269
|
+
},
|
|
270
|
+
bundles: this.getFileBundles(filePath)
|
|
271
|
+
});
|
|
521
272
|
}
|
|
522
273
|
|
|
523
|
-
|
|
524
|
-
return {
|
|
525
|
-
score: complexity,
|
|
526
|
-
level: complexity <= 3 ? 'low' : complexity <= 8 ? 'medium' : 'high'
|
|
527
|
-
}
|
|
274
|
+
return chunks;
|
|
528
275
|
}
|
|
529
276
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
277
|
+
isImportRelevant(importStatement, functionCode) {
|
|
278
|
+
// Heuristic: does the function use any name from the import?
|
|
279
|
+
const match = importStatement.match(/import\s+(?:\{([^}]+)\}|(\w+))/i)
|
|
280
|
+
if (!match) return false
|
|
281
|
+
const importedNames = match[1] ? match[1].split(',').map(n => n.trim()) : [match[2]]
|
|
282
|
+
return importedNames.some(name => functionCode.includes(name))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
calculateComplexity(code) {
|
|
286
|
+
const indicators = ['if', 'else', 'for', 'while', 'switch', 'case', 'catch', '?', '&&', '||'];
|
|
287
|
+
let score = 1;
|
|
288
|
+
indicators.forEach(ind => {
|
|
289
|
+
// Escape special regex characters
|
|
290
|
+
const escaped = ind.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
291
|
+
// Only use word boundaries for word-like indicators
|
|
292
|
+
const pattern = /^[a-zA-Z]+$/.test(ind) ? `\\b${escaped}\\b` : escaped;
|
|
293
|
+
const regex = new RegExp(pattern, 'g');
|
|
294
|
+
score += (code.match(regex) || []).length;
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
score,
|
|
298
|
+
level: score < 5 ? 'low' : score < 15 ? 'medium' : 'high'
|
|
299
|
+
};
|
|
535
300
|
}
|
|
536
301
|
|
|
537
|
-
/**
|
|
538
|
-
* Generate tags for function
|
|
539
|
-
*/
|
|
540
302
|
generateTags(func) {
|
|
541
303
|
const tags = [func.type]
|
|
542
|
-
|
|
543
304
|
if (func.isExported) tags.push('exported')
|
|
544
305
|
if (func.isAsync) tags.push('async')
|
|
545
|
-
if (func.
|
|
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
|
-
|
|
306
|
+
if (func.code.length > 2000) tags.push('large')
|
|
550
307
|
return tags
|
|
551
308
|
}
|
|
552
309
|
|
|
553
|
-
/**
|
|
554
|
-
* Determine which bundles a file belongs to
|
|
555
|
-
*/
|
|
556
310
|
getFileBundles(filePath) {
|
|
557
311
|
if (!this.bundleConfig?.bundles) return []
|
|
558
|
-
|
|
559
312
|
const bundles = []
|
|
560
|
-
for (const [
|
|
561
|
-
|
|
562
|
-
if (bundleName === 'master') continue
|
|
563
|
-
|
|
564
|
-
// Check if file matches any pattern in this bundle
|
|
313
|
+
for (const [name, patterns] of Object.entries(this.bundleConfig.bundles)) {
|
|
314
|
+
if (name === 'master') continue
|
|
565
315
|
for (const pattern of patterns) {
|
|
566
316
|
if (this.matchesPattern(filePath, pattern)) {
|
|
567
|
-
bundles.push(
|
|
568
|
-
break
|
|
317
|
+
bundles.push(name)
|
|
318
|
+
break
|
|
569
319
|
}
|
|
570
320
|
}
|
|
571
321
|
}
|
|
572
|
-
|
|
573
322
|
return bundles
|
|
574
323
|
}
|
|
575
324
|
|
|
576
|
-
/**
|
|
577
|
-
* Simple pattern matching (basic glob support)
|
|
578
|
-
*/
|
|
579
325
|
matchesPattern(filePath, pattern) {
|
|
580
|
-
|
|
581
|
-
const regex = pattern
|
|
582
|
-
.replace(/\*\*/g, '.*') // ** matches any directories
|
|
583
|
-
.replace(/\*/g, '[^/]*') // * matches any characters except /
|
|
584
|
-
.replace(/\./g, '\\.') // Escape dots
|
|
585
|
-
|
|
326
|
+
const regex = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*').replace(/\./g, '\\.')
|
|
586
327
|
return new RegExp(`^${regex}$`).test(filePath)
|
|
587
328
|
}
|
|
588
|
-
}
|
|
329
|
+
}
|