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.
@@ -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 is different from MCP-spec default:
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 {
@@ -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 pre-Spec-11 schemas
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;