carto-md 2.0.7 → 2.0.9
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 +290 -26
- package/docs/anci/v0.1-DRAFT.md +420 -0
- package/docs/scale.md +129 -0
- package/package.json +10 -5
- package/scripts/postinstall.js +413 -0
- package/src/acp/agent.js +5 -5
- package/src/acp/providers/index.js +2 -2
- package/src/agents/leiden.js +11 -17
- package/src/agents/scan-structure.js +1 -1
- package/src/anci/consumer.js +305 -0
- package/src/anci/deserialize.js +160 -0
- package/src/anci/emit.js +85 -0
- package/src/anci/serialize.js +264 -0
- package/src/anci/yaml.js +401 -0
- 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/anci.js +237 -0
- package/src/cli/check.js +57 -0
- package/src/cli/index.js +28 -2
- package/src/cli/init.js +297 -65
- package/src/cli/inspect.js +295 -0
- package/src/cli/pr-impact.js +497 -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 +176 -0
- 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/change-plan.js +8 -8
- package/src/mcp/diff-parser.js +246 -0
- package/src/mcp/server-v2.js +489 -8
- package/src/mcp/validate.js +304 -0
- package/src/store/config-loader.js +77 -0
- package/src/store/sqlite-store.js +389 -4
- package/src/store/sync-v2.js +472 -97
- 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 older 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 };
|