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