@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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findDistinctCallers,
|
|
3
|
+
findFileNodes,
|
|
4
|
+
findImportDependents,
|
|
5
|
+
findImportSources,
|
|
6
|
+
findImportTargets,
|
|
7
|
+
findNodesByFile,
|
|
8
|
+
openReadonlyOrFail,
|
|
9
|
+
} from '../../db/index.js';
|
|
10
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
11
|
+
|
|
12
|
+
/** Symbol kinds meaningful for a file brief — excludes parameters, properties, constants. */
|
|
13
|
+
const BRIEF_KINDS = new Set([
|
|
14
|
+
'function',
|
|
15
|
+
'method',
|
|
16
|
+
'class',
|
|
17
|
+
'interface',
|
|
18
|
+
'type',
|
|
19
|
+
'struct',
|
|
20
|
+
'enum',
|
|
21
|
+
'trait',
|
|
22
|
+
'record',
|
|
23
|
+
'module',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compute file risk tier from symbol roles and max fan-in.
|
|
28
|
+
* @param {{ role: string|null, callerCount: number }[]} symbols
|
|
29
|
+
* @returns {'high'|'medium'|'low'}
|
|
30
|
+
*/
|
|
31
|
+
function computeRiskTier(symbols) {
|
|
32
|
+
let maxCallers = 0;
|
|
33
|
+
let hasCoreRole = false;
|
|
34
|
+
for (const s of symbols) {
|
|
35
|
+
if (s.callerCount > maxCallers) maxCallers = s.callerCount;
|
|
36
|
+
if (s.role === 'core') hasCoreRole = true;
|
|
37
|
+
}
|
|
38
|
+
if (maxCallers >= 10 || hasCoreRole) return 'high';
|
|
39
|
+
if (maxCallers >= 3) return 'medium';
|
|
40
|
+
return 'low';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* BFS to count transitive callers for a single node.
|
|
45
|
+
* Lightweight variant — only counts, does not collect details.
|
|
46
|
+
*/
|
|
47
|
+
function countTransitiveCallers(db, startId, noTests, maxDepth = 5) {
|
|
48
|
+
const visited = new Set([startId]);
|
|
49
|
+
let frontier = [startId];
|
|
50
|
+
|
|
51
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
52
|
+
const nextFrontier = [];
|
|
53
|
+
for (const fid of frontier) {
|
|
54
|
+
const callers = findDistinctCallers(db, fid);
|
|
55
|
+
for (const c of callers) {
|
|
56
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
57
|
+
visited.add(c.id);
|
|
58
|
+
nextFrontier.push(c.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
frontier = nextFrontier;
|
|
63
|
+
if (frontier.length === 0) break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return visited.size - 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Count transitive file-level import dependents via BFS.
|
|
71
|
+
* Depth-bounded to match countTransitiveCallers and keep hook latency predictable.
|
|
72
|
+
*/
|
|
73
|
+
function countTransitiveImporters(db, fileNodeIds, noTests, maxDepth = 5) {
|
|
74
|
+
const visited = new Set(fileNodeIds);
|
|
75
|
+
let frontier = [...fileNodeIds];
|
|
76
|
+
|
|
77
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
78
|
+
const nextFrontier = [];
|
|
79
|
+
for (const current of frontier) {
|
|
80
|
+
const dependents = findImportDependents(db, current);
|
|
81
|
+
for (const dep of dependents) {
|
|
82
|
+
if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
|
|
83
|
+
visited.add(dep.id);
|
|
84
|
+
nextFrontier.push(dep.id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
frontier = nextFrontier;
|
|
89
|
+
if (frontier.length === 0) break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return visited.size - fileNodeIds.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Produce a token-efficient file brief: symbols with roles and caller counts,
|
|
97
|
+
* importer info with transitive count, and file risk tier.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} file - File path (partial match)
|
|
100
|
+
* @param {string} customDbPath - Path to graph.db
|
|
101
|
+
* @param {{ noTests?: boolean }} opts
|
|
102
|
+
* @returns {{ file: string, results: object[] }}
|
|
103
|
+
*/
|
|
104
|
+
export function briefData(file, customDbPath, opts = {}) {
|
|
105
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
106
|
+
try {
|
|
107
|
+
const noTests = opts.noTests || false;
|
|
108
|
+
const fileNodes = findFileNodes(db, `%${file}%`);
|
|
109
|
+
if (fileNodes.length === 0) {
|
|
110
|
+
return { file, results: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const results = fileNodes.map((fn) => {
|
|
114
|
+
// Direct importers
|
|
115
|
+
let importedBy = findImportSources(db, fn.id);
|
|
116
|
+
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
|
|
117
|
+
const directImporters = [...new Set(importedBy.map((i) => i.file))];
|
|
118
|
+
|
|
119
|
+
// Transitive importer count
|
|
120
|
+
const totalImporterCount = countTransitiveImporters(db, [fn.id], noTests);
|
|
121
|
+
|
|
122
|
+
// Direct imports
|
|
123
|
+
let importsTo = findImportTargets(db, fn.id);
|
|
124
|
+
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
|
|
125
|
+
|
|
126
|
+
// Symbol definitions with roles and caller counts
|
|
127
|
+
const defs = findNodesByFile(db, fn.file).filter((d) => BRIEF_KINDS.has(d.kind));
|
|
128
|
+
const symbols = defs.map((d) => {
|
|
129
|
+
const callerCount = countTransitiveCallers(db, d.id, noTests);
|
|
130
|
+
return {
|
|
131
|
+
name: d.name,
|
|
132
|
+
kind: d.kind,
|
|
133
|
+
line: d.line,
|
|
134
|
+
role: d.role || null,
|
|
135
|
+
callerCount,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const riskTier = computeRiskTier(symbols);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
file: fn.file,
|
|
143
|
+
risk: riskTier,
|
|
144
|
+
imports: importsTo.map((i) => i.file),
|
|
145
|
+
importedBy: directImporters,
|
|
146
|
+
totalImporterCount,
|
|
147
|
+
symbols,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return { file, results };
|
|
152
|
+
} finally {
|
|
153
|
+
db.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
findCallees,
|
|
4
|
+
findCallers,
|
|
5
|
+
findCrossFileCallTargets,
|
|
6
|
+
findDbPath,
|
|
7
|
+
findFileNodes,
|
|
8
|
+
findImportSources,
|
|
9
|
+
findImportTargets,
|
|
10
|
+
findIntraFileCallEdges,
|
|
11
|
+
findNodeChildren,
|
|
12
|
+
findNodesByFile,
|
|
13
|
+
getComplexityForNode,
|
|
14
|
+
openReadonlyOrFail,
|
|
15
|
+
} from '../../db/index.js';
|
|
16
|
+
import { debug } from '../../infrastructure/logger.js';
|
|
17
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
18
|
+
import {
|
|
19
|
+
createFileLinesReader,
|
|
20
|
+
extractSignature,
|
|
21
|
+
extractSummary,
|
|
22
|
+
isFileLikeTarget,
|
|
23
|
+
readSourceRange,
|
|
24
|
+
} from '../../shared/file-utils.js';
|
|
25
|
+
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
|
|
26
|
+
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
27
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
28
|
+
import { findMatchingNodes } from './symbol-lookup.js';
|
|
29
|
+
|
|
30
|
+
function buildCallees(db, node, repoRoot, getFileLines, opts) {
|
|
31
|
+
const { noTests, depth } = opts;
|
|
32
|
+
const calleeRows = findCallees(db, node.id);
|
|
33
|
+
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
34
|
+
|
|
35
|
+
const callees = filteredCallees.map((c) => {
|
|
36
|
+
const cLines = getFileLines(c.file);
|
|
37
|
+
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
38
|
+
let calleeSource = null;
|
|
39
|
+
if (depth >= 1) {
|
|
40
|
+
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
name: c.name,
|
|
44
|
+
kind: c.kind,
|
|
45
|
+
file: c.file,
|
|
46
|
+
line: c.line,
|
|
47
|
+
endLine: c.end_line || null,
|
|
48
|
+
summary,
|
|
49
|
+
source: calleeSource,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (depth > 1) {
|
|
54
|
+
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
55
|
+
visited.add(node.id);
|
|
56
|
+
let frontier = filteredCallees.map((c) => c.id);
|
|
57
|
+
const maxDepth = Math.min(depth, 5);
|
|
58
|
+
for (let d = 2; d <= maxDepth; d++) {
|
|
59
|
+
const nextFrontier = [];
|
|
60
|
+
for (const fid of frontier) {
|
|
61
|
+
const deeper = findCallees(db, fid);
|
|
62
|
+
for (const c of deeper) {
|
|
63
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
64
|
+
visited.add(c.id);
|
|
65
|
+
nextFrontier.push(c.id);
|
|
66
|
+
const cLines = getFileLines(c.file);
|
|
67
|
+
callees.push({
|
|
68
|
+
name: c.name,
|
|
69
|
+
kind: c.kind,
|
|
70
|
+
file: c.file,
|
|
71
|
+
line: c.line,
|
|
72
|
+
endLine: c.end_line || null,
|
|
73
|
+
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
74
|
+
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
frontier = nextFrontier;
|
|
80
|
+
if (frontier.length === 0) break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return callees;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildCallers(db, node, noTests) {
|
|
88
|
+
let callerRows = findCallers(db, node.id);
|
|
89
|
+
|
|
90
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
91
|
+
const methodName = node.name.split('.').pop();
|
|
92
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
93
|
+
for (const rm of relatedMethods) {
|
|
94
|
+
if (rm.id === node.id) continue;
|
|
95
|
+
const extraCallers = findCallers(db, rm.id);
|
|
96
|
+
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
100
|
+
|
|
101
|
+
return callerRows.map((c) => ({
|
|
102
|
+
name: c.name,
|
|
103
|
+
kind: c.kind,
|
|
104
|
+
file: c.file,
|
|
105
|
+
line: c.line,
|
|
106
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildRelatedTests(db, node, getFileLines, includeTests) {
|
|
111
|
+
const testCallerRows = findCallers(db, node.id);
|
|
112
|
+
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
113
|
+
|
|
114
|
+
const testsByFile = new Map();
|
|
115
|
+
for (const tc of testCallers) {
|
|
116
|
+
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
117
|
+
testsByFile.get(tc.file).push(tc);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const relatedTests = [];
|
|
121
|
+
for (const [file] of testsByFile) {
|
|
122
|
+
const tLines = getFileLines(file);
|
|
123
|
+
const testNames = [];
|
|
124
|
+
if (tLines) {
|
|
125
|
+
for (const tl of tLines) {
|
|
126
|
+
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
127
|
+
if (tm) testNames.push(tm[1]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
131
|
+
relatedTests.push({
|
|
132
|
+
file,
|
|
133
|
+
testCount: testNames.length,
|
|
134
|
+
testNames,
|
|
135
|
+
source: testSource,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return relatedTests;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getComplexityMetrics(db, nodeId) {
|
|
143
|
+
try {
|
|
144
|
+
const cRow = getComplexityForNode(db, nodeId);
|
|
145
|
+
if (!cRow) return null;
|
|
146
|
+
return {
|
|
147
|
+
cognitive: cRow.cognitive,
|
|
148
|
+
cyclomatic: cRow.cyclomatic,
|
|
149
|
+
maxNesting: cRow.max_nesting,
|
|
150
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
151
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
152
|
+
};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
debug(`complexity lookup failed for node ${nodeId}: ${e.message}`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getNodeChildrenSafe(db, nodeId) {
|
|
160
|
+
try {
|
|
161
|
+
return findNodeChildren(db, nodeId).map((c) => ({
|
|
162
|
+
name: c.name,
|
|
163
|
+
kind: c.kind,
|
|
164
|
+
line: c.line,
|
|
165
|
+
endLine: c.end_line || null,
|
|
166
|
+
}));
|
|
167
|
+
} catch (e) {
|
|
168
|
+
debug(`findNodeChildren failed for node ${nodeId}: ${e.message}`);
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function explainFileImpl(db, target, getFileLines) {
|
|
174
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
175
|
+
if (fileNodes.length === 0) return [];
|
|
176
|
+
|
|
177
|
+
return fileNodes.map((fn) => {
|
|
178
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
179
|
+
|
|
180
|
+
// IDs of symbols that have incoming calls from other files (public)
|
|
181
|
+
const publicIds = findCrossFileCallTargets(db, fn.file);
|
|
182
|
+
|
|
183
|
+
const fileLines = getFileLines(fn.file);
|
|
184
|
+
const mapSymbol = (s) => ({
|
|
185
|
+
name: s.name,
|
|
186
|
+
kind: s.kind,
|
|
187
|
+
line: s.line,
|
|
188
|
+
role: s.role || null,
|
|
189
|
+
summary: fileLines ? extractSummary(fileLines, s.line) : null,
|
|
190
|
+
signature: fileLines ? extractSignature(fileLines, s.line) : null,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
|
|
194
|
+
const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
|
|
195
|
+
|
|
196
|
+
const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
|
|
197
|
+
const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
|
|
198
|
+
|
|
199
|
+
const intraEdges = findIntraFileCallEdges(db, fn.file);
|
|
200
|
+
const dataFlowMap = new Map();
|
|
201
|
+
for (const edge of intraEdges) {
|
|
202
|
+
if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
|
|
203
|
+
dataFlowMap.get(edge.caller_name).push(edge.callee_name);
|
|
204
|
+
}
|
|
205
|
+
const dataFlow = [...dataFlowMap.entries()].map(([caller, callees]) => ({
|
|
206
|
+
caller,
|
|
207
|
+
callees,
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
const metric = db
|
|
211
|
+
.prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`)
|
|
212
|
+
.get(fn.id);
|
|
213
|
+
let lineCount = metric?.line_count || null;
|
|
214
|
+
if (!lineCount) {
|
|
215
|
+
const maxLine = db
|
|
216
|
+
.prepare(`SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?`)
|
|
217
|
+
.get(fn.file);
|
|
218
|
+
lineCount = maxLine?.max_end || null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
file: fn.file,
|
|
223
|
+
lineCount,
|
|
224
|
+
symbolCount: symbols.length,
|
|
225
|
+
publicApi,
|
|
226
|
+
internal,
|
|
227
|
+
imports,
|
|
228
|
+
importedBy,
|
|
229
|
+
dataFlow,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
235
|
+
let nodes = db
|
|
236
|
+
.prepare(
|
|
237
|
+
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module','constant') ORDER BY file, line`,
|
|
238
|
+
)
|
|
239
|
+
.all(`%${target}%`);
|
|
240
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
241
|
+
if (nodes.length === 0) return [];
|
|
242
|
+
|
|
243
|
+
const hc = new Map();
|
|
244
|
+
return nodes.slice(0, 10).map((node) => {
|
|
245
|
+
const fileLines = getFileLines(node.file);
|
|
246
|
+
const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
|
|
247
|
+
const summary = fileLines ? extractSummary(fileLines, node.line) : null;
|
|
248
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
249
|
+
|
|
250
|
+
const callees = findCallees(db, node.id).map((c) => ({
|
|
251
|
+
name: c.name,
|
|
252
|
+
kind: c.kind,
|
|
253
|
+
file: c.file,
|
|
254
|
+
line: c.line,
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
let callers = findCallers(db, node.id).map((c) => ({
|
|
258
|
+
name: c.name,
|
|
259
|
+
kind: c.kind,
|
|
260
|
+
file: c.file,
|
|
261
|
+
line: c.line,
|
|
262
|
+
}));
|
|
263
|
+
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
264
|
+
|
|
265
|
+
const testCallerRows = findCallers(db, node.id);
|
|
266
|
+
const seenFiles = new Set();
|
|
267
|
+
const relatedTests = testCallerRows
|
|
268
|
+
.filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
|
|
269
|
+
.map((r) => ({ file: r.file }));
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
...normalizeSymbol(node, db, hc),
|
|
273
|
+
lineCount,
|
|
274
|
+
summary,
|
|
275
|
+
signature,
|
|
276
|
+
complexity: getComplexityMetrics(db, node.id),
|
|
277
|
+
callees,
|
|
278
|
+
callers,
|
|
279
|
+
relatedTests,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function explainCallees(parentResults, currentDepth, visited, db, noTests, getFileLines) {
|
|
285
|
+
if (currentDepth <= 0) return;
|
|
286
|
+
for (const r of parentResults) {
|
|
287
|
+
const newCallees = [];
|
|
288
|
+
for (const callee of r.callees) {
|
|
289
|
+
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
290
|
+
if (visited.has(key)) continue;
|
|
291
|
+
visited.add(key);
|
|
292
|
+
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
293
|
+
const exact = calleeResults.find((cr) => cr.file === callee.file && cr.line === callee.line);
|
|
294
|
+
if (exact) {
|
|
295
|
+
exact._depth = (r._depth || 0) + 1;
|
|
296
|
+
newCallees.push(exact);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (newCallees.length > 0) {
|
|
300
|
+
r.depDetails = newCallees;
|
|
301
|
+
explainCallees(newCallees, currentDepth - 1, visited, db, noTests, getFileLines);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Exported functions ──────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
export function contextData(name, customDbPath, opts = {}) {
|
|
309
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
310
|
+
try {
|
|
311
|
+
const depth = opts.depth || 0;
|
|
312
|
+
const noSource = opts.noSource || false;
|
|
313
|
+
const noTests = opts.noTests || false;
|
|
314
|
+
const includeTests = opts.includeTests || false;
|
|
315
|
+
|
|
316
|
+
const dbPath = findDbPath(customDbPath);
|
|
317
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
318
|
+
|
|
319
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
320
|
+
if (nodes.length === 0) {
|
|
321
|
+
return { name, results: [] };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const getFileLines = createFileLinesReader(repoRoot);
|
|
325
|
+
|
|
326
|
+
const results = nodes.map((node) => {
|
|
327
|
+
const fileLines = getFileLines(node.file);
|
|
328
|
+
|
|
329
|
+
const source = noSource
|
|
330
|
+
? null
|
|
331
|
+
: readSourceRange(repoRoot, node.file, node.line, node.end_line);
|
|
332
|
+
|
|
333
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
334
|
+
|
|
335
|
+
const callees = buildCallees(db, node, repoRoot, getFileLines, { noTests, depth });
|
|
336
|
+
const callers = buildCallers(db, node, noTests);
|
|
337
|
+
const relatedTests = buildRelatedTests(db, node, getFileLines, includeTests);
|
|
338
|
+
const complexityMetrics = getComplexityMetrics(db, node.id);
|
|
339
|
+
const nodeChildren = getNodeChildrenSafe(db, node.id);
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
name: node.name,
|
|
343
|
+
kind: node.kind,
|
|
344
|
+
file: node.file,
|
|
345
|
+
line: node.line,
|
|
346
|
+
role: node.role || null,
|
|
347
|
+
endLine: node.end_line || null,
|
|
348
|
+
source,
|
|
349
|
+
signature,
|
|
350
|
+
complexity: complexityMetrics,
|
|
351
|
+
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
352
|
+
callees,
|
|
353
|
+
callers,
|
|
354
|
+
relatedTests,
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const base = { name, results };
|
|
359
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
360
|
+
} finally {
|
|
361
|
+
db.close();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function explainData(target, customDbPath, opts = {}) {
|
|
366
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
367
|
+
try {
|
|
368
|
+
const noTests = opts.noTests || false;
|
|
369
|
+
const depth = opts.depth || 0;
|
|
370
|
+
const kind = isFileLikeTarget(target) ? 'file' : 'function';
|
|
371
|
+
|
|
372
|
+
const dbPath = findDbPath(customDbPath);
|
|
373
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
374
|
+
|
|
375
|
+
const getFileLines = createFileLinesReader(repoRoot);
|
|
376
|
+
|
|
377
|
+
const results =
|
|
378
|
+
kind === 'file'
|
|
379
|
+
? explainFileImpl(db, target, getFileLines)
|
|
380
|
+
: explainFunctionImpl(db, target, noTests, getFileLines);
|
|
381
|
+
|
|
382
|
+
if (kind === 'function' && depth > 0 && results.length > 0) {
|
|
383
|
+
const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
|
|
384
|
+
explainCallees(results, depth, visited, db, noTests, getFileLines);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const base = { target, kind, results };
|
|
388
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
389
|
+
} finally {
|
|
390
|
+
db.close();
|
|
391
|
+
}
|
|
392
|
+
}
|