@optave/codegraph 3.1.1 → 3.1.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.
Files changed (68) hide show
  1. package/package.json +7 -7
  2. package/src/ast-analysis/engine.js +365 -0
  3. package/src/ast-analysis/metrics.js +118 -0
  4. package/src/ast-analysis/visitor-utils.js +176 -0
  5. package/src/ast-analysis/visitor.js +162 -0
  6. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  7. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  8. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  9. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  10. package/src/ast.js +13 -140
  11. package/src/audit.js +2 -87
  12. package/src/batch.js +0 -25
  13. package/src/boundaries.js +1 -1
  14. package/src/branch-compare.js +1 -96
  15. package/src/builder.js +48 -179
  16. package/src/cfg.js +89 -883
  17. package/src/check.js +1 -84
  18. package/src/cli.js +20 -19
  19. package/src/cochange.js +1 -39
  20. package/src/commands/audit.js +88 -0
  21. package/src/commands/batch.js +26 -0
  22. package/src/commands/branch-compare.js +97 -0
  23. package/src/commands/cfg.js +55 -0
  24. package/src/commands/check.js +82 -0
  25. package/src/commands/cochange.js +37 -0
  26. package/src/commands/communities.js +69 -0
  27. package/src/commands/complexity.js +77 -0
  28. package/src/commands/dataflow.js +110 -0
  29. package/src/commands/flow.js +70 -0
  30. package/src/commands/manifesto.js +77 -0
  31. package/src/commands/owners.js +52 -0
  32. package/src/commands/query.js +21 -0
  33. package/src/commands/sequence.js +33 -0
  34. package/src/commands/structure.js +64 -0
  35. package/src/commands/triage.js +49 -0
  36. package/src/communities.js +12 -83
  37. package/src/complexity.js +42 -356
  38. package/src/cycles.js +1 -1
  39. package/src/dataflow.js +12 -665
  40. package/src/db/repository/build-stmts.js +104 -0
  41. package/src/db/repository/cfg.js +83 -0
  42. package/src/db/repository/cochange.js +41 -0
  43. package/src/db/repository/complexity.js +15 -0
  44. package/src/db/repository/dataflow.js +12 -0
  45. package/src/db/repository/edges.js +259 -0
  46. package/src/db/repository/embeddings.js +40 -0
  47. package/src/db/repository/graph-read.js +39 -0
  48. package/src/db/repository/index.js +42 -0
  49. package/src/db/repository/nodes.js +236 -0
  50. package/src/db.js +40 -1
  51. package/src/embedder.js +14 -34
  52. package/src/export.js +1 -1
  53. package/src/extractors/javascript.js +130 -5
  54. package/src/flow.js +2 -70
  55. package/src/index.js +23 -19
  56. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  57. package/src/kinds.js +1 -0
  58. package/src/manifesto.js +0 -76
  59. package/src/owners.js +1 -56
  60. package/src/queries-cli.js +1 -1
  61. package/src/queries.js +79 -280
  62. package/src/sequence.js +5 -44
  63. package/src/structure.js +16 -75
  64. package/src/triage.js +1 -54
  65. package/src/viewer.js +1 -1
  66. package/src/watcher.js +7 -4
  67. package/src/db/repository.js +0 -134
  68. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
@@ -1,10 +1,15 @@
1
1
  import path from 'node:path';
2
2
  import Graph from 'graphology';
3
3
  import louvain from 'graphology-communities-louvain';
4
- import { openReadonlyOrFail } from './db.js';
4
+ import {
5
+ getCallableNodes,
6
+ getCallEdges,
7
+ getFileNodesAll,
8
+ getImportEdges,
9
+ openReadonlyOrFail,
10
+ } from './db.js';
11
+ import { isTestFile } from './infrastructure/test-filter.js';
5
12
  import { paginateResult } from './paginate.js';
6
- import { outputResult } from './result-formatter.js';
7
- import { isTestFile } from './test-filter.js';
8
13
 
9
14
  // ─── Graph Construction ───────────────────────────────────────────────
10
15
 
@@ -22,9 +27,7 @@ function buildGraphologyGraph(db, opts = {}) {
22
27
 
23
28
  if (opts.functions) {
24
29
  // Function-level: nodes = function/method/class symbols, edges = calls
25
- let nodes = db
26
- .prepare("SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')")
27
- .all();
30
+ let nodes = getCallableNodes(db);
28
31
  if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
29
32
 
30
33
  const nodeIds = new Set();
@@ -34,7 +37,7 @@ function buildGraphologyGraph(db, opts = {}) {
34
37
  nodeIds.add(n.id);
35
38
  }
36
39
 
37
- const edges = db.prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls'").all();
40
+ const edges = getCallEdges(db);
38
41
  for (const e of edges) {
39
42
  if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
40
43
  const src = String(e.source_id);
@@ -46,7 +49,7 @@ function buildGraphologyGraph(db, opts = {}) {
46
49
  }
47
50
  } else {
48
51
  // File-level: nodes = files, edges = imports + imports-type (deduplicated, cross-file)
49
- let nodes = db.prepare("SELECT id, name, file FROM nodes WHERE kind = 'file'").all();
52
+ let nodes = getFileNodesAll(db);
50
53
  if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
51
54
 
52
55
  const nodeIds = new Set();
@@ -56,9 +59,7 @@ function buildGraphologyGraph(db, opts = {}) {
56
59
  nodeIds.add(n.id);
57
60
  }
58
61
 
59
- const edges = db
60
- .prepare("SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')")
61
- .all();
62
+ const edges = getImportEdges(db);
62
63
  for (const e of edges) {
63
64
  if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
64
65
  const src = String(e.source_id);
@@ -232,75 +233,3 @@ export function communitySummaryForStats(customDbPath, opts = {}) {
232
233
  const data = communitiesData(customDbPath, { ...opts, drift: true });
233
234
  return data.summary;
234
235
  }
235
-
236
- // ─── CLI Display ──────────────────────────────────────────────────────
237
-
238
- /**
239
- * CLI entry point: run community detection and print results.
240
- *
241
- * @param {string} [customDbPath]
242
- * @param {object} [opts]
243
- */
244
- export function communities(customDbPath, opts = {}) {
245
- const data = communitiesData(customDbPath, opts);
246
-
247
- if (outputResult(data, 'communities', opts)) return;
248
-
249
- if (data.summary.communityCount === 0) {
250
- console.log(
251
- '\nNo communities detected. The graph may be too small or disconnected.\n' +
252
- 'Run "codegraph build" first to populate the graph.\n',
253
- );
254
- return;
255
- }
256
-
257
- const mode = opts.functions ? 'Function' : 'File';
258
- console.log(`\n# ${mode}-Level Communities\n`);
259
- console.log(
260
- ` ${data.summary.communityCount} communities | ${data.summary.nodeCount} nodes | modularity: ${data.summary.modularity} | drift: ${data.summary.driftScore}%\n`,
261
- );
262
-
263
- if (!opts.drift) {
264
- for (const c of data.communities) {
265
- const dirs = Object.entries(c.directories)
266
- .sort((a, b) => b[1] - a[1])
267
- .map(([d, n]) => `${d} (${n})`)
268
- .join(', ');
269
- console.log(` Community ${c.id} (${c.size} members): ${dirs}`);
270
- if (c.members) {
271
- const shown = c.members.slice(0, 8);
272
- for (const m of shown) {
273
- const kind = m.kind ? ` [${m.kind}]` : '';
274
- console.log(` - ${m.name}${kind} ${m.file}`);
275
- }
276
- if (c.members.length > 8) {
277
- console.log(` ... and ${c.members.length - 8} more`);
278
- }
279
- }
280
- }
281
- }
282
-
283
- // Drift analysis
284
- const d = data.drift;
285
- if (d.splitCandidates.length > 0 || d.mergeCandidates.length > 0) {
286
- console.log(`\n# Drift Analysis (score: ${data.summary.driftScore}%)\n`);
287
-
288
- if (d.splitCandidates.length > 0) {
289
- console.log(' Split candidates (directories spanning multiple communities):');
290
- for (const s of d.splitCandidates.slice(0, 10)) {
291
- console.log(` - ${s.directory} → ${s.communityCount} communities`);
292
- }
293
- }
294
-
295
- if (d.mergeCandidates.length > 0) {
296
- console.log(' Merge candidates (communities spanning multiple directories):');
297
- for (const m of d.mergeCandidates.slice(0, 10)) {
298
- console.log(
299
- ` - Community ${m.communityId} (${m.size} members) → ${m.directoryCount} dirs: ${m.directories.join(', ')}`,
300
- );
301
- }
302
- }
303
- }
304
-
305
- console.log();
306
- }
package/src/complexity.js CHANGED
@@ -1,19 +1,23 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import {
4
+ computeLOCMetrics as _computeLOCMetrics,
5
+ computeMaintainabilityIndex as _computeMaintainabilityIndex,
6
+ } from './ast-analysis/metrics.js';
3
7
  import { COMPLEXITY_RULES, HALSTEAD_RULES } from './ast-analysis/rules/index.js';
4
8
  import {
5
9
  findFunctionNode as _findFunctionNode,
6
10
  buildExtensionSet,
7
11
  buildExtToLangMap,
8
12
  } from './ast-analysis/shared.js';
13
+ import { walkWithVisitors } from './ast-analysis/visitor.js';
14
+ import { createComplexityVisitor } from './ast-analysis/visitors/complexity-visitor.js';
9
15
  import { loadConfig } from './config.js';
10
- import { openReadonlyOrFail } from './db.js';
16
+ import { getFunctionNodeId, openReadonlyOrFail } from './db.js';
17
+ import { isTestFile } from './infrastructure/test-filter.js';
11
18
  import { info } from './logger.js';
12
19
  import { paginateResult } from './paginate.js';
13
20
 
14
- import { outputResult } from './result-formatter.js';
15
- import { isTestFile } from './test-filter.js';
16
-
17
21
  // Re-export rules for backward compatibility
18
22
  export { COMPLEXITY_RULES, HALSTEAD_RULES };
19
23
 
@@ -95,80 +99,12 @@ export function computeHalsteadMetrics(functionNode, language) {
95
99
  }
96
100
 
97
101
  // ─── LOC Metrics Computation ──────────────────────────────────────────────
98
-
99
- const C_STYLE_PREFIXES = ['//', '/*', '*', '*/'];
100
-
101
- const COMMENT_PREFIXES = new Map([
102
- ['javascript', C_STYLE_PREFIXES],
103
- ['typescript', C_STYLE_PREFIXES],
104
- ['tsx', C_STYLE_PREFIXES],
105
- ['go', C_STYLE_PREFIXES],
106
- ['rust', C_STYLE_PREFIXES],
107
- ['java', C_STYLE_PREFIXES],
108
- ['csharp', C_STYLE_PREFIXES],
109
- ['python', ['#']],
110
- ['ruby', ['#']],
111
- ['php', ['//', '#', '/*', '*', '*/']],
112
- ]);
113
-
114
- /**
115
- * Compute LOC metrics from a function node's source text.
116
- *
117
- * @param {object} functionNode - tree-sitter node
118
- * @param {string} [language] - Language ID (falls back to C-style prefixes)
119
- * @returns {{ loc: number, sloc: number, commentLines: number }}
120
- */
121
- export function computeLOCMetrics(functionNode, language) {
122
- const text = functionNode.text;
123
- const lines = text.split('\n');
124
- const loc = lines.length;
125
- const prefixes = (language && COMMENT_PREFIXES.get(language)) || C_STYLE_PREFIXES;
126
-
127
- let commentLines = 0;
128
- let blankLines = 0;
129
-
130
- for (const line of lines) {
131
- const trimmed = line.trim();
132
- if (trimmed === '') {
133
- blankLines++;
134
- } else if (prefixes.some((p) => trimmed.startsWith(p))) {
135
- commentLines++;
136
- }
137
- }
138
-
139
- const sloc = Math.max(1, loc - blankLines - commentLines);
140
- return { loc, sloc, commentLines };
141
- }
102
+ // Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
103
+ export const computeLOCMetrics = _computeLOCMetrics;
142
104
 
143
105
  // ─── Maintainability Index ────────────────────────────────────────────────
144
-
145
- /**
146
- * Compute normalized Maintainability Index (0-100 scale).
147
- *
148
- * Original SEI formula: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
149
- * Microsoft normalization: max(0, min(100, MI * 100/171))
150
- *
151
- * @param {number} volume - Halstead volume
152
- * @param {number} cyclomatic - Cyclomatic complexity
153
- * @param {number} sloc - Source lines of code
154
- * @param {number} [commentRatio] - Comment ratio (0-1), optional
155
- * @returns {number} Normalized MI (0-100)
156
- */
157
- export function computeMaintainabilityIndex(volume, cyclomatic, sloc, commentRatio) {
158
- // Guard against zero/negative values in logarithms
159
- const safeVolume = Math.max(volume, 1);
160
- const safeSLOC = Math.max(sloc, 1);
161
-
162
- let mi = 171 - 5.2 * Math.log(safeVolume) - 0.23 * cyclomatic - 16.2 * Math.log(safeSLOC);
163
-
164
- if (commentRatio != null && commentRatio > 0) {
165
- mi += 50 * Math.sin(Math.sqrt(2.4 * commentRatio));
166
- }
167
-
168
- // Microsoft normalization: 0-100 scale
169
- const normalized = Math.max(0, Math.min(100, (mi * 100) / 171));
170
- return +normalized.toFixed(1);
171
- }
106
+ // Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
107
+ export const computeMaintainabilityIndex = _computeMaintainabilityIndex;
172
108
 
173
109
  // ─── Algorithm: Single-Traversal DFS ──────────────────────────────────────
174
110
 
@@ -346,6 +282,8 @@ export function computeFunctionComplexity(functionNode, language) {
346
282
  * traversal, avoiding two separate DFS walks per function node at build time.
347
283
  * LOC is text-based (not tree-based) and computed separately (very cheap).
348
284
  *
285
+ * Now delegates to the complexity visitor via the unified walker.
286
+ *
349
287
  * @param {object} functionNode - tree-sitter node for the function
350
288
  * @param {string} langId - Language ID (e.g. 'javascript', 'python')
351
289
  * @returns {{ cognitive: number, cyclomatic: number, maxNesting: number, halstead: object|null, loc: object, mi: number } | null}
@@ -355,207 +293,34 @@ export function computeAllMetrics(functionNode, langId) {
355
293
  if (!cRules) return null;
356
294
  const hRules = HALSTEAD_RULES.get(langId);
357
295
 
358
- // ── Complexity state ──
359
- let cognitive = 0;
360
- let cyclomatic = 1; // McCabe starts at 1
361
- let maxNesting = 0;
362
-
363
- // ── Halstead state ──
364
- const operators = hRules ? new Map() : null;
365
- const operands = hRules ? new Map() : null;
366
-
367
- function walk(node, nestingLevel, isTopFunction, halsteadSkip) {
368
- if (!node) return;
369
-
370
- const type = node.type;
371
-
372
- // ── Halstead classification ──
373
- // Propagate skip through type-annotation subtrees (e.g. TS generics, Java type params)
374
- const skipH = halsteadSkip || (hRules ? hRules.skipTypes.has(type) : false);
375
- if (hRules && !skipH) {
376
- // Compound operators (non-leaf): count node type as operator
377
- if (hRules.compoundOperators.has(type)) {
378
- operators.set(type, (operators.get(type) || 0) + 1);
379
- }
380
- // Leaf nodes: classify as operator or operand
381
- if (node.childCount === 0) {
382
- if (hRules.operatorLeafTypes.has(type)) {
383
- operators.set(type, (operators.get(type) || 0) + 1);
384
- } else if (hRules.operandLeafTypes.has(type)) {
385
- const text = node.text;
386
- operands.set(text, (operands.get(text) || 0) + 1);
387
- }
388
- }
389
- }
390
-
391
- // ── Complexity: track nesting depth ──
392
- if (nestingLevel > maxNesting) maxNesting = nestingLevel;
393
-
394
- // Handle logical operators in binary expressions
395
- if (type === cRules.logicalNodeType) {
396
- const op = node.child(1)?.type;
397
- if (op && cRules.logicalOperators.has(op)) {
398
- cyclomatic++;
399
- const parent = node.parent;
400
- let sameSequence = false;
401
- if (parent && parent.type === cRules.logicalNodeType) {
402
- const parentOp = parent.child(1)?.type;
403
- if (parentOp === op) sameSequence = true;
404
- }
405
- if (!sameSequence) cognitive++;
406
- for (let i = 0; i < node.childCount; i++) {
407
- walk(node.child(i), nestingLevel, false, skipH);
408
- }
409
- return;
410
- }
411
- }
412
-
413
- // Handle optional chaining (cyclomatic only)
414
- if (type === cRules.optionalChainType) {
415
- cyclomatic++;
416
- }
417
-
418
- // Handle branch/control flow nodes (skip keyword leaf tokens like Ruby's `if`)
419
- if (cRules.branchNodes.has(type) && node.childCount > 0) {
420
- // Pattern A: else clause wraps if (JS/C#/Rust)
421
- if (cRules.elseNodeType && type === cRules.elseNodeType) {
422
- const firstChild = node.namedChild(0);
423
- if (firstChild && firstChild.type === cRules.ifNodeType) {
424
- for (let i = 0; i < node.childCount; i++) {
425
- walk(node.child(i), nestingLevel, false, skipH);
426
- }
427
- return;
428
- }
429
- cognitive++;
430
- for (let i = 0; i < node.childCount; i++) {
431
- walk(node.child(i), nestingLevel, false, skipH);
432
- }
433
- return;
434
- }
435
-
436
- // Pattern B: explicit elif node (Python/Ruby/PHP)
437
- if (cRules.elifNodeType && type === cRules.elifNodeType) {
438
- cognitive++;
439
- cyclomatic++;
440
- for (let i = 0; i < node.childCount; i++) {
441
- walk(node.child(i), nestingLevel, false, skipH);
442
- }
443
- return;
444
- }
445
-
446
- // Detect else-if via Pattern A or C
447
- let isElseIf = false;
448
- if (type === cRules.ifNodeType) {
449
- if (cRules.elseViaAlternative) {
450
- isElseIf =
451
- node.parent?.type === cRules.ifNodeType &&
452
- node.parent.childForFieldName('alternative')?.id === node.id;
453
- } else if (cRules.elseNodeType) {
454
- isElseIf = node.parent?.type === cRules.elseNodeType;
455
- }
456
- }
457
-
458
- if (isElseIf) {
459
- cognitive++;
460
- cyclomatic++;
461
- for (let i = 0; i < node.childCount; i++) {
462
- walk(node.child(i), nestingLevel, false, skipH);
463
- }
464
- return;
465
- }
466
-
467
- // Regular branch node
468
- cognitive += 1 + nestingLevel;
469
- cyclomatic++;
470
-
471
- // Switch-like nodes don't add cyclomatic themselves (cases do)
472
- if (cRules.switchLikeNodes?.has(type)) {
473
- cyclomatic--;
474
- }
475
-
476
- if (cRules.nestingNodes.has(type)) {
477
- for (let i = 0; i < node.childCount; i++) {
478
- walk(node.child(i), nestingLevel + 1, false, skipH);
479
- }
480
- return;
481
- }
482
- }
483
-
484
- // Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
485
- if (
486
- cRules.elseViaAlternative &&
487
- type !== cRules.ifNodeType &&
488
- node.parent?.type === cRules.ifNodeType &&
489
- node.parent.childForFieldName('alternative')?.id === node.id
490
- ) {
491
- cognitive++;
492
- for (let i = 0; i < node.childCount; i++) {
493
- walk(node.child(i), nestingLevel, false, skipH);
494
- }
495
- return;
496
- }
497
-
498
- // Handle case nodes (cyclomatic only, skip keyword leaves)
499
- if (cRules.caseNodes.has(type) && node.childCount > 0) {
500
- cyclomatic++;
501
- }
502
-
503
- // Handle nested function definitions (increase nesting)
504
- if (!isTopFunction && cRules.functionNodes.has(type)) {
505
- for (let i = 0; i < node.childCount; i++) {
506
- walk(node.child(i), nestingLevel + 1, false, skipH);
507
- }
508
- return;
509
- }
296
+ const visitor = createComplexityVisitor(cRules, hRules, { langId });
510
297
 
511
- // Walk children
512
- for (let i = 0; i < node.childCount; i++) {
513
- walk(node.child(i), nestingLevel, false, skipH);
514
- }
515
- }
298
+ const nestingNodes = new Set(cRules.nestingNodes);
299
+ // NOTE: do NOT add functionNodes here in function-level mode the walker
300
+ // walks a single function node, and adding it to nestingNodeTypes would
301
+ // inflate context.nestingLevel by +1 for the entire body.
516
302
 
517
- walk(functionNode, 0, true, false);
518
-
519
- // ── Compute Halstead derived metrics ──
520
- let halstead = null;
521
- if (hRules && operators && operands) {
522
- const n1 = operators.size;
523
- const n2 = operands.size;
524
- let bigN1 = 0;
525
- for (const c of operators.values()) bigN1 += c;
526
- let bigN2 = 0;
527
- for (const c of operands.values()) bigN2 += c;
528
-
529
- const vocabulary = n1 + n2;
530
- const length = bigN1 + bigN2;
531
- const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
532
- const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
533
- const effort = difficulty * volume;
534
- const bugs = volume / 3000;
535
-
536
- halstead = {
537
- n1,
538
- n2,
539
- bigN1,
540
- bigN2,
541
- vocabulary,
542
- length,
543
- volume: +volume.toFixed(2),
544
- difficulty: +difficulty.toFixed(2),
545
- effort: +effort.toFixed(2),
546
- bugs: +bugs.toFixed(4),
547
- };
548
- }
303
+ const results = walkWithVisitors(functionNode, [visitor], langId, {
304
+ nestingNodeTypes: nestingNodes,
305
+ });
549
306
 
550
- // ── LOC metrics (text-based, cheap) ──
551
- const loc = computeLOCMetrics(functionNode, langId);
307
+ const rawResult = results.complexity;
552
308
 
553
- // ── Maintainability Index ──
554
- const volume = halstead ? halstead.volume : 0;
309
+ // The visitor's finish() in function-level mode returns the raw metrics
310
+ // but without LOC (needs the functionNode text). Compute LOC + MI here.
311
+ const loc = _computeLOCMetrics(functionNode, langId);
312
+ const volume = rawResult.halstead ? rawResult.halstead.volume : 0;
555
313
  const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
556
- const mi = computeMaintainabilityIndex(volume, cyclomatic, loc.sloc, commentRatio);
314
+ const mi = _computeMaintainabilityIndex(volume, rawResult.cyclomatic, loc.sloc, commentRatio);
557
315
 
558
- return { cognitive, cyclomatic, maxNesting, halstead, loc, mi };
316
+ return {
317
+ cognitive: rawResult.cognitive,
318
+ cyclomatic: rawResult.cyclomatic,
319
+ maxNesting: rawResult.maxNesting,
320
+ halstead: rawResult.halstead,
321
+ loc,
322
+ mi,
323
+ };
559
324
  }
560
325
 
561
326
  // ─── Build-Time: Compute Metrics for Changed Files ────────────────────────
@@ -612,10 +377,6 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
612
377
  maintainability_index)
613
378
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
614
379
  );
615
- const getNodeId = db.prepare(
616
- "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
617
- );
618
-
619
380
  let analyzed = 0;
620
381
 
621
382
  const tx = db.transaction(() => {
@@ -662,12 +423,12 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
662
423
 
663
424
  // Use pre-computed complexity from native engine if available
664
425
  if (def.complexity) {
665
- const row = getNodeId.get(def.name, relPath, def.line);
666
- if (!row) continue;
426
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
427
+ if (!nodeId) continue;
667
428
  const ch = def.complexity.halstead;
668
429
  const cl = def.complexity.loc;
669
430
  upsert.run(
670
- row.id,
431
+ nodeId,
671
432
  def.complexity.cognitive,
672
433
  def.complexity.cyclomatic,
673
434
  def.complexity.maxNesting ?? 0,
@@ -700,12 +461,12 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
700
461
  const metrics = computeAllMetrics(funcNode, langId);
701
462
  if (!metrics) continue;
702
463
 
703
- const row = getNodeId.get(def.name, relPath, def.line);
704
- if (!row) continue;
464
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
465
+ if (!nodeId) continue;
705
466
 
706
467
  const h = metrics.halstead;
707
468
  upsert.run(
708
- row.id,
469
+ nodeId,
709
470
  metrics.cognitive,
710
471
  metrics.cyclomatic,
711
472
  metrics.maxNesting,
@@ -1040,78 +801,3 @@ export function* iterComplexity(customDbPath, opts = {}) {
1040
801
  db.close();
1041
802
  }
1042
803
  }
1043
-
1044
- /**
1045
- * Format complexity output for CLI display.
1046
- */
1047
- export function complexity(customDbPath, opts = {}) {
1048
- const data = complexityData(customDbPath, opts);
1049
-
1050
- if (outputResult(data, 'functions', opts)) return;
1051
-
1052
- if (data.functions.length === 0) {
1053
- if (data.summary === null) {
1054
- if (data.hasGraph) {
1055
- console.log(
1056
- '\nNo complexity data found, but a graph exists. Run "codegraph build --no-incremental" to populate complexity metrics.\n',
1057
- );
1058
- } else {
1059
- console.log(
1060
- '\nNo complexity data found. Run "codegraph build" first to analyze your codebase.\n',
1061
- );
1062
- }
1063
- } else {
1064
- console.log('\nNo functions match the given filters.\n');
1065
- }
1066
- return;
1067
- }
1068
-
1069
- const header = opts.aboveThreshold ? 'Functions Above Threshold' : 'Function Complexity';
1070
- console.log(`\n# ${header}\n`);
1071
-
1072
- if (opts.health) {
1073
- // Health-focused view with Halstead + MI columns
1074
- console.log(
1075
- ` ${'Function'.padEnd(35)} ${'File'.padEnd(25)} ${'MI'.padStart(5)} ${'Vol'.padStart(7)} ${'Diff'.padStart(6)} ${'Effort'.padStart(9)} ${'Bugs'.padStart(6)} ${'LOC'.padStart(5)} ${'SLOC'.padStart(5)}`,
1076
- );
1077
- console.log(
1078
- ` ${'─'.repeat(35)} ${'─'.repeat(25)} ${'─'.repeat(5)} ${'─'.repeat(7)} ${'─'.repeat(6)} ${'─'.repeat(9)} ${'─'.repeat(6)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
1079
- );
1080
-
1081
- for (const fn of data.functions) {
1082
- const name = fn.name.length > 33 ? `${fn.name.slice(0, 32)}…` : fn.name;
1083
- const file = fn.file.length > 23 ? `…${fn.file.slice(-22)}` : fn.file;
1084
- const miWarn = fn.exceeds?.includes('maintainabilityIndex') ? '!' : ' ';
1085
- console.log(
1086
- ` ${name.padEnd(35)} ${file.padEnd(25)} ${String(fn.maintainabilityIndex).padStart(5)}${miWarn}${String(fn.halstead.volume).padStart(7)} ${String(fn.halstead.difficulty).padStart(6)} ${String(fn.halstead.effort).padStart(9)} ${String(fn.halstead.bugs).padStart(6)} ${String(fn.loc).padStart(5)} ${String(fn.sloc).padStart(5)}`,
1087
- );
1088
- }
1089
- } else {
1090
- // Default view with MI column appended
1091
- console.log(
1092
- ` ${'Function'.padEnd(40)} ${'File'.padEnd(30)} ${'Cog'.padStart(4)} ${'Cyc'.padStart(4)} ${'Nest'.padStart(5)} ${'MI'.padStart(5)}`,
1093
- );
1094
- console.log(
1095
- ` ${'─'.repeat(40)} ${'─'.repeat(30)} ${'─'.repeat(4)} ${'─'.repeat(4)} ${'─'.repeat(5)} ${'─'.repeat(5)}`,
1096
- );
1097
-
1098
- for (const fn of data.functions) {
1099
- const name = fn.name.length > 38 ? `${fn.name.slice(0, 37)}…` : fn.name;
1100
- const file = fn.file.length > 28 ? `…${fn.file.slice(-27)}` : fn.file;
1101
- const warn = fn.exceeds ? ' !' : '';
1102
- const mi = fn.maintainabilityIndex > 0 ? String(fn.maintainabilityIndex) : '-';
1103
- console.log(
1104
- ` ${name.padEnd(40)} ${file.padEnd(30)} ${String(fn.cognitive).padStart(4)} ${String(fn.cyclomatic).padStart(4)} ${String(fn.maxNesting).padStart(5)} ${mi.padStart(5)}${warn}`,
1105
- );
1106
- }
1107
- }
1108
-
1109
- if (data.summary) {
1110
- const s = data.summary;
1111
- const miPart = s.avgMI != null ? ` | avg MI: ${s.avgMI}` : '';
1112
- console.log(
1113
- `\n ${s.analyzed} functions analyzed | avg cognitive: ${s.avgCognitive} | avg cyclomatic: ${s.avgCyclomatic}${miPart} | ${s.aboveWarn} above threshold`,
1114
- );
1115
- }
1116
- console.log();
1117
- }
package/src/cycles.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { isTestFile } from './infrastructure/test-filter.js';
1
2
  import { loadNative } from './native.js';
2
- import { isTestFile } from './test-filter.js';
3
3
 
4
4
  /**
5
5
  * Detect circular dependencies in the codebase using Tarjan's SCC algorithm.