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.
Files changed (43) hide show
  1. package/README.md +290 -26
  2. package/docs/anci/v0.1-DRAFT.md +420 -0
  3. package/docs/scale.md +129 -0
  4. package/package.json +10 -5
  5. package/scripts/postinstall.js +413 -0
  6. package/src/acp/agent.js +5 -5
  7. package/src/acp/providers/index.js +2 -2
  8. package/src/agents/leiden.js +11 -17
  9. package/src/agents/scan-structure.js +1 -1
  10. package/src/anci/consumer.js +305 -0
  11. package/src/anci/deserialize.js +160 -0
  12. package/src/anci/emit.js +85 -0
  13. package/src/anci/serialize.js +264 -0
  14. package/src/anci/yaml.js +401 -0
  15. package/src/bitmap/bitset.js +190 -0
  16. package/src/bitmap/index.js +121 -0
  17. package/src/bitmap/sidecar.js +545 -0
  18. package/src/bitmap/tools.js +310 -0
  19. package/src/cli/anci.js +237 -0
  20. package/src/cli/check.js +57 -0
  21. package/src/cli/index.js +28 -2
  22. package/src/cli/init.js +297 -65
  23. package/src/cli/inspect.js +295 -0
  24. package/src/cli/pr-impact.js +497 -0
  25. package/src/cli/serve.js +1 -1
  26. package/src/cli/watch.js +6 -0
  27. package/src/engine/worker.js +24 -4
  28. package/src/extractors/imports.js +176 -0
  29. package/src/extractors/languages/html.js +4 -1
  30. package/src/extractors/languages/javascript.js +5 -0
  31. package/src/extractors/languages/prisma.js +4 -1
  32. package/src/extractors/languages/python.js +5 -1
  33. package/src/extractors/languages/r.js +4 -1
  34. package/src/extractors/languages/typescript.js +2 -0
  35. package/src/extractors/tree-sitter-parser.js +15 -0
  36. package/src/mcp/change-plan.js +8 -8
  37. package/src/mcp/diff-parser.js +246 -0
  38. package/src/mcp/server-v2.js +489 -8
  39. package/src/mcp/validate.js +304 -0
  40. package/src/store/config-loader.js +77 -0
  41. package/src/store/sqlite-store.js +389 -4
  42. package/src/store/sync-v2.js +472 -97
  43. package/BENCHMARK_RESULTS.md +0 -34
@@ -0,0 +1,305 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ANCI v0.1 — read-only consumer library.
5
+ *
6
+ * Format reference: docs/anci/v0.1-DRAFT.md.
7
+ *
8
+ * This module is the **public consumer API** for ANCI files. It is what
9
+ * a Cursor / Cline / Continue / Windsurf / Claude Code plugin imports
10
+ * to read a codebase's published architecture without indexing the
11
+ * codebase itself.
12
+ *
13
+ * Any AI tool that calls `loadAnci(dir)` and uses its return value is
14
+ * cooperating with Carto via the open ANCI format — no Carto runtime
15
+ * required.
16
+ *
17
+ * Contract:
18
+ * loadAnci(dir, opts?) → {
19
+ * header, // parsed YAML metadata
20
+ * domains, // [{name, file_count, ...}]
21
+ * routes, // [{method, path, file, ...}]
22
+ * models, // [{name, kind, file}]
23
+ * blastRadius(file, opts?), // 5-hop BFS from `reverse` bitmap
24
+ * simulateChangeImpact(files, opts?),
25
+ * getHighImpactFiles(n?),
26
+ * getDomainOf(file),
27
+ * close?(), // no-op for v0.1 (kept as forward-compat)
28
+ * }
29
+ *
30
+ * The shape mirrors Carto's MCP tool surface so existing test
31
+ * fixtures port across with minimal change. Returns shapes match the
32
+ * MCP tools' return shapes verbatim (file/hop_distance/count fields).
33
+ *
34
+ * Failure modes:
35
+ * - Missing files → throws Error('ANCI not found at ...')
36
+ * - Bad version → throws Error('ANCI version unsupported: ...')
37
+ * - Corrupt body → throws Error('ANCI body failed to parse')
38
+ * - Missing header → throws (the header is required)
39
+ *
40
+ * Zero runtime dependencies. Uses only Node built-ins, the bundled
41
+ * Bitset class, and the YAML / deserialize modules in this directory.
42
+ */
43
+
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+ const yaml = require('./yaml');
47
+ const { deserializeBody } = require('./deserialize');
48
+ const {
49
+ ANCI_BIN_FILENAME,
50
+ ANCI_YAML_FILENAME,
51
+ VERSION,
52
+ } = require('./serialize');
53
+ const { Bitset } = require('../bitmap/bitset');
54
+
55
+ const ACCEPTED_VERSION_PREFIX = '0.1.'; // accept any 0.1.x version
56
+ const MAX_BFS_HOPS = 5; // matches Carto reference
57
+
58
+ /**
59
+ * loadAnci(dir, opts?) → reader object.
60
+ *
61
+ * `dir` should be the directory containing `anci.yaml` and `anci.bin`
62
+ * (typically `.carto/` of a repo, but the format does not constrain
63
+ * location).
64
+ */
65
+ function loadAnci(dir, opts = {}) {
66
+ const yamlPath = path.join(dir, ANCI_YAML_FILENAME);
67
+ const binPath = path.join(dir, ANCI_BIN_FILENAME);
68
+
69
+ if (!fs.existsSync(yamlPath)) {
70
+ throw new Error(`ANCI not found: ${yamlPath} does not exist`);
71
+ }
72
+ if (!fs.existsSync(binPath)) {
73
+ throw new Error(`ANCI not found: ${binPath} does not exist`);
74
+ }
75
+
76
+ // 1. Parse the YAML header.
77
+ let header;
78
+ try {
79
+ header = yaml.parse(fs.readFileSync(yamlPath, 'utf-8'));
80
+ } catch (err) {
81
+ throw new Error(`ANCI header parse failed: ${err.message}`);
82
+ }
83
+ if (!header || !header.anci || typeof header.anci.version !== 'string') {
84
+ throw new Error('ANCI header missing required `anci.version`');
85
+ }
86
+
87
+ // 2. Reject unsupported versions early.
88
+ if (!header.anci.version.startsWith(ACCEPTED_VERSION_PREFIX)) {
89
+ throw new Error(
90
+ `ANCI version unsupported: got ${JSON.stringify(header.anci.version)}, ` +
91
+ `this consumer accepts ${ACCEPTED_VERSION_PREFIX}x`
92
+ );
93
+ }
94
+
95
+ // 3. Cross-check body length when the header advertises one.
96
+ const bodyBuf = fs.readFileSync(binPath);
97
+ if (header.anci.body && typeof header.anci.body.bytes === 'number') {
98
+ if (bodyBuf.length !== header.anci.body.bytes) {
99
+ // Permissive: warn but don't fail (consumers SHOULD warn but continue).
100
+ // Suppress by default — tests can opt in via opts.warn.
101
+ if (opts.warn) {
102
+ opts.warn(
103
+ `ANCI body size mismatch: header says ${header.anci.body.bytes}, ` +
104
+ `file is ${bodyBuf.length} bytes`
105
+ );
106
+ }
107
+ }
108
+ }
109
+
110
+ // 4. Parse the binary body.
111
+ const body = deserializeBody(bodyBuf);
112
+ if (!body) {
113
+ throw new Error('ANCI body failed to parse (corrupt magic, version, or section)');
114
+ }
115
+
116
+ // 5. Build the reader.
117
+ return makeReader({ header, body });
118
+ }
119
+
120
+ function makeReader({ header, body }) {
121
+ // ── Convenience flat projections ─────────────────────────────────
122
+ const domains = header.domains || [];
123
+ const routes = header.routes || [];
124
+ const models = header.models || [];
125
+ const high_impact = header.high_impact || [];
126
+
127
+ // domain id → name and file → domain id are in the body; build a
128
+ // quick path → domainName map for getDomainOf().
129
+ const fileDomainName = new Map();
130
+ for (const [fid, did] of body.fileDomain) {
131
+ const file = body.fileIdToPath.get(fid);
132
+ const name = body.domainIdToName.get(did);
133
+ if (file && name) fileDomainName.set(file, name);
134
+ }
135
+
136
+ function getDomainOf(file) {
137
+ return fileDomainName.get(file) || null;
138
+ }
139
+
140
+ /**
141
+ * blastRadius(file, opts?) → { file, hops, count, files: [{file, hop_distance}] }
142
+ *
143
+ * 5-hop BFS over `reverse`. Same semantics as Carto's
144
+ * bitmap blastRadius. Returns null if `file` is unknown.
145
+ */
146
+ function blastRadius(file, opts = {}) {
147
+ const fid = body.pathToFileId.get(file);
148
+ if (fid === undefined) return null;
149
+
150
+ const maxHops = Math.min(MAX_BFS_HOPS, opts.maxHops || MAX_BFS_HOPS);
151
+ const visited = new Bitset(body.size);
152
+ const frontier = new Bitset(body.size);
153
+ const next = new Bitset(body.size);
154
+
155
+ const direct = body.reverse.get(fid);
156
+ if (!direct) return { file, hops: 0, count: 0, files: [] };
157
+
158
+ frontier.copyFrom(direct);
159
+ visited.orInPlace(frontier);
160
+ const hopOf = new Map();
161
+ for (let h = 1; h <= maxHops; h++) {
162
+ const fwords = frontier.words;
163
+ next.setAll(0);
164
+ for (let w = 0; w < fwords.length; w++) {
165
+ let v = fwords[w];
166
+ while (v) {
167
+ const bit = v & -v;
168
+ const dep = (w << 5) + (31 - Math.clz32(bit));
169
+ v ^= bit;
170
+ if (!hopOf.has(dep)) hopOf.set(dep, h);
171
+ const deps = body.reverse.get(dep);
172
+ if (deps) next.orInPlace(deps);
173
+ }
174
+ }
175
+ next.andNotInPlace(visited);
176
+ if (next.popcount() === 0) break;
177
+ visited.orInPlace(next);
178
+ frontier.copyFrom(next);
179
+ }
180
+ visited.clear(fid);
181
+ hopOf.delete(fid);
182
+
183
+ const files = [];
184
+ for (const [dep, hop] of hopOf) {
185
+ const p = body.fileIdToPath.get(dep);
186
+ if (p) files.push({ file: p, hop_distance: hop });
187
+ }
188
+ files.sort((a, b) => a.hop_distance - b.hop_distance || a.file.localeCompare(b.file));
189
+ return { file, hops: maxHops, count: files.length, files };
190
+ }
191
+
192
+ /**
193
+ * simulateChangeImpact(files, opts?) → { count, files: [{file, hop_distance}] }
194
+ *
195
+ * Union of blast-radius BFSes seeded from each input file.
196
+ * Sub-millisecond on 7K-file repos via OR-aggregation.
197
+ */
198
+ function simulateChangeImpact(files, opts = {}) {
199
+ if (!Array.isArray(files) || files.length === 0) {
200
+ return { count: 0, files: [] };
201
+ }
202
+ const seedIds = [];
203
+ for (const f of files) {
204
+ const fid = body.pathToFileId.get(f);
205
+ if (fid !== undefined) seedIds.push(fid);
206
+ }
207
+ if (seedIds.length === 0) return { count: 0, files: [] };
208
+
209
+ const maxHops = Math.min(MAX_BFS_HOPS, opts.maxHops || MAX_BFS_HOPS);
210
+ const visited = new Bitset(body.size);
211
+ const frontier = new Bitset(body.size);
212
+ const next = new Bitset(body.size);
213
+ const hopOf = new Map();
214
+
215
+ // Seed: union of direct dependents of all input files.
216
+ for (const fid of seedIds) {
217
+ const deps = body.reverse.get(fid);
218
+ if (deps) frontier.orInPlace(deps);
219
+ }
220
+ visited.orInPlace(frontier);
221
+ {
222
+ const fwords = frontier.words;
223
+ for (let w = 0; w < fwords.length; w++) {
224
+ let v = fwords[w];
225
+ while (v) {
226
+ const bit = v & -v;
227
+ hopOf.set((w << 5) + (31 - Math.clz32(bit)), 1);
228
+ v ^= bit;
229
+ }
230
+ }
231
+ }
232
+ for (let h = 2; h <= maxHops; h++) {
233
+ const fwords = frontier.words;
234
+ next.setAll(0);
235
+ for (let w = 0; w < fwords.length; w++) {
236
+ let v = fwords[w];
237
+ while (v) {
238
+ const bit = v & -v;
239
+ const dep = (w << 5) + (31 - Math.clz32(bit));
240
+ v ^= bit;
241
+ const deps = body.reverse.get(dep);
242
+ if (deps) next.orInPlace(deps);
243
+ }
244
+ }
245
+ next.andNotInPlace(visited);
246
+ if (next.popcount() === 0) break;
247
+ // Record hop distances for newly-reached nodes.
248
+ const nwords = next.words;
249
+ for (let w = 0; w < nwords.length; w++) {
250
+ let v = nwords[w];
251
+ while (v) {
252
+ const bit = v & -v;
253
+ hopOf.set((w << 5) + (31 - Math.clz32(bit)), h);
254
+ v ^= bit;
255
+ }
256
+ }
257
+ visited.orInPlace(next);
258
+ frontier.copyFrom(next);
259
+ }
260
+
261
+ // Drop the input set itself.
262
+ for (const fid of seedIds) {
263
+ visited.clear(fid);
264
+ hopOf.delete(fid);
265
+ }
266
+
267
+ const result = [];
268
+ for (const [dep, hop] of hopOf) {
269
+ const p = body.fileIdToPath.get(dep);
270
+ if (p) result.push({ file: p, hop_distance: hop });
271
+ }
272
+ result.sort((a, b) => a.hop_distance - b.hop_distance || a.file.localeCompare(b.file));
273
+ return { count: result.length, files: result };
274
+ }
275
+
276
+ /**
277
+ * getHighImpactFiles(n) → top N from popcount index, hydrated.
278
+ * Mirrors Carto's MCP tool of the same name.
279
+ */
280
+ function getHighImpactFiles(n = 10) {
281
+ const out = [];
282
+ const limit = Math.min(n, body.popcountIndex.length);
283
+ for (let i = 0; i < limit; i++) {
284
+ const e = body.popcountIndex[i];
285
+ const file = body.fileIdToPath.get(e.fileId);
286
+ if (file) out.push({ file, transitive_dependents: e.count });
287
+ }
288
+ return out;
289
+ }
290
+
291
+ return {
292
+ header,
293
+ domains,
294
+ routes,
295
+ models,
296
+ high_impact,
297
+ blastRadius,
298
+ simulateChangeImpact,
299
+ getHighImpactFiles,
300
+ getDomainOf,
301
+ close() { /* no resources to release in v0.1 */ },
302
+ };
303
+ }
304
+
305
+ module.exports = { loadAnci, ACCEPTED_VERSION_PREFIX };
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ANCI v0.1 — binary body deserializer.
5
+ *
6
+ * Format reference: docs/anci/v0.1-DRAFT.md §5.
7
+ *
8
+ * Symmetric with serialize.js. Validates magic + version up front, then
9
+ * stream-parses each section in declared order.
10
+ *
11
+ * Returns null for malformed inputs (corrupt magic, unsupported version,
12
+ * truncated section). The reference consumer's contract: malformed ANCI
13
+ * is a degraded — not catastrophic — situation. Callers that need a
14
+ * strict error throw should check the return value.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { Bitset } = require('../bitmap/bitset');
20
+ const {
21
+ MAGIC,
22
+ VERSION,
23
+ HEADER_BYTES,
24
+ ANCI_BIN_FILENAME,
25
+ } = require('./serialize');
26
+
27
+ /**
28
+ * decodeBitmap(buf, off, size) → { id, bitmap, nextOff }
29
+ */
30
+ function decodeBitmap(buf, off, size) {
31
+ const id = buf.readUInt32LE(off);
32
+ const wordsLen = buf.readUInt32LE(off + 4);
33
+ const wordBytes = wordsLen * 4;
34
+ const bitmap = new Bitset(size);
35
+ if (bitmap.words.length !== wordsLen) {
36
+ throw new Error(
37
+ `decodeBitmap: word-length mismatch (header size=${size} → ${bitmap.words.length} words, ` +
38
+ `record claims ${wordsLen} words)`
39
+ );
40
+ }
41
+ const u8 = new Uint8Array(bitmap.words.buffer, bitmap.words.byteOffset, wordBytes);
42
+ for (let i = 0; i < wordBytes; i++) u8[i] = buf[off + 8 + i];
43
+ return { id, bitmap, nextOff: off + 8 + wordBytes };
44
+ }
45
+
46
+ /**
47
+ * deserializeBody(buf) → payload object | null on malformed input.
48
+ *
49
+ * Returned shape mirrors `serializeBody`'s input plus a
50
+ * `pathToFileId` inverse map for consumer convenience:
51
+ *
52
+ * {
53
+ * size,
54
+ * forward, reverse,
55
+ * popcountIndex,
56
+ * fileIdToPath, pathToFileId,
57
+ * fileDomain, domainIdToName,
58
+ * }
59
+ */
60
+ function deserializeBody(buf) {
61
+ try {
62
+ if (!Buffer.isBuffer(buf)) return null;
63
+ if (buf.length < HEADER_BYTES) return null;
64
+ if (buf.readUInt32LE(0) !== MAGIC) return null;
65
+ const version = buf.readUInt8(4);
66
+ if (version !== VERSION) return null;
67
+ const size = buf.readUInt32LE(8);
68
+
69
+ let off = HEADER_BYTES;
70
+
71
+ // forward
72
+ const forward = new Map();
73
+ const fwdCount = buf.readUInt32LE(off); off += 4;
74
+ for (let i = 0; i < fwdCount; i++) {
75
+ const dec = decodeBitmap(buf, off, size);
76
+ forward.set(dec.id, dec.bitmap);
77
+ off = dec.nextOff;
78
+ }
79
+
80
+ // reverse
81
+ const reverse = new Map();
82
+ const revCount = buf.readUInt32LE(off); off += 4;
83
+ for (let i = 0; i < revCount; i++) {
84
+ const dec = decodeBitmap(buf, off, size);
85
+ reverse.set(dec.id, dec.bitmap);
86
+ off = dec.nextOff;
87
+ }
88
+
89
+ // popcount
90
+ const popLen = buf.readUInt32LE(off); off += 4;
91
+ const popcountIndex = new Array(popLen);
92
+ for (let i = 0; i < popLen; i++) {
93
+ popcountIndex[i] = {
94
+ fileId: buf.readUInt32LE(off),
95
+ count: buf.readUInt32LE(off + 4),
96
+ };
97
+ off += 8;
98
+ }
99
+
100
+ // paths
101
+ const fileIdToPath = new Map();
102
+ const pathToFileId = new Map();
103
+ const pathCount = buf.readUInt32LE(off); off += 4;
104
+ for (let i = 0; i < pathCount; i++) {
105
+ const fid = buf.readUInt32LE(off);
106
+ const pLen = buf.readUInt32LE(off + 4);
107
+ const p = buf.slice(off + 8, off + 8 + pLen).toString('utf-8');
108
+ fileIdToPath.set(fid, p);
109
+ pathToFileId.set(p, fid);
110
+ off += 8 + pLen;
111
+ }
112
+
113
+ // file_domain
114
+ const fileDomain = new Map();
115
+ const fdCount = buf.readUInt32LE(off); off += 4;
116
+ for (let i = 0; i < fdCount; i++) {
117
+ fileDomain.set(buf.readUInt32LE(off), buf.readUInt32LE(off + 4));
118
+ off += 8;
119
+ }
120
+
121
+ // domain_names
122
+ const domainIdToName = new Map();
123
+ const dnCount = buf.readUInt32LE(off); off += 4;
124
+ for (let i = 0; i < dnCount; i++) {
125
+ const did = buf.readUInt32LE(off);
126
+ const nLen = buf.readUInt32LE(off + 4);
127
+ const name = buf.slice(off + 8, off + 8 + nLen).toString('utf-8');
128
+ domainIdToName.set(did, name);
129
+ off += 8 + nLen;
130
+ }
131
+
132
+ return {
133
+ size,
134
+ forward,
135
+ reverse,
136
+ popcountIndex,
137
+ fileIdToPath,
138
+ pathToFileId,
139
+ fileDomain,
140
+ domainIdToName,
141
+ };
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * loadBodyFromDisk(cartoDir) → payload | null
149
+ */
150
+ function loadBodyFromDisk(cartoDir) {
151
+ const target = path.join(cartoDir, ANCI_BIN_FILENAME);
152
+ let buf;
153
+ try { buf = fs.readFileSync(target); } catch { return null; }
154
+ return deserializeBody(buf);
155
+ }
156
+
157
+ module.exports = {
158
+ deserializeBody,
159
+ loadBodyFromDisk,
160
+ };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ANCI v0.1 — emit orchestrator.
5
+ *
6
+ * Glues serialize.js + yaml.js together and writes both files atomically
7
+ * to `.carto/anci.{yaml,bin}`. Used by the runSyncV2 hook (called after
8
+ * every full sync) and by `carto anci publish`.
9
+ *
10
+ * Atomic write: each file goes to `.tmp` first, then rename. Crash
11
+ * mid-write leaves the previous pair intact (or absent — the consumer's
12
+ * job is to refuse to load when the magic / version don't match).
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const yaml = require('./yaml');
18
+ const {
19
+ serializeBody,
20
+ deriveBodyFromSidecar,
21
+ buildHeader,
22
+ ANCI_BIN_FILENAME,
23
+ ANCI_YAML_FILENAME,
24
+ } = require('./serialize');
25
+
26
+ /**
27
+ * emitToCartoDir({ cartoDir, sidecar, store, generator, generatedAt })
28
+ * → { yamlPath, binPath, bodyBytes }
29
+ *
30
+ * Required:
31
+ * cartoDir — absolute path to `.carto/`. Will be created if missing.
32
+ * sidecar — bitmap sidecar object (from `bitmap/sidecar.js`).
33
+ * store — open SQLiteStore (any mode; used for header metadata).
34
+ *
35
+ * Optional:
36
+ * generator — string, defaults to `carto-md@<version>`.
37
+ * generatedAt — ISO-8601 string, defaults to `new Date().toISOString()`.
38
+ *
39
+ * Throws on filesystem failure (caller wraps in try/catch when
40
+ * best-effort behavior is needed — see runSyncV2 hook).
41
+ */
42
+ function emitToCartoDir({ cartoDir, sidecar, store, generator, generatedAt }) {
43
+ fs.mkdirSync(cartoDir, { recursive: true });
44
+
45
+ // 1. Body — derive from sidecar, serialize, atomic write.
46
+ const body = serializeBody(deriveBodyFromSidecar(sidecar));
47
+ const binPath = path.join(cartoDir, ANCI_BIN_FILENAME);
48
+ const binTmp = binPath + '.tmp';
49
+ fs.writeFileSync(binTmp, body);
50
+ fs.renameSync(binTmp, binPath);
51
+
52
+ // 2. Header — built with the body's actual on-disk byte count so the
53
+ // header's `body.bytes` cross-check matches.
54
+ const header = buildHeader({
55
+ sidecar,
56
+ store,
57
+ generator: generator || resolveGenerator(),
58
+ generatedAt: generatedAt || new Date().toISOString(),
59
+ bodyBytes: body.length,
60
+ });
61
+ const yamlText = yaml.emit(header);
62
+ const yamlPath = path.join(cartoDir, ANCI_YAML_FILENAME);
63
+ const yamlTmp = yamlPath + '.tmp';
64
+ fs.writeFileSync(yamlTmp, yamlText, 'utf-8');
65
+ fs.renameSync(yamlTmp, yamlPath);
66
+
67
+ return { yamlPath, binPath, bodyBytes: body.length };
68
+ }
69
+
70
+ /**
71
+ * resolveGenerator() → "carto-md@<version>"
72
+ *
73
+ * Defensive: if package.json can't be read for any reason, fall back to
74
+ * a stable opaque string rather than crash the publish.
75
+ */
76
+ function resolveGenerator() {
77
+ try {
78
+ const pkg = require('../../package.json');
79
+ return `${pkg.name}@${pkg.version}`;
80
+ } catch {
81
+ return 'carto-md@unknown';
82
+ }
83
+ }
84
+
85
+ module.exports = { emitToCartoDir };