cntx-ui 3.0.3 → 3.0.5
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/bundle-manager.js +118 -28
- package/lib/heuristics-manager.js +4 -2
- package/lib/semantic-splitter.js +17 -42
- package/lib/simple-vector-store.js +20 -10
- package/package.json +1 -1
- package/server.js +7 -3
package/lib/bundle-manager.js
CHANGED
|
@@ -51,38 +51,87 @@ export default class BundleManager {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Generate Smart Bundle definitions from indexed semantic data
|
|
54
|
+
* Generate Smart Bundle definitions from indexed semantic data.
|
|
55
|
+
* Uses business domain, directory structure, and technical patterns
|
|
56
|
+
* instead of raw AST node types.
|
|
55
57
|
*/
|
|
56
58
|
generateSmartBundleDefinitions() {
|
|
57
59
|
const smartBundles = [];
|
|
60
|
+
const MIN_CHUNKS = 3; // Skip bundles with fewer than this many chunks
|
|
61
|
+
|
|
58
62
|
try {
|
|
59
|
-
// 1. Group by
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
// 1. Group by business domain (from metadata JSON)
|
|
64
|
+
const allRows = this.db.db.prepare('SELECT metadata FROM semantic_chunks WHERE metadata IS NOT NULL').all();
|
|
65
|
+
const domainCounts = new Map();
|
|
66
|
+
for (const row of allRows) {
|
|
67
|
+
try {
|
|
68
|
+
const meta = JSON.parse(row.metadata);
|
|
69
|
+
for (const domain of (meta.businessDomain || [])) {
|
|
70
|
+
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
71
|
+
}
|
|
72
|
+
} catch { /* skip malformed */ }
|
|
73
|
+
}
|
|
74
|
+
for (const [domain, count] of domainCounts) {
|
|
75
|
+
if (count < MIN_CHUNKS) continue;
|
|
64
76
|
smartBundles.push({
|
|
65
|
-
name
|
|
66
|
-
purpose:
|
|
67
|
-
fileCount:
|
|
77
|
+
name: `smart:${domain}`,
|
|
78
|
+
purpose: domain,
|
|
79
|
+
fileCount: count,
|
|
68
80
|
type: 'smart',
|
|
69
|
-
description:
|
|
81
|
+
description: `${count} chunks in the ${domain} domain`
|
|
70
82
|
});
|
|
71
|
-
}
|
|
83
|
+
}
|
|
72
84
|
|
|
73
|
-
// 2. Group by
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
// 2. Group by directory structure (components, hooks, pages, api, etc.)
|
|
86
|
+
const dirRows = this.db.db.prepare(`
|
|
87
|
+
SELECT
|
|
88
|
+
CASE
|
|
89
|
+
WHEN file_path LIKE '%/hooks/%' THEN 'hooks'
|
|
90
|
+
WHEN file_path LIKE '%/components/%' THEN 'components'
|
|
91
|
+
WHEN file_path LIKE '%/api/%' OR file_path LIKE '%/services/%' THEN 'api-services'
|
|
92
|
+
WHEN file_path LIKE '%/pages/%' OR file_path LIKE '%/routes/%' THEN 'pages'
|
|
93
|
+
WHEN file_path LIKE '%/stores/%' OR file_path LIKE '%/store/%' THEN 'state'
|
|
94
|
+
WHEN file_path LIKE '%/lib/%' OR file_path LIKE '%/utils/%' THEN 'lib-utils'
|
|
95
|
+
WHEN file_path LIKE '%/types/%' THEN 'types'
|
|
96
|
+
ELSE NULL
|
|
97
|
+
END as dir_group,
|
|
98
|
+
COUNT(DISTINCT file_path) as file_cnt
|
|
99
|
+
FROM semantic_chunks
|
|
100
|
+
GROUP BY dir_group
|
|
101
|
+
HAVING dir_group IS NOT NULL
|
|
102
|
+
`).all();
|
|
103
|
+
for (const row of dirRows) {
|
|
104
|
+
if (row.file_cnt < 2) continue;
|
|
78
105
|
smartBundles.push({
|
|
79
|
-
name
|
|
80
|
-
purpose: row.
|
|
81
|
-
fileCount: row.
|
|
106
|
+
name: `smart:dir-${row.dir_group}`,
|
|
107
|
+
purpose: row.dir_group,
|
|
108
|
+
fileCount: row.file_cnt,
|
|
82
109
|
type: 'smart',
|
|
83
|
-
description:
|
|
110
|
+
description: `${row.file_cnt} files in ${row.dir_group} directories`
|
|
84
111
|
});
|
|
85
|
-
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. Group by technical pattern (react-hooks, async-io, event-driven)
|
|
115
|
+
const patternCounts = new Map();
|
|
116
|
+
for (const row of allRows) {
|
|
117
|
+
try {
|
|
118
|
+
const meta = JSON.parse(row.metadata);
|
|
119
|
+
for (const pattern of (meta.technicalPatterns || [])) {
|
|
120
|
+
if (pattern === 'public-api') continue; // Too generic
|
|
121
|
+
patternCounts.set(pattern, (patternCounts.get(pattern) || 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
} catch { /* skip */ }
|
|
124
|
+
}
|
|
125
|
+
for (const [pattern, count] of patternCounts) {
|
|
126
|
+
if (count < MIN_CHUNKS) continue;
|
|
127
|
+
smartBundles.push({
|
|
128
|
+
name: `smart:pattern-${pattern}`,
|
|
129
|
+
purpose: pattern,
|
|
130
|
+
fileCount: count,
|
|
131
|
+
type: 'smart',
|
|
132
|
+
description: `${count} chunks using ${pattern} patterns`
|
|
133
|
+
});
|
|
134
|
+
}
|
|
86
135
|
} catch (e) {
|
|
87
136
|
if (this.verbose) console.warn('Smart bundle discovery failed:', e.message);
|
|
88
137
|
}
|
|
@@ -113,15 +162,56 @@ export default class BundleManager {
|
|
|
113
162
|
const query = bundleName.replace('smart:', '');
|
|
114
163
|
let rows = [];
|
|
115
164
|
|
|
116
|
-
if (query.startsWith('
|
|
117
|
-
|
|
118
|
-
|
|
165
|
+
if (query.startsWith('dir-')) {
|
|
166
|
+
// Directory-based bundle
|
|
167
|
+
const dirGroup = query.replace('dir-', '');
|
|
168
|
+
const dirPatterns = {
|
|
169
|
+
'hooks': '%/hooks/%',
|
|
170
|
+
'components': '%/components/%',
|
|
171
|
+
'api-services': null, // handled below
|
|
172
|
+
'pages': null,
|
|
173
|
+
'state': null,
|
|
174
|
+
'lib-utils': null,
|
|
175
|
+
'types': '%/types/%'
|
|
176
|
+
};
|
|
177
|
+
if (dirGroup === 'api-services') {
|
|
178
|
+
rows = this.db.db.prepare("SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE '%/api/%' OR file_path LIKE '%/services/%'").all();
|
|
179
|
+
} else if (dirGroup === 'pages') {
|
|
180
|
+
rows = this.db.db.prepare("SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE '%/pages/%' OR file_path LIKE '%/routes/%'").all();
|
|
181
|
+
} else if (dirGroup === 'state') {
|
|
182
|
+
rows = this.db.db.prepare("SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE '%/stores/%' OR file_path LIKE '%/store/%'").all();
|
|
183
|
+
} else if (dirGroup === 'lib-utils') {
|
|
184
|
+
rows = this.db.db.prepare("SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE '%/lib/%' OR file_path LIKE '%/utils/%'").all();
|
|
185
|
+
} else if (dirPatterns[dirGroup]) {
|
|
186
|
+
rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE ?').all(dirPatterns[dirGroup]);
|
|
187
|
+
}
|
|
188
|
+
} else if (query.startsWith('pattern-')) {
|
|
189
|
+
// Technical pattern bundle — search metadata JSON
|
|
190
|
+
const pattern = query.replace('pattern-', '');
|
|
191
|
+
const allRows = this.db.db.prepare('SELECT DISTINCT file_path, metadata FROM semantic_chunks WHERE metadata IS NOT NULL').all();
|
|
192
|
+
const files = new Set();
|
|
193
|
+
for (const row of allRows) {
|
|
194
|
+
try {
|
|
195
|
+
const meta = JSON.parse(row.metadata);
|
|
196
|
+
if ((meta.technicalPatterns || []).includes(pattern)) {
|
|
197
|
+
files.add(row.file_path);
|
|
198
|
+
}
|
|
199
|
+
} catch { /* skip */ }
|
|
200
|
+
}
|
|
201
|
+
return Array.from(files);
|
|
119
202
|
} else {
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
203
|
+
// Business domain bundle — search metadata JSON
|
|
204
|
+
const allRows = this.db.db.prepare('SELECT DISTINCT file_path, metadata FROM semantic_chunks WHERE metadata IS NOT NULL').all();
|
|
205
|
+
const files = new Set();
|
|
206
|
+
for (const row of allRows) {
|
|
207
|
+
try {
|
|
208
|
+
const meta = JSON.parse(row.metadata);
|
|
209
|
+
if ((meta.businessDomain || []).includes(query)) {
|
|
210
|
+
files.add(row.file_path);
|
|
211
|
+
}
|
|
212
|
+
} catch { /* skip */ }
|
|
124
213
|
}
|
|
214
|
+
return Array.from(files);
|
|
125
215
|
}
|
|
126
216
|
return rows.map(r => r.file_path);
|
|
127
217
|
}
|
|
@@ -127,7 +127,7 @@ export default class HeuristicsManager {
|
|
|
127
127
|
const domains = new Set();
|
|
128
128
|
const name = func.name.toLowerCase();
|
|
129
129
|
const path = (func.pathParts || []).join('/').toLowerCase();
|
|
130
|
-
const imports = func.includes?.imports || [];
|
|
130
|
+
const imports = (func.includes?.imports || []).filter(i => typeof i === 'string');
|
|
131
131
|
|
|
132
132
|
// Path-based domains
|
|
133
133
|
if (path.includes('auth')) domains.add('authentication');
|
|
@@ -330,7 +330,9 @@ export default class HeuristicsManager {
|
|
|
330
330
|
if (condition.includes('chunk.imports.includes(')) {
|
|
331
331
|
const importMatch = condition.match(/chunk\.imports\.includes\(['"]([^'"]+)['"]\)/)
|
|
332
332
|
if (importMatch && func.includes?.imports) {
|
|
333
|
-
return func.includes.imports
|
|
333
|
+
return func.includes.imports
|
|
334
|
+
.filter(i => typeof i === 'string')
|
|
335
|
+
.some(imp => imp.includes(importMatch[1]))
|
|
334
336
|
}
|
|
335
337
|
}
|
|
336
338
|
|
package/lib/semantic-splitter.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import { readFileSync, existsSync } from 'fs'
|
|
8
8
|
import { join, extname } from 'path'
|
|
9
|
-
import glob from 'glob'
|
|
10
9
|
import Parser from 'tree-sitter'
|
|
11
10
|
import JavaScript from 'tree-sitter-javascript'
|
|
12
11
|
import TypeScript from 'tree-sitter-typescript'
|
|
@@ -45,18 +44,14 @@ export default class SemanticSplitter {
|
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
46
|
* Main entry point - extract semantic chunks from project
|
|
47
|
+
* Now accepts a pre-filtered list of files from FileSystemManager
|
|
48
48
|
*/
|
|
49
|
-
async extractSemanticChunks(projectPath,
|
|
50
|
-
console.log('🔪 Starting surgical semantic splitting via tree-sitter...')
|
|
51
|
-
console.log(`📂 Project path: ${projectPath}`)
|
|
52
|
-
console.log(`📋 Patterns: ${patterns}`);
|
|
49
|
+
async extractSemanticChunks(projectPath, files = [], bundleConfig = null) {
|
|
50
|
+
console.log('🔪 Starting surgical semantic splitting via tree-sitter...')
|
|
51
|
+
console.log(`📂 Project path: ${projectPath}`)
|
|
53
52
|
|
|
54
|
-
this.bundleConfig = bundleConfig
|
|
55
|
-
|
|
56
|
-
console.log(`📁 Found ${files.length} files to split`);
|
|
57
|
-
if (files.length > 0) {
|
|
58
|
-
console.log('📄 Sample files:', files.slice(0, 5));
|
|
59
|
-
}
|
|
53
|
+
this.bundleConfig = bundleConfig
|
|
54
|
+
console.log(`📁 Processing ${files.length} filtered files`)
|
|
60
55
|
|
|
61
56
|
const allChunks = []
|
|
62
57
|
|
|
@@ -74,48 +69,32 @@ export default class SemanticSplitter {
|
|
|
74
69
|
summary: {
|
|
75
70
|
totalFiles: files.length,
|
|
76
71
|
totalChunks: allChunks.length,
|
|
77
|
-
averageSize: allChunks.reduce((sum, c) => sum + c.code.length, 0) / allChunks.length
|
|
72
|
+
averageSize: allChunks.length > 0 ? allChunks.reduce((sum, c) => sum + c.code.length, 0) / allChunks.length : 0
|
|
78
73
|
},
|
|
79
74
|
chunks: allChunks
|
|
80
75
|
}
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
findFiles(projectPath, patterns) {
|
|
84
|
-
const files = []
|
|
85
|
-
for (const pattern of patterns) {
|
|
86
|
-
const matches = glob.sync(pattern, {
|
|
87
|
-
cwd: projectPath,
|
|
88
|
-
ignore: ['node_modules/**', 'dist/**', '.git/**', '*.test.*', '*.spec.*']
|
|
89
|
-
})
|
|
90
|
-
files.push(...matches)
|
|
91
|
-
}
|
|
92
|
-
return [...new Set(files)]
|
|
93
|
-
}
|
|
94
|
-
|
|
95
78
|
processFile(relativePath, projectPath) {
|
|
96
|
-
const fullPath = join(projectPath, relativePath)
|
|
97
|
-
if (!existsSync(fullPath)) return []
|
|
79
|
+
const fullPath = join(projectPath, relativePath)
|
|
80
|
+
if (!existsSync(fullPath)) return []
|
|
98
81
|
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const root = tree.rootNode;
|
|
82
|
+
const content = readFileSync(fullPath, 'utf8')
|
|
83
|
+
const parser = this.getParser(relativePath)
|
|
84
|
+
const tree = parser.parse(content)
|
|
85
|
+
const root = tree.rootNode
|
|
104
86
|
|
|
105
87
|
const elements = {
|
|
106
88
|
functions: [],
|
|
107
89
|
types: [],
|
|
108
90
|
imports: this.extractImports(root, content, relativePath)
|
|
109
|
-
}
|
|
91
|
+
}
|
|
110
92
|
|
|
111
93
|
// Traverse AST for functions and types
|
|
112
|
-
this.traverse(root, content, relativePath, elements)
|
|
94
|
+
this.traverse(root, content, relativePath, elements)
|
|
113
95
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// console.log(` ✅ Found ${chunks.length} chunks in ${relativePath}`);
|
|
117
|
-
// }
|
|
118
|
-
return chunks;
|
|
96
|
+
// Create chunks from elements
|
|
97
|
+
return this.createChunks(elements, content, relativePath)
|
|
119
98
|
}
|
|
120
99
|
|
|
121
100
|
traverse(node, content, filePath, elements) {
|
|
@@ -235,10 +214,6 @@ export default class SemanticSplitter {
|
|
|
235
214
|
const technicalPatterns = this.heuristicsManager.inferTechnicalPatterns(heuristicContext);
|
|
236
215
|
const tags = this.generateTags(func);
|
|
237
216
|
|
|
238
|
-
// if (businessDomain.length > 0) {
|
|
239
|
-
// console.log(` 💎 ${func.name}: Domains: [${businessDomain}], Patterns: [${technicalPatterns}]`);
|
|
240
|
-
// }
|
|
241
|
-
|
|
242
217
|
let chunkCode = '';
|
|
243
218
|
if (this.options.includeContext) {
|
|
244
219
|
const relevantImports = elements.imports
|
|
@@ -54,20 +54,30 @@ export default class SimpleVectorStore {
|
|
|
54
54
|
const queryEmbedding = await this.generateEmbedding(query);
|
|
55
55
|
|
|
56
56
|
// Load all embeddings from DB
|
|
57
|
-
// Optimization: In a huge codebase, we'd use a real vector DB or FAISS
|
|
58
|
-
// For now, SQLite + Manual Cosine Similarity is fine for local repos
|
|
59
57
|
const rows = this.db.db.prepare('SELECT chunk_id, embedding FROM vector_embeddings WHERE model_name = ?').all(this.modelName);
|
|
60
58
|
|
|
61
59
|
const results = [];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
const batchSize = 100;
|
|
61
|
+
|
|
62
|
+
// Process in batches to prevent blocking the event loop
|
|
63
|
+
for (let i = 0; i < rows.length; i += batchSize) {
|
|
64
|
+
const batch = rows.slice(i, i + batchSize);
|
|
65
|
+
|
|
66
|
+
for (const row of batch) {
|
|
67
|
+
const embedding = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
|
|
68
|
+
const similarity = this.cosineSimilarity(queryEmbedding, embedding);
|
|
69
|
+
|
|
70
|
+
if (similarity >= threshold) {
|
|
71
|
+
results.push({
|
|
72
|
+
chunkId: row.chunk_id,
|
|
73
|
+
similarity
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
65
77
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
similarity
|
|
70
|
-
});
|
|
78
|
+
// Give other tasks a chance to run
|
|
79
|
+
if (i + batchSize < rows.length) {
|
|
80
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cntx-ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.
|
|
4
|
+
"version": "3.0.5",
|
|
5
5
|
"description": "Autonomous Repository Intelligence engine with web UI and MCP server. Unified semantic code understanding, local RAG, and agent working memory.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"repository-intelligence",
|
package/server.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createServer } from 'http';
|
|
7
|
-
import { join, dirname } from 'path';
|
|
7
|
+
import { join, dirname, relative, extname } from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, cpSync } from 'fs';
|
|
10
10
|
import * as fs from 'fs';
|
|
@@ -435,13 +435,17 @@ export class CntxServer {
|
|
|
435
435
|
|
|
436
436
|
// 2. Perform fresh analysis if DB is empty
|
|
437
437
|
try {
|
|
438
|
-
const
|
|
438
|
+
const supportedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs'];
|
|
439
|
+
const files = this.fileSystemManager.getAllFiles()
|
|
440
|
+
.filter(f => supportedExtensions.includes(extname(f).toLowerCase()))
|
|
441
|
+
.map(f => relative(this.CWD, f));
|
|
442
|
+
|
|
439
443
|
let bundleConfig = null;
|
|
440
444
|
if (existsSync(this.configManager.CONFIG_FILE)) {
|
|
441
445
|
bundleConfig = JSON.parse(readFileSync(this.configManager.CONFIG_FILE, 'utf8'));
|
|
442
446
|
}
|
|
443
447
|
|
|
444
|
-
this.semanticCache = await this.semanticSplitter.extractSemanticChunks(this.CWD,
|
|
448
|
+
this.semanticCache = await this.semanticSplitter.extractSemanticChunks(this.CWD, files, bundleConfig);
|
|
445
449
|
this.lastSemanticAnalysis = Date.now();
|
|
446
450
|
|
|
447
451
|
// 3. Persist chunks to SQLite immediately
|