carto-md 1.1.1 → 1.1.2

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 CHANGED
@@ -27,6 +27,16 @@ AI coding tools are blind to your actual project. Every session starts from zero
27
27
 
28
28
  **Carto makes it live. And queryable.**
29
29
 
30
+ | | Without Carto | With Carto |
31
+ |---|---|---|
32
+ | Knows blast radius before editing | Never | Always, instantly |
33
+ | Knows which routes break | Never | Exact list |
34
+ | Plans multi-file changes | Guesses | Fully informed |
35
+ | Hallucinates field names | Often | Never |
36
+ | Understands codebase on session start | 10–20 min | 0 |
37
+ | Works across Kiro, Cursor, Claude, Copilot | Separately | One shared graph |
38
+ | Stays current as code changes | Goes stale | Live on every save |
39
+
30
40
  ---
31
41
 
32
42
  ## Proof — cal.com (800k lines)
package/index.js ADDED
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * carto-md — public module API
5
+ *
6
+ * Usage:
7
+ * const { Carto } = require('carto-md');
8
+ * const carto = new Carto();
9
+ * await carto.index('/path/to/project');
10
+ *
11
+ * // Get everything Kepler needs for a file
12
+ * const ctx = carto.getContextForFile('src/auth/auth.service.ts');
13
+ *
14
+ * // Listen for live updates
15
+ * carto.on('updated', ({ file, blastRadius }) => { ... });
16
+ */
17
+
18
+ const { Carto } = require('./src/engine/carto');
19
+
20
+ module.exports = { Carto };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "The context layer for AI-native development.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
7
7
  },
8
- "main": "./src/sync.js",
8
+ "main": "./index.js",
9
9
  "scripts": {
10
10
  "test": "node test/test.js"
11
11
  },
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ function getHashPath(projectRoot) {
8
+ return path.join(projectRoot, '.carto', 'hashes.json');
9
+ }
10
+
11
+ function loadHashes(projectRoot) {
12
+ try {
13
+ const raw = fs.readFileSync(getHashPath(projectRoot), 'utf-8');
14
+ return JSON.parse(raw);
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ function saveHashes(projectRoot, hashes) {
21
+ const hashPath = getHashPath(projectRoot);
22
+ const tmp = hashPath + '.tmp';
23
+ try {
24
+ fs.writeFileSync(tmp, JSON.stringify(hashes, null, 2), 'utf-8');
25
+ fs.renameSync(tmp, hashPath);
26
+ } catch {}
27
+ }
28
+
29
+ function hashContent(content) {
30
+ return crypto.createHash('sha1').update(content).digest('hex');
31
+ }
32
+
33
+ /**
34
+ * computeChangedFiles(filePaths, storedHashes, projectRoot)
35
+ * Returns { changed: string[], unchanged: string[], hashes: object }
36
+ * changed = files whose content hash differs from stored
37
+ * unchanged = files whose hash matches — can skip re-parsing
38
+ */
39
+ function computeChangedFiles(filePaths, storedHashes, projectRoot) {
40
+ const changed = [];
41
+ const unchanged = [];
42
+ const newHashes = { ...storedHashes };
43
+
44
+ for (const filePath of filePaths) {
45
+ const relPath = path.relative(projectRoot, filePath);
46
+ let content;
47
+ try {
48
+ content = fs.readFileSync(filePath, 'utf-8');
49
+ } catch {
50
+ continue;
51
+ }
52
+ const hash = hashContent(content);
53
+ if (storedHashes[relPath] === hash) {
54
+ unchanged.push(filePath);
55
+ } else {
56
+ changed.push(filePath);
57
+ newHashes[relPath] = hash;
58
+ }
59
+ }
60
+
61
+ return { changed, unchanged, hashes: newHashes };
62
+ }
63
+
64
+ /**
65
+ * updateFileHash(projectRoot, relPath, content)
66
+ * Updates the hash for a single file after incremental re-index.
67
+ */
68
+ function updateFileHash(projectRoot, relPath, content) {
69
+ const hashes = loadHashes(projectRoot);
70
+ hashes[relPath] = hashContent(content);
71
+ saveHashes(projectRoot, hashes);
72
+ }
73
+
74
+ /**
75
+ * removeFileHash(projectRoot, relPath)
76
+ * Removes hash entry when a file is deleted.
77
+ */
78
+ function removeFileHash(projectRoot, relPath) {
79
+ const hashes = loadHashes(projectRoot);
80
+ delete hashes[relPath];
81
+ saveHashes(projectRoot, hashes);
82
+ }
83
+
84
+ module.exports = { loadHashes, saveHashes, hashContent, computeChangedFiles, updateFileHash, removeFileHash };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function getCachePath(projectRoot) {
7
+ return path.join(projectRoot, '.carto', 'graph-cache.json');
8
+ }
9
+
10
+ /**
11
+ * loadGraphCache(projectRoot)
12
+ * Returns the full persisted graph or null if not found / corrupt.
13
+ *
14
+ * Cache shape:
15
+ * {
16
+ * version: '2',
17
+ * generated: ISO string,
18
+ * fileData: {
19
+ * [relPath]: { routes, models, functions, envVars, dbTables, fetches, storageKeys, imports }
20
+ * },
21
+ * importGraph: { [relPath]: [relPath, ...] },
22
+ * routesByFile: { [relPath]: ['METHOD /path', ...] },
23
+ * domains: { [domain]: { files, routes, models, functions, envVars, dbTables } },
24
+ * highImpact: [{ file, dependents }],
25
+ * entryPoints: [relPath, ...],
26
+ * stack: [...],
27
+ * meta: { totalFiles, totalRoutes, totalImportEdges, lastIndexed, indexDuration }
28
+ * }
29
+ */
30
+ function loadGraphCache(projectRoot) {
31
+ try {
32
+ const raw = fs.readFileSync(getCachePath(projectRoot), 'utf-8');
33
+ const parsed = JSON.parse(raw);
34
+ if (parsed.version !== '2') return null;
35
+ return parsed;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function saveGraphCache(projectRoot, cache) {
42
+ const cachePath = getCachePath(projectRoot);
43
+ const tmp = cachePath + '.tmp';
44
+ try {
45
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
46
+ fs.writeFileSync(tmp, JSON.stringify(cache, null, 2) + '\n', 'utf-8');
47
+ fs.renameSync(tmp, cachePath);
48
+ } catch (err) {
49
+ console.warn(`[CARTO] Warning: Could not save graph cache — ${err.message}`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * buildEmptyCache() — starting point for a fresh index
55
+ */
56
+ function buildEmptyCache() {
57
+ return {
58
+ version: '2',
59
+ generated: new Date().toISOString(),
60
+ fileData: {},
61
+ importGraph: {},
62
+ routesByFile: {},
63
+ domains: {},
64
+ highImpact: [],
65
+ entryPoints: [],
66
+ stack: [],
67
+ meta: {
68
+ totalFiles: 0,
69
+ totalRoutes: 0,
70
+ totalImportEdges: 0,
71
+ lastIndexed: null,
72
+ indexDuration: 0
73
+ }
74
+ };
75
+ }
76
+
77
+ module.exports = { loadGraphCache, saveGraphCache, buildEmptyCache, getCachePath };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+ const { Carto } = require('../../index.js');
6
+
7
+ async function run(projectRoot) {
8
+ const carto = new Carto();
9
+
10
+ try {
11
+ await carto.index(projectRoot, { useWorkers: false });
12
+ } catch (err) {
13
+ console.error(`[CARTO] Error loading index: ${err.message}`);
14
+ process.exit(1);
15
+ }
16
+
17
+ const meta = carto.getMeta();
18
+ const domains = carto.getDomainsList();
19
+ const crossDomain = carto.getCrossDomainDeps();
20
+ const highImpact = carto.getHighImpactFiles(20);
21
+
22
+ let hasIssues = false;
23
+
24
+ console.log('\n── Carto Check ─────────────────────────────────────────\n');
25
+
26
+ // ── Summary ──────────────────────────────────────────────────────────────
27
+ console.log(` Files indexed : ${meta.totalFiles}`);
28
+ console.log(` Routes found : ${meta.totalRoutes}`);
29
+ console.log(` Import edges : ${meta.totalImportEdges}`);
30
+ console.log(` Domains : ${domains.map(d => d.name).join(', ') || 'none'}`);
31
+ if (meta.lastIndexed) {
32
+ const age = Math.round((Date.now() - new Date(meta.lastIndexed).getTime()) / 1000);
33
+ const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
34
+ console.log(` Last indexed : ${ageStr}`);
35
+ }
36
+ console.log('');
37
+
38
+ // ── Uncommitted changes that touch high-blast-radius files ───────────────
39
+ const modifiedFiles = getModifiedFiles(projectRoot);
40
+ if (modifiedFiles.length > 0) {
41
+ const highImpactSet = new Set(highImpact.map(f => f.file));
42
+ const riskyChanges = modifiedFiles.filter(f => highImpactSet.has(f));
43
+
44
+ if (riskyChanges.length > 0) {
45
+ hasIssues = true;
46
+ console.log(` ⚠️ High-risk uncommitted changes (${riskyChanges.length}):`);
47
+ for (const f of riskyChanges) {
48
+ const hi = highImpact.find(h => h.file === f);
49
+ const br = carto.getBlastRadius(f);
50
+ console.log(` 🔴 ${f}`);
51
+ console.log(` ${hi.dependents} files depend on this — blast risk: ${br ? br.risk : 'UNKNOWN'}`);
52
+ }
53
+ console.log('');
54
+ } else {
55
+ console.log(` ✅ ${modifiedFiles.length} modified file(s) — none are high-impact\n`);
56
+ }
57
+ }
58
+
59
+ // ── Cross-domain dependency violations ───────────────────────────────────
60
+ if (crossDomain.length > 0) {
61
+ hasIssues = true;
62
+ console.log(` ⚠️ Cross-domain dependencies (${crossDomain.length}):`);
63
+ console.log(' These files import across domain boundaries.\n');
64
+
65
+ // Group by fromDomain → toDomain pair
66
+ const grouped = {};
67
+ for (const d of crossDomain) {
68
+ const key = `${d.fromDomain} → ${d.toDomain}`;
69
+ if (!grouped[key]) grouped[key] = [];
70
+ grouped[key].push(d);
71
+ }
72
+
73
+ for (const [pair, deps] of Object.entries(grouped)) {
74
+ console.log(` ${pair} (${deps.length})`);
75
+ for (const d of deps.slice(0, 3)) {
76
+ console.log(` ${path.basename(d.from)} imports ${path.basename(d.to)}`);
77
+ }
78
+ if (deps.length > 3) console.log(` ...and ${deps.length - 3} more`);
79
+ }
80
+ console.log('');
81
+ } else if (domains.length > 1) {
82
+ console.log(' ✅ No cross-domain dependency violations\n');
83
+ }
84
+
85
+ // ── Top high-impact files ────────────────────────────────────────────────
86
+ if (highImpact.length > 0) {
87
+ console.log(` 🔥 Top high-impact files (changing these = highest blast radius):`);
88
+ for (const f of highImpact.slice(0, 5)) {
89
+ console.log(` ${f.dependents.toString().padStart(3)} dependents — ${f.file}`);
90
+ }
91
+ console.log('');
92
+ }
93
+
94
+ // ── Domain breakdown ─────────────────────────────────────────────────────
95
+ if (domains.length > 0) {
96
+ console.log(' Domains:');
97
+ for (const d of domains) {
98
+ console.log(` ${d.name.padEnd(16)} ${d.fileCount} files ${d.routeCount} routes ${d.modelCount} models`);
99
+ }
100
+ console.log('');
101
+ }
102
+
103
+ console.log('─────────────────────────────────────────────────────────\n');
104
+ console.log(hasIssues ? ' ⚠️ Issues found above.' : ' ✅ All clear.');
105
+ console.log('');
106
+
107
+ carto.terminate();
108
+ }
109
+
110
+ function getModifiedFiles(projectRoot) {
111
+ try {
112
+ const output = execSync('git diff --name-only HEAD 2>/dev/null && git diff --name-only 2>/dev/null', {
113
+ cwd: projectRoot,
114
+ encoding: 'utf-8',
115
+ stdio: ['pipe', 'pipe', 'pipe']
116
+ });
117
+ const files = [...new Set(output.trim().split('\n').filter(Boolean))];
118
+ return files;
119
+ } catch {
120
+ return [];
121
+ }
122
+ }
123
+
124
+ module.exports = { run };
package/src/cli/impact.js CHANGED
@@ -1,167 +1,59 @@
1
- const fs = require('fs');
1
+ 'use strict';
2
+
2
3
  const path = require('path');
4
+ const { Carto } = require('../../index.js');
3
5
 
4
- function run(projectRoot, fileArg) {
6
+ async function run(projectRoot, fileArg) {
5
7
  if (!fileArg) {
6
8
  console.error('[CARTO] Usage: carto impact <file>');
7
9
  process.exit(1);
8
10
  }
9
11
 
10
- const mapPath = path.join(projectRoot, '.carto', 'map.json');
11
- if (!fs.existsSync(mapPath)) {
12
- console.error('[CARTO] Run "carto init" first.');
13
- process.exit(1);
14
- }
15
-
16
- let map;
12
+ const carto = new Carto();
17
13
  try {
18
- map = JSON.parse(fs.readFileSync(mapPath, 'utf-8'));
14
+ await carto.index(projectRoot, { useWorkers: false });
19
15
  } catch (err) {
20
- console.error(`[CARTO] Error reading .carto/map.json: ${err.message}`);
16
+ console.error(`[CARTO] Error loading index: ${err.message}`);
21
17
  process.exit(1);
22
18
  }
23
19
 
24
- const imports = map.imports || {};
25
- const routes = map.routes || [];
26
-
27
- // Resolve file argument — match by basename or partial path
28
- const matchedFile = resolveFile(fileArg, imports);
29
- if (!matchedFile) {
20
+ const br = carto.getBlastRadius(fileArg);
21
+ if (!br) {
30
22
  console.error(`[CARTO] File not found in project graph: ${fileArg}`);
31
23
  process.exit(1);
32
24
  }
33
25
 
34
- // Reverse lookup: which files import this file
35
- const importedBy = [];
36
- for (const [file, deps] of Object.entries(imports)) {
37
- if (deps.includes(matchedFile)) {
38
- importedBy.push(file);
39
- }
40
- }
41
- importedBy.sort();
26
+ console.log(`\nImpact analysis: ${br.file}\n`);
42
27
 
43
- // Collect all files that depend on matchedFile (up to 3 hops)
44
- const dependentFiles = collectDependents(matchedFile, imports, 3);
45
- // Also include the file itself — routes in the target file are affected
46
- dependentFiles.add(matchedFile);
28
+ const riskBadge = { HIGH: '🔴 HIGH', MEDIUM: '🟡 MEDIUM', LOW: '🟢 LOW', SAFE: '✅ SAFE' };
29
+ console.log(`Risk: ${riskBadge[br.risk] || br.risk}`);
30
+ console.log(`Directly affected: ${br.directlyAffected.files} files across ${br.directlyAffected.domains} domain(s)`);
31
+ console.log(`Potentially affected: ${br.potentiallyAffected.files} files total\n`);
47
32
 
48
- // Print output
49
- console.log(`\nImpact analysis: ${matchedFile}\n`);
50
-
51
- console.log('Imported by:');
52
- if (importedBy.length > 0) {
53
- for (const f of importedBy) {
54
- console.log(` → ${f}`);
55
- }
56
- } else {
57
- console.log(' (none — this file is not imported by any other file)');
58
- }
59
-
60
- console.log('\nRoutes affected:');
61
- // Use routesByFile for precise file→route mapping
62
- const affectedRoutes = new Set();
63
- for (const affectedFile of dependentFiles) {
64
- const fileRoutes = map.routesByFile && map.routesByFile[affectedFile];
65
- if (fileRoutes) {
66
- for (const r of fileRoutes) affectedRoutes.add(r);
67
- }
68
- }
69
- // Also check the target file itself
70
- if (map.routesByFile && map.routesByFile[matchedFile]) {
71
- for (const r of map.routesByFile[matchedFile]) affectedRoutes.add(r);
72
- }
73
-
74
- // Fall back to all routes only if no file-specific routes found and an entry point is hit
75
- if (affectedRoutes.size === 0) {
76
- const entryPointsInChain = map.entryPoints
77
- ? map.entryPoints.filter(ep => dependentFiles.has(ep))
78
- : [];
79
- if (entryPointsInChain.length > 0) {
80
- for (const route of routes) {
81
- affectedRoutes.add(`${route.method} ${route.path}`);
82
- }
83
- }
33
+ if (br.domainsImpacted.length > 0) {
34
+ console.log(`Domains impacted: ${br.domainsImpacted.join(', ')}\n`);
84
35
  }
85
36
 
86
- if (affectedRoutes.size > 0) {
87
- for (const r of [...affectedRoutes].sort()) {
88
- console.log(` → ${r}`);
89
- }
37
+ if (br.dependentFiles.length > 0) {
38
+ console.log(`Files that depend on this (${br.dependentFiles.length}):`);
39
+ for (const f of br.dependentFiles) console.log(` → ${f}`);
40
+ console.log('');
90
41
  } else {
91
- console.log(' (none — no route-serving files in the dependency chain)');
92
- }
93
-
94
- // Risk level
95
- const depCount = importedBy.length;
96
- let risk;
97
- if (depCount >= 3) risk = 'HIGH';
98
- else if (depCount === 2) risk = 'MEDIUM';
99
- else if (depCount === 1) risk = 'LOW';
100
- else risk = 'SAFE';
101
-
102
- console.log(`\nRisk: ${risk} — ${depCount} file${depCount !== 1 ? 's' : ''} depend on this\n`);
103
- }
104
-
105
- /**
106
- * Resolve a file argument to a full relative path in the import graph.
107
- * Matches by basename or partial path suffix.
108
- */
109
- function resolveFile(fileArg, imports) {
110
- const allFiles = new Set();
111
- for (const [file, deps] of Object.entries(imports)) {
112
- allFiles.add(file);
113
- for (const dep of deps) allFiles.add(dep);
114
- }
115
-
116
- const normalized = fileArg.replace(/\\/g, '/');
117
- const hasPathSeparator = normalized.includes('/');
118
-
119
- // Exact match
120
- if (allFiles.has(normalized)) return normalized;
121
-
122
- // Match by suffix (partial path)
123
- const matches = [...allFiles].filter(f => f.endsWith('/' + normalized) || f === normalized);
124
- if (matches.length === 1) return matches[0];
125
-
126
- // If input was a path (contains /), don't fall back to basename — it's ambiguous
127
- if (hasPathSeparator) {
128
- if (matches.length > 1) return null;
129
- return null;
42
+ console.log('No files depend on this.\n');
130
43
  }
131
44
 
132
- // Input is just a filename — fall back to basename matching
133
- const byBasename = [...allFiles].filter(f => path.basename(f) === path.basename(normalized));
134
- if (byBasename.length === 1) return byBasename[0];
135
-
136
- // Multiple basename matches — ambiguous, don't guess
137
- return null;
138
- }
139
-
140
- /**
141
- * Collect all files that transitively depend on the target file (reverse BFS).
142
- * maxHops limits the depth of the search.
143
- */
144
- function collectDependents(targetFile, imports, maxHops) {
145
- const dependents = new Set();
146
- let frontier = new Set([targetFile]);
147
-
148
- for (let hop = 0; hop < maxHops; hop++) {
149
- const nextFrontier = new Set();
150
- for (const [file, deps] of Object.entries(imports)) {
151
- if (dependents.has(file)) continue;
152
- for (const dep of deps) {
153
- if (frontier.has(dep)) {
154
- dependents.add(file);
155
- nextFrontier.add(file);
156
- break;
157
- }
158
- }
45
+ if (br.routesImpacted.length > 0) {
46
+ console.log(`Routes at risk (${br.routesImpacted.length}):`);
47
+ for (const r of br.routesImpacted) {
48
+ const badge = r.risk === 'HIGH' ? '🔴' : r.risk === 'MEDIUM' ? '🟡' : '🟢';
49
+ console.log(` ${badge} ${r.method} ${r.path}`);
159
50
  }
160
- if (nextFrontier.size === 0) break;
161
- frontier = nextFrontier;
51
+ } else {
52
+ console.log('No routes directly traceable.');
162
53
  }
163
54
 
164
- return dependents;
55
+ console.log('');
56
+ carto.terminate();
165
57
  }
166
58
 
167
59
  module.exports = { run };
package/src/cli/index.js CHANGED
@@ -12,6 +12,7 @@ Commands:
12
12
  watch Read .carto/config.json, start file watcher
13
13
  sync Read .carto/config.json, run one sync, exit
14
14
  impact <file> Show which files and routes are affected by changing a file
15
+ check Report cross-domain deps, high-risk uncommitted changes, domain health
15
16
  remove Remove AGENTS.md and .carto/ from this project
16
17
  serve Start MCP server for AI tool integration
17
18
 
@@ -48,6 +49,11 @@ if (command === 'init') {
48
49
  } else if (command === 'impact') {
49
50
  const fileArg = process.argv[3];
50
51
  require('./impact').run(process.cwd(), fileArg);
52
+ } else if (command === 'check') {
53
+ require('./check').run(process.cwd()).catch(err => {
54
+ console.error(`[CARTO] Fatal error: ${err.message}`);
55
+ process.exit(1);
56
+ });
51
57
  } else if (command === 'remove') {
52
58
  require('./remove').run(process.cwd());
53
59
  } else if (command === 'serve') {