carto-md 2.0.7 → 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.
@@ -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 pre-2.0.4 install with only map.json, or never initialized.
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({
@@ -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
- let extracted;
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
- parentPort.postMessage({ id, error: err.message, result: null });
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
- const imports = extractImports(content, filePath, projectRoot);
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
  });
@@ -51,6 +51,10 @@ function extractImports(content, filePath, projectRoot) {
51
51
  return extractJavaImports(content, filePath, projectRoot);
52
52
  } else if (ext === '.rb') {
53
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);
54
58
  }
55
59
 
56
60
  // Resolve and deduplicate
@@ -650,3 +654,175 @@ function _resolveRubyPath(reqPath, baseDir, projectRoot) {
650
654
  }
651
655
  return null;
652
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 { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
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 { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
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 { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
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 { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
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
  };
@@ -63,6 +63,8 @@ module.exports = {
63
63
  jobs: extractQueueAndCron(content),
64
64
  _tsImports: tsImports,
65
65
  _tsSymbols: tsSymbols,
66
+ // Promote silent Babel failure to a breadcrumb.
67
+ _errors: [{ phase: 'parse', message: `Babel parse: ${err.message}` }],
66
68
  };
67
69
  }
68
70
 
@@ -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
  };