carto-md 2.0.8 → 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 +149 -9
- package/docs/anci/v0.1-DRAFT.md +420 -0
- package/docs/scale.md +129 -0
- package/package.json +4 -3
- package/scripts/postinstall.js +391 -24
- package/src/agents/leiden.js +4 -4
- 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/cli/anci.js +237 -0
- package/src/cli/index.js +14 -0
- package/src/cli/init.js +1 -1
- package/src/cli/inspect.js +1 -1
- package/src/cli/pr-impact.js +497 -0
- package/src/extractors/languages/javascript.js +3 -3
- package/src/mcp/change-plan.js +8 -8
- package/src/store/sync-v2.js +52 -3
package/src/cli/anci.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `carto anci` — ANCI subcommand dispatcher.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* publish Re-emit `.carto/anci.{yaml,bin}` from the
|
|
8
|
+
* existing index. Does NOT re-sync — assumes
|
|
9
|
+
* the index is already populated.
|
|
10
|
+
* show Print a human-readable summary of the
|
|
11
|
+
* published ANCI files.
|
|
12
|
+
* validate <dir> Validate an external `anci.{yaml,bin}` pair
|
|
13
|
+
* at <dir>. Exits 0 on valid, 1 on invalid.
|
|
14
|
+
*
|
|
15
|
+
* All paths in this module are anchored at `process.cwd()` unless an
|
|
16
|
+
* explicit directory is passed.
|
|
17
|
+
*
|
|
18
|
+
* The `serve` MCP server is unaffected — ANCI is the *export* format,
|
|
19
|
+
* not Carto's runtime format. SQLite + bitmap remain the runtime path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
function printUsage() {
|
|
26
|
+
console.log(`
|
|
27
|
+
Usage: carto anci <subcommand>
|
|
28
|
+
|
|
29
|
+
Subcommands:
|
|
30
|
+
publish Re-emit .carto/anci.{yaml,bin} from the existing
|
|
31
|
+
SQLite index + bitmap sidecar. No re-sync.
|
|
32
|
+
show Print a summary of the published ANCI files.
|
|
33
|
+
validate <dir> Validate an external anci.{yaml,bin} pair at <dir>.
|
|
34
|
+
Exit 0 on valid, 1 on invalid.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
carto anci publish
|
|
38
|
+
carto anci show
|
|
39
|
+
carto anci validate ./.carto
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* run({ argv }) → exit code (0 ok, 1 error).
|
|
45
|
+
*
|
|
46
|
+
* argv shape: array of args AFTER `carto anci` (e.g. `['publish']`,
|
|
47
|
+
* `['validate', './.carto']`). The top-level `cli/index.js` slices the
|
|
48
|
+
* full process.argv before passing it in.
|
|
49
|
+
*/
|
|
50
|
+
function run({ argv }) {
|
|
51
|
+
const sub = argv[0];
|
|
52
|
+
|
|
53
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
54
|
+
printUsage();
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (sub === 'publish') return runPublish(process.cwd());
|
|
59
|
+
if (sub === 'show') return runShow(process.cwd());
|
|
60
|
+
if (sub === 'validate') {
|
|
61
|
+
const dir = argv[1] || path.join(process.cwd(), '.carto');
|
|
62
|
+
return runValidate(dir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.error(`[CARTO] Unknown anci subcommand: ${sub}`);
|
|
66
|
+
printUsage();
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* publish — Re-emit ANCI files from the existing index.
|
|
72
|
+
*
|
|
73
|
+
* Reads the SQLiteStore (read-write so a fresh sidecar can be persisted
|
|
74
|
+
* if the on-disk one is stale) + bitmap sidecar, then writes
|
|
75
|
+
* `anci.{yaml,bin}` atomically. Prints sizes and elapsed time.
|
|
76
|
+
*
|
|
77
|
+
* Exits 1 if no `.carto/carto.db` is present.
|
|
78
|
+
*/
|
|
79
|
+
function runPublish(projectRoot) {
|
|
80
|
+
const cartoDir = path.join(projectRoot, '.carto');
|
|
81
|
+
const dbPath = path.join(cartoDir, 'carto.db');
|
|
82
|
+
if (!fs.existsSync(dbPath)) {
|
|
83
|
+
console.error(`[CARTO] No index found at ${dbPath}. Run \`carto init\` first.`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
const { SQLiteStore } = require('../store/sqlite-store');
|
|
89
|
+
const { ensureBitmapFresh } = require('../bitmap/index');
|
|
90
|
+
const { emitToCartoDir } = require('../anci/emit');
|
|
91
|
+
|
|
92
|
+
const store = new SQLiteStore(projectRoot);
|
|
93
|
+
store.open();
|
|
94
|
+
try {
|
|
95
|
+
const sidecar = ensureBitmapFresh(cartoDir, store);
|
|
96
|
+
const { yamlPath, binPath, bodyBytes } = emitToCartoDir({
|
|
97
|
+
cartoDir, sidecar, store,
|
|
98
|
+
});
|
|
99
|
+
const elapsed = Date.now() - start;
|
|
100
|
+
const yamlBytes = fs.statSync(yamlPath).size;
|
|
101
|
+
|
|
102
|
+
console.log(`[CARTO] Published ANCI v0.1 in ${elapsed}ms`);
|
|
103
|
+
console.log(` ${path.relative(projectRoot, yamlPath)} (${formatBytes(yamlBytes)})`);
|
|
104
|
+
console.log(` ${path.relative(projectRoot, binPath)} (${formatBytes(bodyBytes)})`);
|
|
105
|
+
return 0;
|
|
106
|
+
} finally {
|
|
107
|
+
try { store.close(); } catch {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* show — Print a summary of the published ANCI files.
|
|
113
|
+
*
|
|
114
|
+
* Loads via the consumer library so this surface dogfoods the same
|
|
115
|
+
* code path partner integrations will use. If either file is missing
|
|
116
|
+
* or malformed, prints the error and exits 1.
|
|
117
|
+
*/
|
|
118
|
+
function runShow(projectRoot) {
|
|
119
|
+
const cartoDir = path.join(projectRoot, '.carto');
|
|
120
|
+
let reader;
|
|
121
|
+
try {
|
|
122
|
+
const { loadAnci } = require('../anci/consumer');
|
|
123
|
+
reader = loadAnci(cartoDir);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`[CARTO] ${err.message}`);
|
|
126
|
+
console.error(`[CARTO] Run \`carto anci publish\` to (re)generate.`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const h = reader.header;
|
|
131
|
+
const lines = [];
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('── ANCI ────────────────────────────────────────────────');
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push(` spec version : ${h.anci.version}`);
|
|
136
|
+
lines.push(` generator : ${h.anci.generator}`);
|
|
137
|
+
lines.push(` generated_at : ${h.anci.generated_at}`);
|
|
138
|
+
lines.push(` body file : ${h.anci.body.file} (${formatBytes(h.anci.body.bytes)})`);
|
|
139
|
+
lines.push('');
|
|
140
|
+
if (h.project) {
|
|
141
|
+
lines.push('Project');
|
|
142
|
+
lines.push(` files : ${h.project.total_files}`);
|
|
143
|
+
lines.push(` routes : ${h.project.total_routes}`);
|
|
144
|
+
lines.push(` models : ${h.project.total_models}`);
|
|
145
|
+
lines.push(` import edges : ${h.project.total_import_edges}`);
|
|
146
|
+
lines.push('');
|
|
147
|
+
}
|
|
148
|
+
if (reader.domains.length > 0) {
|
|
149
|
+
lines.push(`Domains (${reader.domains.length})`);
|
|
150
|
+
for (const d of reader.domains) {
|
|
151
|
+
lines.push(` ${d.name.padEnd(16)} ${String(d.file_count).padStart(5)} files ` +
|
|
152
|
+
`${String(d.route_count || 0).padStart(4)} routes ` +
|
|
153
|
+
`${String(d.model_count || 0).padStart(4)} models`);
|
|
154
|
+
}
|
|
155
|
+
lines.push('');
|
|
156
|
+
}
|
|
157
|
+
if (reader.high_impact && reader.high_impact.length > 0) {
|
|
158
|
+
lines.push('Top impact (transitive dependents)');
|
|
159
|
+
const topN = Math.min(10, reader.high_impact.length);
|
|
160
|
+
for (let i = 0; i < topN; i++) {
|
|
161
|
+
const e = reader.high_impact[i];
|
|
162
|
+
lines.push(` ${String(e.transitive_dependents).padStart(4)} ${e.file}`);
|
|
163
|
+
}
|
|
164
|
+
lines.push('');
|
|
165
|
+
}
|
|
166
|
+
lines.push('────────────────────────────────────────────────────────');
|
|
167
|
+
lines.push('');
|
|
168
|
+
process.stdout.write(lines.join('\n'));
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* validate <dir> — Validate an external ANCI pair.
|
|
174
|
+
*
|
|
175
|
+
* Cleanly distinguishes "structural problem with the file" (return 1
|
|
176
|
+
* with message) from "this consumer can't handle this version"
|
|
177
|
+
* (return 1 with message naming the version) from "all checks pass"
|
|
178
|
+
* (return 0).
|
|
179
|
+
*/
|
|
180
|
+
function runValidate(dir) {
|
|
181
|
+
const yaml = require('../anci/yaml');
|
|
182
|
+
const { deserializeBody } = require('../anci/deserialize');
|
|
183
|
+
const { ANCI_BIN_FILENAME, ANCI_YAML_FILENAME } = require('../anci/serialize');
|
|
184
|
+
|
|
185
|
+
const yamlPath = path.join(dir, ANCI_YAML_FILENAME);
|
|
186
|
+
const binPath = path.join(dir, ANCI_BIN_FILENAME);
|
|
187
|
+
|
|
188
|
+
const errors = [];
|
|
189
|
+
if (!fs.existsSync(yamlPath)) errors.push(`missing ${yamlPath}`);
|
|
190
|
+
if (!fs.existsSync(binPath)) errors.push(`missing ${binPath}`);
|
|
191
|
+
if (errors.length > 0) {
|
|
192
|
+
for (const e of errors) console.error(`[CARTO] ${e}`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let header;
|
|
197
|
+
try {
|
|
198
|
+
header = yaml.parse(fs.readFileSync(yamlPath, 'utf-8'));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error(`[CARTO] header parse failed: ${err.message}`);
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
if (!header || !header.anci || typeof header.anci.version !== 'string') {
|
|
204
|
+
console.error('[CARTO] header missing required `anci.version`');
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const bodyBuf = fs.readFileSync(binPath);
|
|
209
|
+
const body = deserializeBody(bodyBuf);
|
|
210
|
+
if (!body) {
|
|
211
|
+
console.error('[CARTO] body failed to parse (corrupt magic, version, or section)');
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Optional cross-check: header.body.bytes vs actual file size.
|
|
216
|
+
if (header.anci.body && typeof header.anci.body.bytes === 'number') {
|
|
217
|
+
if (header.anci.body.bytes !== bodyBuf.length) {
|
|
218
|
+
console.error(
|
|
219
|
+
`[CARTO] header/body size mismatch: header says ${header.anci.body.bytes}, ` +
|
|
220
|
+
`actual ${bodyBuf.length} bytes`
|
|
221
|
+
);
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(`[CARTO] ANCI valid: ${header.anci.version}, ${body.size} files indexed.`);
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function formatBytes(n) {
|
|
231
|
+
if (n === null || n === undefined) return 'n/a';
|
|
232
|
+
if (n < 1024) return `${n} B`;
|
|
233
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
234
|
+
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { run };
|
package/src/cli/index.js
CHANGED
|
@@ -16,10 +16,16 @@ Commands:
|
|
|
16
16
|
Not required — git hooks + lazy MCP re-parse keep the
|
|
17
17
|
index fresh by default. Use only for AI-heavy workflows.
|
|
18
18
|
impact <file> Show which files and routes are affected by changing a file
|
|
19
|
+
pr-impact Render a PR impact report (markdown or JSON) for the
|
|
20
|
+
diff between two git refs. Used by the carto GitHub
|
|
21
|
+
Action; works standalone too.
|
|
19
22
|
check Report cross-domain deps, high-risk uncommitted changes, domain health
|
|
20
23
|
inspect Read-only diagnostic: prints index paths, sizes, freshness,
|
|
21
24
|
bitmap sidecar shape, top-impact files, schema version,
|
|
22
25
|
sync timestamps, extraction errors. Use --json for piping.
|
|
26
|
+
anci ANCI v0.1 DRAFT export. Subcommands: publish, show,
|
|
27
|
+
validate. Writes/reads .carto/anci.{yaml,bin} — the
|
|
28
|
+
public, tool-neutral architecture description spec.
|
|
23
29
|
remove Remove AGENTS.md and .carto/ from this project
|
|
24
30
|
serve Start MCP server for AI tool integration
|
|
25
31
|
agent Start ACP agent mode (for Zed, JetBrains, VS Code)
|
|
@@ -57,6 +63,10 @@ if (command === 'init') {
|
|
|
57
63
|
} else if (command === 'impact') {
|
|
58
64
|
const fileArg = process.argv[3];
|
|
59
65
|
require('./impact').run(process.cwd(), fileArg);
|
|
66
|
+
} else if (command === 'pr-impact') {
|
|
67
|
+
// pr-impact owns its own argv parsing + error handling + exit code.
|
|
68
|
+
const code = require('./pr-impact').run({ argv: process.argv.slice(3) });
|
|
69
|
+
process.exit(code);
|
|
60
70
|
} else if (command === 'check') {
|
|
61
71
|
require('./check').run(process.cwd()).catch(err => {
|
|
62
72
|
console.error(`[CARTO] Fatal error: ${err.message}`);
|
|
@@ -67,6 +77,10 @@ if (command === 'init') {
|
|
|
67
77
|
const json = process.argv.slice(3).includes('--json');
|
|
68
78
|
const code = require('./inspect').run(process.cwd(), { json });
|
|
69
79
|
process.exit(code);
|
|
80
|
+
} else if (command === 'anci') {
|
|
81
|
+
// anci has its own subcommand parser; pass argv after `carto anci`.
|
|
82
|
+
const code = require('./anci').run({ argv: process.argv.slice(3) });
|
|
83
|
+
process.exit(code);
|
|
70
84
|
} else if (command === 'remove') {
|
|
71
85
|
require('./remove').run(process.cwd());
|
|
72
86
|
} else if (command === 'serve') {
|
package/src/cli/init.js
CHANGED
|
@@ -357,7 +357,7 @@ function wireIDEs(projectRoot) {
|
|
|
357
357
|
// ─── VS Code Copilot ───────────────────────────────────────────────
|
|
358
358
|
// Detection: `code` binary on PATH (most reliable; user-profile path
|
|
359
359
|
// varies too much across macOS/Linux/Windows + Insiders/stable to be
|
|
360
|
-
// a good detection signal). Schema
|
|
360
|
+
// a good detection signal). Schema differs from the standard MCP shape:
|
|
361
361
|
// VS Code uses `servers` (not `mcpServers`) and requires `type: stdio`.
|
|
362
362
|
if (binaryExists('code')) {
|
|
363
363
|
try {
|
package/src/cli/inspect.js
CHANGED
|
@@ -113,7 +113,7 @@ function collect(projectRoot) {
|
|
|
113
113
|
try {
|
|
114
114
|
const structure = store.getStructure();
|
|
115
115
|
const domainsList = store.getDomainsList();
|
|
116
|
-
// Defensive: getExtractionErrorCount throws on
|
|
116
|
+
// Defensive: getExtractionErrorCount throws on older schemas
|
|
117
117
|
// (no `extraction_errors` table). Inspect must work on every DB
|
|
118
118
|
// we've ever shipped — fall back to 0 on schema mismatch.
|
|
119
119
|
let errCount = 0;
|