@optave/codegraph 3.1.3 → 3.1.4
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 +17 -19
- package/package.json +10 -7
- package/src/analysis/context.js +408 -0
- package/src/analysis/dependencies.js +341 -0
- package/src/analysis/exports.js +130 -0
- package/src/analysis/impact.js +463 -0
- package/src/analysis/module-map.js +322 -0
- package/src/analysis/roles.js +45 -0
- package/src/analysis/symbol-lookup.js +232 -0
- package/src/ast-analysis/shared.js +5 -4
- package/src/batch.js +2 -1
- package/src/builder/context.js +85 -0
- package/src/builder/helpers.js +218 -0
- package/src/builder/incremental.js +178 -0
- package/src/builder/pipeline.js +130 -0
- package/src/builder/stages/build-edges.js +297 -0
- package/src/builder/stages/build-structure.js +113 -0
- package/src/builder/stages/collect-files.js +44 -0
- package/src/builder/stages/detect-changes.js +413 -0
- package/src/builder/stages/finalize.js +139 -0
- package/src/builder/stages/insert-nodes.js +195 -0
- package/src/builder/stages/parse-files.js +28 -0
- package/src/builder/stages/resolve-imports.js +143 -0
- package/src/builder/stages/run-analyses.js +44 -0
- package/src/builder.js +10 -1485
- package/src/cfg.js +1 -2
- package/src/cli/commands/ast.js +26 -0
- package/src/cli/commands/audit.js +46 -0
- package/src/cli/commands/batch.js +68 -0
- package/src/cli/commands/branch-compare.js +21 -0
- package/src/cli/commands/build.js +26 -0
- package/src/cli/commands/cfg.js +30 -0
- package/src/cli/commands/check.js +79 -0
- package/src/cli/commands/children.js +31 -0
- package/src/cli/commands/co-change.js +65 -0
- package/src/cli/commands/communities.js +23 -0
- package/src/cli/commands/complexity.js +45 -0
- package/src/cli/commands/context.js +34 -0
- package/src/cli/commands/cycles.js +28 -0
- package/src/cli/commands/dataflow.js +32 -0
- package/src/cli/commands/deps.js +16 -0
- package/src/cli/commands/diff-impact.js +30 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +75 -0
- package/src/cli/commands/exports.js +18 -0
- package/src/cli/commands/flow.js +36 -0
- package/src/cli/commands/fn-impact.js +30 -0
- package/src/cli/commands/impact.js +16 -0
- package/src/cli/commands/info.js +76 -0
- package/src/cli/commands/map.js +19 -0
- package/src/cli/commands/mcp.js +18 -0
- package/src/cli/commands/models.js +19 -0
- package/src/cli/commands/owners.js +25 -0
- package/src/cli/commands/path.js +36 -0
- package/src/cli/commands/plot.js +80 -0
- package/src/cli/commands/query.js +49 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +34 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +32 -0
- package/src/cli/commands/snapshot.js +61 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +32 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +24 -0
- package/src/cli/index.js +118 -0
- package/src/cli/shared/options.js +39 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1522
- package/src/commands/check.js +5 -5
- package/src/commands/manifesto.js +3 -3
- package/src/commands/structure.js +1 -1
- package/src/communities.js +15 -87
- package/src/cycles.js +30 -85
- package/src/dataflow.js +1 -2
- package/src/db/connection.js +4 -4
- package/src/db/migrations.js +41 -0
- package/src/db/query-builder.js +6 -5
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/graph-read.js +5 -2
- package/src/db/repository/in-memory-repository.js +584 -0
- package/src/db/repository/index.js +5 -1
- package/src/db/repository/nodes.js +63 -4
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/db.js +5 -0
- package/src/embeddings/generator.js +163 -0
- package/src/embeddings/index.js +13 -0
- package/src/embeddings/models.js +218 -0
- package/src/embeddings/search/cli-formatter.js +151 -0
- package/src/embeddings/search/filters.js +46 -0
- package/src/embeddings/search/hybrid.js +121 -0
- package/src/embeddings/search/keyword.js +68 -0
- package/src/embeddings/search/prepare.js +66 -0
- package/src/embeddings/search/semantic.js +145 -0
- package/src/embeddings/stores/fts5.js +27 -0
- package/src/embeddings/stores/sqlite-blob.js +24 -0
- package/src/embeddings/strategies/source.js +14 -0
- package/src/embeddings/strategies/structured.js +43 -0
- package/src/embeddings/strategies/text-utils.js +43 -0
- package/src/errors.js +78 -0
- package/src/export.js +217 -520
- package/src/extractors/csharp.js +10 -2
- package/src/extractors/go.js +3 -1
- package/src/extractors/helpers.js +71 -0
- package/src/extractors/java.js +9 -2
- package/src/extractors/javascript.js +38 -1
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- package/src/graph/algorithms/bfs.js +49 -0
- package/src/graph/algorithms/centrality.js +16 -0
- package/src/graph/algorithms/index.js +5 -0
- package/src/graph/algorithms/louvain.js +26 -0
- package/src/graph/algorithms/shortest-path.js +41 -0
- package/src/graph/algorithms/tarjan.js +49 -0
- package/src/graph/builders/dependency.js +91 -0
- package/src/graph/builders/index.js +3 -0
- package/src/graph/builders/structure.js +40 -0
- package/src/graph/builders/temporal.js +33 -0
- package/src/graph/classifiers/index.js +2 -0
- package/src/graph/classifiers/risk.js +85 -0
- package/src/graph/classifiers/roles.js +64 -0
- package/src/graph/index.js +13 -0
- package/src/graph/model.js +230 -0
- package/src/index.js +33 -210
- package/src/infrastructure/result-formatter.js +2 -21
- package/src/mcp/index.js +2 -0
- package/src/mcp/middleware.js +26 -0
- package/src/mcp/server.js +128 -0
- package/src/mcp/tool-registry.js +801 -0
- package/src/mcp/tools/ast-query.js +14 -0
- package/src/mcp/tools/audit.js +21 -0
- package/src/mcp/tools/batch-query.js +11 -0
- package/src/mcp/tools/branch-compare.js +10 -0
- package/src/mcp/tools/cfg.js +21 -0
- package/src/mcp/tools/check.js +43 -0
- package/src/mcp/tools/co-changes.js +20 -0
- package/src/mcp/tools/code-owners.js +12 -0
- package/src/mcp/tools/communities.js +15 -0
- package/src/mcp/tools/complexity.js +18 -0
- package/src/mcp/tools/context.js +17 -0
- package/src/mcp/tools/dataflow.js +26 -0
- package/src/mcp/tools/diff-impact.js +24 -0
- package/src/mcp/tools/execution-flow.js +26 -0
- package/src/mcp/tools/export-graph.js +57 -0
- package/src/mcp/tools/file-deps.js +12 -0
- package/src/mcp/tools/file-exports.js +13 -0
- package/src/mcp/tools/find-cycles.js +15 -0
- package/src/mcp/tools/fn-impact.js +15 -0
- package/src/mcp/tools/impact-analysis.js +12 -0
- package/src/mcp/tools/index.js +71 -0
- package/src/mcp/tools/list-functions.js +14 -0
- package/src/mcp/tools/list-repos.js +11 -0
- package/src/mcp/tools/module-map.js +6 -0
- package/src/mcp/tools/node-roles.js +14 -0
- package/src/mcp/tools/path.js +12 -0
- package/src/mcp/tools/query.js +30 -0
- package/src/mcp/tools/semantic-search.js +65 -0
- package/src/mcp/tools/sequence.js +17 -0
- package/src/mcp/tools/structure.js +15 -0
- package/src/mcp/tools/symbol-children.js +14 -0
- package/src/mcp/tools/triage.js +35 -0
- package/src/mcp/tools/where.js +13 -0
- package/src/mcp.js +2 -1470
- package/src/native.js +3 -1
- package/src/presentation/colors.js +44 -0
- package/src/presentation/export.js +444 -0
- package/src/presentation/result-formatter.js +21 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/presentation/table.js +47 -0
- package/src/presentation/viewer.js +634 -0
- package/src/queries.js +35 -2276
- package/src/resolve.js +1 -1
- package/src/sequence.js +2 -38
- package/src/shared/file-utils.js +153 -0
- package/src/shared/generators.js +125 -0
- package/src/shared/hierarchy.js +27 -0
- package/src/shared/normalize.js +59 -0
- package/src/snapshot.js +6 -5
- package/src/structure.js +15 -40
- package/src/triage.js +20 -72
- package/src/viewer.js +35 -656
- package/src/watcher.js +8 -148
- package/src/embedder.js +0 -1097
package/src/commands/check.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { checkData } from '../check.js';
|
|
2
|
+
import { AnalysisError } from '../errors.js';
|
|
2
3
|
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* CLI formatter — prints check results and
|
|
6
|
+
* CLI formatter — prints check results and sets exitCode 1 on failure.
|
|
6
7
|
*/
|
|
7
8
|
export function check(customDbPath, opts = {}) {
|
|
8
9
|
const data = checkData(customDbPath, opts);
|
|
9
10
|
|
|
10
11
|
if (data.error) {
|
|
11
|
-
|
|
12
|
-
process.exit(1);
|
|
12
|
+
throw new AnalysisError(data.error);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
if (outputResult(data, null, opts)) {
|
|
16
|
-
if (!data.passed) process.
|
|
16
|
+
if (!data.passed) process.exitCode = 1;
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -77,6 +77,6 @@ export function check(customDbPath, opts = {}) {
|
|
|
77
77
|
console.log(`\n ${s.total} predicates | ${s.passed} passed | ${s.failed} failed\n`);
|
|
78
78
|
|
|
79
79
|
if (!data.passed) {
|
|
80
|
-
process.
|
|
80
|
+
process.exitCode = 1;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -2,13 +2,13 @@ import { outputResult } from '../infrastructure/result-formatter.js';
|
|
|
2
2
|
import { manifestoData } from '../manifesto.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* CLI formatter — prints manifesto results and
|
|
5
|
+
* CLI formatter — prints manifesto results and sets exitCode 1 on failure.
|
|
6
6
|
*/
|
|
7
7
|
export function manifesto(customDbPath, opts = {}) {
|
|
8
8
|
const data = manifestoData(customDbPath, opts);
|
|
9
9
|
|
|
10
10
|
if (outputResult(data, 'violations', opts)) {
|
|
11
|
-
if (!data.passed) process.
|
|
11
|
+
if (!data.passed) process.exitCode = 1;
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -72,6 +72,6 @@ export function manifesto(customDbPath, opts = {}) {
|
|
|
72
72
|
console.log();
|
|
73
73
|
|
|
74
74
|
if (!data.passed) {
|
|
75
|
-
process.
|
|
75
|
+
process.exitCode = 1;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { hotspotsData, moduleBoundariesData, structureData } from '../structure.js';
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export { hotspotsData, moduleBoundariesData, structureData };
|
|
5
5
|
|
|
6
6
|
export function formatStructure(data) {
|
|
7
7
|
if (data.count === 0) return 'No directory structure found. Run "codegraph build" first.';
|
package/src/communities.js
CHANGED
|
@@ -1,79 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
getCallableNodes,
|
|
6
|
-
getCallEdges,
|
|
7
|
-
getFileNodesAll,
|
|
8
|
-
getImportEdges,
|
|
9
|
-
openReadonlyOrFail,
|
|
10
|
-
} from './db.js';
|
|
11
|
-
import { isTestFile } from './infrastructure/test-filter.js';
|
|
2
|
+
import { openReadonlyOrFail } from './db.js';
|
|
3
|
+
import { louvainCommunities } from './graph/algorithms/louvain.js';
|
|
4
|
+
import { buildDependencyGraph } from './graph/builders/dependency.js';
|
|
12
5
|
import { paginateResult } from './paginate.js';
|
|
13
6
|
|
|
14
|
-
// ─── Graph Construction ───────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Build a graphology graph from the codegraph SQLite database.
|
|
18
|
-
*
|
|
19
|
-
* @param {object} db - open better-sqlite3 database (readonly)
|
|
20
|
-
* @param {object} opts
|
|
21
|
-
* @param {boolean} [opts.functions] - Function-level instead of file-level
|
|
22
|
-
* @param {boolean} [opts.noTests] - Exclude test files
|
|
23
|
-
* @returns {Graph}
|
|
24
|
-
*/
|
|
25
|
-
function buildGraphologyGraph(db, opts = {}) {
|
|
26
|
-
const graph = new Graph({ type: 'undirected' });
|
|
27
|
-
|
|
28
|
-
if (opts.functions) {
|
|
29
|
-
// Function-level: nodes = function/method/class symbols, edges = calls
|
|
30
|
-
let nodes = getCallableNodes(db);
|
|
31
|
-
if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
32
|
-
|
|
33
|
-
const nodeIds = new Set();
|
|
34
|
-
for (const n of nodes) {
|
|
35
|
-
const key = String(n.id);
|
|
36
|
-
graph.addNode(key, { label: n.name, file: n.file, kind: n.kind });
|
|
37
|
-
nodeIds.add(n.id);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const edges = getCallEdges(db);
|
|
41
|
-
for (const e of edges) {
|
|
42
|
-
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
43
|
-
const src = String(e.source_id);
|
|
44
|
-
const tgt = String(e.target_id);
|
|
45
|
-
if (src === tgt) continue;
|
|
46
|
-
if (!graph.hasEdge(src, tgt)) {
|
|
47
|
-
graph.addEdge(src, tgt);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
// File-level: nodes = files, edges = imports + imports-type (deduplicated, cross-file)
|
|
52
|
-
let nodes = getFileNodesAll(db);
|
|
53
|
-
if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
54
|
-
|
|
55
|
-
const nodeIds = new Set();
|
|
56
|
-
for (const n of nodes) {
|
|
57
|
-
const key = String(n.id);
|
|
58
|
-
graph.addNode(key, { label: n.file, file: n.file });
|
|
59
|
-
nodeIds.add(n.id);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const edges = getImportEdges(db);
|
|
63
|
-
for (const e of edges) {
|
|
64
|
-
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
65
|
-
const src = String(e.source_id);
|
|
66
|
-
const tgt = String(e.target_id);
|
|
67
|
-
if (src === tgt) continue;
|
|
68
|
-
if (!graph.hasEdge(src, tgt)) {
|
|
69
|
-
graph.addEdge(src, tgt);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return graph;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
7
|
// ─── Directory Helpers ────────────────────────────────────────────────
|
|
78
8
|
|
|
79
9
|
function getDirectory(filePath) {
|
|
@@ -97,11 +27,10 @@ function getDirectory(filePath) {
|
|
|
97
27
|
*/
|
|
98
28
|
export function communitiesData(customDbPath, opts = {}) {
|
|
99
29
|
const db = openReadonlyOrFail(customDbPath);
|
|
100
|
-
const resolution = opts.resolution ?? 1.0;
|
|
101
30
|
let graph;
|
|
102
31
|
try {
|
|
103
|
-
graph =
|
|
104
|
-
|
|
32
|
+
graph = buildDependencyGraph(db, {
|
|
33
|
+
fileLevel: !opts.functions,
|
|
105
34
|
noTests: opts.noTests,
|
|
106
35
|
});
|
|
107
36
|
} finally {
|
|
@@ -109,27 +38,27 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
109
38
|
}
|
|
110
39
|
|
|
111
40
|
// Handle empty or trivial graphs
|
|
112
|
-
if (graph.
|
|
41
|
+
if (graph.nodeCount === 0 || graph.edgeCount === 0) {
|
|
113
42
|
return {
|
|
114
43
|
communities: [],
|
|
115
44
|
modularity: 0,
|
|
116
45
|
drift: { splitCandidates: [], mergeCandidates: [] },
|
|
117
|
-
summary: { communityCount: 0, modularity: 0, nodeCount: graph.
|
|
46
|
+
summary: { communityCount: 0, modularity: 0, nodeCount: graph.nodeCount, driftScore: 0 },
|
|
118
47
|
};
|
|
119
48
|
}
|
|
120
49
|
|
|
121
50
|
// Run Louvain
|
|
122
|
-
const
|
|
123
|
-
const assignments
|
|
124
|
-
const modularity = details.modularity;
|
|
51
|
+
const resolution = opts.resolution ?? 1.0;
|
|
52
|
+
const { assignments, modularity } = louvainCommunities(graph, { resolution });
|
|
125
53
|
|
|
126
54
|
// Group nodes by community
|
|
127
55
|
const communityMap = new Map(); // community id → node keys[]
|
|
128
|
-
graph.
|
|
129
|
-
const cid = assignments
|
|
56
|
+
for (const [key] of graph.nodes()) {
|
|
57
|
+
const cid = assignments.get(key);
|
|
58
|
+
if (cid == null) continue;
|
|
130
59
|
if (!communityMap.has(cid)) communityMap.set(cid, []);
|
|
131
60
|
communityMap.get(cid).push(key);
|
|
132
|
-
}
|
|
61
|
+
}
|
|
133
62
|
|
|
134
63
|
// Build community objects
|
|
135
64
|
const communities = [];
|
|
@@ -139,7 +68,7 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
139
68
|
const dirCounts = {};
|
|
140
69
|
const memberData = [];
|
|
141
70
|
for (const key of members) {
|
|
142
|
-
const attrs = graph.
|
|
71
|
+
const attrs = graph.getNodeAttrs(key);
|
|
143
72
|
const dir = getDirectory(attrs.file);
|
|
144
73
|
dirCounts[dir] = (dirCounts[dir] || 0) + 1;
|
|
145
74
|
memberData.push({
|
|
@@ -196,7 +125,6 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
196
125
|
mergeCandidates.sort((a, b) => b.directoryCount - a.directoryCount);
|
|
197
126
|
|
|
198
127
|
// Drift score: 0-100 based on how much directory structure diverges from communities
|
|
199
|
-
// Higher = more drift (directories don't match communities)
|
|
200
128
|
const totalDirs = dirToCommunities.size;
|
|
201
129
|
const splitDirs = splitCandidates.length;
|
|
202
130
|
const splitRatio = totalDirs > 0 ? splitDirs / totalDirs : 0;
|
|
@@ -214,7 +142,7 @@ export function communitiesData(customDbPath, opts = {}) {
|
|
|
214
142
|
summary: {
|
|
215
143
|
communityCount: communities.length,
|
|
216
144
|
modularity: +modularity.toFixed(4),
|
|
217
|
-
nodeCount: graph.
|
|
145
|
+
nodeCount: graph.nodeCount,
|
|
218
146
|
driftScore,
|
|
219
147
|
},
|
|
220
148
|
};
|
package/src/cycles.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tarjan } from './graph/algorithms/tarjan.js';
|
|
2
|
+
import { buildDependencyGraph } from './graph/builders/dependency.js';
|
|
3
|
+
import { CodeGraph } from './graph/model.js';
|
|
2
4
|
import { loadNative } from './native.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -12,107 +14,50 @@ export function findCycles(db, opts = {}) {
|
|
|
12
14
|
const fileLevel = opts.fileLevel !== false;
|
|
13
15
|
const noTests = opts.noTests || false;
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`)
|
|
26
|
-
.all();
|
|
27
|
-
if (noTests) {
|
|
28
|
-
edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
edges = db
|
|
32
|
-
.prepare(`
|
|
33
|
-
SELECT DISTINCT
|
|
34
|
-
(n1.name || '|' || n1.file) AS source,
|
|
35
|
-
(n2.name || '|' || n2.file) AS target
|
|
36
|
-
FROM edges e
|
|
37
|
-
JOIN nodes n1 ON e.source_id = n1.id
|
|
38
|
-
JOIN nodes n2 ON e.target_id = n2.id
|
|
39
|
-
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
40
|
-
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
41
|
-
AND e.kind = 'calls'
|
|
42
|
-
AND n1.id != n2.id
|
|
43
|
-
`)
|
|
44
|
-
.all();
|
|
45
|
-
if (noTests) {
|
|
46
|
-
edges = edges.filter((e) => {
|
|
47
|
-
const sourceFile = e.source.split('|').pop();
|
|
48
|
-
const targetFile = e.target.split('|').pop();
|
|
49
|
-
return !isTestFile(sourceFile) && !isTestFile(targetFile);
|
|
50
|
-
});
|
|
17
|
+
const graph = buildDependencyGraph(db, { fileLevel, noTests });
|
|
18
|
+
|
|
19
|
+
// Build a label map: DB string ID → human-readable key
|
|
20
|
+
// File-level: file path; Function-level: name|file composite (for native Rust compat)
|
|
21
|
+
const idToLabel = new Map();
|
|
22
|
+
for (const [id, attrs] of graph.nodes()) {
|
|
23
|
+
if (fileLevel) {
|
|
24
|
+
idToLabel.set(id, attrs.file);
|
|
25
|
+
} else {
|
|
26
|
+
idToLabel.set(id, `${attrs.label}|${attrs.file}`);
|
|
51
27
|
}
|
|
52
28
|
}
|
|
53
29
|
|
|
30
|
+
// Build edge array with human-readable keys (for native engine)
|
|
31
|
+
const edges = graph.toEdgeArray().map((e) => ({
|
|
32
|
+
source: idToLabel.get(e.source),
|
|
33
|
+
target: idToLabel.get(e.target),
|
|
34
|
+
}));
|
|
35
|
+
|
|
54
36
|
// Try native Rust implementation
|
|
55
37
|
const native = loadNative();
|
|
56
38
|
if (native) {
|
|
57
39
|
return native.detectCycles(edges);
|
|
58
40
|
}
|
|
59
41
|
|
|
60
|
-
// Fallback: JS Tarjan
|
|
61
|
-
|
|
42
|
+
// Fallback: JS Tarjan via graph subsystem
|
|
43
|
+
// Re-key graph with human-readable labels for consistent output
|
|
44
|
+
const labelGraph = new CodeGraph();
|
|
45
|
+
for (const { source, target } of edges) {
|
|
46
|
+
labelGraph.addEdge(source, target);
|
|
47
|
+
}
|
|
48
|
+
return tarjan(labelGraph);
|
|
62
49
|
}
|
|
63
50
|
|
|
64
51
|
/**
|
|
65
52
|
* Pure-JS Tarjan's SCC implementation.
|
|
53
|
+
* Kept for backward compatibility — accepts raw {source, target}[] edges.
|
|
66
54
|
*/
|
|
67
55
|
export function findCyclesJS(edges) {
|
|
68
|
-
const graph = new
|
|
56
|
+
const graph = new CodeGraph();
|
|
69
57
|
for (const { source, target } of edges) {
|
|
70
|
-
|
|
71
|
-
graph.get(source).push(target);
|
|
72
|
-
if (!graph.has(target)) graph.set(target, []);
|
|
58
|
+
graph.addEdge(source, target);
|
|
73
59
|
}
|
|
74
|
-
|
|
75
|
-
// Tarjan's strongly connected components algorithm
|
|
76
|
-
let index = 0;
|
|
77
|
-
const stack = [];
|
|
78
|
-
const onStack = new Set();
|
|
79
|
-
const indices = new Map();
|
|
80
|
-
const lowlinks = new Map();
|
|
81
|
-
const sccs = [];
|
|
82
|
-
|
|
83
|
-
function strongconnect(v) {
|
|
84
|
-
indices.set(v, index);
|
|
85
|
-
lowlinks.set(v, index);
|
|
86
|
-
index++;
|
|
87
|
-
stack.push(v);
|
|
88
|
-
onStack.add(v);
|
|
89
|
-
|
|
90
|
-
for (const w of graph.get(v) || []) {
|
|
91
|
-
if (!indices.has(w)) {
|
|
92
|
-
strongconnect(w);
|
|
93
|
-
lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
|
|
94
|
-
} else if (onStack.has(w)) {
|
|
95
|
-
lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (lowlinks.get(v) === indices.get(v)) {
|
|
100
|
-
const scc = [];
|
|
101
|
-
let w;
|
|
102
|
-
do {
|
|
103
|
-
w = stack.pop();
|
|
104
|
-
onStack.delete(w);
|
|
105
|
-
scc.push(w);
|
|
106
|
-
} while (w !== v);
|
|
107
|
-
if (scc.length > 1) sccs.push(scc);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
for (const node of graph.keys()) {
|
|
112
|
-
if (!indices.has(node)) strongconnect(node);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return sccs;
|
|
60
|
+
return tarjan(graph);
|
|
116
61
|
}
|
|
117
62
|
|
|
118
63
|
/**
|
package/src/dataflow.js
CHANGED
|
@@ -26,8 +26,7 @@ import { paginateResult } from './paginate.js';
|
|
|
26
26
|
import { ALL_SYMBOL_KINDS, normalizeSymbol } from './queries.js';
|
|
27
27
|
|
|
28
28
|
// Re-export for backward compatibility
|
|
29
|
-
export { DATAFLOW_RULES };
|
|
30
|
-
export { _makeDataflowRules as makeDataflowRules };
|
|
29
|
+
export { _makeDataflowRules as makeDataflowRules, DATAFLOW_RULES };
|
|
31
30
|
|
|
32
31
|
export const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
|
|
33
32
|
|
package/src/db/connection.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
4
|
+
import { DbError } from '../errors.js';
|
|
4
5
|
import { warn } from '../logger.js';
|
|
5
6
|
|
|
6
7
|
function isProcessAlive(pid) {
|
|
@@ -78,11 +79,10 @@ export function findDbPath(customPath) {
|
|
|
78
79
|
export function openReadonlyOrFail(customPath) {
|
|
79
80
|
const dbPath = findDbPath(customPath);
|
|
80
81
|
if (!fs.existsSync(dbPath)) {
|
|
81
|
-
|
|
82
|
-
`No codegraph database found at ${dbPath}.\
|
|
83
|
-
|
|
82
|
+
throw new DbError(
|
|
83
|
+
`No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`,
|
|
84
|
+
{ file: dbPath },
|
|
84
85
|
);
|
|
85
|
-
process.exit(1);
|
|
86
86
|
}
|
|
87
87
|
return new Database(dbPath, { readonly: true });
|
|
88
88
|
}
|
package/src/db/migrations.js
CHANGED
|
@@ -229,6 +229,17 @@ export const MIGRATIONS = [
|
|
|
229
229
|
CREATE INDEX IF NOT EXISTS idx_nodes_exported ON nodes(exported);
|
|
230
230
|
`,
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
version: 15,
|
|
234
|
+
up: `
|
|
235
|
+
ALTER TABLE nodes ADD COLUMN qualified_name TEXT;
|
|
236
|
+
ALTER TABLE nodes ADD COLUMN scope TEXT;
|
|
237
|
+
ALTER TABLE nodes ADD COLUMN visibility TEXT;
|
|
238
|
+
UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL;
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name);
|
|
240
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope);
|
|
241
|
+
`,
|
|
242
|
+
},
|
|
232
243
|
];
|
|
233
244
|
|
|
234
245
|
export function getBuildMeta(db, key) {
|
|
@@ -309,4 +320,34 @@ export function initSchema(db) {
|
|
|
309
320
|
} catch {
|
|
310
321
|
/* already exists */
|
|
311
322
|
}
|
|
323
|
+
try {
|
|
324
|
+
db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
|
|
325
|
+
} catch {
|
|
326
|
+
/* already exists */
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
|
|
330
|
+
} catch {
|
|
331
|
+
/* already exists */
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
|
|
335
|
+
} catch {
|
|
336
|
+
/* already exists */
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
|
|
340
|
+
} catch {
|
|
341
|
+
/* nodes table may not exist yet */
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
|
|
345
|
+
} catch {
|
|
346
|
+
/* already exists */
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
|
|
350
|
+
} catch {
|
|
351
|
+
/* already exists */
|
|
352
|
+
}
|
|
312
353
|
}
|
package/src/db/query-builder.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DbError } from '../errors.js';
|
|
1
2
|
import { EVERY_EDGE_KIND } from '../kinds.js';
|
|
2
3
|
|
|
3
4
|
// ─── Validation Helpers ─────────────────────────────────────────────
|
|
@@ -12,13 +13,13 @@ const SAFE_SELECT_TOKEN_RE =
|
|
|
12
13
|
|
|
13
14
|
function validateAlias(alias) {
|
|
14
15
|
if (!SAFE_ALIAS_RE.test(alias)) {
|
|
15
|
-
throw new
|
|
16
|
+
throw new DbError(`Invalid SQL alias: ${alias}`);
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function validateColumn(column) {
|
|
20
21
|
if (!SAFE_COLUMN_RE.test(column)) {
|
|
21
|
-
throw new
|
|
22
|
+
throw new DbError(`Invalid SQL column: ${column}`);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -26,7 +27,7 @@ function validateOrderBy(clause) {
|
|
|
26
27
|
const terms = clause.split(',').map((t) => t.trim());
|
|
27
28
|
for (const term of terms) {
|
|
28
29
|
if (!SAFE_ORDER_TERM_RE.test(term)) {
|
|
29
|
-
throw new
|
|
30
|
+
throw new DbError(`Invalid ORDER BY term: ${term}`);
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -51,14 +52,14 @@ function validateSelectCols(cols) {
|
|
|
51
52
|
const tokens = splitTopLevelCommas(cols);
|
|
52
53
|
for (const token of tokens) {
|
|
53
54
|
if (!SAFE_SELECT_TOKEN_RE.test(token)) {
|
|
54
|
-
throw new
|
|
55
|
+
throw new DbError(`Invalid SELECT expression: ${token}`);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function validateEdgeKind(edgeKind) {
|
|
60
61
|
if (!EVERY_EDGE_KIND.includes(edgeKind)) {
|
|
61
|
-
throw new
|
|
62
|
+
throw new DbError(
|
|
62
63
|
`Invalid edge kind: ${edgeKind} (expected one of ${EVERY_EDGE_KIND.join(', ')})`,
|
|
63
64
|
);
|
|
64
65
|
}
|