carto-md 2.0.6 → 2.0.8
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/README.md +297 -130
- package/docs/screenshots/claude-code-supabase.png +0 -0
- package/package.json +7 -3
- package/scripts/postinstall.js +46 -0
- package/src/acp/agent.js +6 -13
- package/src/acp/providers/index.js +4 -12
- package/src/agents/leiden.js +7 -13
- package/src/bitmap/bitset.js +190 -0
- package/src/bitmap/index.js +121 -0
- package/src/bitmap/sidecar.js +545 -0
- package/src/bitmap/tools.js +310 -0
- package/src/cli/check.js +57 -0
- package/src/cli/impact.js +6 -1
- package/src/cli/index.js +14 -2
- package/src/cli/init.js +297 -50
- package/src/cli/inspect.js +295 -0
- package/src/cli/serve.js +1 -1
- package/src/cli/watch.js +6 -0
- package/src/engine/worker.js +24 -4
- package/src/extractors/imports.js +181 -1
- package/src/extractors/languages/html.js +4 -1
- package/src/extractors/languages/javascript.js +5 -0
- package/src/extractors/languages/prisma.js +4 -1
- package/src/extractors/languages/python.js +5 -1
- package/src/extractors/languages/r.js +4 -1
- package/src/extractors/languages/typescript.js +2 -0
- package/src/extractors/tree-sitter-parser.js +15 -0
- package/src/mcp/diff-parser.js +246 -0
- package/src/mcp/server-v2.js +535 -8
- package/src/mcp/validate.js +304 -0
- package/src/security/ignore.js +56 -1
- package/src/store/config-loader.js +77 -0
- package/src/store/path-utils.js +50 -0
- package/src/store/sqlite-store.js +419 -8
- package/src/store/sync-v2.js +422 -96
- package/BENCHMARK_RESULTS.md +0 -34
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `carto inspect` — read-only diagnostic command.
|
|
5
|
+
*
|
|
6
|
+
* Prints a snapshot of the on-disk index state — paths, sizes,
|
|
7
|
+
* freshness, sidecar shape, top-N popcount entries, schema version, sync
|
|
8
|
+
* timestamps, extraction error count, unavailable language grammars.
|
|
9
|
+
*
|
|
10
|
+
* Use cases:
|
|
11
|
+
* - "Why is bitmap.bin stale?" — shows mtime gap vs carto.db.
|
|
12
|
+
* - "Are my popcounts well-formed?" — top-10 entries match what
|
|
13
|
+
* `get_high_impact_files` would return.
|
|
14
|
+
* - "Did extractors fail silently?" — surfaces the breadcrumb count.
|
|
15
|
+
* - "Are tree-sitter grammars healthy?" — surfaces the unavailable list.
|
|
16
|
+
*
|
|
17
|
+
* Two output modes:
|
|
18
|
+
* carto inspect — human-readable (sectioned text)
|
|
19
|
+
* carto inspect --json — single JSON object suitable for `| jq`
|
|
20
|
+
*
|
|
21
|
+
* **Strict invariant — never triggers a rebuild.** Uses the readonly
|
|
22
|
+
* SQLite path and `loadFromDisk` (not `ensureBitmapFresh`),
|
|
23
|
+
* so a missing or stale `bitmap.bin` shows up as `null` / `stale: true`
|
|
24
|
+
* in the output rather than mutating disk state. This is the diagnostic
|
|
25
|
+
* tool — its job is to report, not to fix.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const { SQLiteStore } = require('../store/sqlite-store');
|
|
31
|
+
const { loadFromDisk, BITMAP_FILENAME } = require('../bitmap/sidecar');
|
|
32
|
+
|
|
33
|
+
function fileSize(p) {
|
|
34
|
+
try { return fs.statSync(p).size; } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fileMtime(p) {
|
|
38
|
+
try { return fs.statSync(p).mtimeMs; } catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatBytes(n) {
|
|
42
|
+
if (n === null || n === undefined) return 'n/a';
|
|
43
|
+
if (n < 1024) return `${n} B`;
|
|
44
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
45
|
+
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatAge(ts) {
|
|
49
|
+
if (!ts) return 'never';
|
|
50
|
+
const ageSec = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
51
|
+
if (ageSec < 60) return `${ageSec}s ago`;
|
|
52
|
+
if (ageSec < 3600) return `${Math.round(ageSec / 60)}m ago`;
|
|
53
|
+
if (ageSec < 86400) return `${Math.round(ageSec / 3600)}h ago`;
|
|
54
|
+
return `${Math.round(ageSec / 86400)}d ago`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collect everything the inspect command reports, as a plain object.
|
|
59
|
+
* Pure data extraction — no rendering. Both human and JSON modes
|
|
60
|
+
* consume this. Exposed for tests (asserts on the JSON shape are
|
|
61
|
+
* cheaper and more stable than parsing terminal output).
|
|
62
|
+
*/
|
|
63
|
+
function collect(projectRoot) {
|
|
64
|
+
const cartoDir = path.join(projectRoot, '.carto');
|
|
65
|
+
const dbPath = path.join(cartoDir, 'carto.db');
|
|
66
|
+
const bitmapPath = path.join(cartoDir, BITMAP_FILENAME);
|
|
67
|
+
|
|
68
|
+
const dbSize = fileSize(dbPath);
|
|
69
|
+
const dbMtime = fileMtime(dbPath);
|
|
70
|
+
const bitmapSize = fileSize(bitmapPath);
|
|
71
|
+
const bitmapMtime = fileMtime(bitmapPath);
|
|
72
|
+
|
|
73
|
+
// Bitmap freshness: stale if it's older than the DB (or missing).
|
|
74
|
+
let stale = null;
|
|
75
|
+
if (bitmapMtime === null) stale = true;
|
|
76
|
+
else if (dbMtime !== null) stale = bitmapMtime < dbMtime;
|
|
77
|
+
|
|
78
|
+
const out = {
|
|
79
|
+
paths: {
|
|
80
|
+
projectRoot,
|
|
81
|
+
cartoDir,
|
|
82
|
+
dbPath,
|
|
83
|
+
bitmapPath,
|
|
84
|
+
},
|
|
85
|
+
files: {
|
|
86
|
+
dbExists: dbSize !== null,
|
|
87
|
+
dbSize,
|
|
88
|
+
dbMtime,
|
|
89
|
+
bitmapExists: bitmapSize !== null,
|
|
90
|
+
bitmapSize,
|
|
91
|
+
bitmapMtime,
|
|
92
|
+
bitmapStale: stale,
|
|
93
|
+
},
|
|
94
|
+
meta: null,
|
|
95
|
+
bitmap: null,
|
|
96
|
+
topImpact: [],
|
|
97
|
+
domains: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// SQLite read — readonly path so this can never mutate the index.
|
|
101
|
+
// If the DB is missing, return what we have and let the caller render
|
|
102
|
+
// the "run carto init first" message.
|
|
103
|
+
if (!out.files.dbExists) return out;
|
|
104
|
+
|
|
105
|
+
const store = new SQLiteStore(projectRoot);
|
|
106
|
+
try {
|
|
107
|
+
store.open({ readonly: true });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
out.error = `Failed to open ${dbPath}: ${err.message}`;
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const structure = store.getStructure();
|
|
115
|
+
const domainsList = store.getDomainsList();
|
|
116
|
+
// Defensive: getExtractionErrorCount throws on pre-Spec-11 schemas
|
|
117
|
+
// (no `extraction_errors` table). Inspect must work on every DB
|
|
118
|
+
// we've ever shipped — fall back to 0 on schema mismatch.
|
|
119
|
+
let errCount = 0;
|
|
120
|
+
try { errCount = store.getExtractionErrorCount(); } catch { errCount = 0; }
|
|
121
|
+
const unavailRaw = store.getMeta('unavailable_languages_json');
|
|
122
|
+
let unavail = [];
|
|
123
|
+
if (unavailRaw) {
|
|
124
|
+
try { unavail = JSON.parse(unavailRaw); } catch { unavail = []; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
out.meta = {
|
|
128
|
+
schemaVersion: store.getMeta('schema_version'),
|
|
129
|
+
lastFullSync: store.getMeta('last_full_sync'),
|
|
130
|
+
lastPartialSync: store.getMeta('last_partial_sync'),
|
|
131
|
+
lastIndexed: structure.meta.lastIndexed,
|
|
132
|
+
indexDurationMs: structure.meta.indexDuration,
|
|
133
|
+
totalFiles: structure.meta.totalFiles,
|
|
134
|
+
totalRoutes: structure.meta.totalRoutes,
|
|
135
|
+
totalImportEdges: structure.meta.totalImportEdges,
|
|
136
|
+
extractionErrorCount: errCount,
|
|
137
|
+
unavailableLanguages: unavail,
|
|
138
|
+
};
|
|
139
|
+
out.domains = domainsList.map(d => ({
|
|
140
|
+
name: d.name,
|
|
141
|
+
files: d.fileCount,
|
|
142
|
+
routes: d.routeCount,
|
|
143
|
+
models: d.modelCount,
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
// Bitmap shape — load from disk only. Returns null if missing or
|
|
147
|
+
// corrupt; we report that rather than rebuilding (see invariant
|
|
148
|
+
// in the file header).
|
|
149
|
+
if (out.files.bitmapExists) {
|
|
150
|
+
const sidecar = loadFromDisk(cartoDir);
|
|
151
|
+
if (sidecar) {
|
|
152
|
+
out.bitmap = {
|
|
153
|
+
loaded: true,
|
|
154
|
+
size: sidecar.size,
|
|
155
|
+
forwardCount: sidecar.forward.size,
|
|
156
|
+
reverseCount: sidecar.reverse.size,
|
|
157
|
+
crossForwardCount: sidecar.crossForward ? sidecar.crossForward.size : 0,
|
|
158
|
+
domainBitmapCount: sidecar.domainBitmaps.size,
|
|
159
|
+
popcountIndexLength: sidecar.popcountIndex.length,
|
|
160
|
+
fileIdMapped: sidecar.fileIdToPath.size,
|
|
161
|
+
};
|
|
162
|
+
// Top-N from popcountIndex, with hydrated paths so caller can
|
|
163
|
+
// sanity-check that get_high_impact_files would return these.
|
|
164
|
+
const topN = Math.min(10, sidecar.popcountIndex.length);
|
|
165
|
+
for (let i = 0; i < topN; i++) {
|
|
166
|
+
const e = sidecar.popcountIndex[i];
|
|
167
|
+
out.topImpact.push({
|
|
168
|
+
file: sidecar.fileIdToPath.get(e.fileId) || `<id ${e.fileId}>`,
|
|
169
|
+
dependents: e.count,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
out.bitmap = { loaded: false, reason: 'corrupt or version mismatch' };
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
out.bitmap = { loaded: false, reason: 'bitmap.bin not present' };
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
try { store.close(); } catch {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderHuman(data) {
|
|
186
|
+
const lines = [];
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push('── Carto Inspect ───────────────────────────────────────');
|
|
189
|
+
lines.push('');
|
|
190
|
+
|
|
191
|
+
// ── Paths
|
|
192
|
+
lines.push('Paths');
|
|
193
|
+
lines.push(` project : ${data.paths.projectRoot}`);
|
|
194
|
+
lines.push(` .carto/ : ${data.paths.cartoDir}`);
|
|
195
|
+
lines.push(` carto.db : ${formatBytes(data.files.dbSize)}` +
|
|
196
|
+
(data.files.dbExists ? ` (${formatAge(data.files.dbMtime)})` : ' (missing)'));
|
|
197
|
+
lines.push(` bitmap.bin : ${formatBytes(data.files.bitmapSize)}` +
|
|
198
|
+
(data.files.bitmapExists
|
|
199
|
+
? ` (${formatAge(data.files.bitmapMtime)}${data.files.bitmapStale ? ', ⚠️ stale vs DB' : ''})`
|
|
200
|
+
: ' (missing)'));
|
|
201
|
+
lines.push('');
|
|
202
|
+
|
|
203
|
+
if (!data.files.dbExists) {
|
|
204
|
+
lines.push(' ⚠️ No .carto/carto.db found. Run `carto init` first.');
|
|
205
|
+
lines.push('');
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (data.error) {
|
|
210
|
+
lines.push(` ⚠️ ${data.error}`);
|
|
211
|
+
lines.push('');
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Meta
|
|
216
|
+
const m = data.meta;
|
|
217
|
+
lines.push('Meta');
|
|
218
|
+
lines.push(` schema version : ${m.schemaVersion || '?'}`);
|
|
219
|
+
lines.push(` last full sync : ${m.lastFullSync || 'never'}`);
|
|
220
|
+
lines.push(` last partial sync : ${m.lastPartialSync || 'never'}`);
|
|
221
|
+
lines.push(` index duration : ${m.indexDurationMs}ms`);
|
|
222
|
+
lines.push(` files indexed : ${m.totalFiles}`);
|
|
223
|
+
lines.push(` routes : ${m.totalRoutes}`);
|
|
224
|
+
lines.push(` import edges : ${m.totalImportEdges}`);
|
|
225
|
+
lines.push(` extraction errors : ${m.extractionErrorCount}` +
|
|
226
|
+
(m.extractionErrorCount > 0 ? ' ⚠️ (run `carto check`)' : ''));
|
|
227
|
+
if (m.unavailableLanguages && m.unavailableLanguages.length > 0) {
|
|
228
|
+
lines.push(` grammars unavailable : ⚠️ ${m.unavailableLanguages.join(', ')}`);
|
|
229
|
+
}
|
|
230
|
+
lines.push('');
|
|
231
|
+
|
|
232
|
+
// ── Bitmap
|
|
233
|
+
lines.push('Bitmap');
|
|
234
|
+
if (!data.bitmap || !data.bitmap.loaded) {
|
|
235
|
+
lines.push(` ⚠️ not loaded — ${data.bitmap ? data.bitmap.reason : 'unknown'}`);
|
|
236
|
+
} else {
|
|
237
|
+
const b = data.bitmap;
|
|
238
|
+
lines.push(` size (bits) : ${b.size}`);
|
|
239
|
+
lines.push(` forward bitmaps : ${b.forwardCount}`);
|
|
240
|
+
lines.push(` reverse bitmaps : ${b.reverseCount}`);
|
|
241
|
+
lines.push(` crossForward bitmaps : ${b.crossForwardCount}`);
|
|
242
|
+
lines.push(` domain bitmaps : ${b.domainBitmapCount}`);
|
|
243
|
+
lines.push(` popcount index : ${b.popcountIndexLength} entries`);
|
|
244
|
+
lines.push(` file paths mapped : ${b.fileIdMapped}`);
|
|
245
|
+
}
|
|
246
|
+
lines.push('');
|
|
247
|
+
|
|
248
|
+
// ── Top impact (popcount-index head)
|
|
249
|
+
if (data.topImpact.length > 0) {
|
|
250
|
+
lines.push('Top impact (from popcount index)');
|
|
251
|
+
for (const e of data.topImpact) {
|
|
252
|
+
lines.push(` ${String(e.dependents).padStart(4)} ${e.file}`);
|
|
253
|
+
}
|
|
254
|
+
lines.push('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Domains
|
|
258
|
+
if (data.domains.length > 0) {
|
|
259
|
+
lines.push('Domains');
|
|
260
|
+
for (const d of data.domains) {
|
|
261
|
+
lines.push(` ${d.name.padEnd(16)} ${String(d.files).padStart(5)} files ` +
|
|
262
|
+
`${String(d.routes).padStart(4)} routes ` +
|
|
263
|
+
`${String(d.models).padStart(4)} models`);
|
|
264
|
+
}
|
|
265
|
+
lines.push('');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
lines.push('────────────────────────────────────────────────────────');
|
|
269
|
+
lines.push('');
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* run(projectRoot, options)
|
|
275
|
+
* options.json — emit machine-readable JSON instead of human text.
|
|
276
|
+
*
|
|
277
|
+
* Exit code:
|
|
278
|
+
* 0 — DB present, output rendered.
|
|
279
|
+
* 1 — DB missing (caller should run `carto init`).
|
|
280
|
+
*/
|
|
281
|
+
function run(projectRoot, options = {}) {
|
|
282
|
+
const data = collect(projectRoot);
|
|
283
|
+
|
|
284
|
+
if (options.json) {
|
|
285
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
286
|
+
} else {
|
|
287
|
+
process.stdout.write(renderHuman(data));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!data.files.dbExists) return 1;
|
|
291
|
+
if (data.error) return 1;
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = { run, collect, renderHuman };
|
package/src/cli/serve.js
CHANGED
|
@@ -7,7 +7,7 @@ function run(projectRoot) {
|
|
|
7
7
|
const dbPath = path.join(projectRoot, '.carto', 'carto.db');
|
|
8
8
|
|
|
9
9
|
if (!fs.existsSync(dbPath)) {
|
|
10
|
-
// Could be
|
|
10
|
+
// Could be an old install with only map.json, or never initialized.
|
|
11
11
|
const mapPath = path.join(projectRoot, '.carto', 'map.json');
|
|
12
12
|
if (fs.existsSync(mapPath)) {
|
|
13
13
|
console.error('[CARTO] Legacy index found (map.json) but no SQLite DB. Run `carto init` to upgrade your index.');
|
package/src/cli/watch.js
CHANGED
|
@@ -169,6 +169,12 @@ async function run(projectRoot) {
|
|
|
169
169
|
process.exit(1);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// `carto watch` is opt-in. Git hooks + lazy MCP re-parse
|
|
173
|
+
// handle the 90% case automatically with no background process. Surface
|
|
174
|
+
// that to anyone who runs this command so they know it's not required.
|
|
175
|
+
console.log('[CARTO] Note: `carto watch` is opt-in. Git hooks + lazy MCP re-parse (installed by `carto init`) keep the index fresh without a background process.');
|
|
176
|
+
console.log('[CARTO] Use `carto watch` for AI-heavy workflows where many files are edited between commits and you want sub-second freshness.');
|
|
177
|
+
|
|
172
178
|
// Initial full sync using V2
|
|
173
179
|
console.log('[CARTO] Starting initial sync...');
|
|
174
180
|
await runSyncV2({
|
package/src/engine/worker.js
CHANGED
|
@@ -15,6 +15,9 @@ parentPort.on('message', (task) => {
|
|
|
15
15
|
try {
|
|
16
16
|
content = fs.readFileSync(filePath, 'utf-8');
|
|
17
17
|
} catch (err) {
|
|
18
|
+
// Real I/O failure (file deleted between discovery and extraction,
|
|
19
|
+
// permission denied, etc.) — no point trying to extract. Caller
|
|
20
|
+
// turns this into a skip; nothing reaches the index.
|
|
18
21
|
parentPort.postMessage({ id, error: err.message, result: null });
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
@@ -27,17 +30,33 @@ parentPort.on('message', (task) => {
|
|
|
27
30
|
return;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
// Capture extractor failures as breadcrumbs instead of
|
|
34
|
+
// dropping the file. The file still gets indexed (with empty
|
|
35
|
+
// extraction arrays) so its existence and imports are visible — and
|
|
36
|
+
// the failure shows up in `carto check` instead of vanishing.
|
|
37
|
+
const errors = [];
|
|
38
|
+
|
|
39
|
+
let extracted = { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
|
|
31
40
|
try {
|
|
32
41
|
extracted = plugin.extract(content, relPath);
|
|
42
|
+
// Plugin-internal failure breadcrumbs — see extractFile().
|
|
43
|
+
if (Array.isArray(extracted._errors) && extracted._errors.length > 0) {
|
|
44
|
+
for (const e of extracted._errors) {
|
|
45
|
+
if (e && e.phase && e.message) errors.push({ phase: e.phase, message: e.message });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
33
48
|
} catch (err) {
|
|
34
|
-
|
|
35
|
-
return;
|
|
49
|
+
errors.push({ phase: 'extract', message: err.message || String(err) });
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
// Use tree-sitter imports if the plugin produced them (faster, no file I/O)
|
|
39
53
|
// Fall back to the regex-based extractImports for resolution
|
|
40
|
-
|
|
54
|
+
let imports = [];
|
|
55
|
+
try {
|
|
56
|
+
imports = extractImports(content, filePath, projectRoot);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
errors.push({ phase: 'imports', message: err.message || String(err) });
|
|
59
|
+
}
|
|
41
60
|
|
|
42
61
|
// Attach tree-sitter symbols if available (richer than legacy functions array)
|
|
43
62
|
const tsSymbols = extracted._tsSymbols || null;
|
|
@@ -56,6 +75,7 @@ parentPort.on('message', (task) => {
|
|
|
56
75
|
storageKeys: extracted.storageKeys || [],
|
|
57
76
|
imports,
|
|
58
77
|
tsSymbols,
|
|
78
|
+
errors,
|
|
59
79
|
}
|
|
60
80
|
});
|
|
61
81
|
});
|
|
@@ -36,7 +36,11 @@ function extractImports(content, filePath, projectRoot) {
|
|
|
36
36
|
if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) {
|
|
37
37
|
rawImports = extractJSImports(content);
|
|
38
38
|
} else if (ext === '.py') {
|
|
39
|
-
|
|
39
|
+
// Python returns absolute paths from resolvePythonRelativeImport / tryResolvePythonModule.
|
|
40
|
+
// Relativize to project root and return early — the JS-style dedup loop below only handles
|
|
41
|
+
// `./` and `@/~/#` prefixed strings, so falling through silently drops every Python edge.
|
|
42
|
+
const abs = extractPythonImports(content, filePath, projectRoot);
|
|
43
|
+
return [...new Set(abs.map(p => path.relative(projectRoot, p)))].sort();
|
|
40
44
|
} else if (ext === '.r') {
|
|
41
45
|
return extractRImports(content, filePath, projectRoot);
|
|
42
46
|
} else if (ext === '.go') {
|
|
@@ -47,6 +51,10 @@ function extractImports(content, filePath, projectRoot) {
|
|
|
47
51
|
return extractJavaImports(content, filePath, projectRoot);
|
|
48
52
|
} else if (ext === '.rb') {
|
|
49
53
|
return extractRubyImports(content, filePath, projectRoot);
|
|
54
|
+
} else if (['.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.hh', '.hxx'].includes(ext)) {
|
|
55
|
+
return extractCppImports(content, filePath, projectRoot);
|
|
56
|
+
} else if (ext === '.cs') {
|
|
57
|
+
return extractCsharpImports(content, filePath, projectRoot);
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
// Resolve and deduplicate
|
|
@@ -646,3 +654,175 @@ function _resolveRubyPath(reqPath, baseDir, projectRoot) {
|
|
|
646
654
|
}
|
|
647
655
|
return null;
|
|
648
656
|
}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
// ─── C / C++ imports ──────────────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* extractCppImports(content, filePath, projectRoot) → Array<string>
|
|
663
|
+
*
|
|
664
|
+
* Resolves #include directives to actual files in the project.
|
|
665
|
+
*
|
|
666
|
+
* #include "foo.h" → resolved relative to current file's dir,
|
|
667
|
+
* then common include search dirs.
|
|
668
|
+
* #include <foo> → skipped (system header, external).
|
|
669
|
+
*
|
|
670
|
+
* Uses tree-sitter-cpp when available, otherwise falls back to a
|
|
671
|
+
* simple regex over the source.
|
|
672
|
+
*/
|
|
673
|
+
function extractCppImports(content, filePath, projectRoot) {
|
|
674
|
+
const results = new Set();
|
|
675
|
+
const fileDir = path.dirname(filePath);
|
|
676
|
+
const ext = path.extname(filePath);
|
|
677
|
+
|
|
678
|
+
// Pull raw includes via tree-sitter when available; fall back to regex.
|
|
679
|
+
let rawImports = [];
|
|
680
|
+
try {
|
|
681
|
+
const tsParser = require('./tree-sitter-parser');
|
|
682
|
+
if (tsParser.isAvailable()) {
|
|
683
|
+
rawImports = tsParser.extractImports(content, ext);
|
|
684
|
+
}
|
|
685
|
+
} catch { /* tree-sitter unavailable */ }
|
|
686
|
+
|
|
687
|
+
if (rawImports.length === 0) {
|
|
688
|
+
const re = /^\s*#\s*include\s+(?:"([^"]+)"|<([^>]+)>)/gm;
|
|
689
|
+
let m;
|
|
690
|
+
while ((m = re.exec(content)) !== null) {
|
|
691
|
+
// m[1] is quoted form, m[2] is angle-bracket form. Keep both shapes
|
|
692
|
+
// so the system-header filter below can drop the angle ones.
|
|
693
|
+
if (m[1]) rawImports.push(m[1]);
|
|
694
|
+
else if (m[2]) rawImports.push('<' + m[2] + '>');
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
for (const raw of rawImports) {
|
|
699
|
+
// Drop system headers (angle brackets). Tree-sitter returns these as
|
|
700
|
+
// "<string>" wrappers; the regex form we just constructed does too.
|
|
701
|
+
if (raw.startsWith('<') && raw.endsWith('>')) continue;
|
|
702
|
+
// Defensive: a tree-sitter grammar quirk could leak the wrappers in.
|
|
703
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
704
|
+
const inner = raw.slice(1, -1);
|
|
705
|
+
_tryResolveCppInclude(inner, fileDir, projectRoot, results);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
_tryResolveCppInclude(raw, fileDir, projectRoot, results);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return [...results].sort();
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function _tryResolveCppInclude(includePath, fileDir, projectRoot, results) {
|
|
715
|
+
// Search order: relative to current file, then common include roots.
|
|
716
|
+
const candidates = [
|
|
717
|
+
path.resolve(fileDir, includePath),
|
|
718
|
+
path.join(projectRoot, 'include', includePath),
|
|
719
|
+
path.join(projectRoot, 'src', 'include', includePath),
|
|
720
|
+
path.join(projectRoot, 'src', includePath),
|
|
721
|
+
path.join(projectRoot, includePath),
|
|
722
|
+
];
|
|
723
|
+
for (const c of candidates) {
|
|
724
|
+
if (fs.existsSync(c) && fs.statSync(c).isFile()) {
|
|
725
|
+
results.add(path.relative(projectRoot, c));
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ─── C# imports ───────────────────────────────────────────────────────────────
|
|
732
|
+
|
|
733
|
+
// Cache: projectRoot → Map<namespace, Array<relativeFilePath>>
|
|
734
|
+
const _csNamespaceCache = new Map();
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Build a namespace → files map by scanning every .cs file in the project
|
|
738
|
+
* once. Cached per projectRoot. This is the only reliable way to resolve
|
|
739
|
+
* `using A.B.C;` in C# because (unlike Java) namespace and folder structure
|
|
740
|
+
* are decoupled — a namespace can span many files in many directories.
|
|
741
|
+
*/
|
|
742
|
+
function _getCsharpNamespaceMap(projectRoot) {
|
|
743
|
+
if (_csNamespaceCache.has(projectRoot)) return _csNamespaceCache.get(projectRoot);
|
|
744
|
+
const map = new Map();
|
|
745
|
+
|
|
746
|
+
const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', 'packages', '.git', 'dist', 'build']);
|
|
747
|
+
const walk = (dir) => {
|
|
748
|
+
let entries;
|
|
749
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
750
|
+
catch { return; }
|
|
751
|
+
for (const e of entries) {
|
|
752
|
+
if (e.name.startsWith('.')) continue;
|
|
753
|
+
if (e.isDirectory()) {
|
|
754
|
+
if (!SKIP_DIRS.has(e.name)) walk(path.join(dir, e.name));
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (!e.isFile() || !e.name.endsWith('.cs')) continue;
|
|
758
|
+
const full = path.join(dir, e.name);
|
|
759
|
+
let c;
|
|
760
|
+
try { c = fs.readFileSync(full, 'utf-8'); } catch { continue; }
|
|
761
|
+
// Match both file-scoped (`namespace Foo;`) and block-scoped (`namespace Foo {`).
|
|
762
|
+
const m = c.match(/^\s*namespace\s+([\w.]+)\s*[;{]/m);
|
|
763
|
+
if (!m) continue;
|
|
764
|
+
const ns = m[1];
|
|
765
|
+
const rel = path.relative(projectRoot, full);
|
|
766
|
+
if (!map.has(ns)) map.set(ns, []);
|
|
767
|
+
map.get(ns).push(rel);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
walk(projectRoot);
|
|
772
|
+
_csNamespaceCache.set(projectRoot, map);
|
|
773
|
+
return map;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* extractCsharpImports(content, filePath, projectRoot) → Array<string>
|
|
778
|
+
*
|
|
779
|
+
* Resolves `using Foo.Bar.Baz;` to project files that declare the
|
|
780
|
+
* matching namespace. Skips standard library namespaces (System.*,
|
|
781
|
+
* Microsoft.*, etc.) — those produce only noise.
|
|
782
|
+
*/
|
|
783
|
+
function extractCsharpImports(content, filePath, projectRoot) {
|
|
784
|
+
const results = new Set();
|
|
785
|
+
const nsMap = _getCsharpNamespaceMap(projectRoot);
|
|
786
|
+
|
|
787
|
+
// System namespaces and well-known third-party prefixes that almost never
|
|
788
|
+
// resolve to project files. We still check the map (a project could ship a
|
|
789
|
+
// Microsoft.* namespace), but skip the FS fallback for these.
|
|
790
|
+
const EXTERNAL_PREFIXES = ['System', 'Microsoft', 'Windows', 'Newtonsoft', 'Xunit', 'NUnit', 'Mono'];
|
|
791
|
+
|
|
792
|
+
const re = /^\s*using\s+(?:static\s+)?([\w.]+)\s*;/gm;
|
|
793
|
+
let m;
|
|
794
|
+
while ((m = re.exec(content)) !== null) {
|
|
795
|
+
const ns = m[1];
|
|
796
|
+
const isExternal = EXTERNAL_PREFIXES.includes(ns.split('.')[0]);
|
|
797
|
+
|
|
798
|
+
// Primary path: namespace map lookup. Works regardless of folder layout.
|
|
799
|
+
const matched = nsMap.get(ns);
|
|
800
|
+
if (matched) {
|
|
801
|
+
for (const f of matched) results.add(f);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (isExternal) continue;
|
|
806
|
+
|
|
807
|
+
// Fallback: filename convention (Foo.Bar.Baz → Foo/Bar/Baz.cs).
|
|
808
|
+
const parts = ns.split('.');
|
|
809
|
+
const asFile = parts.join(path.sep) + '.cs';
|
|
810
|
+
const candidates = [
|
|
811
|
+
path.join(projectRoot, 'src', asFile),
|
|
812
|
+
path.join(projectRoot, asFile),
|
|
813
|
+
];
|
|
814
|
+
for (const c of candidates) {
|
|
815
|
+
if (fs.existsSync(c)) {
|
|
816
|
+
results.add(path.relative(projectRoot, c));
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Drop self-import — a file's `namespace X;` can match itself when the
|
|
823
|
+
// file imports its own namespace via `using X;`.
|
|
824
|
+
const selfRel = path.relative(projectRoot, filePath);
|
|
825
|
+
results.delete(selfRel);
|
|
826
|
+
|
|
827
|
+
return [...results].sort();
|
|
828
|
+
}
|
|
@@ -9,7 +9,10 @@ module.exports = {
|
|
|
9
9
|
return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches, storageKeys };
|
|
10
10
|
} catch (err) {
|
|
11
11
|
console.warn(`[CARTO] html plugin error on ${filename}: ${err.message}`);
|
|
12
|
-
return {
|
|
12
|
+
return {
|
|
13
|
+
routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [],
|
|
14
|
+
_errors: [{ phase: 'extract', message: err.message || String(err) }],
|
|
15
|
+
};
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
18
|
};
|
|
@@ -94,6 +94,11 @@ module.exports = {
|
|
|
94
94
|
storageKeys: [],
|
|
95
95
|
_tsImports: tsImports,
|
|
96
96
|
_tsSymbols: tsSymbols,
|
|
97
|
+
// Surface the parse failure as a breadcrumb so the
|
|
98
|
+
// caller can record it in extraction_errors. Pre-Spec-11 this
|
|
99
|
+
// warning only hit stderr and the file lost all of its routes
|
|
100
|
+
// and models silently.
|
|
101
|
+
_errors: [{ phase: 'parse', message: `Babel parse: ${err.message}` }],
|
|
97
102
|
};
|
|
98
103
|
}
|
|
99
104
|
|
|
@@ -20,7 +20,10 @@ module.exports = {
|
|
|
20
20
|
};
|
|
21
21
|
} catch (err) {
|
|
22
22
|
console.warn(`[CARTO] prisma plugin error on ${filename}: ${err.message}`);
|
|
23
|
-
return {
|
|
23
|
+
return {
|
|
24
|
+
routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [],
|
|
25
|
+
_errors: [{ phase: 'extract', message: err.message || String(err) }],
|
|
26
|
+
};
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
};
|
|
@@ -34,7 +34,11 @@ module.exports = {
|
|
|
34
34
|
};
|
|
35
35
|
} catch (err) {
|
|
36
36
|
console.warn(`[CARTO] python plugin error on ${filename}: ${err.message}`);
|
|
37
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [],
|
|
39
|
+
// Record the failure so it's visible in `carto check`.
|
|
40
|
+
_errors: [{ phase: 'extract', message: err.message || String(err) }],
|
|
41
|
+
};
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
};
|
|
@@ -249,7 +249,10 @@ module.exports = {
|
|
|
249
249
|
};
|
|
250
250
|
} catch (err) {
|
|
251
251
|
console.warn(`[CARTO] r plugin error on ${filename}: ${err.message}`);
|
|
252
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [],
|
|
254
|
+
_errors: [{ phase: 'extract', message: err.message || String(err) }],
|
|
255
|
+
};
|
|
253
256
|
}
|
|
254
257
|
}
|
|
255
258
|
};
|
|
@@ -395,11 +395,26 @@ function _inferKind(nameNode, langName) {
|
|
|
395
395
|
return 'variable';
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
/**
|
|
399
|
+
* getUnavailableLanguages() → string[]
|
|
400
|
+
* Returns language names whose grammars failed to load (or tree-sitter itself unavailable).
|
|
401
|
+
*/
|
|
402
|
+
function getUnavailableLanguages() {
|
|
403
|
+
if (!treeSitterAvailable) return GRAMMAR_DEFS.map(d => d.name);
|
|
404
|
+
const unavailable = [];
|
|
405
|
+
for (const def of GRAMMAR_DEFS) {
|
|
406
|
+
const ext = def.extensions[0];
|
|
407
|
+
if (!_getCompiledGrammar(ext)) unavailable.push(def.name);
|
|
408
|
+
}
|
|
409
|
+
return unavailable;
|
|
410
|
+
}
|
|
411
|
+
|
|
398
412
|
module.exports = {
|
|
399
413
|
isAvailable,
|
|
400
414
|
supportsExtension,
|
|
401
415
|
extractImports,
|
|
402
416
|
extractSymbols,
|
|
403
417
|
extractAll,
|
|
418
|
+
getUnavailableLanguages,
|
|
404
419
|
GRAMMAR_DEFS,
|
|
405
420
|
};
|