carto-md 2.0.5 → 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/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 };