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 +1 -1
- package/src/agents/validator.js +88 -0
- package/src/extractors/frontend.js +20 -1
- package/src/extractors/functions.js +27 -4
- package/src/sync.js +38 -5
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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', '
|
|
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
|
-
|
|
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:
|
|
173
|
-
dbTables:
|
|
174
|
-
envVars
|
|
205
|
+
functions: validated.functions,
|
|
206
|
+
dbTables: validated.dbTables,
|
|
207
|
+
envVars: validated.envVars
|
|
175
208
|
});
|
|
176
209
|
|
|
177
210
|
mergeIntoAgentsMd(config.output, autoContent);
|