carto-md 1.0.0 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "The context layer for AI-native development.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
@@ -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 };
@@ -37,7 +37,26 @@ function extractFrontend(content) {
37
37
  });
38
38
  }
39
39
 
40
- return { fetches, storageKeys };
40
+ // Collapse dynamic fetches into a single summary row
41
+ const staticFetches = fetches.filter(f => f.url !== '[dynamic]');
42
+ const dynamicCount = fetches.filter(f => f.url === '[dynamic]').length;
43
+ if (dynamicCount > 0) {
44
+ staticFetches.push({
45
+ url: `dynamic calls detected (${dynamicCount} unresolved)`,
46
+ method: '\u2014'
47
+ });
48
+ }
49
+
50
+ // Deduplicate storage keys — keep first occurrence only
51
+ const seen = new Set();
52
+ const dedupedStorageKeys = storageKeys.filter(({ operation, key }) => {
53
+ const id = `${operation}::${key}`;
54
+ if (seen.has(id)) return false;
55
+ seen.add(id);
56
+ return true;
57
+ });
58
+
59
+ return { fetches: staticFetches, storageKeys: dedupedStorageKeys };
41
60
  }
42
61
 
43
62
  module.exports = { extractFrontend };
@@ -43,15 +43,16 @@ function extractFunctions(content, filename) {
43
43
  const rawParams = match[3];
44
44
  const returnType = match[4] ? match[4].trim() : '\u2014';
45
45
 
46
- // Step 3: Clean params
47
- const skipParams = new Set(['self', '*args', '**kwargs', '*', '']);
48
- const params = rawParams
49
- .split(',')
46
+ // Step 3: Clean params — use smart split to handle commas inside brackets
47
+ const skipParams = new Set(['self', '']);
48
+ const params = splitParams(rawParams)
50
49
  .map(p => {
51
50
  // Strip type annotation (everything after first ":")
52
51
  let cleaned = p.split(':')[0];
53
52
  // Strip default value (everything after first "=")
54
53
  cleaned = cleaned.split('=')[0];
54
+ // Strip leading * or **
55
+ cleaned = cleaned.replace(/^\*{1,2}/, '');
55
56
  return cleaned.trim();
56
57
  })
57
58
  .filter(p => !skipParams.has(p));
@@ -66,4 +67,26 @@ function extractFunctions(content, filename) {
66
67
  return functions;
67
68
  }
68
69
 
70
+ /**
71
+ * Splits params by comma at the top level only.
72
+ * Commas inside brackets [], parentheses (), or braces {} do NOT split.
73
+ */
74
+ function splitParams(rawParams) {
75
+ const params = [];
76
+ let depth = 0;
77
+ let current = '';
78
+ for (const char of rawParams) {
79
+ if (char === '[' || char === '(' || char === '{') depth++;
80
+ else if (char === ']' || char === ')' || char === '}') depth--;
81
+ else if (char === ',' && depth === 0) {
82
+ params.push(current.trim());
83
+ current = '';
84
+ continue;
85
+ }
86
+ current += char;
87
+ }
88
+ if (current.trim()) params.push(current.trim());
89
+ return params;
90
+ }
91
+
69
92
  module.exports = { extractFunctions };
package/src/sync.js CHANGED
@@ -4,8 +4,9 @@ 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');
7
8
 
8
- const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto']);
9
+ const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto', 'AGENTS.md']);
9
10
 
10
11
  // Load plugins once at module load
11
12
  const plugins = loadLanguagePlugins();
@@ -142,6 +143,29 @@ async function runFullSync(config) {
142
143
  allStorageKeys = allStorageKeys.concat(result.storageKeys);
143
144
  }
144
145
 
146
+ // Global dedup: collapse dynamic fetches across all files into one summary row
147
+ const staticFetches = allFetches.filter(f => f.url !== '[dynamic]' && !f.url.startsWith('dynamic calls detected'));
148
+ let totalDynamic = 0;
149
+ for (const f of allFetches) {
150
+ if (f.url === '[dynamic]') totalDynamic++;
151
+ // Also count already-collapsed per-file rows
152
+ const m = f.url.match(/^dynamic calls detected \((\d+) unresolved\)$/);
153
+ if (m) totalDynamic += parseInt(m[1], 10);
154
+ }
155
+ if (totalDynamic > 0) {
156
+ staticFetches.push({ url: `dynamic calls detected (${totalDynamic} unresolved)`, method: '\u2014' });
157
+ }
158
+ allFetches = staticFetches;
159
+
160
+ // Global dedup: storage keys across all files
161
+ const skSeen = new Set();
162
+ allStorageKeys = allStorageKeys.filter(({ operation, key }) => {
163
+ const id = `${operation}::${key}`;
164
+ if (skSeen.has(id)) return false;
165
+ skSeen.add(id);
166
+ return true;
167
+ });
168
+
145
169
  // Build file map
146
170
  const fileMap = [];
147
171
  for (const filePath of allCodeFiles) {
@@ -162,16 +186,25 @@ async function runFullSync(config) {
162
186
  // Scan project structure
163
187
  const structure = await scanStructure(config.projectRoot);
164
188
 
165
- const autoContent = formatSections({
189
+ // Validate extracted data — drop anything malformed
190
+ const validated = validateExtracted({
166
191
  routes: allRoutes,
167
192
  models: allModels,
193
+ functions: functionsMap,
194
+ envVars,
195
+ dbTables: dbTableList
196
+ });
197
+
198
+ const autoContent = formatSections({
199
+ routes: validated.routes,
200
+ models: validated.models,
168
201
  frontend: { fetches: allFetches, storageKeys: allStorageKeys },
169
202
  structure,
170
203
  warnings,
171
204
  fileMap,
172
- functions: functionsMap,
173
- dbTables: dbTableList,
174
- envVars
205
+ functions: validated.functions,
206
+ dbTables: validated.dbTables,
207
+ envVars: validated.envVars
175
208
  });
176
209
 
177
210
  mergeIntoAgentsMd(config.output, autoContent);