cntx-ui 2.0.12 → 2.0.13
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/lib/function-level-chunker.js +406 -0
- package/lib/semantic-integration.js +441 -0
- package/lib/semantic-splitter.js +595 -0
- package/lib/treesitter-semantic-chunker.js +1485 -0
- package/package.json +5 -1
- package/server.js +285 -45
- package/web/dist/assets/index-Ci1Q-YrQ.js +611 -0
- package/web/dist/assets/index-IUp4q_fr.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/vite.svg +21 -1
- package/web/dist/assets/index-8Kli5657.js +0 -541
- package/web/dist/assets/index-C-Ldi33E.css +0 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function-Level Semantic Chunker
|
|
3
|
+
* Extracts individual functions/methods/components as discrete chunks
|
|
4
|
+
* with intelligent context inclusion
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'fs'
|
|
8
|
+
import { extname, basename, dirname, join } from 'path'
|
|
9
|
+
import glob from 'glob'
|
|
10
|
+
|
|
11
|
+
export default class FunctionLevelChunker {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
includeContext: true,
|
|
15
|
+
maxContextLines: 50,
|
|
16
|
+
groupRelated: true,
|
|
17
|
+
...options
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract function-level chunks from project
|
|
23
|
+
*/
|
|
24
|
+
async extractFunctionChunks(projectPath, patterns = ['**/*.{js,jsx,ts,tsx,mjs}']) {
|
|
25
|
+
console.log('🔍 Starting function-level extraction...')
|
|
26
|
+
|
|
27
|
+
const files = this.findFiles(projectPath, patterns)
|
|
28
|
+
console.log(`📁 Found ${files.length} files to analyze`)
|
|
29
|
+
|
|
30
|
+
const allFunctions = []
|
|
31
|
+
let processedFiles = 0
|
|
32
|
+
|
|
33
|
+
for (const filePath of files) {
|
|
34
|
+
try {
|
|
35
|
+
const functions = this.extractFunctionsFromFile(filePath, projectPath)
|
|
36
|
+
allFunctions.push(...functions)
|
|
37
|
+
processedFiles++
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn(`Failed to extract from ${filePath}: ${error.message}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`✅ Extracted ${allFunctions.length} functions from ${processedFiles} files`)
|
|
44
|
+
|
|
45
|
+
// Create semantic chunks from functions
|
|
46
|
+
const chunks = this.createFunctionChunks(allFunctions)
|
|
47
|
+
console.log(`📦 Created ${chunks.length} function-level chunks`)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
summary: {
|
|
51
|
+
totalFiles: processedFiles,
|
|
52
|
+
totalFunctions: allFunctions.length,
|
|
53
|
+
totalChunks: chunks.length
|
|
54
|
+
},
|
|
55
|
+
functions: allFunctions,
|
|
56
|
+
chunks: chunks
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Find files to analyze
|
|
62
|
+
*/
|
|
63
|
+
findFiles(projectPath, patterns) {
|
|
64
|
+
const files = []
|
|
65
|
+
|
|
66
|
+
for (const pattern of patterns) {
|
|
67
|
+
const matches = glob.sync(pattern, {
|
|
68
|
+
cwd: projectPath,
|
|
69
|
+
ignore: [
|
|
70
|
+
'node_modules/**', 'dist/**', 'build/**', '.git/**',
|
|
71
|
+
'*.test.*', '*.spec.*', '**/test/**', '**/tests/**',
|
|
72
|
+
'**/*.min.js', '**/*.bundle.js'
|
|
73
|
+
]
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
files.push(...matches.filter(file =>
|
|
77
|
+
!file.includes('node_modules') &&
|
|
78
|
+
!file.includes('dist/') &&
|
|
79
|
+
!file.includes('.min.')
|
|
80
|
+
))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [...new Set(files)]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract all functions from a single file
|
|
88
|
+
*/
|
|
89
|
+
extractFunctionsFromFile(relativePath, projectPath) {
|
|
90
|
+
const fullPath = join(projectPath, relativePath)
|
|
91
|
+
if (!existsSync(fullPath)) return []
|
|
92
|
+
|
|
93
|
+
const content = readFileSync(fullPath, 'utf8')
|
|
94
|
+
const lines = content.split('\n')
|
|
95
|
+
|
|
96
|
+
const functions = []
|
|
97
|
+
|
|
98
|
+
// Try tree-sitter first, fallback to regex
|
|
99
|
+
try {
|
|
100
|
+
const treeSitterFunctions = this.extractWithTreeSitter(content, relativePath)
|
|
101
|
+
functions.push(...treeSitterFunctions)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Fallback to regex extraction
|
|
104
|
+
const regexFunctions = this.extractWithRegex(content, lines, relativePath)
|
|
105
|
+
functions.push(...regexFunctions)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return functions
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract functions using regex patterns (robust fallback)
|
|
113
|
+
*/
|
|
114
|
+
extractWithRegex(content, lines, filePath) {
|
|
115
|
+
const functions = []
|
|
116
|
+
|
|
117
|
+
// Patterns for different function types
|
|
118
|
+
const patterns = [
|
|
119
|
+
// Function declarations: function name() {}
|
|
120
|
+
{
|
|
121
|
+
pattern: /^[\s]*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm,
|
|
122
|
+
type: 'function'
|
|
123
|
+
},
|
|
124
|
+
// Arrow functions: const name = () => {}
|
|
125
|
+
{
|
|
126
|
+
pattern: /^[\s]*(?:export\s+)?const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/gm,
|
|
127
|
+
type: 'arrow_function'
|
|
128
|
+
},
|
|
129
|
+
// Class methods: methodName() {}
|
|
130
|
+
{
|
|
131
|
+
pattern: /^[\s]*(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm,
|
|
132
|
+
type: 'method'
|
|
133
|
+
},
|
|
134
|
+
// React components: export function ComponentName() {}
|
|
135
|
+
{
|
|
136
|
+
pattern: /^[\s]*export\s+(?:default\s+)?function\s+([A-Z][a-zA-Z0-9_$]*)\s*\(/gm,
|
|
137
|
+
type: 'react_component'
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
for (const { pattern, type } of patterns) {
|
|
142
|
+
let match
|
|
143
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
144
|
+
const functionName = match[1]
|
|
145
|
+
const startIndex = match.index
|
|
146
|
+
|
|
147
|
+
// Find the line number
|
|
148
|
+
const lineNumber = content.substring(0, startIndex).split('\n').length
|
|
149
|
+
|
|
150
|
+
// Extract function body
|
|
151
|
+
const functionInfo = this.extractFunctionBody(content, startIndex, lines, lineNumber)
|
|
152
|
+
|
|
153
|
+
if (functionInfo && functionInfo.code.length > 10) { // Only include substantial functions
|
|
154
|
+
functions.push({
|
|
155
|
+
name: functionName,
|
|
156
|
+
type: type,
|
|
157
|
+
filePath: filePath,
|
|
158
|
+
startLine: lineNumber,
|
|
159
|
+
endLine: functionInfo.endLine,
|
|
160
|
+
code: functionInfo.code,
|
|
161
|
+
context: this.extractContext(content, functionInfo, filePath),
|
|
162
|
+
signature: match[0].trim()
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return functions
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract function body by finding matching braces
|
|
173
|
+
*/
|
|
174
|
+
extractFunctionBody(content, startIndex, lines, startLine) {
|
|
175
|
+
// Find opening brace
|
|
176
|
+
let braceIndex = content.indexOf('{', startIndex)
|
|
177
|
+
if (braceIndex === -1) {
|
|
178
|
+
// Handle arrow functions without braces: const fn = () => expression
|
|
179
|
+
const lineEnd = content.indexOf('\n', startIndex)
|
|
180
|
+
if (lineEnd !== -1) {
|
|
181
|
+
const functionCode = content.substring(startIndex, lineEnd)
|
|
182
|
+
return {
|
|
183
|
+
code: functionCode,
|
|
184
|
+
endLine: startLine
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Count braces to find matching closing brace
|
|
191
|
+
let braceCount = 1
|
|
192
|
+
let currentIndex = braceIndex + 1
|
|
193
|
+
let inString = false
|
|
194
|
+
let stringChar = null
|
|
195
|
+
|
|
196
|
+
while (currentIndex < content.length && braceCount > 0) {
|
|
197
|
+
const char = content[currentIndex]
|
|
198
|
+
const prevChar = content[currentIndex - 1]
|
|
199
|
+
|
|
200
|
+
// Handle string literals to avoid counting braces inside strings
|
|
201
|
+
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
202
|
+
if (!inString) {
|
|
203
|
+
inString = true
|
|
204
|
+
stringChar = char
|
|
205
|
+
} else if (char === stringChar) {
|
|
206
|
+
inString = false
|
|
207
|
+
stringChar = null
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!inString) {
|
|
212
|
+
if (char === '{') braceCount++
|
|
213
|
+
else if (char === '}') braceCount--
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
currentIndex++
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (braceCount !== 0) return null // Unmatched braces
|
|
220
|
+
|
|
221
|
+
// Extract the function code
|
|
222
|
+
const functionCode = content.substring(startIndex, currentIndex)
|
|
223
|
+
const endLine = startLine + functionCode.split('\n').length - 1
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
code: functionCode.trim(),
|
|
227
|
+
endLine: endLine
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extract relevant context for a function
|
|
233
|
+
*/
|
|
234
|
+
extractContext(content, functionInfo, filePath) {
|
|
235
|
+
const context = {
|
|
236
|
+
imports: this.extractImports(content),
|
|
237
|
+
types: this.extractTypes(content),
|
|
238
|
+
dependencies: [],
|
|
239
|
+
calledFunctions: this.extractCalledFunctions(functionInfo.code)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return context
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Extract import statements
|
|
247
|
+
*/
|
|
248
|
+
extractImports(content) {
|
|
249
|
+
const imports = []
|
|
250
|
+
const importPattern = /import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+['"`]([^'"`]+)['"`]/g
|
|
251
|
+
|
|
252
|
+
let match
|
|
253
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
254
|
+
imports.push(match[1])
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return imports
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Extract type definitions
|
|
262
|
+
*/
|
|
263
|
+
extractTypes(content) {
|
|
264
|
+
const types = []
|
|
265
|
+
const typePattern = /(?:type|interface)\s+([A-Z][a-zA-Z0-9]*)/g
|
|
266
|
+
|
|
267
|
+
let match
|
|
268
|
+
while ((match = typePattern.exec(content)) !== null) {
|
|
269
|
+
types.push(match[1])
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return types
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Extract function calls within code
|
|
277
|
+
*/
|
|
278
|
+
extractCalledFunctions(code) {
|
|
279
|
+
const calls = []
|
|
280
|
+
const callPattern = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g
|
|
281
|
+
|
|
282
|
+
let match
|
|
283
|
+
while ((match = callPattern.exec(code)) !== null) {
|
|
284
|
+
const funcName = match[1]
|
|
285
|
+
// Filter out common keywords and built-ins
|
|
286
|
+
if (!['if', 'for', 'while', 'switch', 'catch', 'console', 'Object', 'Array'].includes(funcName)) {
|
|
287
|
+
calls.push(funcName)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return [...new Set(calls)] // Remove duplicates
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Create semantic chunks from extracted functions
|
|
296
|
+
*/
|
|
297
|
+
createFunctionChunks(functions) {
|
|
298
|
+
const chunks = []
|
|
299
|
+
|
|
300
|
+
// Group functions by file and related functionality
|
|
301
|
+
const fileGroups = this.groupFunctionsByFile(functions)
|
|
302
|
+
|
|
303
|
+
for (const [filePath, fileFunctions] of Object.entries(fileGroups)) {
|
|
304
|
+
// Create individual function chunks
|
|
305
|
+
for (const func of fileFunctions) {
|
|
306
|
+
chunks.push({
|
|
307
|
+
name: `${func.name}`,
|
|
308
|
+
type: 'function',
|
|
309
|
+
subtype: func.type,
|
|
310
|
+
filePath: func.filePath,
|
|
311
|
+
functions: [func],
|
|
312
|
+
size: func.code.length,
|
|
313
|
+
complexity: this.calculateComplexity(func.code),
|
|
314
|
+
context: func.context,
|
|
315
|
+
purpose: this.determinePurpose(func),
|
|
316
|
+
tags: this.generateTags(func)
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return chunks
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Group functions by file
|
|
326
|
+
*/
|
|
327
|
+
groupFunctionsByFile(functions) {
|
|
328
|
+
const groups = {}
|
|
329
|
+
|
|
330
|
+
for (const func of functions) {
|
|
331
|
+
if (!groups[func.filePath]) {
|
|
332
|
+
groups[func.filePath] = []
|
|
333
|
+
}
|
|
334
|
+
groups[func.filePath].push(func)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return groups
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Calculate function complexity
|
|
342
|
+
*/
|
|
343
|
+
calculateComplexity(code) {
|
|
344
|
+
const complexityIndicators = [
|
|
345
|
+
'if', 'else', 'for', 'while', 'switch', 'case', 'try', 'catch',
|
|
346
|
+
'&&', '||', '?', ':', 'async', 'await'
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
let complexity = 1 // Base complexity
|
|
350
|
+
|
|
351
|
+
for (const indicator of complexityIndicators) {
|
|
352
|
+
const count = (code.match(new RegExp(`\\b${indicator}\\b`, 'g')) || []).length
|
|
353
|
+
complexity += count
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
score: complexity,
|
|
358
|
+
level: complexity < 5 ? 'low' : complexity < 15 ? 'medium' : 'high'
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Determine function purpose
|
|
364
|
+
*/
|
|
365
|
+
determinePurpose(func) {
|
|
366
|
+
const name = func.name.toLowerCase()
|
|
367
|
+
const code = func.code.toLowerCase()
|
|
368
|
+
|
|
369
|
+
if (func.type === 'react_component') return 'React component'
|
|
370
|
+
if (name.startsWith('use') && func.type === 'function') return 'React hook'
|
|
371
|
+
if (name.includes('test') || name.includes('spec')) return 'Test function'
|
|
372
|
+
if (name.includes('get') || name.includes('fetch')) return 'Data retrieval'
|
|
373
|
+
if (name.includes('create') || name.includes('add')) return 'Data creation'
|
|
374
|
+
if (name.includes('update') || name.includes('edit')) return 'Data modification'
|
|
375
|
+
if (name.includes('delete') || name.includes('remove')) return 'Data deletion'
|
|
376
|
+
if (name.includes('validate') || name.includes('check')) return 'Validation'
|
|
377
|
+
if (code.includes('express') || code.includes('router')) return 'API endpoint'
|
|
378
|
+
|
|
379
|
+
return 'Utility function'
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Generate tags for function
|
|
384
|
+
*/
|
|
385
|
+
generateTags(func) {
|
|
386
|
+
const tags = []
|
|
387
|
+
|
|
388
|
+
tags.push(func.type)
|
|
389
|
+
if (func.context.imports.length > 0) tags.push('has-imports')
|
|
390
|
+
if (func.context.calledFunctions.length > 3) tags.push('complex-logic')
|
|
391
|
+
if (func.code.includes('async')) tags.push('async')
|
|
392
|
+
if (func.code.includes('export')) tags.push('exported')
|
|
393
|
+
if (func.name.match(/^[A-Z]/)) tags.push('component-style')
|
|
394
|
+
|
|
395
|
+
return tags
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Fallback tree-sitter extraction (if available)
|
|
400
|
+
*/
|
|
401
|
+
extractWithTreeSitter(content, filePath) {
|
|
402
|
+
// TODO: Implement tree-sitter extraction for functions
|
|
403
|
+
// For now, return empty to force regex fallback
|
|
404
|
+
return []
|
|
405
|
+
}
|
|
406
|
+
}
|