@optave/codegraph 2.4.0 → 2.5.1
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/README.md +66 -10
- package/package.json +15 -5
- package/src/branch-compare.js +568 -0
- package/src/builder.js +183 -22
- package/src/cli.js +253 -8
- package/src/cochange.js +8 -8
- package/src/communities.js +303 -0
- package/src/complexity.js +2056 -0
- package/src/config.js +20 -1
- package/src/db.js +111 -1
- package/src/embedder.js +49 -12
- package/src/export.js +25 -1
- package/src/flow.js +361 -0
- package/src/index.js +32 -2
- package/src/manifesto.js +442 -0
- package/src/mcp.js +244 -5
- package/src/paginate.js +70 -0
- package/src/parser.js +21 -5
- package/src/queries.js +396 -7
- package/src/registry.js +6 -3
- package/src/structure.js +88 -24
- package/src/update-check.js +1 -0
- package/src/watcher.js +2 -2
package/src/structure.js
CHANGED
|
@@ -162,6 +162,48 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// Build reverse index: file → set of ancestor directories (O(files × depth))
|
|
166
|
+
const fileToAncestorDirs = new Map();
|
|
167
|
+
for (const [dir, files] of dirFiles) {
|
|
168
|
+
for (const f of files) {
|
|
169
|
+
if (!fileToAncestorDirs.has(f)) fileToAncestorDirs.set(f, new Set());
|
|
170
|
+
fileToAncestorDirs.get(f).add(dir);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Single O(E) pass: pre-aggregate edge counts per directory
|
|
175
|
+
const dirEdgeCounts = new Map();
|
|
176
|
+
for (const dir of allDirs) {
|
|
177
|
+
dirEdgeCounts.set(dir, { intra: 0, fanIn: 0, fanOut: 0 });
|
|
178
|
+
}
|
|
179
|
+
for (const { source_file, target_file } of importEdges) {
|
|
180
|
+
const srcDirs = fileToAncestorDirs.get(source_file);
|
|
181
|
+
const tgtDirs = fileToAncestorDirs.get(target_file);
|
|
182
|
+
if (!srcDirs && !tgtDirs) continue;
|
|
183
|
+
|
|
184
|
+
// For each directory that contains the source file
|
|
185
|
+
if (srcDirs) {
|
|
186
|
+
for (const dir of srcDirs) {
|
|
187
|
+
const counts = dirEdgeCounts.get(dir);
|
|
188
|
+
if (!counts) continue;
|
|
189
|
+
if (tgtDirs?.has(dir)) {
|
|
190
|
+
counts.intra++;
|
|
191
|
+
} else {
|
|
192
|
+
counts.fanOut++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// For each directory that contains the target but NOT the source
|
|
197
|
+
if (tgtDirs) {
|
|
198
|
+
for (const dir of tgtDirs) {
|
|
199
|
+
if (srcDirs?.has(dir)) continue; // already counted as intra
|
|
200
|
+
const counts = dirEdgeCounts.get(dir);
|
|
201
|
+
if (!counts) continue;
|
|
202
|
+
counts.fanIn++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
165
207
|
const computeDirMetrics = db.transaction(() => {
|
|
166
208
|
for (const [dir, files] of dirFiles) {
|
|
167
209
|
const dirRow = getNodeId.get(dir, 'directory', dir, 0);
|
|
@@ -169,9 +211,6 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
169
211
|
|
|
170
212
|
const fileCount = files.length;
|
|
171
213
|
let symbolCount = 0;
|
|
172
|
-
let totalFanIn = 0;
|
|
173
|
-
let totalFanOut = 0;
|
|
174
|
-
const filesInDir = new Set(files);
|
|
175
214
|
|
|
176
215
|
for (const f of files) {
|
|
177
216
|
const sym = fileSymbols.get(f);
|
|
@@ -187,23 +226,10 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
187
226
|
}
|
|
188
227
|
}
|
|
189
228
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const srcInside = filesInDir.has(source_file);
|
|
195
|
-
const tgtInside = filesInDir.has(target_file);
|
|
196
|
-
if (srcInside && tgtInside) {
|
|
197
|
-
intraEdges++;
|
|
198
|
-
} else if (srcInside || tgtInside) {
|
|
199
|
-
crossEdges++;
|
|
200
|
-
if (!srcInside && tgtInside) totalFanIn++;
|
|
201
|
-
if (srcInside && !tgtInside) totalFanOut++;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const totalEdges = intraEdges + crossEdges;
|
|
206
|
-
const cohesion = totalEdges > 0 ? intraEdges / totalEdges : null;
|
|
229
|
+
// O(1) lookup from pre-aggregated edge counts
|
|
230
|
+
const counts = dirEdgeCounts.get(dir) || { intra: 0, fanIn: 0, fanOut: 0 };
|
|
231
|
+
const totalEdges = counts.intra + counts.fanIn + counts.fanOut;
|
|
232
|
+
const cohesion = totalEdges > 0 ? counts.intra / totalEdges : null;
|
|
207
233
|
|
|
208
234
|
upsertMetric.run(
|
|
209
235
|
dirRow.id,
|
|
@@ -211,8 +237,8 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
211
237
|
symbolCount,
|
|
212
238
|
null,
|
|
213
239
|
null,
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
counts.fanIn,
|
|
241
|
+
counts.fanOut,
|
|
216
242
|
cohesion,
|
|
217
243
|
fileCount,
|
|
218
244
|
);
|
|
@@ -226,6 +252,8 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
226
252
|
|
|
227
253
|
// ─── Node role classification ─────────────────────────────────────────
|
|
228
254
|
|
|
255
|
+
export const FRAMEWORK_ENTRY_PREFIXES = ['route:', 'event:', 'command:'];
|
|
256
|
+
|
|
229
257
|
function median(sorted) {
|
|
230
258
|
if (sorted.length === 0) return 0;
|
|
231
259
|
const mid = Math.floor(sorted.length / 2);
|
|
@@ -235,7 +263,7 @@ function median(sorted) {
|
|
|
235
263
|
export function classifyNodeRoles(db) {
|
|
236
264
|
const rows = db
|
|
237
265
|
.prepare(
|
|
238
|
-
`SELECT n.id, n.kind, n.file,
|
|
266
|
+
`SELECT n.id, n.name, n.kind, n.file,
|
|
239
267
|
COALESCE(fi.cnt, 0) AS fan_in,
|
|
240
268
|
COALESCE(fo.cnt, 0) AS fan_out
|
|
241
269
|
FROM nodes n
|
|
@@ -287,7 +315,10 @@ export function classifyNodeRoles(db) {
|
|
|
287
315
|
const isExported = exportedIds.has(row.id);
|
|
288
316
|
|
|
289
317
|
let role;
|
|
290
|
-
|
|
318
|
+
const isFrameworkEntry = FRAMEWORK_ENTRY_PREFIXES.some((p) => row.name.startsWith(p));
|
|
319
|
+
if (isFrameworkEntry) {
|
|
320
|
+
role = 'entry';
|
|
321
|
+
} else if (row.fan_in === 0 && !isExported) {
|
|
291
322
|
role = 'dead';
|
|
292
323
|
} else if (row.fan_in === 0 && isExported) {
|
|
293
324
|
role = 'entry';
|
|
@@ -330,6 +361,8 @@ export function structureData(customDbPath, opts = {}) {
|
|
|
330
361
|
const maxDepth = opts.depth || null;
|
|
331
362
|
const sortBy = opts.sort || 'files';
|
|
332
363
|
const noTests = opts.noTests || false;
|
|
364
|
+
const full = opts.full || false;
|
|
365
|
+
const fileLimit = opts.fileLimit || 25;
|
|
333
366
|
|
|
334
367
|
// Get all directory nodes with their metrics
|
|
335
368
|
let dirs = db
|
|
@@ -403,6 +436,33 @@ export function structureData(customDbPath, opts = {}) {
|
|
|
403
436
|
});
|
|
404
437
|
|
|
405
438
|
db.close();
|
|
439
|
+
|
|
440
|
+
// Apply global file limit unless full mode
|
|
441
|
+
if (!full) {
|
|
442
|
+
const totalFiles = result.reduce((sum, d) => sum + d.files.length, 0);
|
|
443
|
+
if (totalFiles > fileLimit) {
|
|
444
|
+
let shown = 0;
|
|
445
|
+
for (const d of result) {
|
|
446
|
+
const remaining = fileLimit - shown;
|
|
447
|
+
if (remaining <= 0) {
|
|
448
|
+
d.files = [];
|
|
449
|
+
} else if (d.files.length > remaining) {
|
|
450
|
+
d.files = d.files.slice(0, remaining);
|
|
451
|
+
shown = fileLimit;
|
|
452
|
+
} else {
|
|
453
|
+
shown += d.files.length;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const suppressed = totalFiles - fileLimit;
|
|
457
|
+
return {
|
|
458
|
+
directories: result,
|
|
459
|
+
count: result.length,
|
|
460
|
+
suppressed,
|
|
461
|
+
warning: `${suppressed} files omitted (showing ${fileLimit}/${totalFiles}). Use --full to show all files, or narrow with --directory.`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
406
466
|
return { directories: result, count: result.length };
|
|
407
467
|
}
|
|
408
468
|
|
|
@@ -539,6 +599,10 @@ export function formatStructure(data) {
|
|
|
539
599
|
);
|
|
540
600
|
}
|
|
541
601
|
}
|
|
602
|
+
if (data.warning) {
|
|
603
|
+
lines.push('');
|
|
604
|
+
lines.push(`⚠ ${data.warning}`);
|
|
605
|
+
}
|
|
542
606
|
return lines.join('\n');
|
|
543
607
|
}
|
|
544
608
|
|
package/src/update-check.js
CHANGED
|
@@ -109,6 +109,7 @@ export async function checkForUpdates(currentVersion, options = {}) {
|
|
|
109
109
|
if (process.env.CI) return null;
|
|
110
110
|
if (process.env.NO_UPDATE_CHECK) return null;
|
|
111
111
|
if (!process.stderr.isTTY) return null;
|
|
112
|
+
if (currentVersion.includes('-')) return null;
|
|
112
113
|
|
|
113
114
|
const cachePath = options.cachePath || CACHE_PATH;
|
|
114
115
|
const fetchFn = options._fetchLatest || fetchLatestVersion;
|
package/src/watcher.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { readFileSafe } from './builder.js';
|
|
4
4
|
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
|
|
5
|
-
import { initSchema, openDb } from './db.js';
|
|
5
|
+
import { closeDb, initSchema, openDb } from './db.js';
|
|
6
6
|
import { appendJournalEntries } from './journal.js';
|
|
7
7
|
import { info, warn } from './logger.js';
|
|
8
8
|
import { createParseTreeCache, getActiveEngine, parseFileIncremental } from './parser.js';
|
|
@@ -261,7 +261,7 @@ export async function watchProject(rootDir, opts = {}) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
if (cache) cache.clear();
|
|
264
|
-
db
|
|
264
|
+
closeDb(db);
|
|
265
265
|
process.exit(0);
|
|
266
266
|
});
|
|
267
267
|
}
|