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 +1 -1
- package/src/cli/impact.js +9 -8
- package/src/detector/files.js +181 -51
package/package.json
CHANGED
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
package/src/detector/files.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
const
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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:
|
|
77
|
-
modelFiles:
|
|
78
|
-
frontendFiles:
|
|
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 };
|