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 +1 -1
- package/src/detector/files.js +183 -51
- package/src/extractors/imports.js +6 -1
- package/src/extractors/languages/prisma.js +34 -18
package/package.json
CHANGED
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 = 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
|
-
|
|
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
|
+
// 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:
|
|
77
|
-
modelFiles:
|
|
78
|
-
frontendFiles:
|
|
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
|
|
30
|
+
const blocks = extractModelBlocks(content);
|
|
31
31
|
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
66
|
-
const
|
|
67
|
-
const
|
|
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
|
-
|
|
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
|
|
88
|
+
return blocks;
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
function toSnakeCase(str) {
|