carto-md 1.0.1 → 1.0.3

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.1",
3
+ "version": "1.0.3",
4
4
  "description": "The context layer for AI-native development.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
@@ -9,10 +9,11 @@
9
9
  * 5. Functions (auto)
10
10
  * 6. Database Tables (auto)
11
11
  * 7. Environment Variables (auto)
12
- * 8. Frontend API Calls (auto)
13
- * 9. Frontend Storage Keys (auto)
12
+ * 8. File Relationships (auto)
13
+ * 9. Frontend API Calls (auto)
14
+ * 10. Frontend Storage Keys (auto)
14
15
  */
15
- function formatSections({ routes, models, frontend, structure, warnings, fileMap, functions, dbTables, envVars }) {
16
+ function formatSections({ routes, models, frontend, structure, warnings, fileMap, functions, dbTables, envVars, importGraph }) {
16
17
  const sections = [];
17
18
 
18
19
  // 1. Project Structure
@@ -114,7 +115,21 @@ function formatSections({ routes, models, frontend, structure, warnings, fileMap
114
115
  sections.push('_No environment variables detected._');
115
116
  }
116
117
 
117
- // 8. Frontend API Calls
118
+ // 8. File Relationships
119
+ sections.push('\n## File Relationships (auto)\n');
120
+ if (importGraph && Object.keys(importGraph).length > 0) {
121
+ const sortedFiles = Object.keys(importGraph).sort();
122
+ for (const file of sortedFiles) {
123
+ const deps = importGraph[file];
124
+ if (deps.length > 0) {
125
+ sections.push(`${file} \u2192 ${deps.join(', ')}`);
126
+ }
127
+ }
128
+ } else {
129
+ sections.push('_No file relationships detected._');
130
+ }
131
+
132
+ // 9. Frontend API Calls
118
133
  sections.push('\n## Frontend API Calls (auto)\n');
119
134
  if (frontend.fetches.length > 0) {
120
135
  sections.push('| Method | URL |');
@@ -126,7 +141,7 @@ function formatSections({ routes, models, frontend, structure, warnings, fileMap
126
141
  sections.push('_No fetch calls found._');
127
142
  }
128
143
 
129
- // 9. Frontend Storage Keys
144
+ // 10. Frontend Storage Keys
130
145
  sections.push('\n## Frontend Storage Keys (auto)\n');
131
146
  if (frontend.storageKeys.length > 0) {
132
147
  sections.push('| Operation | Key |');
@@ -0,0 +1,88 @@
1
+ /**
2
+ * validateExtracted(data) → cleaned data
3
+ *
4
+ * Runs after extraction, before formatting.
5
+ * Drops anything that looks wrong. Never throws.
6
+ */
7
+ function validateExtracted({ routes, models, functions, envVars, dbTables }) {
8
+ return {
9
+ routes: validateRoutes(routes || []),
10
+ models: validateModels(models || []),
11
+ functions: validateFunctions(functions || {}),
12
+ envVars: validateEnvVars(envVars || []),
13
+ dbTables: validateDBTables(dbTables || [])
14
+ };
15
+ }
16
+
17
+ const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']);
18
+ const VALID_HANDLER = /^[a-zA-Z_]\w*$|^\[anonymous\]$/;
19
+ const VALID_CLASS_NAME = /^[A-Z][a-zA-Z0-9]*$/;
20
+ const VALID_FIELD_NAME = /^[a-z_]\w*$/;
21
+ const VALID_FUNC_NAME = /^_?[a-zA-Z][a-zA-Z0-9_]*$/;
22
+ const VALID_ENV_VAR = /^[A-Z][A-Z0-9_]*$/;
23
+ const VALID_TABLE_NAME = /^[a-z][a-z0-9_]*$/;
24
+
25
+ function validateRoutes(routes) {
26
+ return routes.filter(r => {
27
+ if (!r.method || !VALID_METHODS.has(r.method)) return false;
28
+ if (!r.path || (!r.path.startsWith('/') && r.path !== '[dynamic]' && r.path !== '[inferred]')) return false;
29
+ if (!r.functionName || !VALID_HANDLER.test(r.functionName)) return false;
30
+ return true;
31
+ });
32
+ }
33
+
34
+ function validateModels(models) {
35
+ return models
36
+ .map(m => {
37
+ if (!m.className || !VALID_CLASS_NAME.test(m.className)) return null;
38
+ if (!Array.isArray(m.fields)) return null;
39
+
40
+ const validFields = m.fields.filter(f => {
41
+ if (!f.name || !VALID_FIELD_NAME.test(f.name)) return false;
42
+ return true;
43
+ });
44
+
45
+ if (validFields.length === 0) return null;
46
+ return { className: m.className, fields: validFields };
47
+ })
48
+ .filter(Boolean);
49
+ }
50
+
51
+ function validateFunctions(functionsMap) {
52
+ const cleaned = {};
53
+
54
+ for (const [filename, funcs] of Object.entries(functionsMap)) {
55
+ const validFuncs = funcs.filter(f => {
56
+ // Name must be a valid identifier
57
+ if (!f.name || !VALID_FUNC_NAME.test(f.name)) return false;
58
+ // Name must not contain spaces, brackets, commas
59
+ if (/[\s\[\],]/.test(f.name)) return false;
60
+ // Params must not contain parse artifacts
61
+ if (f.params && (/\[\[/.test(f.params) || /\]\]/.test(f.params) || /Any\]\]/.test(f.params))) return false;
62
+ return true;
63
+ });
64
+
65
+ if (validFuncs.length > 0) {
66
+ cleaned[filename] = validFuncs;
67
+ }
68
+ }
69
+
70
+ return cleaned;
71
+ }
72
+
73
+ function validateEnvVars(envVars) {
74
+ return envVars.filter(v => {
75
+ if (!v.name || !VALID_ENV_VAR.test(v.name)) return false;
76
+ return true;
77
+ });
78
+ }
79
+
80
+ function validateDBTables(dbTables) {
81
+ return dbTables.filter(t => {
82
+ if (!t.tableName || !VALID_TABLE_NAME.test(t.tableName)) return false;
83
+ if (!t.modelName || !VALID_CLASS_NAME.test(t.modelName)) return false;
84
+ return true;
85
+ });
86
+ }
87
+
88
+ module.exports = { validateExtracted };
@@ -42,7 +42,7 @@ function extractFrontend(content) {
42
42
  const dynamicCount = fetches.filter(f => f.url === '[dynamic]').length;
43
43
  if (dynamicCount > 0) {
44
44
  staticFetches.push({
45
- url: `[dynamic \u00d7${dynamicCount}]`,
45
+ url: `dynamic calls detected (${dynamicCount} unresolved)`,
46
46
  method: '\u2014'
47
47
  });
48
48
  }
@@ -0,0 +1,212 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ /**
5
+ * extractImports(content, filePath, projectRoot) → Array<string>
6
+ *
7
+ * Extracts relative import paths from a source file.
8
+ * Returns resolved relative paths (from project root) of local dependencies.
9
+ *
10
+ * JS/TS patterns:
11
+ * import X from './Y'
12
+ * import { X } from './Y'
13
+ * import './Y'
14
+ * const X = require('./Y')
15
+ * require('./Y')
16
+ *
17
+ * Python patterns:
18
+ * from .module import X (relative)
19
+ * from ..module import X (relative)
20
+ * from app.module import X (local package — resolved if file exists)
21
+ * import .module (relative)
22
+ *
23
+ * Only includes paths that resolve to actual files in the project.
24
+ * Skips: node_modules, non-code files, anything that doesn't resolve.
25
+ */
26
+ function extractImports(content, filePath, projectRoot) {
27
+ const ext = path.extname(filePath).toLowerCase();
28
+ const fileDir = path.dirname(filePath);
29
+
30
+ let rawImports = [];
31
+
32
+ if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) {
33
+ rawImports = extractJSImports(content);
34
+ } else if (ext === '.py') {
35
+ rawImports = extractPythonImports(content, filePath, projectRoot);
36
+ }
37
+
38
+ // Resolve and deduplicate
39
+ const resolved = new Set();
40
+
41
+ for (const imp of rawImports) {
42
+ const resolvedPath = resolveImportPath(imp, fileDir, projectRoot, ext);
43
+ if (resolvedPath) {
44
+ // Store as relative to project root
45
+ const rel = path.relative(projectRoot, resolvedPath);
46
+ resolved.add(rel);
47
+ }
48
+ }
49
+
50
+ return [...resolved].sort();
51
+ }
52
+
53
+ /**
54
+ * Extract import paths from JS/TS content. Relative paths only.
55
+ */
56
+ function extractJSImports(content) {
57
+ const imports = [];
58
+
59
+ // import ... from './path' or import './path'
60
+ const importPattern = /import\s+(?:[\s\S]*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
61
+ let match;
62
+ while ((match = importPattern.exec(content)) !== null) {
63
+ imports.push(match[1]);
64
+ }
65
+
66
+ // require('./path')
67
+ const requirePattern = /require\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
68
+ while ((match = requirePattern.exec(content)) !== null) {
69
+ imports.push(match[1]);
70
+ }
71
+
72
+ return imports;
73
+ }
74
+
75
+ /**
76
+ * Extract import paths from Python content. Relative imports only.
77
+ */
78
+ function extractPythonImports(content, filePath, projectRoot) {
79
+ const imports = [];
80
+ const fileDir = path.dirname(filePath);
81
+
82
+ // from .module import X or from ..module import X
83
+ const fromRelPattern = /^from\s+(\.+\w*(?:\.\w+)*)\s+import/gm;
84
+ let match;
85
+ while ((match = fromRelPattern.exec(content)) !== null) {
86
+ const resolved = resolvePythonRelativeImport(match[1], fileDir);
87
+ if (resolved) imports.push(resolved);
88
+ }
89
+
90
+ // from app.module import X — try to resolve as local file
91
+ const fromAbsPattern = /^from\s+(\w+(?:\.\w+)+)\s+import/gm;
92
+ while ((match = fromAbsPattern.exec(content)) !== null) {
93
+ const modulePath = match[1].replace(/\./g, path.sep);
94
+ // Try from project root
95
+ const resolved = tryResolvePythonModule(modulePath, projectRoot);
96
+ if (resolved) {
97
+ imports.push(resolved);
98
+ continue;
99
+ }
100
+ // Try from file's directory (for cases like `from app.models` when inside aws-risk-agent/)
101
+ const fromFileDir = tryResolvePythonModule(modulePath, fileDir);
102
+ if (fromFileDir) {
103
+ imports.push(fromFileDir);
104
+ continue;
105
+ }
106
+ // Try from parent directories up to project root
107
+ let searchDir = path.dirname(fileDir);
108
+ while (searchDir.startsWith(projectRoot) && searchDir !== projectRoot) {
109
+ const fromParent = tryResolvePythonModule(modulePath, searchDir);
110
+ if (fromParent) {
111
+ imports.push(fromParent);
112
+ break;
113
+ }
114
+ searchDir = path.dirname(searchDir);
115
+ }
116
+ }
117
+
118
+ return imports;
119
+ }
120
+
121
+ /**
122
+ * Try to resolve a dotted Python module path from a base directory.
123
+ */
124
+ function tryResolvePythonModule(modulePath, baseDir) {
125
+ const asFile = path.join(baseDir, modulePath + '.py');
126
+ if (fs.existsSync(asFile)) return asFile;
127
+ const asInit = path.join(baseDir, modulePath, '__init__.py');
128
+ if (fs.existsSync(asInit)) return asInit;
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * Resolve a Python relative import like '.models' or '..utils' to an absolute path.
134
+ */
135
+ function resolvePythonRelativeImport(importStr, fileDir) {
136
+ // Count leading dots
137
+ let dots = 0;
138
+ while (dots < importStr.length && importStr[dots] === '.') dots++;
139
+
140
+ const modulePart = importStr.substring(dots);
141
+
142
+ // Go up (dots - 1) directories from fileDir
143
+ let baseDir = fileDir;
144
+ for (let i = 1; i < dots; i++) {
145
+ baseDir = path.dirname(baseDir);
146
+ }
147
+
148
+ if (!modulePart) return null;
149
+
150
+ const modulePath = modulePart.replace(/\./g, path.sep);
151
+ const asFile = path.join(baseDir, modulePath + '.py');
152
+ if (fs.existsSync(asFile)) return asFile;
153
+
154
+ const asInit = path.join(baseDir, modulePath, '__init__.py');
155
+ if (fs.existsSync(asInit)) return asInit;
156
+
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Resolve a JS/TS import path to an actual file.
162
+ * Tries: exact, .js, .ts, .jsx, .tsx, /index.js, /index.ts
163
+ */
164
+ function resolveImportPath(importPath, fileDir, projectRoot, sourceExt) {
165
+ // For Python, importPath is already absolute
166
+ if (path.isAbsolute(importPath)) {
167
+ return fs.existsSync(importPath) ? importPath : null;
168
+ }
169
+
170
+ const base = path.resolve(fileDir, importPath);
171
+
172
+ // Try exact
173
+ if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
174
+
175
+ // Try extensions
176
+ const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
177
+ for (const ext of extensions) {
178
+ const withExt = base + ext;
179
+ if (fs.existsSync(withExt)) return withExt;
180
+ }
181
+
182
+ // Try index files
183
+ for (const ext of extensions) {
184
+ const indexFile = path.join(base, 'index' + ext);
185
+ if (fs.existsSync(indexFile)) return indexFile;
186
+ }
187
+
188
+ return null;
189
+ }
190
+
191
+ /**
192
+ * buildImportGraph(fileContents, projectRoot) → { 'relative/path.js': ['relative/dep.js', ...] }
193
+ *
194
+ * fileContents: Array of { filePath, content } (absolute paths)
195
+ * Returns a map of relative file paths to their relative dependencies.
196
+ * Only includes files that have at least one resolved dependency.
197
+ */
198
+ function buildImportGraph(fileContents, projectRoot) {
199
+ const graph = {};
200
+
201
+ for (const { filePath, content } of fileContents) {
202
+ const deps = extractImports(content, filePath, projectRoot);
203
+ if (deps.length > 0) {
204
+ const relPath = path.relative(projectRoot, filePath);
205
+ graph[relPath] = deps;
206
+ }
207
+ }
208
+
209
+ return graph;
210
+ }
211
+
212
+ module.exports = { extractImports, buildImportGraph };
package/src/sync.js CHANGED
@@ -4,8 +4,10 @@ const { loadLanguagePlugins, getPluginForFile } = require('./extractors/loader')
4
4
  const { formatSections } = require('./agents/formatter');
5
5
  const { mergeIntoAgentsMd } = require('./agents/merger');
6
6
  const { inferResponsibility } = require('./extractors/filemap');
7
+ const { validateExtracted } = require('./agents/validator');
8
+ const { buildImportGraph } = require('./extractors/imports');
7
9
 
8
- const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto']);
10
+ const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto', 'AGENTS.md']);
9
11
 
10
12
  // Load plugins once at module load
11
13
  const plugins = loadLanguagePlugins();
@@ -143,16 +145,16 @@ async function runFullSync(config) {
143
145
  }
144
146
 
145
147
  // Global dedup: collapse dynamic fetches across all files into one summary row
146
- const staticFetches = allFetches.filter(f => !f.url.startsWith('[dynamic'));
148
+ const staticFetches = allFetches.filter(f => f.url !== '[dynamic]' && !f.url.startsWith('dynamic calls detected'));
147
149
  let totalDynamic = 0;
148
150
  for (const f of allFetches) {
149
151
  if (f.url === '[dynamic]') totalDynamic++;
150
- // Also count already-collapsed per-file rows like "[dynamic ×1]"
151
- const m = f.url.match(/^\[dynamic\s*\u00d7(\d+)\]$/);
152
+ // Also count already-collapsed per-file rows
153
+ const m = f.url.match(/^dynamic calls detected \((\d+) unresolved\)$/);
152
154
  if (m) totalDynamic += parseInt(m[1], 10);
153
155
  }
154
156
  if (totalDynamic > 0) {
155
- staticFetches.push({ url: `[dynamic \u00d7${totalDynamic}]`, method: '\u2014' });
157
+ staticFetches.push({ url: `dynamic calls detected (${totalDynamic} unresolved)`, method: '\u2014' });
156
158
  }
157
159
  allFetches = staticFetches;
158
160
 
@@ -165,6 +167,20 @@ async function runFullSync(config) {
165
167
  return true;
166
168
  });
167
169
 
170
+ // Build import graph from all processed files
171
+ const fileContentsForImports = [];
172
+ const allProcessedPaths = [...new Set([...allCodeFiles, ...allFrontendFiles])];
173
+ // Re-read is avoided — collect during processing. Use a second pass for simplicity.
174
+ for (const filePath of allProcessedPaths) {
175
+ try {
176
+ const content = fs.readFileSync(filePath, 'utf-8');
177
+ fileContentsForImports.push({ filePath, content });
178
+ } catch {
179
+ // skip — already warned during extraction
180
+ }
181
+ }
182
+ const importGraph = buildImportGraph(fileContentsForImports, config.projectRoot);
183
+
168
184
  // Build file map
169
185
  const fileMap = [];
170
186
  for (const filePath of allCodeFiles) {
@@ -185,16 +201,26 @@ async function runFullSync(config) {
185
201
  // Scan project structure
186
202
  const structure = await scanStructure(config.projectRoot);
187
203
 
188
- const autoContent = formatSections({
204
+ // Validate extracted data — drop anything malformed
205
+ const validated = validateExtracted({
189
206
  routes: allRoutes,
190
207
  models: allModels,
208
+ functions: functionsMap,
209
+ envVars,
210
+ dbTables: dbTableList
211
+ });
212
+
213
+ const autoContent = formatSections({
214
+ routes: validated.routes,
215
+ models: validated.models,
191
216
  frontend: { fetches: allFetches, storageKeys: allStorageKeys },
192
217
  structure,
193
218
  warnings,
194
219
  fileMap,
195
- functions: functionsMap,
196
- dbTables: dbTableList,
197
- envVars
220
+ functions: validated.functions,
221
+ dbTables: validated.dbTables,
222
+ envVars: validated.envVars,
223
+ importGraph
198
224
  });
199
225
 
200
226
  mergeIntoAgentsMd(config.output, autoContent);