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.
@@ -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 Purpose (Heuristics)
60
- const purposeRows = this.db.db.prepare('SELECT DISTINCT purpose, COUNT(*) as count FROM semantic_chunks GROUP BY purpose').all();
61
- purposeRows.forEach(row => {
62
- if (!row.purpose) return;
63
- const name = `smart:${row.purpose.toLowerCase().replace(/\s+/g, '-')}`;
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: row.purpose,
67
- fileCount: row.count,
77
+ name: `smart:${domain}`,
78
+ purpose: domain,
79
+ fileCount: count,
68
80
  type: 'smart',
69
- description: `Automatically grouped by purpose: ${row.purpose}`
81
+ description: `${count} chunks in the ${domain} domain`
70
82
  });
71
- });
83
+ }
72
84
 
73
- // 2. Group by Component Types (Subtypes)
74
- const subtypeRows = this.db.db.prepare('SELECT DISTINCT subtype, COUNT(*) as count FROM semantic_chunks GROUP BY subtype').all();
75
- subtypeRows.forEach(row => {
76
- if (!row.subtype) return;
77
- const name = `smart:type-${row.subtype.toLowerCase().replace(/_/g, '-')}`;
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.subtype,
81
- fileCount: row.count,
106
+ name: `smart:dir-${row.dir_group}`,
107
+ purpose: row.dir_group,
108
+ fileCount: row.file_cnt,
82
109
  type: 'smart',
83
- description: `All ${row.subtype} elements across the codebase`
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('type-')) {
117
- const type = query.replace('type-', '').replace(/-/g, '_');
118
- rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE LOWER(subtype) = ?').all(type);
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
- const purposeRows = this.db.db.prepare('SELECT DISTINCT purpose FROM semantic_chunks').all();
121
- const matched = purposeRows.find(r => r.purpose?.toLowerCase().replace(/\s+/g, '-') === query);
122
- if (matched) {
123
- rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE purpose = ?').all(matched.purpose);
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.some(imp => imp.includes(importMatch[1]))
333
+ return func.includes.imports
334
+ .filter(i => typeof i === 'string')
335
+ .some(imp => imp.includes(importMatch[1]))
334
336
  }
335
337
  }
336
338
 
@@ -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, patterns = ['**/*.{js,jsx,ts,tsx,mjs}'], bundleConfig = null) {
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
- 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
- }
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
- // 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;
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
- 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;
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
- for (const row of rows) {
63
- const embedding = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
64
- const similarity = this.cosineSimilarity(queryEmbedding, embedding);
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
- if (similarity >= threshold) {
67
- results.push({
68
- chunkId: row.chunk_id,
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.3",
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 patterns = ['**/*.{js,jsx,ts,tsx,mjs}'];
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, patterns, bundleConfig);
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