carto-md 1.0.14 → 1.0.16
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/CONTRIBUTING.md +4 -4
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/cli/sync.js +2 -0
- package/src/cli/update-check.js +48 -0
- package/src/cli/watch.js +2 -0
- package/src/detector/files.js +11 -9
- package/src/extractors/imports.js +6 -1
- package/src/extractors/languages/prisma.js +34 -18
package/CONTRIBUTING.md
CHANGED
|
@@ -33,7 +33,7 @@ Wanted: Django, Rails, Laravel, NestJS, Hono, Gin, Spring.
|
|
|
33
33
|
|
|
34
34
|
## How to add a language
|
|
35
35
|
|
|
36
|
-
1. Create `src/
|
|
36
|
+
1. Create `src/extractors/languages/yourlanguage.js`
|
|
37
37
|
2. Export a single function: `extractFromFile(filePath, fileContent)`
|
|
38
38
|
3. Return:
|
|
39
39
|
```js
|
|
@@ -44,7 +44,7 @@ Wanted: Django, Rails, Laravel, NestJS, Hono, Gin, Spring.
|
|
|
44
44
|
exports: [{ name }]
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
|
-
4. Add it to `src/
|
|
47
|
+
4. Add it to `src/extractors/loader.js` language map
|
|
48
48
|
5. Test on at least 3 real open-source projects
|
|
49
49
|
6. Open a PR with before/after AGENTS.md examples
|
|
50
50
|
|
|
@@ -80,7 +80,7 @@ Wanted: Django, Rails, Laravel, NestJS, Hono, Gin, Spring.
|
|
|
80
80
|
## Development setup
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
git clone https://github.com/
|
|
83
|
+
git clone https://github.com/theanshsonkar/carto
|
|
84
84
|
cd carto-ansh
|
|
85
85
|
npm install
|
|
86
86
|
node src/cli/index.js init # test in any project
|
|
@@ -95,7 +95,7 @@ node src/cli/index.js init # test in any project
|
|
|
95
95
|
- [ ] No changes to merger logic (unless explicitly fixing a merger bug)
|
|
96
96
|
- [ ] No network calls added
|
|
97
97
|
- [ ] `carto --version` still works
|
|
98
|
-
- [ ]
|
|
98
|
+
- [ ] `npm test` passes
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
package/README.md
CHANGED
|
@@ -52,6 +52,8 @@ Same task, two Claude sessions: *"Add a `notes` field to the booking model."*
|
|
|
52
52
|
|
|
53
53
|
Not smarter AI. The same AI with accurate facts.
|
|
54
54
|
|
|
55
|
+
*Stress tested on cal.com (5,018 files): 87% route coverage, 100% model field accuracy, import graph with zero phantom links.*
|
|
56
|
+
|
|
55
57
|
---
|
|
56
58
|
|
|
57
59
|
## Know what breaks before you break it
|
package/package.json
CHANGED
package/src/cli/sync.js
CHANGED
|
@@ -2,8 +2,10 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { runFullSync } = require('../sync');
|
|
4
4
|
const { resolveConfig } = require('./init');
|
|
5
|
+
const { checkForUpdate } = require('./update-check');
|
|
5
6
|
|
|
6
7
|
async function run(projectRoot) {
|
|
8
|
+
checkForUpdate(); // fire and forget
|
|
7
9
|
const configPath = path.join(projectRoot, '.carto', 'config.json');
|
|
8
10
|
|
|
9
11
|
if (!fs.existsSync(configPath)) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const pkg = require('../../package.json');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fire-and-forget version check against the npm registry.
|
|
6
|
+
* Prints a one-liner to stderr if a newer version exists.
|
|
7
|
+
* Never throws, never blocks — safe to call without await.
|
|
8
|
+
*/
|
|
9
|
+
function checkForUpdate() {
|
|
10
|
+
const req = https.get('https://registry.npmjs.org/carto-md/latest', {
|
|
11
|
+
timeout: 3000,
|
|
12
|
+
}, (res) => {
|
|
13
|
+
let body = '';
|
|
14
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
15
|
+
res.on('end', () => {
|
|
16
|
+
try {
|
|
17
|
+
const data = JSON.parse(body);
|
|
18
|
+
const latest = data.version;
|
|
19
|
+
if (latest && latest !== pkg.version && isNewer(latest, pkg.version)) {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
`[CARTO] Update available: ${pkg.version} → ${latest} | npm install -g carto-md\n`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
} catch (_) {
|
|
25
|
+
// malformed JSON — ignore
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
req.on('timeout', () => { req.destroy(); });
|
|
31
|
+
req.on('error', () => { /* offline / DNS failure — ignore */ });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns true if `a` is a newer semver than `b`.
|
|
36
|
+
* Only handles numeric major.minor.patch — good enough for this use case.
|
|
37
|
+
*/
|
|
38
|
+
function isNewer(a, b) {
|
|
39
|
+
const pa = a.split('.').map(Number);
|
|
40
|
+
const pb = b.split('.').map(Number);
|
|
41
|
+
for (let i = 0; i < 3; i++) {
|
|
42
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
43
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { checkForUpdate };
|
package/src/cli/watch.js
CHANGED
|
@@ -3,8 +3,10 @@ const path = require('path');
|
|
|
3
3
|
const { startWatcher } = require('../watcher/watch');
|
|
4
4
|
const { runFullSync } = require('../sync');
|
|
5
5
|
const { resolveConfig } = require('./init');
|
|
6
|
+
const { checkForUpdate } = require('./update-check');
|
|
6
7
|
|
|
7
8
|
async function run(projectRoot) {
|
|
9
|
+
checkForUpdate(); // fire and forget
|
|
8
10
|
const configPath = path.join(projectRoot, '.carto', 'config.json');
|
|
9
11
|
|
|
10
12
|
if (!fs.existsSync(configPath)) {
|
package/src/detector/files.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
const MAX_FILES_TOTAL =
|
|
5
|
-
const
|
|
4
|
+
const MAX_FILES_TOTAL = 80;
|
|
5
|
+
const BASE_ROUTE_BUDGET = 20;
|
|
6
6
|
const MODEL_BUDGET = 10;
|
|
7
|
-
const
|
|
7
|
+
const BASE_UTILITY_BUDGET = 20;
|
|
8
8
|
|
|
9
9
|
const PYTHON_IGNORE = new Set(['__pycache__', '.venv', 'venv', 'migrations', 'node_modules', '.git', '.carto']);
|
|
10
10
|
const JS_IGNORE = new Set(['node_modules', '.git', 'dist', 'build', '.carto', '.next', '.turbo', 'coverage', 'out', '.cache', 'generated', '__generated__', 'storybook-static', 'public', 'static']);
|
|
@@ -92,17 +92,19 @@ function smartSelect(allFiles, htmlFiles, projectRoot) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
//
|
|
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
|
+
|
|
96
101
|
routeCandidates.sort((a, b) => scoreRoute(b) - scoreRoute(a));
|
|
97
|
-
const selectedRoutes = routeCandidates.slice(0,
|
|
102
|
+
const selectedRoutes = routeCandidates.slice(0, routeBudget);
|
|
98
103
|
|
|
99
|
-
// Sort models by score (schema files first)
|
|
100
104
|
modelCandidates.sort((a, b) => scoreModel(b) - scoreModel(a));
|
|
101
105
|
const selectedModels = modelCandidates.slice(0, MODEL_BUDGET);
|
|
102
106
|
|
|
103
|
-
// For utilities: scan all files for import statements, count how many times each is imported
|
|
104
107
|
const importCounts = countImportReferences(allFiles, projectRoot);
|
|
105
|
-
// Rank other files by how often they're imported
|
|
106
108
|
otherFiles.sort((a, b) => {
|
|
107
109
|
const relA = path.relative(projectRoot, a);
|
|
108
110
|
const relB = path.relative(projectRoot, b);
|
|
@@ -113,7 +115,7 @@ function smartSelect(allFiles, htmlFiles, projectRoot) {
|
|
|
113
115
|
const remainingBudget = MAX_FILES_TOTAL - alreadySelected.size;
|
|
114
116
|
const selectedUtilities = otherFiles
|
|
115
117
|
.filter(f => !alreadySelected.has(f))
|
|
116
|
-
.slice(0, Math.min(
|
|
118
|
+
.slice(0, Math.min(utilityBudget, remainingBudget));
|
|
117
119
|
|
|
118
120
|
const allSelected = [...new Set([...selectedRoutes, ...selectedModels, ...selectedUtilities])];
|
|
119
121
|
|
|
@@ -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) {
|