carto-md 2.0.4 → 2.0.6
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/CONTRIBUTING.md +5 -5
- package/README.md +13 -20
- package/index.js +17 -7
- package/package.json +1 -1
- package/src/acp/session.js +9 -11
- package/src/cli/check.js +2 -0
- package/src/cli/impact.js +7 -5
- package/src/cli/init.js +3 -19
- package/src/cli/serve.js +13 -12
- package/src/cli/sync.js +2 -14
- package/src/cli/update-check.js +50 -7
- package/src/store/sqlite-store.js +35 -6
- package/src/store/store-adapter.js +169 -0
- package/src/store/sync-v2.js +37 -4
- package/src/cache/file-hash.js +0 -84
- package/src/cache/graph-cache.js +0 -77
- package/src/detector/files.js +0 -289
- package/src/engine/carto.js +0 -606
- package/src/engine/incremental.js +0 -149
- package/src/mcp/server.js +0 -431
- package/src/sync.js +0 -317
package/src/sync.js
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { loadLanguagePlugins, getPluginForFile } = require('./extractors/loader');
|
|
6
|
-
const { formatSections, formatDomainFile } = require('./agents/formatter');
|
|
7
|
-
const { clusterByDomain } = require('./agents/domains');
|
|
8
|
-
const { mergeIntoAgentsMd } = require('./agents/merger');
|
|
9
|
-
const { inferResponsibility } = require('./extractors/filemap');
|
|
10
|
-
const { validateExtracted } = require('./agents/validator');
|
|
11
|
-
const { buildImportGraph } = require('./extractors/imports');
|
|
12
|
-
const { buildStackLine } = require('./extractors/stack');
|
|
13
|
-
const { loadHashes, saveHashes, computeChangedFiles } = require('./cache/file-hash');
|
|
14
|
-
const { loadGraphCache, saveGraphCache, buildEmptyCache } = require('./cache/graph-cache');
|
|
15
|
-
const { applyIncrementalUpdate, recomputeGraphMetrics } = require('./engine/incremental');
|
|
16
|
-
const { scanStructure } = require('./agents/scan-structure');
|
|
17
|
-
|
|
18
|
-
// Load plugins once at module load
|
|
19
|
-
const plugins = loadLanguagePlugins();
|
|
20
|
-
|
|
21
|
-
async function safeReadFile(filePath, warnings) {
|
|
22
|
-
try {
|
|
23
|
-
return await fs.promises.readFile(filePath, 'utf-8');
|
|
24
|
-
} catch (err) {
|
|
25
|
-
if (warnings) warnings.push(`Could not read ${filePath} — ${err.code || err.message}`);
|
|
26
|
-
console.warn(`[CARTO] Warning: Could not read ${filePath} — skipping`);
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* runFullSync(config)
|
|
33
|
-
*
|
|
34
|
-
* Cache-aware full sync:
|
|
35
|
-
* 1. Load stored file hashes + graph cache
|
|
36
|
-
* 2. Only re-parse files whose hash changed (or new files)
|
|
37
|
-
* 3. Use cached data for unchanged files
|
|
38
|
-
* 4. Save updated hashes + graph cache
|
|
39
|
-
*/
|
|
40
|
-
async function runFullSync(config) {
|
|
41
|
-
const startTime = Date.now();
|
|
42
|
-
const warnings = [];
|
|
43
|
-
const projectRoot = config.projectRoot || process.cwd();
|
|
44
|
-
|
|
45
|
-
const allRouteFiles = config.watch.routeFiles || [];
|
|
46
|
-
const allModelFiles = config.watch.modelFiles || [];
|
|
47
|
-
const allFrontendFiles = config.watch.frontendFiles || [];
|
|
48
|
-
const allCodeFiles = [...new Set([...allRouteFiles, ...allModelFiles])];
|
|
49
|
-
const allProcessedPaths = [...new Set([...allCodeFiles, ...allFrontendFiles])];
|
|
50
|
-
|
|
51
|
-
// Load existing hashes and graph cache
|
|
52
|
-
const storedHashes = loadHashes(projectRoot);
|
|
53
|
-
const existingCache = loadGraphCache(projectRoot) || buildEmptyCache();
|
|
54
|
-
|
|
55
|
-
// Determine which files actually changed
|
|
56
|
-
const { changed, hashes: newHashes } = computeChangedFiles(allProcessedPaths, storedHashes, projectRoot);
|
|
57
|
-
const changedSet = new Set(changed.map(f => path.relative(projectRoot, f)));
|
|
58
|
-
|
|
59
|
-
const cacheHit = allProcessedPaths.length - changed.length;
|
|
60
|
-
if (cacheHit > 0) {
|
|
61
|
-
console.log(`[CARTO] Cache: ${cacheHit} files unchanged, ${changed.length} to re-parse`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Build a working cache — start from existing, overwrite changed files
|
|
65
|
-
const cache = existingCache;
|
|
66
|
-
|
|
67
|
-
// Remove stale data for files that no longer exist
|
|
68
|
-
const currentRelPaths = new Set(allProcessedPaths.map(f => path.relative(projectRoot, f)));
|
|
69
|
-
for (const relPath of Object.keys(cache.fileData)) {
|
|
70
|
-
if (!currentRelPaths.has(relPath)) {
|
|
71
|
-
delete cache.fileData[relPath];
|
|
72
|
-
delete cache.importGraph[relPath];
|
|
73
|
-
delete cache.routesByFile[relPath];
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Parse only changed files
|
|
78
|
-
const processedFiles = new Set();
|
|
79
|
-
|
|
80
|
-
for (const filePath of allCodeFiles) {
|
|
81
|
-
if (processedFiles.has(filePath)) continue;
|
|
82
|
-
processedFiles.add(filePath);
|
|
83
|
-
|
|
84
|
-
const relPath = path.relative(projectRoot, filePath);
|
|
85
|
-
|
|
86
|
-
// Skip if file unchanged and we have cached data
|
|
87
|
-
if (!changedSet.has(relPath) && cache.fileData[relPath]) continue;
|
|
88
|
-
|
|
89
|
-
const content = await safeReadFile(filePath, warnings);
|
|
90
|
-
if (!content) continue;
|
|
91
|
-
|
|
92
|
-
const plugin = getPluginForFile(plugins, filePath);
|
|
93
|
-
if (!plugin) continue;
|
|
94
|
-
|
|
95
|
-
const result = plugin.extract(content, relPath);
|
|
96
|
-
|
|
97
|
-
cache.fileData[relPath] = {
|
|
98
|
-
routes: result.routes || [],
|
|
99
|
-
models: result.models || [],
|
|
100
|
-
functions: result.functions || [],
|
|
101
|
-
envVars: result.envVars || [],
|
|
102
|
-
dbTables: (result.dbTables || []).map(t => ({ ...t, file: relPath })),
|
|
103
|
-
fetches: result.fetches || [],
|
|
104
|
-
storageKeys: result.storageKeys || [],
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
if (result.routes && result.routes.length > 0) {
|
|
108
|
-
cache.routesByFile[relPath] = result.routes.map(r => `${r.method} ${r.path}`);
|
|
109
|
-
} else {
|
|
110
|
-
delete cache.routesByFile[relPath];
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
for (const filePath of allFrontendFiles) {
|
|
115
|
-
if (processedFiles.has(filePath)) continue;
|
|
116
|
-
processedFiles.add(filePath);
|
|
117
|
-
|
|
118
|
-
const relPath = path.relative(projectRoot, filePath);
|
|
119
|
-
if (!changedSet.has(relPath) && cache.fileData[relPath]) continue;
|
|
120
|
-
|
|
121
|
-
const content = await safeReadFile(filePath, warnings);
|
|
122
|
-
if (!content) continue;
|
|
123
|
-
|
|
124
|
-
const plugin = getPluginForFile(plugins, filePath);
|
|
125
|
-
if (!plugin) continue;
|
|
126
|
-
|
|
127
|
-
const result = plugin.extract(content, path.basename(filePath));
|
|
128
|
-
cache.fileData[relPath] = {
|
|
129
|
-
routes: [],
|
|
130
|
-
models: [],
|
|
131
|
-
functions: [],
|
|
132
|
-
envVars: [],
|
|
133
|
-
dbTables: [],
|
|
134
|
-
fetches: result.fetches || [],
|
|
135
|
-
storageKeys: result.storageKeys || [],
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Rebuild import graph only for changed files; keep rest from cache
|
|
140
|
-
const fileContentsForImports = [];
|
|
141
|
-
for (const filePath of allProcessedPaths) {
|
|
142
|
-
const relPath = path.relative(projectRoot, filePath);
|
|
143
|
-
// Only re-read changed files for import extraction
|
|
144
|
-
if (!changedSet.has(relPath) && cache.importGraph[relPath] !== undefined) continue;
|
|
145
|
-
try {
|
|
146
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
147
|
-
fileContentsForImports.push({ filePath, content });
|
|
148
|
-
} catch {}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (fileContentsForImports.length > 0) {
|
|
152
|
-
const deltaGraph = buildImportGraph(fileContentsForImports, projectRoot);
|
|
153
|
-
for (const [relPath, deps] of Object.entries(deltaGraph)) {
|
|
154
|
-
cache.importGraph[relPath] = deps;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Stack detection only from changed files
|
|
159
|
-
if (changed.length > 0) {
|
|
160
|
-
const changedContents = [];
|
|
161
|
-
for (const filePath of changed) {
|
|
162
|
-
try {
|
|
163
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
164
|
-
changedContents.push({ filePath, content });
|
|
165
|
-
} catch {}
|
|
166
|
-
}
|
|
167
|
-
if (changedContents.length > 0) {
|
|
168
|
-
cache.stack = buildStackLine(changedContents, projectRoot);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Recompute graph metrics (highImpact, entryPoints, domains)
|
|
173
|
-
recomputeGraphMetrics(cache);
|
|
174
|
-
cache.meta.indexDuration = Date.now() - startTime;
|
|
175
|
-
cache.meta.lastIndexed = new Date().toISOString();
|
|
176
|
-
cache.generated = new Date().toISOString();
|
|
177
|
-
|
|
178
|
-
// --- Assemble aggregated data for output ---
|
|
179
|
-
let allRoutes = [];
|
|
180
|
-
let allModels = [];
|
|
181
|
-
let allFetches = [];
|
|
182
|
-
let allStorageKeys = [];
|
|
183
|
-
const functionsMap = {};
|
|
184
|
-
const routeCountMap = {};
|
|
185
|
-
const envVarMap = new Map();
|
|
186
|
-
const dbTableList = [];
|
|
187
|
-
|
|
188
|
-
for (const [relPath, data] of Object.entries(cache.fileData)) {
|
|
189
|
-
allRoutes = allRoutes.concat(data.routes);
|
|
190
|
-
allModels = allModels.concat(data.models);
|
|
191
|
-
|
|
192
|
-
if (data.routes.length > 0) routeCountMap[relPath] = data.routes.length;
|
|
193
|
-
if (data.functions && data.functions.length > 0) {
|
|
194
|
-
const basename = path.basename(relPath);
|
|
195
|
-
if (basename !== '__init__.py') functionsMap[relPath] = data.functions;
|
|
196
|
-
}
|
|
197
|
-
for (const varName of data.envVars) {
|
|
198
|
-
if (!envVarMap.has(varName)) envVarMap.set(varName, new Set());
|
|
199
|
-
envVarMap.get(varName).add(relPath);
|
|
200
|
-
}
|
|
201
|
-
for (const t of data.dbTables) {
|
|
202
|
-
dbTableList.push(t.file ? t : { ...t, file: relPath });
|
|
203
|
-
}
|
|
204
|
-
allFetches = allFetches.concat(data.fetches || []);
|
|
205
|
-
allStorageKeys = allStorageKeys.concat(data.storageKeys || []);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Deduplicate fetches
|
|
209
|
-
const staticFetches = allFetches.filter(f => f.url !== '[dynamic]' && !f.url.startsWith('dynamic calls detected'));
|
|
210
|
-
let totalDynamic = 0;
|
|
211
|
-
for (const f of allFetches) {
|
|
212
|
-
if (f.url === '[dynamic]') totalDynamic++;
|
|
213
|
-
const m = f.url.match(/^dynamic calls detected \((\d+) unresolved\)$/);
|
|
214
|
-
if (m) totalDynamic += parseInt(m[1], 10);
|
|
215
|
-
}
|
|
216
|
-
if (totalDynamic > 0) staticFetches.push({ url: `dynamic calls detected (${totalDynamic} unresolved)`, method: '—' });
|
|
217
|
-
allFetches = staticFetches;
|
|
218
|
-
|
|
219
|
-
const skSeen = new Set();
|
|
220
|
-
allStorageKeys = allStorageKeys.filter(({ operation, key }) => {
|
|
221
|
-
const id = `${operation}::${key}`;
|
|
222
|
-
if (skSeen.has(id)) return false;
|
|
223
|
-
skSeen.add(id);
|
|
224
|
-
return true;
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const envVars = [...envVarMap.keys()].sort().map(name => ({ name, files: [...envVarMap.get(name)].sort() }));
|
|
228
|
-
|
|
229
|
-
const fileMap = [];
|
|
230
|
-
for (const [relPath, data] of Object.entries(cache.fileData)) {
|
|
231
|
-
const funcCount = (data.functions || []).length;
|
|
232
|
-
const routeCount = data.routes.length;
|
|
233
|
-
const responsibility = inferResponsibility(path.basename(relPath), funcCount, routeCount);
|
|
234
|
-
if (responsibility && responsibility !== '—') fileMap.push({ file: relPath, responsibility });
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const structure = await scanStructure(projectRoot);
|
|
238
|
-
|
|
239
|
-
const validated = validateExtracted({ routes: allRoutes, models: allModels, functions: functionsMap, envVars, dbTables: dbTableList });
|
|
240
|
-
|
|
241
|
-
const autoContent = formatSections({
|
|
242
|
-
routes: validated.routes,
|
|
243
|
-
models: validated.models,
|
|
244
|
-
frontend: { fetches: allFetches, storageKeys: allStorageKeys },
|
|
245
|
-
structure,
|
|
246
|
-
warnings,
|
|
247
|
-
fileMap,
|
|
248
|
-
functions: validated.functions,
|
|
249
|
-
dbTables: validated.dbTables,
|
|
250
|
-
envVars: validated.envVars,
|
|
251
|
-
importGraph: cache.importGraph,
|
|
252
|
-
stackItems: cache.stack,
|
|
253
|
-
entryPoints: cache.entryPoints,
|
|
254
|
-
highImpact: cache.highImpact.map(h => ({ file: h.file, count: h.dependents }))
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
mergeIntoAgentsMd(config.output, autoContent);
|
|
258
|
-
|
|
259
|
-
// Write domain context files
|
|
260
|
-
const contextDir = path.join(projectRoot, '.carto', 'context');
|
|
261
|
-
try { fs.mkdirSync(contextDir, { recursive: true }); } catch {}
|
|
262
|
-
|
|
263
|
-
for (const [domain, cluster] of Object.entries(cache.domains || {})) {
|
|
264
|
-
const hasContent = (cluster.routes || []).length > 0 ||
|
|
265
|
-
(cluster.models || []).length > 0 ||
|
|
266
|
-
Object.keys(cluster.functions || {}).length > 0 ||
|
|
267
|
-
(cluster.dbTables || []).length > 0;
|
|
268
|
-
if (!hasContent) continue;
|
|
269
|
-
|
|
270
|
-
const domainContent = formatDomainFile(domain, cluster);
|
|
271
|
-
const domainPath = path.join(contextDir, `${domain}.md`);
|
|
272
|
-
const tmpPath = domainPath + '.tmp';
|
|
273
|
-
try {
|
|
274
|
-
fs.writeFileSync(tmpPath, domainContent, 'utf-8');
|
|
275
|
-
fs.renameSync(tmpPath, domainPath);
|
|
276
|
-
} catch (err) {
|
|
277
|
-
console.warn(`[CARTO] Warning: Could not write ${domain}.md — ${err.message}`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Write map.json
|
|
282
|
-
const cartoDir = path.join(projectRoot, '.carto');
|
|
283
|
-
const mapData = {
|
|
284
|
-
version: '2',
|
|
285
|
-
generated: cache.generated,
|
|
286
|
-
imports: cache.importGraph,
|
|
287
|
-
routes: validated.routes,
|
|
288
|
-
routesByFile: cache.routesByFile,
|
|
289
|
-
models: validated.models,
|
|
290
|
-
highImpact: cache.highImpact,
|
|
291
|
-
entryPoints: cache.entryPoints,
|
|
292
|
-
stack: cache.stack,
|
|
293
|
-
domains: Object.keys(cache.domains || {}),
|
|
294
|
-
meta: cache.meta
|
|
295
|
-
};
|
|
296
|
-
try {
|
|
297
|
-
const tmp = path.join(cartoDir, 'map.tmp.json');
|
|
298
|
-
const mapPath = path.join(cartoDir, 'map.json');
|
|
299
|
-
fs.writeFileSync(tmp, JSON.stringify(mapData, null, 2) + '\n', 'utf-8');
|
|
300
|
-
fs.renameSync(tmp, mapPath);
|
|
301
|
-
} catch (err) {
|
|
302
|
-
console.warn(`[CARTO] Warning: Could not write .carto/map.json — ${err.message}`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Persist hash cache and graph cache
|
|
306
|
-
saveHashes(projectRoot, newHashes);
|
|
307
|
-
saveGraphCache(projectRoot, cache);
|
|
308
|
-
|
|
309
|
-
const elapsed = Date.now() - startTime;
|
|
310
|
-
const total = allProcessedPaths.length;
|
|
311
|
-
const skipped = total - changed.length;
|
|
312
|
-
console.log(`[CARTO] Indexed ${changed.length} files (${skipped} cached) in ${elapsed}ms`);
|
|
313
|
-
|
|
314
|
-
return cache;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
module.exports = { runFullSync, safeReadFile, scanStructure };
|