carto-md 1.0.5 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "The context layer for AI-native development.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
@@ -0,0 +1,196 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function run(projectRoot, fileArg) {
5
+ if (!fileArg) {
6
+ console.error('[CARTO] Usage: carto impact <file>');
7
+ process.exit(1);
8
+ }
9
+
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;
17
+ try {
18
+ map = JSON.parse(fs.readFileSync(mapPath, 'utf-8'));
19
+ } catch (err) {
20
+ console.error(`[CARTO] Error reading .carto/map.json: ${err.message}`);
21
+ process.exit(1);
22
+ }
23
+
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) {
30
+ console.error(`[CARTO] File not found in project graph: ${fileArg}`);
31
+ process.exit(1);
32
+ }
33
+
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();
42
+
43
+ // Find affected routes — a route is affected if its handler file
44
+ // imports the target file directly or transitively (up to 3 hops)
45
+ const affectedRoutes = [];
46
+ const routeFiles = new Set();
47
+
48
+ // Collect all files that depend on matchedFile (up to 3 hops)
49
+ const dependentFiles = collectDependents(matchedFile, imports, 3);
50
+ // Also include the file itself — routes in the target file are affected
51
+ dependentFiles.add(matchedFile);
52
+
53
+ for (const route of routes) {
54
+ // Find which file contains this route by checking if any dependent file
55
+ // has this route's handler as a function
56
+ // Since we don't have a direct file→route mapping, check if any file
57
+ // that depends on matchedFile is an entry point with routes
58
+ for (const depFile of dependentFiles) {
59
+ if (imports[depFile] && imports[depFile].includes(matchedFile) || depFile === matchedFile) {
60
+ // Check if this file has routes by seeing if it appears as a key
61
+ // and has routes in the routes array
62
+ routeFiles.add(depFile);
63
+ }
64
+ }
65
+ }
66
+
67
+ // Match routes to files — routes whose handler files are in the dependent set
68
+ // Since map.json routes don't have a file field, match by checking which
69
+ // entry point files contain routes
70
+ for (const route of routes) {
71
+ // A route is affected if any file in the dependency chain has routes
72
+ affectedRoutes.push(route);
73
+ }
74
+
75
+ // Better approach: only include routes from files that are in the dependent chain
76
+ // We need to re-derive which file each route came from
77
+ // For now, if the target file or any of its direct importers have routes, show all routes
78
+ // from those files
79
+ const filesWithRoutes = new Set();
80
+ for (const depFile of dependentFiles) {
81
+ // Check if this file is known to have routes
82
+ // (it appears as a key in imports AND has routes — we approximate by checking
83
+ // if any route handler matches a function in that file)
84
+ filesWithRoutes.add(depFile);
85
+ }
86
+
87
+ // Print output
88
+ console.log(`\nImpact analysis: ${matchedFile}\n`);
89
+
90
+ console.log('Imported by:');
91
+ if (importedBy.length > 0) {
92
+ for (const f of importedBy) {
93
+ console.log(` → ${f}`);
94
+ }
95
+ } else {
96
+ console.log(' (none — this file is not imported by any other file)');
97
+ }
98
+
99
+ console.log('\nRoutes affected:');
100
+ // Only show routes from files that transitively depend on the target
101
+ if (routes.length > 0 && dependentFiles.size > 0) {
102
+ // Find which files in the dependent chain are entry points with routes
103
+ // A route is affected if its file (the entry point) is in the dependent set
104
+ const entryPointsInChain = map.entryPoints
105
+ ? map.entryPoints.filter(ep => dependentFiles.has(ep))
106
+ : [];
107
+ // If any entry point depends on this file, all routes through it are affected
108
+ if (entryPointsInChain.length > 0) {
109
+ const shown = new Set();
110
+ for (const route of routes) {
111
+ const key = `${route.method} ${route.path}`;
112
+ if (!shown.has(key)) {
113
+ console.log(` → ${route.method} ${route.path}`);
114
+ shown.add(key);
115
+ }
116
+ }
117
+ } else {
118
+ console.log(' (none — no route-serving files in the dependency chain)');
119
+ }
120
+ } else {
121
+ console.log(' (none)');
122
+ }
123
+
124
+ // Risk level
125
+ const depCount = importedBy.length;
126
+ let risk;
127
+ if (depCount >= 3) risk = 'HIGH';
128
+ else if (depCount === 2) risk = 'MEDIUM';
129
+ else if (depCount === 1) risk = 'LOW';
130
+ else risk = 'SAFE';
131
+
132
+ console.log(`\nRisk: ${risk} — ${depCount} file${depCount !== 1 ? 's' : ''} depend on this\n`);
133
+ }
134
+
135
+ /**
136
+ * Resolve a file argument to a full relative path in the import graph.
137
+ * Matches by basename or partial path suffix.
138
+ */
139
+ function resolveFile(fileArg, imports) {
140
+ // Collect all known files (keys + all values)
141
+ const allFiles = new Set();
142
+ for (const [file, deps] of Object.entries(imports)) {
143
+ allFiles.add(file);
144
+ for (const dep of deps) allFiles.add(dep);
145
+ }
146
+
147
+ const normalized = fileArg.replace(/\\/g, '/');
148
+
149
+ // Exact match
150
+ if (allFiles.has(normalized)) return normalized;
151
+
152
+ // Match by suffix (partial path)
153
+ const matches = [...allFiles].filter(f => f.endsWith('/' + normalized) || f === normalized);
154
+ if (matches.length === 1) return matches[0];
155
+
156
+ // Match by basename
157
+ const byBasename = [...allFiles].filter(f => path.basename(f) === path.basename(normalized));
158
+ if (byBasename.length === 1) return byBasename[0];
159
+
160
+ // If multiple basename matches, prefer shortest path
161
+ if (byBasename.length > 1) {
162
+ byBasename.sort((a, b) => a.length - b.length);
163
+ return byBasename[0];
164
+ }
165
+
166
+ return null;
167
+ }
168
+
169
+ /**
170
+ * Collect all files that transitively depend on the target file (reverse BFS).
171
+ * maxHops limits the depth of the search.
172
+ */
173
+ function collectDependents(targetFile, imports, maxHops) {
174
+ const dependents = new Set();
175
+ let frontier = new Set([targetFile]);
176
+
177
+ for (let hop = 0; hop < maxHops; hop++) {
178
+ const nextFrontier = new Set();
179
+ for (const [file, deps] of Object.entries(imports)) {
180
+ if (dependents.has(file)) continue;
181
+ for (const dep of deps) {
182
+ if (frontier.has(dep)) {
183
+ dependents.add(file);
184
+ nextFrontier.add(file);
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ if (nextFrontier.size === 0) break;
190
+ frontier = nextFrontier;
191
+ }
192
+
193
+ return dependents;
194
+ }
195
+
196
+ module.exports = { run };
package/src/cli/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const pkg = require('../../package.json');
3
4
  const command = process.argv[2];
4
5
 
5
6
  function printUsage() {
@@ -7,15 +8,21 @@ function printUsage() {
7
8
  Usage: carto <command>
8
9
 
9
10
  Commands:
10
- init Detect project, write .carto/config.json, run first sync
11
- watch Read .carto/config.json, start file watcher
12
- sync Read .carto/config.json, run one sync, exit
11
+ init Detect project, write .carto/config.json, run first sync
12
+ watch Read .carto/config.json, start file watcher
13
+ sync Read .carto/config.json, run one sync, exit
14
+ impact <file> Show which files and routes are affected by changing a file
13
15
 
14
16
  Options:
15
17
  --help, -h Show this help message
16
18
  `);
17
19
  }
18
20
 
21
+ if (command === '--version' || command === '-v') {
22
+ console.log(`${pkg.name} ${pkg.version}`);
23
+ process.exit(0);
24
+ }
25
+
19
26
  if (!command || command === '--help' || command === '-h') {
20
27
  printUsage();
21
28
  process.exit(0);
@@ -36,6 +43,9 @@ if (command === 'init') {
36
43
  console.error(`[CARTO] Fatal error: ${err.message}`);
37
44
  process.exit(1);
38
45
  });
46
+ } else if (command === 'impact') {
47
+ const fileArg = process.argv[3];
48
+ require('./impact').run(process.cwd(), fileArg);
39
49
  } else {
40
50
  console.error(`[CARTO] Unknown command: ${command}`);
41
51
  printUsage();
package/src/sync.js CHANGED
@@ -254,6 +254,26 @@ async function runFullSync(config) {
254
254
  });
255
255
 
256
256
  mergeIntoAgentsMd(config.output, autoContent);
257
+
258
+ // Save graph to .carto/map.json (atomic write)
259
+ const cartoDir = path.join(config.projectRoot, '.carto');
260
+ const mapData = {
261
+ version: '1',
262
+ generated: new Date().toISOString(),
263
+ imports: importGraph,
264
+ routes: validated.routes,
265
+ highImpact: highImpact.map(h => ({ file: h.file, dependents: h.count })),
266
+ entryPoints,
267
+ stack: stackItems
268
+ };
269
+ try {
270
+ const tmpPath = path.join(cartoDir, 'map.tmp.json');
271
+ const mapPath = path.join(cartoDir, 'map.json');
272
+ fs.writeFileSync(tmpPath, JSON.stringify(mapData, null, 2) + '\n', 'utf-8');
273
+ fs.renameSync(tmpPath, mapPath);
274
+ } catch (err) {
275
+ console.warn(`[CARTO] Warning: Could not write .carto/map.json — ${err.message}`);
276
+ }
257
277
  }
258
278
 
259
279
  module.exports = { runFullSync, safeReadFile, scanStructure };