carto-md 1.0.1 → 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 +1 -1
- package/src/sync.js +19 -9
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 };
|
|
@@ -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: `
|
|
45
|
+
url: `dynamic calls detected (${dynamicCount} unresolved)`,
|
|
46
46
|
method: '\u2014'
|
|
47
47
|
});
|
|
48
48
|
}
|
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();
|
|
@@ -143,16 +144,16 @@ async function runFullSync(config) {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
// Global dedup: collapse dynamic fetches across all files into one summary row
|
|
146
|
-
const staticFetches = allFetches.filter(f => !f.url.startsWith('
|
|
147
|
+
const staticFetches = allFetches.filter(f => f.url !== '[dynamic]' && !f.url.startsWith('dynamic calls detected'));
|
|
147
148
|
let totalDynamic = 0;
|
|
148
149
|
for (const f of allFetches) {
|
|
149
150
|
if (f.url === '[dynamic]') totalDynamic++;
|
|
150
|
-
// Also count already-collapsed per-file rows
|
|
151
|
-
const m = f.url.match(
|
|
151
|
+
// Also count already-collapsed per-file rows
|
|
152
|
+
const m = f.url.match(/^dynamic calls detected \((\d+) unresolved\)$/);
|
|
152
153
|
if (m) totalDynamic += parseInt(m[1], 10);
|
|
153
154
|
}
|
|
154
155
|
if (totalDynamic > 0) {
|
|
155
|
-
staticFetches.push({ url: `
|
|
156
|
+
staticFetches.push({ url: `dynamic calls detected (${totalDynamic} unresolved)`, method: '\u2014' });
|
|
156
157
|
}
|
|
157
158
|
allFetches = staticFetches;
|
|
158
159
|
|
|
@@ -185,16 +186,25 @@ async function runFullSync(config) {
|
|
|
185
186
|
// Scan project structure
|
|
186
187
|
const structure = await scanStructure(config.projectRoot);
|
|
187
188
|
|
|
188
|
-
|
|
189
|
+
// Validate extracted data — drop anything malformed
|
|
190
|
+
const validated = validateExtracted({
|
|
189
191
|
routes: allRoutes,
|
|
190
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,
|
|
191
201
|
frontend: { fetches: allFetches, storageKeys: allStorageKeys },
|
|
192
202
|
structure,
|
|
193
203
|
warnings,
|
|
194
204
|
fileMap,
|
|
195
|
-
functions:
|
|
196
|
-
dbTables:
|
|
197
|
-
envVars
|
|
205
|
+
functions: validated.functions,
|
|
206
|
+
dbTables: validated.dbTables,
|
|
207
|
+
envVars: validated.envVars
|
|
198
208
|
});
|
|
199
209
|
|
|
200
210
|
mergeIntoAgentsMd(config.output, autoContent);
|