@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/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
- // Compute cross-boundary fan-in/fan-out and cohesion
191
- let intraEdges = 0;
192
- let crossEdges = 0;
193
- for (const { source_file, target_file } of importEdges) {
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
- totalFanIn,
215
- totalFanOut,
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
- if (row.fan_in === 0 && !isExported) {
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
 
@@ -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.close();
264
+ closeDb(db);
265
265
  process.exit(0);
266
266
  });
267
267
  }