@optave/codegraph 2.5.1 → 3.0.0

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/cochange.js CHANGED
@@ -11,6 +11,7 @@ import path from 'node:path';
11
11
  import { normalizePath } from './constants.js';
12
12
  import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
13
13
  import { warn } from './logger.js';
14
+ import { paginateResult } from './paginate.js';
14
15
  import { isTestFile } from './queries.js';
15
16
 
16
17
  /**
@@ -313,7 +314,8 @@ export function coChangeData(file, customDbPath, opts = {}) {
313
314
  const meta = getCoChangeMeta(db);
314
315
  closeDb(db);
315
316
 
316
- return { file: resolvedFile, partners, meta };
317
+ const base = { file: resolvedFile, partners, meta };
318
+ return paginateResult(base, 'partners', { limit: opts.limit, offset: opts.offset });
317
319
  }
318
320
 
319
321
  /**
@@ -365,7 +367,8 @@ export function coChangeTopData(customDbPath, opts = {}) {
365
367
  const meta = getCoChangeMeta(db);
366
368
  closeDb(db);
367
369
 
368
- return { pairs, meta };
370
+ const base = { pairs, meta };
371
+ return paginateResult(base, 'pairs', { limit: opts.limit, offset: opts.offset });
369
372
  }
370
373
 
371
374
  /**
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import Graph from 'graphology';
3
3
  import louvain from 'graphology-communities-louvain';
4
4
  import { openReadonlyOrFail } from './db.js';
5
+ import { paginateResult, printNdjson } from './paginate.js';
5
6
  import { isTestFile } from './queries.js';
6
7
 
7
8
  // ─── Graph Construction ───────────────────────────────────────────────
@@ -201,7 +202,7 @@ export function communitiesData(customDbPath, opts = {}) {
201
202
 
202
203
  const driftScore = Math.round(((splitRatio + mergeRatio) / 2) * 100);
203
204
 
204
- return {
205
+ const base = {
205
206
  communities: opts.drift ? [] : communities,
206
207
  modularity: +modularity.toFixed(4),
207
208
  drift: { splitCandidates, mergeCandidates },
@@ -212,6 +213,7 @@ export function communitiesData(customDbPath, opts = {}) {
212
213
  driftScore,
213
214
  },
214
215
  };
216
+ return paginateResult(base, 'communities', { limit: opts.limit, offset: opts.offset });
215
217
  }
216
218
 
217
219
  /**
@@ -238,6 +240,10 @@ export function communitySummaryForStats(customDbPath, opts = {}) {
238
240
  export function communities(customDbPath, opts = {}) {
239
241
  const data = communitiesData(customDbPath, opts);
240
242
 
243
+ if (opts.ndjson) {
244
+ printNdjson(data, 'communities');
245
+ return;
246
+ }
241
247
  if (opts.json) {
242
248
  console.log(JSON.stringify(data, null, 2));
243
249
  return;
package/src/complexity.js CHANGED
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { loadConfig } from './config.js';
4
4
  import { openReadonlyOrFail } from './db.js';
5
5
  import { info } from './logger.js';
6
+ import { paginateResult, printNdjson } from './paginate.js';
6
7
  import { LANGUAGE_REGISTRY } from './parser.js';
7
8
  import { isTestFile } from './queries.js';
8
9
 
@@ -182,7 +183,7 @@ const CSHARP_RULES = {
182
183
  'if_statement',
183
184
  'else_clause',
184
185
  'for_statement',
185
- 'for_each_statement',
186
+ 'foreach_statement',
186
187
  'while_statement',
187
188
  'do_statement',
188
189
  'catch_clause',
@@ -196,7 +197,7 @@ const CSHARP_RULES = {
196
197
  nestingNodes: new Set([
197
198
  'if_statement',
198
199
  'for_statement',
199
- 'for_each_statement',
200
+ 'foreach_statement',
200
201
  'while_statement',
201
202
  'do_statement',
202
203
  'catch_clause',
@@ -210,9 +211,9 @@ const CSHARP_RULES = {
210
211
  'local_function_statement',
211
212
  ]),
212
213
  ifNodeType: 'if_statement',
213
- elseNodeType: 'else_clause',
214
+ elseNodeType: null,
214
215
  elifNodeType: null,
215
- elseViaAlternative: false,
216
+ elseViaAlternative: true,
216
217
  switchLikeNodes: new Set(['switch_statement']),
217
218
  };
218
219
 
@@ -290,7 +291,7 @@ export const COMPLEXITY_RULES = new Map([
290
291
  ['go', GO_RULES],
291
292
  ['rust', RUST_RULES],
292
293
  ['java', JAVA_RULES],
293
- ['c_sharp', CSHARP_RULES],
294
+ ['csharp', CSHARP_RULES],
294
295
  ['ruby', RUBY_RULES],
295
296
  ['php', PHP_RULES],
296
297
  ]);
@@ -1025,7 +1026,7 @@ export const HALSTEAD_RULES = new Map([
1025
1026
  ['go', GO_HALSTEAD],
1026
1027
  ['rust', RUST_HALSTEAD],
1027
1028
  ['java', JAVA_HALSTEAD],
1028
- ['c_sharp', CSHARP_HALSTEAD],
1029
+ ['csharp', CSHARP_HALSTEAD],
1029
1030
  ['ruby', RUBY_HALSTEAD],
1030
1031
  ['php', PHP_HALSTEAD],
1031
1032
  ]);
@@ -1115,7 +1116,7 @@ const COMMENT_PREFIXES = new Map([
1115
1116
  ['go', C_STYLE_PREFIXES],
1116
1117
  ['rust', C_STYLE_PREFIXES],
1117
1118
  ['java', C_STYLE_PREFIXES],
1118
- ['c_sharp', C_STYLE_PREFIXES],
1119
+ ['csharp', C_STYLE_PREFIXES],
1119
1120
  ['python', ['#']],
1120
1121
  ['ruby', ['#']],
1121
1122
  ['php', ['//', '#', '/*', '*', '*/']],
@@ -1573,7 +1574,7 @@ export function computeAllMetrics(functionNode, langId) {
1573
1574
  /**
1574
1575
  * Find the function body node in a parse tree that matches a given line range.
1575
1576
  */
1576
- function findFunctionNode(rootNode, startLine, _endLine, rules) {
1577
+ export function findFunctionNode(rootNode, startLine, _endLine, rules) {
1577
1578
  // tree-sitter lines are 0-indexed
1578
1579
  const targetStart = startLine - 1;
1579
1580
 
@@ -1799,7 +1800,6 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
1799
1800
  */
1800
1801
  export function complexityData(customDbPath, opts = {}) {
1801
1802
  const db = openReadonlyOrFail(customDbPath);
1802
- const limit = opts.limit || 20;
1803
1803
  const sort = opts.sort || 'cognitive';
1804
1804
  const noTests = opts.noTests || false;
1805
1805
  const aboveThreshold = opts.aboveThreshold || false;
@@ -1887,13 +1887,19 @@ export function complexityData(customDbPath, opts = {}) {
1887
1887
  FROM function_complexity fc
1888
1888
  JOIN nodes n ON fc.node_id = n.id
1889
1889
  ${where} ${having}
1890
- ORDER BY ${orderBy}
1891
- LIMIT ?`,
1890
+ ORDER BY ${orderBy}`,
1892
1891
  )
1893
- .all(...params, limit);
1892
+ .all(...params);
1894
1893
  } catch {
1894
+ // Check if graph has nodes even though complexity table is missing/empty
1895
+ let hasGraph = false;
1896
+ try {
1897
+ hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
1898
+ } catch {
1899
+ /* ignore */
1900
+ }
1895
1901
  db.close();
1896
- return { functions: [], summary: null, thresholds };
1902
+ return { functions: [], summary: null, thresholds, hasGraph };
1897
1903
  }
1898
1904
 
1899
1905
  // Post-filter test files if needed (belt-and-suspenders for isTestFile)
@@ -1979,8 +1985,99 @@ export function complexityData(customDbPath, opts = {}) {
1979
1985
  /* ignore */
1980
1986
  }
1981
1987
 
1988
+ // When summary is null (no complexity rows), check if graph has nodes
1989
+ let hasGraph = false;
1990
+ if (summary === null) {
1991
+ try {
1992
+ hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
1993
+ } catch {
1994
+ /* ignore */
1995
+ }
1996
+ }
1997
+
1982
1998
  db.close();
1983
- return { functions, summary, thresholds };
1999
+ const base = { functions, summary, thresholds, hasGraph };
2000
+ return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
2001
+ }
2002
+
2003
+ /**
2004
+ * Generator: stream complexity rows one-by-one using .iterate() for memory efficiency.
2005
+ * @param {string} [customDbPath]
2006
+ * @param {object} [opts]
2007
+ * @param {boolean} [opts.noTests]
2008
+ * @param {string} [opts.file]
2009
+ * @param {string} [opts.target]
2010
+ * @param {string} [opts.kind]
2011
+ * @param {string} [opts.sort]
2012
+ * @yields {{ name: string, kind: string, file: string, line: number, cognitive: number, cyclomatic: number, maxNesting: number, loc: number, sloc: number }}
2013
+ */
2014
+ export function* iterComplexity(customDbPath, opts = {}) {
2015
+ const db = openReadonlyOrFail(customDbPath);
2016
+ try {
2017
+ const noTests = opts.noTests || false;
2018
+ const sort = opts.sort || 'cognitive';
2019
+
2020
+ let where = "WHERE n.kind IN ('function','method')";
2021
+ const params = [];
2022
+
2023
+ if (noTests) {
2024
+ where += ` AND n.file NOT LIKE '%.test.%'
2025
+ AND n.file NOT LIKE '%.spec.%'
2026
+ AND n.file NOT LIKE '%__test__%'
2027
+ AND n.file NOT LIKE '%__tests__%'
2028
+ AND n.file NOT LIKE '%.stories.%'`;
2029
+ }
2030
+ if (opts.target) {
2031
+ where += ' AND n.name LIKE ?';
2032
+ params.push(`%${opts.target}%`);
2033
+ }
2034
+ if (opts.file) {
2035
+ where += ' AND n.file LIKE ?';
2036
+ params.push(`%${opts.file}%`);
2037
+ }
2038
+ if (opts.kind) {
2039
+ where += ' AND n.kind = ?';
2040
+ params.push(opts.kind);
2041
+ }
2042
+
2043
+ const orderMap = {
2044
+ cognitive: 'fc.cognitive DESC',
2045
+ cyclomatic: 'fc.cyclomatic DESC',
2046
+ nesting: 'fc.max_nesting DESC',
2047
+ mi: 'fc.maintainability_index ASC',
2048
+ volume: 'fc.halstead_volume DESC',
2049
+ effort: 'fc.halstead_effort DESC',
2050
+ bugs: 'fc.halstead_bugs DESC',
2051
+ loc: 'fc.loc DESC',
2052
+ };
2053
+ const orderBy = orderMap[sort] || 'fc.cognitive DESC';
2054
+
2055
+ const stmt = db.prepare(
2056
+ `SELECT n.name, n.kind, n.file, n.line, n.end_line,
2057
+ fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.loc, fc.sloc
2058
+ FROM function_complexity fc
2059
+ JOIN nodes n ON fc.node_id = n.id
2060
+ ${where}
2061
+ ORDER BY ${orderBy}`,
2062
+ );
2063
+ for (const r of stmt.iterate(...params)) {
2064
+ if (noTests && isTestFile(r.file)) continue;
2065
+ yield {
2066
+ name: r.name,
2067
+ kind: r.kind,
2068
+ file: r.file,
2069
+ line: r.line,
2070
+ endLine: r.end_line || null,
2071
+ cognitive: r.cognitive,
2072
+ cyclomatic: r.cyclomatic,
2073
+ maxNesting: r.max_nesting,
2074
+ loc: r.loc || 0,
2075
+ sloc: r.sloc || 0,
2076
+ };
2077
+ }
2078
+ } finally {
2079
+ db.close();
2080
+ }
1984
2081
  }
1985
2082
 
1986
2083
  /**
@@ -1989,6 +2086,10 @@ export function complexityData(customDbPath, opts = {}) {
1989
2086
  export function complexity(customDbPath, opts = {}) {
1990
2087
  const data = complexityData(customDbPath, opts);
1991
2088
 
2089
+ if (opts.ndjson) {
2090
+ printNdjson(data, 'functions');
2091
+ return;
2092
+ }
1992
2093
  if (opts.json) {
1993
2094
  console.log(JSON.stringify(data, null, 2));
1994
2095
  return;
@@ -1996,9 +2097,15 @@ export function complexity(customDbPath, opts = {}) {
1996
2097
 
1997
2098
  if (data.functions.length === 0) {
1998
2099
  if (data.summary === null) {
1999
- console.log(
2000
- '\nNo complexity data found. Run "codegraph build" first to analyze your codebase.\n',
2001
- );
2100
+ if (data.hasGraph) {
2101
+ console.log(
2102
+ '\nNo complexity data found, but a graph exists. Run "codegraph build --no-incremental" to populate complexity metrics.\n',
2103
+ );
2104
+ } else {
2105
+ console.log(
2106
+ '\nNo complexity data found. Run "codegraph build" first to analyze your codebase.\n',
2107
+ );
2108
+ }
2002
2109
  } else {
2003
2110
  console.log('\nNo functions match the given filters.\n');
2004
2111
  }
package/src/config.js CHANGED
@@ -14,6 +14,7 @@ export const DEFAULTS = {
14
14
  build: {
15
15
  incremental: true,
16
16
  dbPath: '.codegraph/graph.db',
17
+ driftThreshold: 0.2,
17
18
  },
18
19
  query: {
19
20
  defaultDepth: 3,
@@ -36,7 +37,16 @@ export const DEFAULTS = {
36
37
  fanIn: { warn: null, fail: null },
37
38
  fanOut: { warn: null, fail: null },
38
39
  noCycles: { warn: null, fail: null },
40
+ boundaries: { warn: null, fail: null },
39
41
  },
42
+ boundaries: null,
43
+ },
44
+ check: {
45
+ cycles: true,
46
+ blastRadius: null,
47
+ signatures: true,
48
+ boundaries: true,
49
+ depth: 3,
40
50
  },
41
51
  coChange: {
42
52
  since: '1 year ago',