carto-md 1.0.13 → 1.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "The context layer for AI-native development.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
@@ -1,7 +1,10 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
- const MAX_FILES_PER_CATEGORY = 50;
4
+ const MAX_FILES_TOTAL = 80;
5
+ const BASE_ROUTE_BUDGET = 20;
6
+ const MODEL_BUDGET = 10;
7
+ const BASE_UTILITY_BUDGET = 20;
5
8
 
6
9
  const PYTHON_IGNORE = new Set(['__pycache__', '.venv', 'venv', 'migrations', 'node_modules', '.git', '.carto']);
7
10
  const JS_IGNORE = new Set(['node_modules', '.git', 'dist', 'build', '.carto', '.next', '.turbo', 'coverage', 'out', '.cache', 'generated', '__generated__', 'storybook-static', 'public', 'static']);
@@ -9,9 +12,6 @@ const HTML_IGNORE = new Set(['node_modules', '.git', '.carto']);
9
12
 
10
13
  /**
11
14
  * discoverFiles(projectRoot, framework, isIgnored, secondaryFramework) → { routeFiles, modelFiles, frontendFiles }
12
- *
13
- * isIgnored is an optional function (filePath) → boolean from the .cartoignore parser.
14
- * If secondaryFramework is provided, discovers files for both and merges.
15
15
  */
16
16
  function discoverFiles(projectRoot, framework, isIgnored, secondaryFramework) {
17
17
  const ignoreFn = isIgnored || (() => false);
@@ -20,22 +20,16 @@ function discoverFiles(projectRoot, framework, isIgnored, secondaryFramework) {
20
20
 
21
21
  if (secondaryFramework) {
22
22
  const secondary = discoverForFramework(projectRoot, secondaryFramework, ignoreFn);
23
- // Merge and deduplicate
24
23
  const routeFiles = [...new Set([...primary.routeFiles, ...secondary.routeFiles])];
25
24
  const modelFiles = [...new Set([...primary.modelFiles, ...secondary.modelFiles])];
26
25
  const frontendFiles = [...new Set([...primary.frontendFiles, ...secondary.frontendFiles])];
27
- return {
28
- routeFiles: cap(routeFiles),
29
- modelFiles: cap(modelFiles),
30
- frontendFiles: cap(frontendFiles)
31
- };
26
+ return { routeFiles, modelFiles, frontendFiles };
32
27
  }
33
28
 
34
29
  return primary;
35
30
  }
36
31
 
37
32
  function discoverForFramework(projectRoot, framework, ignoreFn) {
38
-
39
33
  if (['fastapi', 'django', 'flask', 'python-generic'].includes(framework)) {
40
34
  const pyFiles = findFilesRecursive(projectRoot, ['.py'], PYTHON_IGNORE, ignoreFn)
41
35
  .filter(f => {
@@ -44,41 +38,202 @@ function discoverForFramework(projectRoot, framework, ignoreFn) {
44
38
  });
45
39
  const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
46
40
 
47
- const cappedPy = cap(pyFiles);
48
- return {
49
- routeFiles: cappedPy,
50
- modelFiles: cappedPy,
51
- frontendFiles: cap(htmlFiles)
52
- };
41
+ if (pyFiles.length <= MAX_FILES_TOTAL) {
42
+ return { routeFiles: pyFiles, modelFiles: pyFiles, frontendFiles: htmlFiles };
43
+ }
44
+
45
+ return smartSelect(pyFiles, htmlFiles, projectRoot);
53
46
  }
54
47
 
55
48
  if (['express', 'nextjs', 'react', 'node-generic'].includes(framework)) {
56
- const jsFiles = findFilesRecursive(projectRoot, ['.js', '.ts', '.jsx', '.tsx'], JS_IGNORE, ignoreFn)
49
+ const jsFiles = findFilesRecursive(projectRoot, ['.js', '.ts', '.jsx', '.tsx', '.prisma'], JS_IGNORE, ignoreFn)
57
50
  .filter(f => {
58
51
  const base = path.basename(f);
59
52
  return !base.includes('.test.') && !base.includes('.spec.');
60
53
  });
61
54
  const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
62
55
 
63
- const cappedJs = cap(jsFiles);
64
- return {
65
- routeFiles: cappedJs,
66
- modelFiles: cappedJs,
67
- frontendFiles: cap(htmlFiles)
68
- };
56
+ if (jsFiles.length <= MAX_FILES_TOTAL) {
57
+ return { routeFiles: jsFiles, modelFiles: jsFiles, frontendFiles: htmlFiles };
58
+ }
59
+
60
+ return smartSelect(jsFiles, htmlFiles, projectRoot);
69
61
  }
70
62
 
71
- // Unknown framework — best effort
63
+ // Unknown framework
72
64
  const allCode = findFilesRecursive(projectRoot, ['.py', '.js', '.ts'], new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.carto']), ignoreFn);
73
65
  const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
74
66
 
67
+ if (allCode.length <= MAX_FILES_TOTAL) {
68
+ return { routeFiles: allCode, modelFiles: allCode, frontendFiles: htmlFiles };
69
+ }
70
+
71
+ return smartSelect(allCode, htmlFiles, projectRoot);
72
+ }
73
+
74
+ /**
75
+ * Smart file selection within the 50-file budget.
76
+ * Allocates: up to 20 route files, up to 10 model files, up to 20 utility files.
77
+ */
78
+ function smartSelect(allFiles, htmlFiles, projectRoot) {
79
+ console.warn(`[CARTO] Warning: Found ${allFiles.length} files, selecting top ${MAX_FILES_TOTAL} by importance`);
80
+
81
+ const routeCandidates = [];
82
+ const modelCandidates = [];
83
+ const otherFiles = [];
84
+
85
+ for (const f of allFiles) {
86
+ if (isRouteFile(f)) {
87
+ routeCandidates.push(f);
88
+ } else if (isModelFile(f)) {
89
+ modelCandidates.push(f);
90
+ } else {
91
+ otherFiles.push(f);
92
+ }
93
+ }
94
+
95
+ // Dynamic route budget — expand if many route files, compensate from utility budget
96
+ const routeBudget = routeCandidates.length > BASE_ROUTE_BUDGET
97
+ ? Math.min(routeCandidates.length, 40)
98
+ : BASE_ROUTE_BUDGET;
99
+ const utilityBudget = Math.max(10, MAX_FILES_TOTAL - routeBudget - MODEL_BUDGET);
100
+
101
+ routeCandidates.sort((a, b) => scoreRoute(b) - scoreRoute(a));
102
+ const selectedRoutes = routeCandidates.slice(0, routeBudget);
103
+
104
+ modelCandidates.sort((a, b) => scoreModel(b) - scoreModel(a));
105
+ const selectedModels = modelCandidates.slice(0, MODEL_BUDGET);
106
+
107
+ const importCounts = countImportReferences(allFiles, projectRoot);
108
+ otherFiles.sort((a, b) => {
109
+ const relA = path.relative(projectRoot, a);
110
+ const relB = path.relative(projectRoot, b);
111
+ return (importCounts[relB] || 0) - (importCounts[relA] || 0);
112
+ });
113
+
114
+ const alreadySelected = new Set([...selectedRoutes, ...selectedModels]);
115
+ const remainingBudget = MAX_FILES_TOTAL - alreadySelected.size;
116
+ const selectedUtilities = otherFiles
117
+ .filter(f => !alreadySelected.has(f))
118
+ .slice(0, Math.min(utilityBudget, remainingBudget));
119
+
120
+ const allSelected = [...new Set([...selectedRoutes, ...selectedModels, ...selectedUtilities])];
121
+
75
122
  return {
76
- routeFiles: cap(allCode),
77
- modelFiles: cap(allCode),
78
- frontendFiles: cap(htmlFiles)
123
+ routeFiles: allSelected,
124
+ modelFiles: allSelected,
125
+ frontendFiles: htmlFiles.slice(0, 10)
79
126
  };
80
127
  }
81
128
 
129
+ function isRouteFile(filePath) {
130
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
131
+ const base = path.basename(p);
132
+ if (base === 'route.ts' || base === 'route.js' || base === 'routes.ts' || base === 'routes.js') return true;
133
+ if (p.includes('/api/') || p.includes('/routes/') || p.includes('/route/')) return true;
134
+ if (base === 'main.py' || base === 'app.py' || base === 'server.ts' || base === 'server.js') return true;
135
+ if (p.includes('/pages/api/')) return true;
136
+ return false;
137
+ }
138
+
139
+ function isModelFile(filePath) {
140
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
141
+ const base = path.basename(p);
142
+ if (base === 'schema.prisma' || base.endsWith('.prisma')) return true;
143
+ if (base.startsWith('models.') || base.includes('.model.') || base.includes('.models.')) return true;
144
+ if (p.includes('/models/') || p.includes('/schemas/') || p.includes('/schema/')) return true;
145
+ if (base.includes('entity') || base.includes('schema')) return true;
146
+ return false;
147
+ }
148
+
149
+ function scoreRoute(filePath) {
150
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
151
+ const base = path.basename(p);
152
+ if (base === 'main.py' || base === 'app.py' || base === 'server.ts' || base === 'server.js') return 10;
153
+ if (p.includes('/app/api/')) return 9;
154
+ if (p.includes('/pages/api/')) return 8;
155
+ if (p.includes('/routes/') || p.includes('/api/')) return 7;
156
+ return 5;
157
+ }
158
+
159
+ function scoreModel(filePath) {
160
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
161
+ const base = path.basename(p);
162
+ if (base === 'schema.prisma') return 10;
163
+ if (base.startsWith('models.')) return 9;
164
+ if (p.includes('/models/')) return 8;
165
+ if (base.includes('.model.')) return 7;
166
+ return 5;
167
+ }
168
+
169
+ /**
170
+ * Quick scan of all files for import/require statements.
171
+ * Returns { 'relative/path': count } of how many files import each path.
172
+ */
173
+ function countImportReferences(allFiles, projectRoot) {
174
+ const counts = {};
175
+
176
+ // Build a set of known relative paths for matching
177
+ const knownPaths = new Set();
178
+ for (const f of allFiles) {
179
+ knownPaths.add(path.relative(projectRoot, f));
180
+ }
181
+
182
+ // Quick regex scan — don't parse AST, just count references
183
+ const importPattern = /(?:from|import)\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
184
+
185
+ for (const filePath of allFiles) {
186
+ let content;
187
+ try {
188
+ content = fs.readFileSync(filePath, 'utf-8');
189
+ } catch {
190
+ continue;
191
+ }
192
+
193
+ const fileDir = path.dirname(filePath);
194
+ let match;
195
+ importPattern.lastIndex = 0;
196
+ while ((match = importPattern.exec(content)) !== null) {
197
+ const importPath = match[1] || match[2];
198
+ if (!importPath || !importPath.startsWith('.')) continue;
199
+
200
+ // Try to resolve to a known file
201
+ const resolved = tryResolve(importPath, fileDir, projectRoot, knownPaths);
202
+ if (resolved) {
203
+ counts[resolved] = (counts[resolved] || 0) + 1;
204
+ }
205
+ }
206
+ }
207
+
208
+ return counts;
209
+ }
210
+
211
+ /**
212
+ * Try to resolve a relative import to a known file path.
213
+ */
214
+ function tryResolve(importPath, fileDir, projectRoot, knownPaths) {
215
+ const base = path.resolve(fileDir, importPath);
216
+ const rel = path.relative(projectRoot, base);
217
+
218
+ // Try exact
219
+ if (knownPaths.has(rel)) return rel;
220
+
221
+ // Try extensions
222
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py'];
223
+ for (const ext of extensions) {
224
+ const withExt = rel + ext;
225
+ if (knownPaths.has(withExt)) return withExt;
226
+ }
227
+
228
+ // Try index files
229
+ for (const ext of extensions) {
230
+ const indexFile = path.join(rel, 'index' + ext);
231
+ if (knownPaths.has(indexFile)) return indexFile;
232
+ }
233
+
234
+ return null;
235
+ }
236
+
82
237
  function findFilesRecursive(dir, extensions, ignoreDirs, isIgnored, results = []) {
83
238
  let items;
84
239
  try {
@@ -104,27 +259,4 @@ function findFilesRecursive(dir, extensions, ignoreDirs, isIgnored, results = []
104
259
  return results;
105
260
  }
106
261
 
107
- function scoreFile(filePath) {
108
- const p = filePath.toLowerCase().replace(/\\/g, '/');
109
- const base = path.basename(p);
110
- if (base === 'main.py' || base === 'app.py' || base === 'server.ts' ||
111
- base === 'server.js' || base === 'app.ts' || base === 'index.ts' ||
112
- base === 'route.ts' || base === 'routes.ts' || base === 'routes.js') return 10;
113
- if (p.includes('/api/') || p.includes('/routes/') || p.includes('/route/')) return 9;
114
- if (p.includes('/models/') || p.includes('/schemas/') || p.includes('/schema/')) return 8;
115
- if (p.includes('/services/') || p.includes('/controllers/') || p.includes('/handlers/')) return 6;
116
- if (p.includes('/lib/') || p.includes('/utils/') || p.includes('/helpers/')) return 2;
117
- return 4;
118
- }
119
-
120
- function cap(files) {
121
- const scored = files.map(f => ({ f, score: scoreFile(f) }))
122
- .sort((a, b) => b.score - a.score);
123
- if (scored.length > MAX_FILES_PER_CATEGORY) {
124
- console.warn(`[CARTO] Warning: Found ${scored.length} files, capping at ${MAX_FILES_PER_CATEGORY}`);
125
- return scored.slice(0, MAX_FILES_PER_CATEGORY).map(x => x.f);
126
- }
127
- return scored.map(x => x.f);
128
- }
129
-
130
262
  module.exports = { discoverFiles };
@@ -199,9 +199,14 @@ function buildImportGraph(fileContents, projectRoot) {
199
199
  const graph = {};
200
200
 
201
201
  for (const { filePath, content } of fileContents) {
202
+ const relPath = path.relative(projectRoot, filePath);
203
+ const base = path.basename(filePath);
204
+
205
+ // Skip generated files — they produce massive noisy edges
206
+ if (base.includes('.generated.') || relPath.includes('__generated__')) continue;
207
+
202
208
  const deps = extractImports(content, filePath, projectRoot);
203
209
  if (deps.length > 0) {
204
- const relPath = path.relative(projectRoot, filePath);
205
210
  graph[relPath] = deps;
206
211
  }
207
212
  }
@@ -27,18 +27,13 @@ module.exports = {
27
27
 
28
28
  function extractPrismaModels(content) {
29
29
  const models = [];
30
- const modelPattern = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
30
+ const blocks = extractModelBlocks(content);
31
31
 
32
- let match;
33
- while ((match = modelPattern.exec(content)) !== null) {
34
- const className = match[1];
35
- const body = match[2];
32
+ for (const { name, body } of blocks) {
36
33
  const fields = [];
37
-
38
34
  const lines = body.split('\n');
39
35
  for (const line of lines) {
40
36
  const trimmed = line.trim();
41
- // Skip empty lines, comments, and @@directives
42
37
  if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
43
38
 
44
39
  const fieldMatch = trimmed.match(/^(\w+)\s+(\w+[\[\]?]*)/);
@@ -46,8 +41,7 @@ function extractPrismaModels(content) {
46
41
  fields.push({ name: fieldMatch[1], type: fieldMatch[2] });
47
42
  }
48
43
  }
49
-
50
- models.push({ className, fields });
44
+ models.push({ className: name, fields });
51
45
  }
52
46
 
53
47
  return models;
@@ -55,21 +49,43 @@ function extractPrismaModels(content) {
55
49
 
56
50
  function extractPrismaDBTables(content) {
57
51
  const tables = [];
58
- const modelPattern = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
52
+ const blocks = extractModelBlocks(content);
53
+
54
+ for (const { name, body } of blocks) {
55
+ const mapMatch = body.match(/@@map\s*\(\s*['"]([^'"]+)['"]\s*\)/);
56
+ const tableName = mapMatch ? mapMatch[1] : toSnakeCase(name);
57
+ tables.push({ tableName, modelName: name });
58
+ }
59
59
 
60
+ return tables;
61
+ }
62
+
63
+ /**
64
+ * Brace-counting approach to extract model blocks.
65
+ * Handles } inside /// comments and @zod annotations.
66
+ */
67
+ function extractModelBlocks(content) {
68
+ const blocks = [];
69
+ const modelStart = /^model\s+(\w+)\s*\{/gm;
60
70
  let match;
61
- while ((match = modelPattern.exec(content)) !== null) {
62
- const modelName = match[1];
63
- const body = match[2];
64
71
 
65
- // Check for @@map('table_name')
66
- const mapMatch = body.match(/@@map\s*\(\s*['"]([^'"]+)['"]\s*\)/);
67
- const tableName = mapMatch ? mapMatch[1] : toSnakeCase(modelName);
72
+ while ((match = modelStart.exec(content)) !== null) {
73
+ const name = match[1];
74
+ const startIdx = match.index + match[0].length;
75
+ let depth = 1;
76
+ let i = startIdx;
68
77
 
69
- tables.push({ tableName, modelName });
78
+ while (i < content.length && depth > 0) {
79
+ if (content[i] === '{') depth++;
80
+ else if (content[i] === '}') depth--;
81
+ if (depth > 0) i++;
82
+ }
83
+
84
+ const body = content.substring(startIdx, i);
85
+ blocks.push({ name, body });
70
86
  }
71
87
 
72
- return tables;
88
+ return blocks;
73
89
  }
74
90
 
75
91
  function toSnakeCase(str) {