@optave/codegraph 3.1.1 → 3.1.3

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 (72) hide show
  1. package/README.md +6 -6
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +365 -0
  4. package/src/ast-analysis/metrics.js +118 -0
  5. package/src/ast-analysis/visitor-utils.js +176 -0
  6. package/src/ast-analysis/visitor.js +162 -0
  7. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  8. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  9. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  10. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  11. package/src/ast.js +13 -140
  12. package/src/audit.js +2 -87
  13. package/src/batch.js +0 -25
  14. package/src/boundaries.js +1 -1
  15. package/src/branch-compare.js +1 -96
  16. package/src/builder.js +60 -178
  17. package/src/cfg.js +89 -883
  18. package/src/check.js +1 -84
  19. package/src/cli.js +31 -22
  20. package/src/cochange.js +1 -39
  21. package/src/commands/audit.js +88 -0
  22. package/src/commands/batch.js +26 -0
  23. package/src/commands/branch-compare.js +97 -0
  24. package/src/commands/cfg.js +55 -0
  25. package/src/commands/check.js +82 -0
  26. package/src/commands/cochange.js +37 -0
  27. package/src/commands/communities.js +69 -0
  28. package/src/commands/complexity.js +77 -0
  29. package/src/commands/dataflow.js +110 -0
  30. package/src/commands/flow.js +70 -0
  31. package/src/commands/manifesto.js +77 -0
  32. package/src/commands/owners.js +52 -0
  33. package/src/commands/query.js +21 -0
  34. package/src/commands/sequence.js +33 -0
  35. package/src/commands/structure.js +64 -0
  36. package/src/commands/triage.js +49 -0
  37. package/src/communities.js +12 -83
  38. package/src/complexity.js +43 -357
  39. package/src/cycles.js +1 -1
  40. package/src/dataflow.js +12 -665
  41. package/src/db/repository/build-stmts.js +104 -0
  42. package/src/db/repository/cached-stmt.js +19 -0
  43. package/src/db/repository/cfg.js +72 -0
  44. package/src/db/repository/cochange.js +54 -0
  45. package/src/db/repository/complexity.js +20 -0
  46. package/src/db/repository/dataflow.js +17 -0
  47. package/src/db/repository/edges.js +281 -0
  48. package/src/db/repository/embeddings.js +51 -0
  49. package/src/db/repository/graph-read.js +59 -0
  50. package/src/db/repository/index.js +43 -0
  51. package/src/db/repository/nodes.js +247 -0
  52. package/src/db.js +40 -1
  53. package/src/embedder.js +14 -34
  54. package/src/export.js +1 -1
  55. package/src/extractors/javascript.js +130 -5
  56. package/src/flow.js +2 -70
  57. package/src/index.js +30 -20
  58. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  59. package/src/kinds.js +1 -0
  60. package/src/manifesto.js +0 -76
  61. package/src/native.js +31 -9
  62. package/src/owners.js +1 -56
  63. package/src/parser.js +53 -2
  64. package/src/queries-cli.js +1 -1
  65. package/src/queries.js +79 -280
  66. package/src/sequence.js +5 -44
  67. package/src/structure.js +16 -75
  68. package/src/triage.js +1 -54
  69. package/src/viewer.js +1 -1
  70. package/src/watcher.js +7 -4
  71. package/src/db/repository.js +0 -134
  72. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/README.md CHANGED
@@ -562,14 +562,14 @@ Self-measured on every release via CI ([build benchmarks](generated/benchmarks/B
562
562
 
563
563
  | Metric | Latest |
564
564
  |---|---|
565
- | Build speed (native) | **6.1 ms/file** |
566
- | Build speed (WASM) | **16.5 ms/file** |
567
- | Query time | **3ms** |
568
- | No-op rebuild (native) | **5ms** |
569
- | 1-file rebuild (native) | **332ms** |
565
+ | Build speed (native) | **5.2 ms/file** |
566
+ | Build speed (WASM) | **15 ms/file** |
567
+ | Query time | **4ms** |
568
+ | No-op rebuild (native) | **6ms** |
569
+ | 1-file rebuild (native) | **296ms** |
570
570
  | Query: fn-deps | **0.8ms** |
571
571
  | Query: path | **0.8ms** |
572
- | ~50,000 files (est.) | **~305.0s build** |
572
+ | ~50,000 files (est.) | **~260.0s build** |
573
573
 
574
574
  Metrics are normalized per file for cross-version comparability. Times above are for a full initial build — incremental rebuilds only re-parse changed files.
575
575
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optave/codegraph",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -71,12 +71,12 @@
71
71
  },
72
72
  "optionalDependencies": {
73
73
  "@modelcontextprotocol/sdk": "^1.0.0",
74
- "@optave/codegraph-darwin-arm64": "3.1.1",
75
- "@optave/codegraph-darwin-x64": "3.1.1",
76
- "@optave/codegraph-linux-arm64-gnu": "3.1.1",
77
- "@optave/codegraph-linux-x64-gnu": "3.1.1",
78
- "@optave/codegraph-linux-x64-musl": "3.1.1",
79
- "@optave/codegraph-win32-x64-msvc": "3.1.1"
74
+ "@optave/codegraph-darwin-arm64": "3.1.3",
75
+ "@optave/codegraph-darwin-x64": "3.1.3",
76
+ "@optave/codegraph-linux-arm64-gnu": "3.1.3",
77
+ "@optave/codegraph-linux-x64-gnu": "3.1.3",
78
+ "@optave/codegraph-linux-x64-musl": "3.1.3",
79
+ "@optave/codegraph-win32-x64-msvc": "3.1.3"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@biomejs/biome": "^2.4.4",
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Unified AST analysis engine — orchestrates all analysis passes in one file-iteration loop.
3
+ *
4
+ * Replaces the 4 sequential buildXxx calls in builder.js with a single coordinated pass:
5
+ * - AST node extraction (calls, new, string, regex, throw, await)
6
+ * - Complexity metrics (cognitive, cyclomatic, nesting, Halstead, MI)
7
+ * - CFG construction (basic blocks + edges)
8
+ * - Dataflow analysis (define-use chains, arg flows, mutations)
9
+ *
10
+ * All 4 analyses run as visitors in a single DFS walk via walkWithVisitors.
11
+ *
12
+ * Optimization strategy: for files with WASM trees, run all applicable visitors
13
+ * in a single walkWithVisitors call. Store results in the format that buildXxx
14
+ * functions already expect as pre-computed data (same fields as native engine
15
+ * output). This eliminates redundant tree traversals per file.
16
+ */
17
+
18
+ import path from 'node:path';
19
+ import { performance } from 'node:perf_hooks';
20
+ import { bulkNodeIdsByFile } from '../db.js';
21
+ import { debug } from '../logger.js';
22
+ import { computeLOCMetrics, computeMaintainabilityIndex } from './metrics.js';
23
+ import {
24
+ AST_TYPE_MAPS,
25
+ CFG_RULES,
26
+ COMPLEXITY_RULES,
27
+ DATAFLOW_RULES,
28
+ HALSTEAD_RULES,
29
+ } from './rules/index.js';
30
+ import { buildExtensionSet, buildExtToLangMap } from './shared.js';
31
+ import { walkWithVisitors } from './visitor.js';
32
+ import { functionName as getFuncName } from './visitor-utils.js';
33
+ import { createAstStoreVisitor } from './visitors/ast-store-visitor.js';
34
+ import { createCfgVisitor } from './visitors/cfg-visitor.js';
35
+ import { createComplexityVisitor } from './visitors/complexity-visitor.js';
36
+ import { createDataflowVisitor } from './visitors/dataflow-visitor.js';
37
+
38
+ // ─── Extension sets for quick language-support checks ────────────────────
39
+
40
+ const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
41
+ const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
42
+ const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
43
+
44
+ // ─── Lazy imports (heavy modules loaded only when needed) ────────────────
45
+
46
+ let _parserModule = null;
47
+ async function getParserModule() {
48
+ if (!_parserModule) _parserModule = await import('../parser.js');
49
+ return _parserModule;
50
+ }
51
+
52
+ // ─── Public API ──────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Run all enabled AST analyses in a coordinated pass.
56
+ *
57
+ * @param {object} db - open better-sqlite3 database (read-write)
58
+ * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, calls, _tree, _langId, ... }>
59
+ * @param {string} rootDir - absolute project root path
60
+ * @param {object} opts - build options (ast, complexity, cfg, dataflow toggles)
61
+ * @param {object} [engineOpts] - engine options
62
+ * @returns {Promise<{ astMs: number, complexityMs: number, cfgMs: number, dataflowMs: number }>}
63
+ */
64
+ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
65
+ const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
66
+
67
+ const doAst = opts.ast !== false;
68
+ const doComplexity = opts.complexity !== false;
69
+ const doCfg = opts.cfg !== false;
70
+ const doDataflow = opts.dataflow !== false;
71
+
72
+ if (!doAst && !doComplexity && !doCfg && !doDataflow) return timing;
73
+
74
+ const extToLang = buildExtToLangMap();
75
+
76
+ // ── WASM pre-parse for files that need it ───────────────────────────
77
+ // CFG now runs as a visitor in the unified walk, so only dataflow
78
+ // triggers WASM pre-parse when no tree exists.
79
+ if (doDataflow) {
80
+ let needsWasmTrees = false;
81
+ for (const [relPath, symbols] of fileSymbols) {
82
+ if (symbols._tree) continue;
83
+ const ext = path.extname(relPath).toLowerCase();
84
+
85
+ if (!symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext)) {
86
+ needsWasmTrees = true;
87
+ break;
88
+ }
89
+ }
90
+
91
+ if (needsWasmTrees) {
92
+ try {
93
+ const { ensureWasmTrees } = await getParserModule();
94
+ await ensureWasmTrees(fileSymbols, rootDir);
95
+ } catch (err) {
96
+ debug(`ensureWasmTrees failed: ${err.message}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ // ── Phase 7 Optimization: Unified pre-walk ─────────────────────────
102
+ // For files with WASM trees, run all applicable visitors in a SINGLE
103
+ // walkWithVisitors call. Store results in the format that buildXxx
104
+ // functions already expect as pre-computed data (same fields as native
105
+ // engine output). This eliminates ~3 redundant tree traversals per file.
106
+ const t0walk = performance.now();
107
+
108
+ for (const [relPath, symbols] of fileSymbols) {
109
+ if (!symbols._tree) continue; // No WASM tree — native path handles it
110
+
111
+ const ext = path.extname(relPath).toLowerCase();
112
+ const langId = symbols._langId || extToLang.get(ext);
113
+ if (!langId) continue;
114
+
115
+ const defs = symbols.definitions || [];
116
+ const visitors = [];
117
+ const walkerOpts = {
118
+ functionNodeTypes: new Set(),
119
+ nestingNodeTypes: new Set(),
120
+ getFunctionName: (_node) => null,
121
+ };
122
+
123
+ // ─ AST-store visitor ─
124
+ const astTypeMap = AST_TYPE_MAPS.get(langId);
125
+ let astVisitor = null;
126
+ if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !symbols.astNodes?.length) {
127
+ const nodeIdMap = new Map();
128
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
129
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
130
+ }
131
+ astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
132
+ visitors.push(astVisitor);
133
+ }
134
+
135
+ // ─ Complexity visitor (file-level mode) ─
136
+ const cRules = COMPLEXITY_RULES.get(langId);
137
+ const hRules = HALSTEAD_RULES.get(langId);
138
+ let complexityVisitor = null;
139
+ if (doComplexity && cRules) {
140
+ // Only use visitor if some functions lack pre-computed complexity
141
+ const needsWasmComplexity = defs.some(
142
+ (d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity,
143
+ );
144
+ if (needsWasmComplexity) {
145
+ complexityVisitor = createComplexityVisitor(cRules, hRules, {
146
+ fileLevelWalk: true,
147
+ langId,
148
+ });
149
+ visitors.push(complexityVisitor);
150
+
151
+ // Merge nesting nodes for complexity tracking
152
+ // NOTE: do NOT add functionNodes here — funcDepth in the complexity
153
+ // visitor already tracks function-level nesting. Adding them to
154
+ // nestingNodeTypes would inflate context.nestingLevel by +1 inside
155
+ // every function body, double-counting in cognitive += 1 + nestingLevel.
156
+ for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes.add(t);
157
+
158
+ // Provide getFunctionName for complexity visitor
159
+ const dfRules = DATAFLOW_RULES.get(langId);
160
+ walkerOpts.getFunctionName = (node) => {
161
+ // Try complexity rules' function name field first
162
+ const nameNode = node.childForFieldName('name');
163
+ if (nameNode) return nameNode.text;
164
+ // Fall back to dataflow rules' richer name extraction
165
+ if (dfRules) return getFuncName(node, dfRules);
166
+ return null;
167
+ };
168
+ }
169
+ }
170
+
171
+ // ─ CFG visitor ─
172
+ const cfgRulesForLang = CFG_RULES.get(langId);
173
+ let cfgVisitor = null;
174
+ if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
175
+ // Only use visitor if some functions lack pre-computed CFG
176
+ const needsWasmCfg = defs.some(
177
+ (d) =>
178
+ (d.kind === 'function' || d.kind === 'method') &&
179
+ d.line &&
180
+ d.cfg !== null &&
181
+ !Array.isArray(d.cfg?.blocks),
182
+ );
183
+ if (needsWasmCfg) {
184
+ cfgVisitor = createCfgVisitor(cfgRulesForLang);
185
+ visitors.push(cfgVisitor);
186
+ }
187
+ }
188
+
189
+ // ─ Dataflow visitor ─
190
+ const dfRules = DATAFLOW_RULES.get(langId);
191
+ let dataflowVisitor = null;
192
+ if (doDataflow && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
193
+ dataflowVisitor = createDataflowVisitor(dfRules);
194
+ visitors.push(dataflowVisitor);
195
+ }
196
+
197
+ // ─ Run unified walk if we have visitors ─
198
+ if (visitors.length === 0) continue;
199
+
200
+ const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
201
+
202
+ // ─ Store AST results (buildAstNodes will find symbols.astNodes and skip its walk) ─
203
+ if (astVisitor) {
204
+ const astRows = results['ast-store'] || [];
205
+ if (astRows.length > 0) {
206
+ // Store in the format buildAstNodes expects for the native path
207
+ symbols.astNodes = astRows;
208
+ }
209
+ }
210
+
211
+ // ─ Store complexity results on definitions (buildComplexityMetrics will find def.complexity) ─
212
+ if (complexityVisitor) {
213
+ const complexityResults = results.complexity || [];
214
+ // Match results back to definitions by function start line
215
+ // Store the full result (metrics + funcNode) for O(1) lookup
216
+ const resultByLine = new Map();
217
+ for (const r of complexityResults) {
218
+ if (r.funcNode) {
219
+ const line = r.funcNode.startPosition.row + 1;
220
+ if (!resultByLine.has(line)) resultByLine.set(line, []);
221
+ resultByLine.get(line).push(r);
222
+ }
223
+ }
224
+ for (const def of defs) {
225
+ if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
226
+ const candidates = resultByLine.get(def.line);
227
+ const funcResult = !candidates
228
+ ? undefined
229
+ : candidates.length === 1
230
+ ? candidates[0]
231
+ : (candidates.find((r) => {
232
+ const n = r.funcNode.childForFieldName('name');
233
+ return n && n.text === def.name;
234
+ }) ?? candidates[0]);
235
+ if (funcResult) {
236
+ const { metrics } = funcResult;
237
+ const loc = computeLOCMetrics(funcResult.funcNode, langId);
238
+ const volume = metrics.halstead ? metrics.halstead.volume : 0;
239
+ const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
240
+ const mi = computeMaintainabilityIndex(
241
+ volume,
242
+ metrics.cyclomatic,
243
+ loc.sloc,
244
+ commentRatio,
245
+ );
246
+
247
+ def.complexity = {
248
+ cognitive: metrics.cognitive,
249
+ cyclomatic: metrics.cyclomatic,
250
+ maxNesting: metrics.maxNesting,
251
+ halstead: metrics.halstead,
252
+ loc,
253
+ maintainabilityIndex: mi,
254
+ };
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ // ─ Store CFG results on definitions (buildCFGData will find def.cfg and skip its walk) ─
261
+ if (cfgVisitor) {
262
+ const cfgResults = results.cfg || [];
263
+ const cfgByLine = new Map();
264
+ for (const r of cfgResults) {
265
+ if (r.funcNode) {
266
+ const line = r.funcNode.startPosition.row + 1;
267
+ if (!cfgByLine.has(line)) cfgByLine.set(line, []);
268
+ cfgByLine.get(line).push(r);
269
+ }
270
+ }
271
+ for (const def of defs) {
272
+ if (
273
+ (def.kind === 'function' || def.kind === 'method') &&
274
+ def.line &&
275
+ !def.cfg?.blocks?.length
276
+ ) {
277
+ const candidates = cfgByLine.get(def.line);
278
+ const cfgResult = !candidates
279
+ ? undefined
280
+ : candidates.length === 1
281
+ ? candidates[0]
282
+ : (candidates.find((r) => {
283
+ const n = r.funcNode.childForFieldName('name');
284
+ return n && n.text === def.name;
285
+ }) ?? candidates[0]);
286
+ if (cfgResult) {
287
+ def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
288
+
289
+ // Override complexity's cyclomatic with CFG-derived value (single source of truth)
290
+ // and recompute maintainability index to stay consistent
291
+ if (def.complexity && cfgResult.cyclomatic != null) {
292
+ def.complexity.cyclomatic = cfgResult.cyclomatic;
293
+ const { loc, halstead } = def.complexity;
294
+ const volume = halstead ? halstead.volume : 0;
295
+ const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
296
+ def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
297
+ volume,
298
+ cfgResult.cyclomatic,
299
+ loc?.sloc ?? 0,
300
+ commentRatio,
301
+ );
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ // ─ Store dataflow results (buildDataflowEdges will find symbols.dataflow and skip its walk) ─
309
+ if (dataflowVisitor) {
310
+ symbols.dataflow = results.dataflow;
311
+ }
312
+ }
313
+
314
+ timing._unifiedWalkMs = performance.now() - t0walk;
315
+
316
+ // ── Delegate to buildXxx functions ─────────────────────────────────
317
+ // Each function finds pre-computed data from the unified walk above
318
+ // (or from the native engine) and only does DB writes + native fallback.
319
+
320
+ if (doAst) {
321
+ const t0 = performance.now();
322
+ try {
323
+ const { buildAstNodes } = await import('../ast.js');
324
+ await buildAstNodes(db, fileSymbols, rootDir, engineOpts);
325
+ } catch (err) {
326
+ debug(`buildAstNodes failed: ${err.message}`);
327
+ }
328
+ timing.astMs = performance.now() - t0;
329
+ }
330
+
331
+ if (doComplexity) {
332
+ const t0 = performance.now();
333
+ try {
334
+ const { buildComplexityMetrics } = await import('../complexity.js');
335
+ await buildComplexityMetrics(db, fileSymbols, rootDir, engineOpts);
336
+ } catch (err) {
337
+ debug(`buildComplexityMetrics failed: ${err.message}`);
338
+ }
339
+ timing.complexityMs = performance.now() - t0;
340
+ }
341
+
342
+ if (doCfg) {
343
+ const t0 = performance.now();
344
+ try {
345
+ const { buildCFGData } = await import('../cfg.js');
346
+ await buildCFGData(db, fileSymbols, rootDir, engineOpts);
347
+ } catch (err) {
348
+ debug(`buildCFGData failed: ${err.message}`);
349
+ }
350
+ timing.cfgMs = performance.now() - t0;
351
+ }
352
+
353
+ if (doDataflow) {
354
+ const t0 = performance.now();
355
+ try {
356
+ const { buildDataflowEdges } = await import('../dataflow.js');
357
+ await buildDataflowEdges(db, fileSymbols, rootDir, engineOpts);
358
+ } catch (err) {
359
+ debug(`buildDataflowEdges failed: ${err.message}`);
360
+ }
361
+ timing.dataflowMs = performance.now() - t0;
362
+ }
363
+
364
+ return timing;
365
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Pure metric computations extracted from complexity.js.
3
+ *
4
+ * Contains Halstead derived metrics, LOC metrics, and Maintainability Index —
5
+ * all stateless math that can be reused by visitor-based and standalone paths.
6
+ */
7
+
8
+ // ─── Halstead Derived Metrics ─────────────────────────────────────────────
9
+
10
+ /**
11
+ * Compute Halstead derived metrics from raw operator/operand counts.
12
+ *
13
+ * @param {Map<string, number>} operators - operator type/text → count
14
+ * @param {Map<string, number>} operands - operand text → count
15
+ * @returns {{ n1: number, n2: number, bigN1: number, bigN2: number, vocabulary: number, length: number, volume: number, difficulty: number, effort: number, bugs: number }}
16
+ */
17
+ export function computeHalsteadDerived(operators, operands) {
18
+ const n1 = operators.size;
19
+ const n2 = operands.size;
20
+ let bigN1 = 0;
21
+ for (const c of operators.values()) bigN1 += c;
22
+ let bigN2 = 0;
23
+ for (const c of operands.values()) bigN2 += c;
24
+
25
+ const vocabulary = n1 + n2;
26
+ const length = bigN1 + bigN2;
27
+ const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
28
+ const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
29
+ const effort = difficulty * volume;
30
+ const bugs = volume / 3000;
31
+
32
+ return {
33
+ n1,
34
+ n2,
35
+ bigN1,
36
+ bigN2,
37
+ vocabulary,
38
+ length,
39
+ volume: +volume.toFixed(2),
40
+ difficulty: +difficulty.toFixed(2),
41
+ effort: +effort.toFixed(2),
42
+ bugs: +bugs.toFixed(4),
43
+ };
44
+ }
45
+
46
+ // ─── LOC Metrics ──────────────────────────────────────────────────────────
47
+
48
+ const C_STYLE_PREFIXES = ['//', '/*', '*', '*/'];
49
+
50
+ const COMMENT_PREFIXES = new Map([
51
+ ['javascript', C_STYLE_PREFIXES],
52
+ ['typescript', C_STYLE_PREFIXES],
53
+ ['tsx', C_STYLE_PREFIXES],
54
+ ['go', C_STYLE_PREFIXES],
55
+ ['rust', C_STYLE_PREFIXES],
56
+ ['java', C_STYLE_PREFIXES],
57
+ ['csharp', C_STYLE_PREFIXES],
58
+ ['python', ['#']],
59
+ ['ruby', ['#']],
60
+ ['php', ['//', '#', '/*', '*', '*/']],
61
+ ]);
62
+
63
+ /**
64
+ * Compute LOC metrics from a function node's source text.
65
+ *
66
+ * @param {object} functionNode - tree-sitter node
67
+ * @param {string} [language] - Language ID (falls back to C-style prefixes)
68
+ * @returns {{ loc: number, sloc: number, commentLines: number }}
69
+ */
70
+ export function computeLOCMetrics(functionNode, language) {
71
+ const text = functionNode.text;
72
+ const lines = text.split('\n');
73
+ const loc = lines.length;
74
+ const prefixes = (language && COMMENT_PREFIXES.get(language)) || C_STYLE_PREFIXES;
75
+
76
+ let commentLines = 0;
77
+ let blankLines = 0;
78
+
79
+ for (const line of lines) {
80
+ const trimmed = line.trim();
81
+ if (trimmed === '') {
82
+ blankLines++;
83
+ } else if (prefixes.some((p) => trimmed.startsWith(p))) {
84
+ commentLines++;
85
+ }
86
+ }
87
+
88
+ const sloc = Math.max(1, loc - blankLines - commentLines);
89
+ return { loc, sloc, commentLines };
90
+ }
91
+
92
+ // ─── Maintainability Index ────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Compute normalized Maintainability Index (0-100 scale).
96
+ *
97
+ * Original SEI formula: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
98
+ * Microsoft normalization: max(0, min(100, MI * 100/171))
99
+ *
100
+ * @param {number} volume - Halstead volume
101
+ * @param {number} cyclomatic - Cyclomatic complexity
102
+ * @param {number} sloc - Source lines of code
103
+ * @param {number} [commentRatio] - Comment ratio (0-1), optional
104
+ * @returns {number} Normalized MI (0-100)
105
+ */
106
+ export function computeMaintainabilityIndex(volume, cyclomatic, sloc, commentRatio) {
107
+ const safeVolume = Math.max(volume, 1);
108
+ const safeSLOC = Math.max(sloc, 1);
109
+
110
+ let mi = 171 - 5.2 * Math.log(safeVolume) - 0.23 * cyclomatic - 16.2 * Math.log(safeSLOC);
111
+
112
+ if (commentRatio != null && commentRatio > 0) {
113
+ mi += 50 * Math.sin(Math.sqrt(2.4 * commentRatio));
114
+ }
115
+
116
+ const normalized = Math.max(0, Math.min(100, (mi * 100) / 171));
117
+ return +normalized.toFixed(1);
118
+ }