@optave/codegraph 3.1.4 → 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 +29 -72
- package/package.json +10 -8
- package/src/ast-analysis/engine.js +260 -246
- package/src/ast-analysis/shared.js +2 -14
- 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 +4 -7
- package/src/cli/commands/audit.js +11 -11
- package/src/cli/commands/batch.js +6 -5
- package/src/cli/commands/branch-compare.js +1 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/build.js +1 -1
- package/src/cli/commands/cfg.js +5 -8
- package/src/cli/commands/check.js +28 -36
- package/src/cli/commands/children.js +9 -7
- package/src/cli/commands/co-change.js +5 -3
- package/src/cli/commands/communities.js +2 -6
- package/src/cli/commands/complexity.js +5 -3
- package/src/cli/commands/context.js +9 -8
- package/src/cli/commands/cycles.js +12 -8
- package/src/cli/commands/dataflow.js +5 -8
- package/src/cli/commands/deps.js +9 -8
- package/src/cli/commands/diff-impact.js +2 -6
- package/src/cli/commands/embed.js +1 -1
- package/src/cli/commands/export.js +34 -31
- package/src/cli/commands/exports.js +2 -6
- package/src/cli/commands/flow.js +5 -8
- package/src/cli/commands/fn-impact.js +9 -8
- package/src/cli/commands/impact.js +2 -6
- package/src/cli/commands/info.js +2 -2
- package/src/cli/commands/map.js +1 -1
- package/src/cli/commands/mcp.js +1 -1
- package/src/cli/commands/models.js +1 -1
- package/src/cli/commands/owners.js +5 -3
- package/src/cli/commands/path.js +2 -2
- package/src/cli/commands/plot.js +40 -31
- package/src/cli/commands/query.js +9 -8
- package/src/cli/commands/registry.js +2 -2
- package/src/cli/commands/roles.js +5 -8
- package/src/cli/commands/search.js +9 -3
- package/src/cli/commands/sequence.js +5 -8
- package/src/cli/commands/snapshot.js +6 -1
- package/src/cli/commands/stats.js +1 -1
- package/src/cli/commands/structure.js +5 -4
- package/src/cli/commands/triage.js +41 -30
- package/src/cli/commands/watch.js +1 -1
- package/src/cli/commands/where.js +2 -6
- package/src/cli/index.js +11 -5
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +22 -2
- package/src/cli.js +1 -1
- package/src/db/connection.js +140 -11
- package/src/{db.js → db/index.js} +12 -5
- package/src/db/migrations.js +42 -65
- package/src/db/query-builder.js +72 -9
- package/src/db/repository/base.js +1 -1
- package/src/db/repository/graph-read.js +3 -3
- package/src/db/repository/in-memory-repository.js +30 -28
- package/src/db/repository/nodes.js +10 -17
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +392 -0
- package/src/domain/analysis/dependencies.js +395 -0
- package/src/{analysis → domain/analysis}/exports.js +11 -6
- package/src/domain/analysis/impact.js +581 -0
- package/src/domain/analysis/module-map.js +348 -0
- package/src/{analysis → domain/analysis}/roles.js +12 -9
- package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
- package/src/{builder → domain/graph/builder}/helpers.js +4 -4
- package/src/{builder → domain/graph/builder}/incremental.js +119 -93
- package/src/domain/graph/builder/pipeline.js +156 -0
- package/src/domain/graph/builder/stages/build-edges.js +376 -0
- package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
- package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
- package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
- package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
- package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
- package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/{cycles.js → domain/graph/cycles.js} +4 -4
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +2 -2
- package/src/{watcher.js → domain/graph/watcher.js} +7 -7
- package/src/{parser.js → domain/parser.js} +24 -15
- package/src/{queries.js → domain/queries.js} +17 -16
- package/src/{embeddings → domain/search}/generator.js +3 -3
- package/src/{embeddings → domain/search}/models.js +2 -2
- package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
- package/src/{embeddings → domain/search}/search/filters.js +9 -5
- package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
- package/src/{embeddings → domain/search}/search/keyword.js +13 -6
- package/src/{embeddings → domain/search}/search/prepare.js +15 -7
- package/src/{embeddings → domain/search}/search/semantic.js +1 -1
- package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
- 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 +275 -305
- 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/{ast.js → features/ast.js} +13 -11
- package/src/{audit.js → features/audit.js} +20 -46
- package/src/{batch.js → features/batch.js} +5 -5
- package/src/{boundaries.js → features/boundaries.js} +100 -85
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +141 -150
- package/src/{check.js → features/check.js} +13 -30
- package/src/{cochange.js → features/cochange.js} +5 -5
- package/src/{communities.js → features/communities.js} +72 -57
- package/src/{complexity.js → features/complexity.js} +154 -143
- package/src/{dataflow.js → features/dataflow.js} +155 -158
- package/src/{export.js → features/export.js} +6 -6
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/{viewer.js → features/graph-enrichment.js} +8 -8
- package/src/{manifesto.js → features/manifesto.js} +15 -12
- package/src/{owners.js → features/owners.js} +6 -5
- package/src/features/sequence.js +300 -0
- package/src/features/shared/find-nodes.js +31 -0
- package/src/{snapshot.js → features/snapshot.js} +3 -3
- package/src/{structure.js → features/structure.js} +139 -108
- package/src/features/triage.js +141 -0
- package/src/graph/builders/dependency.js +33 -14
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.cjs +16 -0
- package/src/index.js +40 -39
- package/src/{native.js → infrastructure/native.js} +1 -1
- package/src/mcp/middleware.js +1 -1
- package/src/mcp/server.js +68 -59
- package/src/mcp/tool-registry.js +15 -2
- package/src/mcp/tools/ast-query.js +1 -1
- package/src/mcp/tools/audit.js +1 -1
- package/src/mcp/tools/batch-query.js +1 -1
- package/src/mcp/tools/branch-compare.js +3 -1
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/cfg.js +1 -1
- package/src/mcp/tools/check.js +3 -3
- package/src/mcp/tools/co-changes.js +1 -1
- package/src/mcp/tools/code-owners.js +1 -1
- package/src/mcp/tools/communities.js +1 -1
- package/src/mcp/tools/complexity.js +1 -1
- package/src/mcp/tools/dataflow.js +2 -2
- package/src/mcp/tools/execution-flow.js +2 -2
- package/src/mcp/tools/export-graph.js +2 -2
- package/src/mcp/tools/find-cycles.js +2 -2
- package/src/mcp/tools/index.js +2 -0
- package/src/mcp/tools/list-repos.js +1 -1
- package/src/mcp/tools/sequence.js +1 -1
- package/src/mcp/tools/structure.js +1 -1
- package/src/mcp/tools/triage.js +2 -2
- package/src/{commands → presentation}/audit.js +2 -2
- package/src/{commands → presentation}/batch.js +1 -1
- package/src/{commands → presentation}/branch-compare.js +2 -2
- package/src/presentation/brief.js +51 -0
- package/src/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +2 -2
- package/src/{commands → presentation}/communities.js +1 -1
- package/src/{commands → presentation}/complexity.js +1 -1
- package/src/{commands → presentation}/dataflow.js +1 -1
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +1 -1
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +53 -0
- package/src/presentation/queries-cli/impact.js +214 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +329 -0
- package/src/presentation/queries-cli/overview.js +196 -0
- package/src/presentation/queries-cli/path.js +65 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +126 -3
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +1 -1
- package/src/presentation/table.js +0 -8
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/file-utils.js +2 -2
- package/src/shared/generators.js +9 -5
- package/src/shared/hierarchy.js +1 -1
- package/src/{kinds.js → shared/kinds.js} +1 -1
- package/src/analysis/context.js +0 -408
- package/src/analysis/dependencies.js +0 -341
- package/src/analysis/impact.js +0 -463
- package/src/analysis/module-map.js +0 -322
- package/src/builder/pipeline.js +0 -130
- package/src/builder/stages/build-edges.js +0 -297
- package/src/builder/stages/insert-nodes.js +0 -195
- package/src/mcp.js +0 -2
- package/src/queries-cli.js +0 -866
- package/src/sequence.js +0 -289
- package/src/triage.js +0 -126
- /package/src/{builder → domain/graph/builder}/context.js +0 -0
- /package/src/{builder.js → domain/graph/builder.js} +0 -0
- /package/src/{embeddings → domain/search}/index.js +0 -0
- /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
- /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
- /package/src/{config.js → infrastructure/config.js} +0 -0
- /package/src/{logger.js → infrastructure/logger.js} +0 -0
- /package/src/{registry.js → infrastructure/registry.js} +0 -0
- /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
- /package/src/{commands → presentation}/cochange.js +0 -0
- /package/src/{errors.js → shared/errors.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { isTestFile } from '
|
|
14
|
-
import {
|
|
15
|
-
import { paginateResult } from '
|
|
11
|
+
import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from '../db/index.js';
|
|
12
|
+
import { warn } from '../infrastructure/logger.js';
|
|
13
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
14
|
+
import { normalizePath } from '../shared/constants.js';
|
|
15
|
+
import { paginateResult } from '../shared/paginate.js';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Scan git history and return parsed commit data.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import { louvainCommunities } from '
|
|
4
|
-
import { buildDependencyGraph } from '
|
|
5
|
-
import { paginateResult } from '
|
|
2
|
+
import { openRepo } from '../db/index.js';
|
|
3
|
+
import { louvainCommunities } from '../graph/algorithms/louvain.js';
|
|
4
|
+
import { buildDependencyGraph } from '../graph/builders/dependency.js';
|
|
5
|
+
import { paginateResult } from '../shared/paginate.js';
|
|
6
6
|
|
|
7
7
|
// ─── Directory Helpers ────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -11,48 +11,18 @@ function getDirectory(filePath) {
|
|
|
11
11
|
return dir === '.' ? '(root)' : dir;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// ───
|
|
14
|
+
// ─── Community Building ──────────────────────────────────────────────
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* @param {string}
|
|
20
|
-
* @param {object}
|
|
21
|
-
* @param {boolean} [opts.
|
|
22
|
-
* @
|
|
23
|
-
* @param {boolean} [opts.noTests] - Exclude test files
|
|
24
|
-
* @param {boolean} [opts.drift] - Drift-only mode (omit community member lists)
|
|
25
|
-
* @param {boolean} [opts.json] - JSON output (used by CLI wrapper only)
|
|
26
|
-
* @returns {{ communities: object[], modularity: number, drift: object, summary: object }}
|
|
17
|
+
* Group graph nodes by Louvain community assignment and build structured objects.
|
|
18
|
+
* @param {object} graph - The dependency graph
|
|
19
|
+
* @param {Map<string, number>} assignments - Node key → community ID
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {boolean} [opts.drift] - If true, omit member lists
|
|
22
|
+
* @returns {{ communities: object[], communityDirs: Map<number, Set<string>> }}
|
|
27
23
|
*/
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
let graph;
|
|
31
|
-
try {
|
|
32
|
-
graph = buildDependencyGraph(db, {
|
|
33
|
-
fileLevel: !opts.functions,
|
|
34
|
-
noTests: opts.noTests,
|
|
35
|
-
});
|
|
36
|
-
} finally {
|
|
37
|
-
db.close();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Handle empty or trivial graphs
|
|
41
|
-
if (graph.nodeCount === 0 || graph.edgeCount === 0) {
|
|
42
|
-
return {
|
|
43
|
-
communities: [],
|
|
44
|
-
modularity: 0,
|
|
45
|
-
drift: { splitCandidates: [], mergeCandidates: [] },
|
|
46
|
-
summary: { communityCount: 0, modularity: 0, nodeCount: graph.nodeCount, driftScore: 0 },
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Run Louvain
|
|
51
|
-
const resolution = opts.resolution ?? 1.0;
|
|
52
|
-
const { assignments, modularity } = louvainCommunities(graph, { resolution });
|
|
53
|
-
|
|
54
|
-
// Group nodes by community
|
|
55
|
-
const communityMap = new Map(); // community id → node keys[]
|
|
24
|
+
function buildCommunityObjects(graph, assignments, opts) {
|
|
25
|
+
const communityMap = new Map();
|
|
56
26
|
for (const [key] of graph.nodes()) {
|
|
57
27
|
const cid = assignments.get(key);
|
|
58
28
|
if (cid == null) continue;
|
|
@@ -60,9 +30,8 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
60
30
|
communityMap.get(cid).push(key);
|
|
61
31
|
}
|
|
62
32
|
|
|
63
|
-
// Build community objects
|
|
64
33
|
const communities = [];
|
|
65
|
-
const communityDirs = new Map();
|
|
34
|
+
const communityDirs = new Map();
|
|
66
35
|
|
|
67
36
|
for (const [cid, members] of communityMap) {
|
|
68
37
|
const dirCounts = {};
|
|
@@ -88,19 +57,27 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
88
57
|
});
|
|
89
58
|
}
|
|
90
59
|
|
|
91
|
-
// Sort by size descending
|
|
92
60
|
communities.sort((a, b) => b.size - a.size);
|
|
61
|
+
return { communities, communityDirs };
|
|
62
|
+
}
|
|
93
63
|
|
|
94
|
-
|
|
64
|
+
// ─── Drift Analysis ──────────────────────────────────────────────────
|
|
95
65
|
|
|
96
|
-
|
|
97
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Compute split/merge candidates and drift score from community directory data.
|
|
68
|
+
* @param {object[]} communities - Community objects with `directories`
|
|
69
|
+
* @param {Map<number, Set<string>>} communityDirs - Community ID → directory set
|
|
70
|
+
* @returns {{ splitCandidates: object[], mergeCandidates: object[], driftScore: number }}
|
|
71
|
+
*/
|
|
72
|
+
function analyzeDrift(communities, communityDirs) {
|
|
73
|
+
const dirToCommunities = new Map();
|
|
98
74
|
for (const [cid, dirs] of communityDirs) {
|
|
99
75
|
for (const dir of dirs) {
|
|
100
76
|
if (!dirToCommunities.has(dir)) dirToCommunities.set(dir, new Set());
|
|
101
77
|
dirToCommunities.get(dir).add(cid);
|
|
102
78
|
}
|
|
103
79
|
}
|
|
80
|
+
|
|
104
81
|
const splitCandidates = [];
|
|
105
82
|
for (const [dir, cids] of dirToCommunities) {
|
|
106
83
|
if (cids.size >= 2) {
|
|
@@ -109,7 +86,6 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
109
86
|
}
|
|
110
87
|
splitCandidates.sort((a, b) => b.communityCount - a.communityCount);
|
|
111
88
|
|
|
112
|
-
// Merge candidates: communities spanning 2+ directories
|
|
113
89
|
const mergeCandidates = [];
|
|
114
90
|
for (const c of communities) {
|
|
115
91
|
const dirCount = Object.keys(c.directories).length;
|
|
@@ -124,17 +100,56 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
124
100
|
}
|
|
125
101
|
mergeCandidates.sort((a, b) => b.directoryCount - a.directoryCount);
|
|
126
102
|
|
|
127
|
-
// Drift score: 0-100 based on how much directory structure diverges from communities
|
|
128
103
|
const totalDirs = dirToCommunities.size;
|
|
129
|
-
const
|
|
130
|
-
const splitRatio = totalDirs > 0 ? splitDirs / totalDirs : 0;
|
|
131
|
-
|
|
104
|
+
const splitRatio = totalDirs > 0 ? splitCandidates.length / totalDirs : 0;
|
|
132
105
|
const totalComms = communities.length;
|
|
133
|
-
const
|
|
134
|
-
const mergeRatio = totalComms > 0 ? mergeComms / totalComms : 0;
|
|
135
|
-
|
|
106
|
+
const mergeRatio = totalComms > 0 ? mergeCandidates.length / totalComms : 0;
|
|
136
107
|
const driftScore = Math.round(((splitRatio + mergeRatio) / 2) * 100);
|
|
137
108
|
|
|
109
|
+
return { splitCandidates, mergeCandidates, driftScore };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Core Analysis ────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run Louvain community detection and return structured data.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} [customDbPath] - Path to graph.db
|
|
118
|
+
* @param {object} [opts]
|
|
119
|
+
* @param {boolean} [opts.functions] - Function-level instead of file-level
|
|
120
|
+
* @param {number} [opts.resolution] - Louvain resolution (default 1.0)
|
|
121
|
+
* @param {boolean} [opts.noTests] - Exclude test files
|
|
122
|
+
* @param {boolean} [opts.drift] - Drift-only mode (omit community member lists)
|
|
123
|
+
* @param {boolean} [opts.json] - JSON output (used by CLI wrapper only)
|
|
124
|
+
* @returns {{ communities: object[], modularity: number, drift: object, summary: object }}
|
|
125
|
+
*/
|
|
126
|
+
export function communitiesData(customDbPath, opts = {}) {
|
|
127
|
+
const { repo, close } = openRepo(customDbPath, opts);
|
|
128
|
+
let graph;
|
|
129
|
+
try {
|
|
130
|
+
graph = buildDependencyGraph(repo, {
|
|
131
|
+
fileLevel: !opts.functions,
|
|
132
|
+
noTests: opts.noTests,
|
|
133
|
+
});
|
|
134
|
+
} finally {
|
|
135
|
+
close();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (graph.nodeCount === 0 || graph.edgeCount === 0) {
|
|
139
|
+
return {
|
|
140
|
+
communities: [],
|
|
141
|
+
modularity: 0,
|
|
142
|
+
drift: { splitCandidates: [], mergeCandidates: [] },
|
|
143
|
+
summary: { communityCount: 0, modularity: 0, nodeCount: graph.nodeCount, driftScore: 0 },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const resolution = opts.resolution ?? 1.0;
|
|
148
|
+
const { assignments, modularity } = louvainCommunities(graph, { resolution });
|
|
149
|
+
|
|
150
|
+
const { communities, communityDirs } = buildCommunityObjects(graph, assignments, opts);
|
|
151
|
+
const { splitCandidates, mergeCandidates, driftScore } = analyzeDrift(communities, communityDirs);
|
|
152
|
+
|
|
138
153
|
const base = {
|
|
139
154
|
communities: opts.drift ? [] : communities,
|
|
140
155
|
modularity: +modularity.toFixed(4),
|
|
@@ -3,20 +3,21 @@ import path from 'node:path';
|
|
|
3
3
|
import {
|
|
4
4
|
computeLOCMetrics as _computeLOCMetrics,
|
|
5
5
|
computeMaintainabilityIndex as _computeMaintainabilityIndex,
|
|
6
|
-
} from '
|
|
7
|
-
import { COMPLEXITY_RULES, HALSTEAD_RULES } from '
|
|
6
|
+
} from '../ast-analysis/metrics.js';
|
|
7
|
+
import { COMPLEXITY_RULES, HALSTEAD_RULES } from '../ast-analysis/rules/index.js';
|
|
8
8
|
import {
|
|
9
9
|
findFunctionNode as _findFunctionNode,
|
|
10
10
|
buildExtensionSet,
|
|
11
11
|
buildExtToLangMap,
|
|
12
|
-
} from '
|
|
13
|
-
import { walkWithVisitors } from '
|
|
14
|
-
import { createComplexityVisitor } from '
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { info } from '
|
|
19
|
-
import {
|
|
12
|
+
} from '../ast-analysis/shared.js';
|
|
13
|
+
import { walkWithVisitors } from '../ast-analysis/visitor.js';
|
|
14
|
+
import { createComplexityVisitor } from '../ast-analysis/visitors/complexity-visitor.js';
|
|
15
|
+
import { getFunctionNodeId, openReadonlyOrFail } from '../db/index.js';
|
|
16
|
+
import { buildFileConditionSQL } from '../db/query-builder.js';
|
|
17
|
+
import { loadConfig } from '../infrastructure/config.js';
|
|
18
|
+
import { debug, info } from '../infrastructure/logger.js';
|
|
19
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
20
|
+
import { paginateResult } from '../shared/paginate.js';
|
|
20
21
|
|
|
21
22
|
// Re-export rules for backward compatibility
|
|
22
23
|
export { COMPLEXITY_RULES, HALSTEAD_RULES };
|
|
@@ -330,42 +331,139 @@ 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
|
|
|
368
|
-
|
|
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);
|
|
466
|
+
const { getParser } = await import('../domain/parser.js');
|
|
369
467
|
|
|
370
468
|
const upsert = db.prepare(
|
|
371
469
|
`INSERT OR REPLACE INTO function_complexity
|
|
@@ -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
|
});
|
|
@@ -524,7 +532,7 @@ export function complexityData(customDbPath, opts = {}) {
|
|
|
524
532
|
const kindFilter = opts.kind || null;
|
|
525
533
|
|
|
526
534
|
// Load thresholds from config
|
|
527
|
-
const config = loadConfig(process.cwd());
|
|
535
|
+
const config = opts.config || loadConfig(process.cwd());
|
|
528
536
|
const thresholds = config.manifesto?.rules || {
|
|
529
537
|
cognitive: { warn: 15, fail: null },
|
|
530
538
|
cyclomatic: { warn: 10, fail: null },
|
|
@@ -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 = ?';
|