carto-md 1.0.12 → 1.0.14

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.12",
3
+ "version": "1.0.14",
4
4
  "description": "The context layer for AI-native development.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
package/src/cli/impact.js CHANGED
@@ -137,7 +137,6 @@ function run(projectRoot, fileArg) {
137
137
  * Matches by basename or partial path suffix.
138
138
  */
139
139
  function resolveFile(fileArg, imports) {
140
- // Collect all known files (keys + all values)
141
140
  const allFiles = new Set();
142
141
  for (const [file, deps] of Object.entries(imports)) {
143
142
  allFiles.add(file);
@@ -145,6 +144,7 @@ function resolveFile(fileArg, imports) {
145
144
  }
146
145
 
147
146
  const normalized = fileArg.replace(/\\/g, '/');
147
+ const hasPathSeparator = normalized.includes('/');
148
148
 
149
149
  // Exact match
150
150
  if (allFiles.has(normalized)) return normalized;
@@ -153,16 +153,17 @@ function resolveFile(fileArg, imports) {
153
153
  const matches = [...allFiles].filter(f => f.endsWith('/' + normalized) || f === normalized);
154
154
  if (matches.length === 1) return matches[0];
155
155
 
156
- // Match by basename
156
+ // If input was a path (contains /), don't fall back to basename — it's ambiguous
157
+ if (hasPathSeparator) {
158
+ if (matches.length > 1) return null;
159
+ return null;
160
+ }
161
+
162
+ // Input is just a filename — fall back to basename matching
157
163
  const byBasename = [...allFiles].filter(f => path.basename(f) === path.basename(normalized));
158
164
  if (byBasename.length === 1) return byBasename[0];
159
165
 
160
- // If multiple basename matches, prefer shortest path
161
- if (byBasename.length > 1) {
162
- byBasename.sort((a, b) => a.length - b.length);
163
- return byBasename[0];
164
- }
165
-
166
+ // Multiple basename matches — ambiguous, don't guess
166
167
  return null;
167
168
  }
168
169
 
@@ -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 = 50;
5
+ const ROUTE_BUDGET = 20;
6
+ const MODEL_BUDGET = 10;
7
+ const 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,200 @@ 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
+ // Sort routes by score (entry points first)
96
+ routeCandidates.sort((a, b) => scoreRoute(b) - scoreRoute(a));
97
+ const selectedRoutes = routeCandidates.slice(0, ROUTE_BUDGET);
98
+
99
+ // Sort models by score (schema files first)
100
+ modelCandidates.sort((a, b) => scoreModel(b) - scoreModel(a));
101
+ const selectedModels = modelCandidates.slice(0, MODEL_BUDGET);
102
+
103
+ // For utilities: scan all files for import statements, count how many times each is imported
104
+ const importCounts = countImportReferences(allFiles, projectRoot);
105
+ // Rank other files by how often they're imported
106
+ otherFiles.sort((a, b) => {
107
+ const relA = path.relative(projectRoot, a);
108
+ const relB = path.relative(projectRoot, b);
109
+ return (importCounts[relB] || 0) - (importCounts[relA] || 0);
110
+ });
111
+
112
+ const alreadySelected = new Set([...selectedRoutes, ...selectedModels]);
113
+ const remainingBudget = MAX_FILES_TOTAL - alreadySelected.size;
114
+ const selectedUtilities = otherFiles
115
+ .filter(f => !alreadySelected.has(f))
116
+ .slice(0, Math.min(UTILITY_BUDGET, remainingBudget));
117
+
118
+ const allSelected = [...new Set([...selectedRoutes, ...selectedModels, ...selectedUtilities])];
119
+
75
120
  return {
76
- routeFiles: cap(allCode),
77
- modelFiles: cap(allCode),
78
- frontendFiles: cap(htmlFiles)
121
+ routeFiles: allSelected,
122
+ modelFiles: allSelected,
123
+ frontendFiles: htmlFiles.slice(0, 10)
79
124
  };
80
125
  }
81
126
 
127
+ function isRouteFile(filePath) {
128
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
129
+ const base = path.basename(p);
130
+ if (base === 'route.ts' || base === 'route.js' || base === 'routes.ts' || base === 'routes.js') return true;
131
+ if (p.includes('/api/') || p.includes('/routes/') || p.includes('/route/')) return true;
132
+ if (base === 'main.py' || base === 'app.py' || base === 'server.ts' || base === 'server.js') return true;
133
+ if (p.includes('/pages/api/')) return true;
134
+ return false;
135
+ }
136
+
137
+ function isModelFile(filePath) {
138
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
139
+ const base = path.basename(p);
140
+ if (base === 'schema.prisma' || base.endsWith('.prisma')) return true;
141
+ if (base.startsWith('models.') || base.includes('.model.') || base.includes('.models.')) return true;
142
+ if (p.includes('/models/') || p.includes('/schemas/') || p.includes('/schema/')) return true;
143
+ if (base.includes('entity') || base.includes('schema')) return true;
144
+ return false;
145
+ }
146
+
147
+ function scoreRoute(filePath) {
148
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
149
+ const base = path.basename(p);
150
+ if (base === 'main.py' || base === 'app.py' || base === 'server.ts' || base === 'server.js') return 10;
151
+ if (p.includes('/app/api/')) return 9;
152
+ if (p.includes('/pages/api/')) return 8;
153
+ if (p.includes('/routes/') || p.includes('/api/')) return 7;
154
+ return 5;
155
+ }
156
+
157
+ function scoreModel(filePath) {
158
+ const p = filePath.toLowerCase().replace(/\\/g, '/');
159
+ const base = path.basename(p);
160
+ if (base === 'schema.prisma') return 10;
161
+ if (base.startsWith('models.')) return 9;
162
+ if (p.includes('/models/')) return 8;
163
+ if (base.includes('.model.')) return 7;
164
+ return 5;
165
+ }
166
+
167
+ /**
168
+ * Quick scan of all files for import/require statements.
169
+ * Returns { 'relative/path': count } of how many files import each path.
170
+ */
171
+ function countImportReferences(allFiles, projectRoot) {
172
+ const counts = {};
173
+
174
+ // Build a set of known relative paths for matching
175
+ const knownPaths = new Set();
176
+ for (const f of allFiles) {
177
+ knownPaths.add(path.relative(projectRoot, f));
178
+ }
179
+
180
+ // Quick regex scan — don't parse AST, just count references
181
+ const importPattern = /(?:from|import)\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
182
+
183
+ for (const filePath of allFiles) {
184
+ let content;
185
+ try {
186
+ content = fs.readFileSync(filePath, 'utf-8');
187
+ } catch {
188
+ continue;
189
+ }
190
+
191
+ const fileDir = path.dirname(filePath);
192
+ let match;
193
+ importPattern.lastIndex = 0;
194
+ while ((match = importPattern.exec(content)) !== null) {
195
+ const importPath = match[1] || match[2];
196
+ if (!importPath || !importPath.startsWith('.')) continue;
197
+
198
+ // Try to resolve to a known file
199
+ const resolved = tryResolve(importPath, fileDir, projectRoot, knownPaths);
200
+ if (resolved) {
201
+ counts[resolved] = (counts[resolved] || 0) + 1;
202
+ }
203
+ }
204
+ }
205
+
206
+ return counts;
207
+ }
208
+
209
+ /**
210
+ * Try to resolve a relative import to a known file path.
211
+ */
212
+ function tryResolve(importPath, fileDir, projectRoot, knownPaths) {
213
+ const base = path.resolve(fileDir, importPath);
214
+ const rel = path.relative(projectRoot, base);
215
+
216
+ // Try exact
217
+ if (knownPaths.has(rel)) return rel;
218
+
219
+ // Try extensions
220
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py'];
221
+ for (const ext of extensions) {
222
+ const withExt = rel + ext;
223
+ if (knownPaths.has(withExt)) return withExt;
224
+ }
225
+
226
+ // Try index files
227
+ for (const ext of extensions) {
228
+ const indexFile = path.join(rel, 'index' + ext);
229
+ if (knownPaths.has(indexFile)) return indexFile;
230
+ }
231
+
232
+ return null;
233
+ }
234
+
82
235
  function findFilesRecursive(dir, extensions, ignoreDirs, isIgnored, results = []) {
83
236
  let items;
84
237
  try {
@@ -104,27 +257,4 @@ function findFilesRecursive(dir, extensions, ignoreDirs, isIgnored, results = []
104
257
  return results;
105
258
  }
106
259
 
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
260
  module.exports = { discoverFiles };