@optave/codegraph 3.1.5 → 3.2.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/README.md +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- package/src/shared/kinds.js +1 -1
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
import { walkWithVisitors } from '../ast-analysis/visitor.js';
|
|
14
14
|
import { createComplexityVisitor } from '../ast-analysis/visitors/complexity-visitor.js';
|
|
15
15
|
import { getFunctionNodeId, openReadonlyOrFail } from '../db/index.js';
|
|
16
|
+
import { buildFileConditionSQL } from '../db/query-builder.js';
|
|
16
17
|
import { loadConfig } from '../infrastructure/config.js';
|
|
17
|
-
import { info } from '../infrastructure/logger.js';
|
|
18
|
+
import { debug, info } from '../infrastructure/logger.js';
|
|
18
19
|
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
19
20
|
import { paginateResult } from '../shared/paginate.js';
|
|
20
21
|
|
|
@@ -330,41 +331,138 @@ export function computeAllMetrics(functionNode, langId) {
|
|
|
330
331
|
*/
|
|
331
332
|
export { _findFunctionNode as findFunctionNode };
|
|
332
333
|
|
|
333
|
-
|
|
334
|
-
* Re-parse changed files with WASM tree-sitter, find function AST subtrees,
|
|
335
|
-
* compute complexity, and upsert into function_complexity table.
|
|
336
|
-
*
|
|
337
|
-
* @param {object} db - open better-sqlite3 database (read-write)
|
|
338
|
-
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, ... }>
|
|
339
|
-
* @param {string} rootDir - absolute project root path
|
|
340
|
-
* @param {object} [engineOpts] - engine options (unused; always uses WASM for AST)
|
|
341
|
-
*/
|
|
342
|
-
export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOpts) {
|
|
343
|
-
// Only initialize WASM parsers if some files lack both a cached tree AND pre-computed complexity
|
|
344
|
-
let parsers = null;
|
|
345
|
-
let extToLang = null;
|
|
346
|
-
let needsFallback = false;
|
|
334
|
+
async function initWasmParsersIfNeeded(fileSymbols) {
|
|
347
335
|
for (const [relPath, symbols] of fileSymbols) {
|
|
348
336
|
if (!symbols._tree) {
|
|
349
|
-
// Only consider files whose language actually has complexity rules
|
|
350
337
|
const ext = path.extname(relPath).toLowerCase();
|
|
351
338
|
if (!COMPLEXITY_EXTENSIONS.has(ext)) continue;
|
|
352
|
-
// Check if all function/method defs have pre-computed complexity (native engine)
|
|
353
339
|
const hasPrecomputed = symbols.definitions.every(
|
|
354
340
|
(d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
|
|
355
341
|
);
|
|
356
342
|
if (!hasPrecomputed) {
|
|
357
|
-
|
|
358
|
-
|
|
343
|
+
const { createParsers } = await import('../domain/parser.js');
|
|
344
|
+
const parsers = await createParsers();
|
|
345
|
+
const extToLang = buildExtToLangMap();
|
|
346
|
+
return { parsers, extToLang };
|
|
359
347
|
}
|
|
360
348
|
}
|
|
361
349
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
350
|
+
return { parsers: null, extToLang: null };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function getTreeForFile(symbols, relPath, rootDir, parsers, extToLang, getParser) {
|
|
354
|
+
let tree = symbols._tree;
|
|
355
|
+
let langId = symbols._langId;
|
|
356
|
+
|
|
357
|
+
const allPrecomputed = symbols.definitions.every(
|
|
358
|
+
(d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (!allPrecomputed && !tree) {
|
|
362
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
363
|
+
if (!COMPLEXITY_EXTENSIONS.has(ext)) return null;
|
|
364
|
+
if (!extToLang) return null;
|
|
365
|
+
langId = extToLang.get(ext);
|
|
366
|
+
if (!langId) return null;
|
|
367
|
+
|
|
368
|
+
const absPath = path.join(rootDir, relPath);
|
|
369
|
+
let code;
|
|
370
|
+
try {
|
|
371
|
+
code = fs.readFileSync(absPath, 'utf-8');
|
|
372
|
+
} catch (e) {
|
|
373
|
+
debug(`complexity: cannot read ${relPath}: ${e.message}`);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const parser = getParser(parsers, absPath);
|
|
378
|
+
if (!parser) return null;
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
tree = parser.parse(code);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
debug(`complexity: parse failed for ${relPath}: ${e.message}`);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
366
386
|
}
|
|
367
387
|
|
|
388
|
+
return { tree, langId };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function upsertPrecomputedComplexity(db, upsert, def, relPath) {
|
|
392
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
393
|
+
if (!nodeId) return 0;
|
|
394
|
+
const ch = def.complexity.halstead;
|
|
395
|
+
const cl = def.complexity.loc;
|
|
396
|
+
upsert.run(
|
|
397
|
+
nodeId,
|
|
398
|
+
def.complexity.cognitive,
|
|
399
|
+
def.complexity.cyclomatic,
|
|
400
|
+
def.complexity.maxNesting ?? 0,
|
|
401
|
+
cl ? cl.loc : 0,
|
|
402
|
+
cl ? cl.sloc : 0,
|
|
403
|
+
cl ? cl.commentLines : 0,
|
|
404
|
+
ch ? ch.n1 : 0,
|
|
405
|
+
ch ? ch.n2 : 0,
|
|
406
|
+
ch ? ch.bigN1 : 0,
|
|
407
|
+
ch ? ch.bigN2 : 0,
|
|
408
|
+
ch ? ch.vocabulary : 0,
|
|
409
|
+
ch ? ch.length : 0,
|
|
410
|
+
ch ? ch.volume : 0,
|
|
411
|
+
ch ? ch.difficulty : 0,
|
|
412
|
+
ch ? ch.effort : 0,
|
|
413
|
+
ch ? ch.bugs : 0,
|
|
414
|
+
def.complexity.maintainabilityIndex ?? 0,
|
|
415
|
+
);
|
|
416
|
+
return 1;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function upsertAstComplexity(db, upsert, def, relPath, tree, langId, rules) {
|
|
420
|
+
if (!tree || !rules) return 0;
|
|
421
|
+
|
|
422
|
+
const funcNode = _findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
|
|
423
|
+
if (!funcNode) return 0;
|
|
424
|
+
|
|
425
|
+
const metrics = computeAllMetrics(funcNode, langId);
|
|
426
|
+
if (!metrics) return 0;
|
|
427
|
+
|
|
428
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
429
|
+
if (!nodeId) return 0;
|
|
430
|
+
|
|
431
|
+
const h = metrics.halstead;
|
|
432
|
+
upsert.run(
|
|
433
|
+
nodeId,
|
|
434
|
+
metrics.cognitive,
|
|
435
|
+
metrics.cyclomatic,
|
|
436
|
+
metrics.maxNesting,
|
|
437
|
+
metrics.loc.loc,
|
|
438
|
+
metrics.loc.sloc,
|
|
439
|
+
metrics.loc.commentLines,
|
|
440
|
+
h ? h.n1 : 0,
|
|
441
|
+
h ? h.n2 : 0,
|
|
442
|
+
h ? h.bigN1 : 0,
|
|
443
|
+
h ? h.bigN2 : 0,
|
|
444
|
+
h ? h.vocabulary : 0,
|
|
445
|
+
h ? h.length : 0,
|
|
446
|
+
h ? h.volume : 0,
|
|
447
|
+
h ? h.difficulty : 0,
|
|
448
|
+
h ? h.effort : 0,
|
|
449
|
+
h ? h.bugs : 0,
|
|
450
|
+
metrics.mi,
|
|
451
|
+
);
|
|
452
|
+
return 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Re-parse changed files with WASM tree-sitter, find function AST subtrees,
|
|
457
|
+
* compute complexity, and upsert into function_complexity table.
|
|
458
|
+
*
|
|
459
|
+
* @param {object} db - open better-sqlite3 database (read-write)
|
|
460
|
+
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, ... }>
|
|
461
|
+
* @param {string} rootDir - absolute project root path
|
|
462
|
+
* @param {object} [engineOpts] - engine options (unused; always uses WASM for AST)
|
|
463
|
+
*/
|
|
464
|
+
export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOpts) {
|
|
465
|
+
const { parsers, extToLang } = await initWasmParsersIfNeeded(fileSymbols);
|
|
368
466
|
const { getParser } = await import('../domain/parser.js');
|
|
369
467
|
|
|
370
468
|
const upsert = db.prepare(
|
|
@@ -381,39 +479,9 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
|
|
|
381
479
|
|
|
382
480
|
const tx = db.transaction(() => {
|
|
383
481
|
for (const [relPath, symbols] of fileSymbols) {
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
let tree = symbols._tree;
|
|
390
|
-
let langId = symbols._langId;
|
|
391
|
-
|
|
392
|
-
// Only attempt WASM fallback if we actually need AST-based computation
|
|
393
|
-
if (!allPrecomputed && !tree) {
|
|
394
|
-
const ext = path.extname(relPath).toLowerCase();
|
|
395
|
-
if (!COMPLEXITY_EXTENSIONS.has(ext)) continue; // Language has no complexity rules
|
|
396
|
-
if (!extToLang) continue; // No WASM parsers available
|
|
397
|
-
langId = extToLang.get(ext);
|
|
398
|
-
if (!langId) continue;
|
|
399
|
-
|
|
400
|
-
const absPath = path.join(rootDir, relPath);
|
|
401
|
-
let code;
|
|
402
|
-
try {
|
|
403
|
-
code = fs.readFileSync(absPath, 'utf-8');
|
|
404
|
-
} catch {
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const parser = getParser(parsers, absPath);
|
|
409
|
-
if (!parser) continue;
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
tree = parser.parse(code);
|
|
413
|
-
} catch {
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
482
|
+
const result = getTreeForFile(symbols, relPath, rootDir, parsers, extToLang, getParser);
|
|
483
|
+
const tree = result ? result.tree : null;
|
|
484
|
+
const langId = result ? result.langId : null;
|
|
417
485
|
|
|
418
486
|
const rules = langId ? COMPLEXITY_RULES.get(langId) : null;
|
|
419
487
|
|
|
@@ -421,71 +489,11 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
|
|
|
421
489
|
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
422
490
|
if (!def.line) continue;
|
|
423
491
|
|
|
424
|
-
// Use pre-computed complexity from native engine if available
|
|
425
492
|
if (def.complexity) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const cl = def.complexity.loc;
|
|
430
|
-
upsert.run(
|
|
431
|
-
nodeId,
|
|
432
|
-
def.complexity.cognitive,
|
|
433
|
-
def.complexity.cyclomatic,
|
|
434
|
-
def.complexity.maxNesting ?? 0,
|
|
435
|
-
cl ? cl.loc : 0,
|
|
436
|
-
cl ? cl.sloc : 0,
|
|
437
|
-
cl ? cl.commentLines : 0,
|
|
438
|
-
ch ? ch.n1 : 0,
|
|
439
|
-
ch ? ch.n2 : 0,
|
|
440
|
-
ch ? ch.bigN1 : 0,
|
|
441
|
-
ch ? ch.bigN2 : 0,
|
|
442
|
-
ch ? ch.vocabulary : 0,
|
|
443
|
-
ch ? ch.length : 0,
|
|
444
|
-
ch ? ch.volume : 0,
|
|
445
|
-
ch ? ch.difficulty : 0,
|
|
446
|
-
ch ? ch.effort : 0,
|
|
447
|
-
ch ? ch.bugs : 0,
|
|
448
|
-
def.complexity.maintainabilityIndex ?? 0,
|
|
449
|
-
);
|
|
450
|
-
analyzed++;
|
|
451
|
-
continue;
|
|
493
|
+
analyzed += upsertPrecomputedComplexity(db, upsert, def, relPath);
|
|
494
|
+
} else {
|
|
495
|
+
analyzed += upsertAstComplexity(db, upsert, def, relPath, tree, langId, rules);
|
|
452
496
|
}
|
|
453
|
-
|
|
454
|
-
// Fallback: compute from AST tree
|
|
455
|
-
if (!tree || !rules) continue;
|
|
456
|
-
|
|
457
|
-
const funcNode = _findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
|
|
458
|
-
if (!funcNode) continue;
|
|
459
|
-
|
|
460
|
-
// Single-pass: complexity + Halstead + LOC + MI in one DFS walk
|
|
461
|
-
const metrics = computeAllMetrics(funcNode, langId);
|
|
462
|
-
if (!metrics) continue;
|
|
463
|
-
|
|
464
|
-
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
465
|
-
if (!nodeId) continue;
|
|
466
|
-
|
|
467
|
-
const h = metrics.halstead;
|
|
468
|
-
upsert.run(
|
|
469
|
-
nodeId,
|
|
470
|
-
metrics.cognitive,
|
|
471
|
-
metrics.cyclomatic,
|
|
472
|
-
metrics.maxNesting,
|
|
473
|
-
metrics.loc.loc,
|
|
474
|
-
metrics.loc.sloc,
|
|
475
|
-
metrics.loc.commentLines,
|
|
476
|
-
h ? h.n1 : 0,
|
|
477
|
-
h ? h.n2 : 0,
|
|
478
|
-
h ? h.bigN1 : 0,
|
|
479
|
-
h ? h.bigN2 : 0,
|
|
480
|
-
h ? h.vocabulary : 0,
|
|
481
|
-
h ? h.length : 0,
|
|
482
|
-
h ? h.volume : 0,
|
|
483
|
-
h ? h.difficulty : 0,
|
|
484
|
-
h ? h.effort : 0,
|
|
485
|
-
h ? h.bugs : 0,
|
|
486
|
-
metrics.mi,
|
|
487
|
-
);
|
|
488
|
-
analyzed++;
|
|
489
497
|
}
|
|
490
498
|
}
|
|
491
499
|
});
|
|
@@ -547,9 +555,10 @@ export function complexityData(customDbPath, opts = {}) {
|
|
|
547
555
|
where += ' AND n.name LIKE ?';
|
|
548
556
|
params.push(`%${target}%`);
|
|
549
557
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
558
|
+
{
|
|
559
|
+
const fc = buildFileConditionSQL(fileFilter, 'n.file');
|
|
560
|
+
where += fc.sql;
|
|
561
|
+
params.push(...fc.params);
|
|
553
562
|
}
|
|
554
563
|
if (kindFilter) {
|
|
555
564
|
where += ' AND n.kind = ?';
|
|
@@ -606,13 +615,14 @@ export function complexityData(customDbPath, opts = {}) {
|
|
|
606
615
|
ORDER BY ${orderBy}`,
|
|
607
616
|
)
|
|
608
617
|
.all(...params);
|
|
609
|
-
} catch {
|
|
618
|
+
} catch (e) {
|
|
619
|
+
debug(`complexity query failed (table may not exist): ${e.message}`);
|
|
610
620
|
// Check if graph has nodes even though complexity table is missing/empty
|
|
611
621
|
let hasGraph = false;
|
|
612
622
|
try {
|
|
613
623
|
hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
|
|
614
|
-
} catch {
|
|
615
|
-
|
|
624
|
+
} catch (e2) {
|
|
625
|
+
debug(`nodes table check failed: ${e2.message}`);
|
|
616
626
|
}
|
|
617
627
|
return { functions: [], summary: null, thresholds, hasGraph };
|
|
618
628
|
}
|
|
@@ -701,8 +711,8 @@ export function complexityData(customDbPath, opts = {}) {
|
|
|
701
711
|
).length,
|
|
702
712
|
};
|
|
703
713
|
}
|
|
704
|
-
} catch {
|
|
705
|
-
|
|
714
|
+
} catch (e) {
|
|
715
|
+
debug(`complexity summary query failed: ${e.message}`);
|
|
706
716
|
}
|
|
707
717
|
|
|
708
718
|
// When summary is null (no complexity rows), check if graph has nodes
|
|
@@ -710,8 +720,8 @@ export function complexityData(customDbPath, opts = {}) {
|
|
|
710
720
|
if (summary === null) {
|
|
711
721
|
try {
|
|
712
722
|
hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
|
|
713
|
-
} catch {
|
|
714
|
-
|
|
723
|
+
} catch (e) {
|
|
724
|
+
debug(`nodes table check failed: ${e.message}`);
|
|
715
725
|
}
|
|
716
726
|
}
|
|
717
727
|
|
|
@@ -753,9 +763,10 @@ export function* iterComplexity(customDbPath, opts = {}) {
|
|
|
753
763
|
where += ' AND n.name LIKE ?';
|
|
754
764
|
params.push(`%${opts.target}%`);
|
|
755
765
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
766
|
+
{
|
|
767
|
+
const fc = buildFileConditionSQL(opts.file, 'n.file');
|
|
768
|
+
where += fc.sql;
|
|
769
|
+
params.push(...fc.params);
|
|
759
770
|
}
|
|
760
771
|
if (opts.kind) {
|
|
761
772
|
where += ' AND n.kind = ?';
|