@optave/codegraph 3.1.3 → 3.1.5
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 +38 -84
- package/package.json +13 -8
- package/src/ast-analysis/engine.js +32 -12
- package/src/ast-analysis/shared.js +6 -5
- package/src/cli/commands/ast.js +22 -0
- package/src/cli/commands/audit.js +45 -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 +26 -0
- package/src/cli/commands/check.js +74 -0
- package/src/cli/commands/children.js +28 -0
- package/src/cli/commands/co-change.js +67 -0
- package/src/cli/commands/communities.js +19 -0
- package/src/cli/commands/complexity.js +46 -0
- package/src/cli/commands/context.js +30 -0
- package/src/cli/commands/cycles.js +32 -0
- package/src/cli/commands/dataflow.js +28 -0
- package/src/cli/commands/deps.js +12 -0
- package/src/cli/commands/diff-impact.js +26 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +78 -0
- package/src/cli/commands/exports.js +14 -0
- package/src/cli/commands/flow.js +32 -0
- package/src/cli/commands/fn-impact.js +26 -0
- package/src/cli/commands/impact.js +12 -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 +89 -0
- package/src/cli/commands/query.js +45 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +30 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +28 -0
- package/src/cli/commands/snapshot.js +66 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +33 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +20 -0
- package/src/cli/index.js +124 -0
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +59 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1522
- package/src/db/connection.js +130 -7
- package/src/{db.js → db/index.js} +17 -5
- package/src/db/migrations.js +42 -1
- package/src/db/query-builder.js +20 -12
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/graph-read.js +7 -4
- package/src/db/repository/in-memory-repository.js +575 -0
- package/src/db/repository/index.js +5 -1
- package/src/db/repository/nodes.js +60 -6
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/domain/analysis/context.js +408 -0
- package/src/domain/analysis/dependencies.js +341 -0
- package/src/domain/analysis/exports.js +134 -0
- package/src/domain/analysis/impact.js +466 -0
- package/src/domain/analysis/module-map.js +322 -0
- package/src/domain/analysis/roles.js +45 -0
- package/src/domain/analysis/symbol-lookup.js +238 -0
- package/src/domain/graph/builder/context.js +85 -0
- package/src/domain/graph/builder/helpers.js +218 -0
- package/src/domain/graph/builder/incremental.js +178 -0
- package/src/domain/graph/builder/pipeline.js +130 -0
- package/src/domain/graph/builder/stages/build-edges.js +297 -0
- package/src/domain/graph/builder/stages/build-structure.js +113 -0
- package/src/domain/graph/builder/stages/collect-files.js +44 -0
- package/src/domain/graph/builder/stages/detect-changes.js +413 -0
- package/src/domain/graph/builder/stages/finalize.js +139 -0
- package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
- package/src/domain/graph/builder/stages/parse-files.js +28 -0
- package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
- package/src/domain/graph/builder/stages/run-analyses.js +44 -0
- package/src/domain/graph/builder.js +11 -0
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/domain/graph/cycles.js +82 -0
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +3 -3
- package/src/{watcher.js → domain/graph/watcher.js} +10 -150
- package/src/{parser.js → domain/parser.js} +5 -5
- package/src/domain/queries.js +48 -0
- package/src/domain/search/generator.js +163 -0
- package/src/domain/search/index.js +13 -0
- package/src/domain/search/models.js +218 -0
- package/src/domain/search/search/cli-formatter.js +151 -0
- package/src/domain/search/search/filters.js +46 -0
- package/src/domain/search/search/hybrid.js +121 -0
- package/src/domain/search/search/keyword.js +68 -0
- package/src/domain/search/search/prepare.js +66 -0
- package/src/domain/search/search/semantic.js +145 -0
- package/src/domain/search/stores/fts5.js +27 -0
- package/src/domain/search/stores/sqlite-blob.js +24 -0
- package/src/domain/search/strategies/source.js +14 -0
- package/src/domain/search/strategies/structured.js +43 -0
- package/src/domain/search/strategies/text-utils.js +43 -0
- 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 +39 -2
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- package/src/{ast.js → features/ast.js} +8 -8
- package/src/{audit.js → features/audit.js} +16 -44
- package/src/{batch.js → features/batch.js} +6 -5
- package/src/{boundaries.js → features/boundaries.js} +2 -2
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +11 -12
- 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} +18 -90
- package/src/{complexity.js → features/complexity.js} +13 -13
- package/src/{dataflow.js → features/dataflow.js} +12 -13
- package/src/features/export.js +378 -0
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/features/graph-enrichment.js +327 -0
- package/src/{manifesto.js → features/manifesto.js} +6 -6
- package/src/{owners.js → features/owners.js} +2 -2
- package/src/{sequence.js → features/sequence.js} +16 -52
- package/src/{snapshot.js → features/snapshot.js} +8 -7
- package/src/{structure.js → features/structure.js} +20 -45
- package/src/{triage.js → features/triage.js} +27 -79
- 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 +110 -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.cjs +16 -0
- package/src/index.js +42 -219
- package/src/{native.js → infrastructure/native.js} +3 -1
- 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.js → mcp/tool-registry.js} +6 -675
- 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 +12 -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/{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/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +6 -6
- package/src/presentation/colors.js +44 -0
- 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/presentation/export.js +444 -0
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +4 -4
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +46 -0
- package/src/presentation/queries-cli/impact.js +198 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +334 -0
- package/src/presentation/queries-cli/overview.js +197 -0
- package/src/presentation/queries-cli/path.js +58 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +144 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +2 -2
- package/src/presentation/table.js +47 -0
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{viewer.js → presentation/viewer.js} +68 -382
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/errors.js +78 -0
- 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/builder.js +0 -1486
- package/src/cycles.js +0 -137
- package/src/embedder.js +0 -1097
- package/src/export.js +0 -681
- package/src/queries-cli.js +0 -866
- package/src/queries.js +0 -2289
- /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/{kinds.js → shared/kinds.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Repository } from './base.js';
|
|
2
|
+
import { hasCfgTables } from './cfg.js';
|
|
3
|
+
import { getComplexityForNode } from './complexity.js';
|
|
4
|
+
import { hasDataflowTable } from './dataflow.js';
|
|
5
|
+
import {
|
|
6
|
+
countCrossFileCallers,
|
|
7
|
+
findAllIncomingEdges,
|
|
8
|
+
findAllOutgoingEdges,
|
|
9
|
+
findCalleeNames,
|
|
10
|
+
findCallees,
|
|
11
|
+
findCallerNames,
|
|
12
|
+
findCallers,
|
|
13
|
+
findCrossFileCallTargets,
|
|
14
|
+
findDistinctCallers,
|
|
15
|
+
findImportDependents,
|
|
16
|
+
findImportSources,
|
|
17
|
+
findImportTargets,
|
|
18
|
+
findIntraFileCallEdges,
|
|
19
|
+
getClassHierarchy,
|
|
20
|
+
} from './edges.js';
|
|
21
|
+
import { hasEmbeddings } from './embeddings.js';
|
|
22
|
+
import { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from './graph-read.js';
|
|
23
|
+
import {
|
|
24
|
+
bulkNodeIdsByFile,
|
|
25
|
+
countEdges,
|
|
26
|
+
countFiles,
|
|
27
|
+
countNodes,
|
|
28
|
+
findFileNodes,
|
|
29
|
+
findNodeById,
|
|
30
|
+
findNodeByQualifiedName,
|
|
31
|
+
findNodeChildren,
|
|
32
|
+
findNodesByFile,
|
|
33
|
+
findNodesByScope,
|
|
34
|
+
findNodesForTriage,
|
|
35
|
+
findNodesWithFanIn,
|
|
36
|
+
getFunctionNodeId,
|
|
37
|
+
getNodeId,
|
|
38
|
+
iterateFunctionNodes,
|
|
39
|
+
listFunctionNodes,
|
|
40
|
+
} from './nodes.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* SqliteRepository — wraps existing `fn(db, ...)` repository functions
|
|
44
|
+
* behind the Repository interface so callers can use `repo.method(...)`.
|
|
45
|
+
*/
|
|
46
|
+
export class SqliteRepository extends Repository {
|
|
47
|
+
#db;
|
|
48
|
+
|
|
49
|
+
/** @param {object} db - better-sqlite3 Database instance */
|
|
50
|
+
constructor(db) {
|
|
51
|
+
super();
|
|
52
|
+
this.#db = db;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Expose the underlying db for code that still needs raw access. */
|
|
56
|
+
get db() {
|
|
57
|
+
return this.#db;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Node lookups ──────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
findNodeById(id) {
|
|
63
|
+
return findNodeById(this.#db, id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
findNodesByFile(file) {
|
|
67
|
+
return findNodesByFile(this.#db, file);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
findFileNodes(fileLike) {
|
|
71
|
+
return findFileNodes(this.#db, fileLike);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
findNodesWithFanIn(namePattern, opts) {
|
|
75
|
+
return findNodesWithFanIn(this.#db, namePattern, opts);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
countNodes() {
|
|
79
|
+
return countNodes(this.#db);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
countEdges() {
|
|
83
|
+
return countEdges(this.#db);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
countFiles() {
|
|
87
|
+
return countFiles(this.#db);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getNodeId(name, kind, file, line) {
|
|
91
|
+
return getNodeId(this.#db, name, kind, file, line);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getFunctionNodeId(name, file, line) {
|
|
95
|
+
return getFunctionNodeId(this.#db, name, file, line);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
bulkNodeIdsByFile(file) {
|
|
99
|
+
return bulkNodeIdsByFile(this.#db, file);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
findNodeChildren(parentId) {
|
|
103
|
+
return findNodeChildren(this.#db, parentId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
findNodesByScope(scopeName, opts) {
|
|
107
|
+
return findNodesByScope(this.#db, scopeName, opts);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
findNodeByQualifiedName(qualifiedName, opts) {
|
|
111
|
+
return findNodeByQualifiedName(this.#db, qualifiedName, opts);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
listFunctionNodes(opts) {
|
|
115
|
+
return listFunctionNodes(this.#db, opts);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
iterateFunctionNodes(opts) {
|
|
119
|
+
return iterateFunctionNodes(this.#db, opts);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
findNodesForTriage(opts) {
|
|
123
|
+
return findNodesForTriage(this.#db, opts);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Edge queries ──────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
findCallees(nodeId) {
|
|
129
|
+
return findCallees(this.#db, nodeId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
findCallers(nodeId) {
|
|
133
|
+
return findCallers(this.#db, nodeId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
findDistinctCallers(nodeId) {
|
|
137
|
+
return findDistinctCallers(this.#db, nodeId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
findAllOutgoingEdges(nodeId) {
|
|
141
|
+
return findAllOutgoingEdges(this.#db, nodeId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
findAllIncomingEdges(nodeId) {
|
|
145
|
+
return findAllIncomingEdges(this.#db, nodeId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
findCalleeNames(nodeId) {
|
|
149
|
+
return findCalleeNames(this.#db, nodeId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
findCallerNames(nodeId) {
|
|
153
|
+
return findCallerNames(this.#db, nodeId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
findImportTargets(nodeId) {
|
|
157
|
+
return findImportTargets(this.#db, nodeId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
findImportSources(nodeId) {
|
|
161
|
+
return findImportSources(this.#db, nodeId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
findImportDependents(nodeId) {
|
|
165
|
+
return findImportDependents(this.#db, nodeId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
findCrossFileCallTargets(file) {
|
|
169
|
+
return findCrossFileCallTargets(this.#db, file);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
countCrossFileCallers(nodeId, file) {
|
|
173
|
+
return countCrossFileCallers(this.#db, nodeId, file);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getClassHierarchy(classNodeId) {
|
|
177
|
+
return getClassHierarchy(this.#db, classNodeId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
findIntraFileCallEdges(file) {
|
|
181
|
+
return findIntraFileCallEdges(this.#db, file);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Graph-read queries ────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
getCallableNodes() {
|
|
187
|
+
return getCallableNodes(this.#db);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getCallEdges() {
|
|
191
|
+
return getCallEdges(this.#db);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getFileNodesAll() {
|
|
195
|
+
return getFileNodesAll(this.#db);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getImportEdges() {
|
|
199
|
+
return getImportEdges(this.#db);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Optional table checks ─────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
hasCfgTables() {
|
|
205
|
+
return hasCfgTables(this.#db);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
hasEmbeddings() {
|
|
209
|
+
return hasEmbeddings(this.#db);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
hasDataflowTable() {
|
|
213
|
+
return hasDataflowTable(this.#db);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getComplexityForNode(nodeId) {
|
|
217
|
+
return getComplexityForNode(this.#db, nodeId);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
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 { isTestFile } from '../../infrastructure/test-filter.js';
|
|
17
|
+
import {
|
|
18
|
+
createFileLinesReader,
|
|
19
|
+
extractSignature,
|
|
20
|
+
extractSummary,
|
|
21
|
+
isFileLikeTarget,
|
|
22
|
+
readSourceRange,
|
|
23
|
+
} from '../../shared/file-utils.js';
|
|
24
|
+
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
|
|
25
|
+
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
26
|
+
import { paginateResult } from '../../shared/paginate.js';
|
|
27
|
+
import { findMatchingNodes } from './symbol-lookup.js';
|
|
28
|
+
|
|
29
|
+
function explainFileImpl(db, target, getFileLines) {
|
|
30
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
31
|
+
if (fileNodes.length === 0) return [];
|
|
32
|
+
|
|
33
|
+
return fileNodes.map((fn) => {
|
|
34
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
35
|
+
|
|
36
|
+
// IDs of symbols that have incoming calls from other files (public)
|
|
37
|
+
const publicIds = findCrossFileCallTargets(db, fn.file);
|
|
38
|
+
|
|
39
|
+
const fileLines = getFileLines(fn.file);
|
|
40
|
+
const mapSymbol = (s) => ({
|
|
41
|
+
name: s.name,
|
|
42
|
+
kind: s.kind,
|
|
43
|
+
line: s.line,
|
|
44
|
+
role: s.role || null,
|
|
45
|
+
summary: fileLines ? extractSummary(fileLines, s.line) : null,
|
|
46
|
+
signature: fileLines ? extractSignature(fileLines, s.line) : null,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
|
|
50
|
+
const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
|
|
51
|
+
|
|
52
|
+
// Imports / importedBy
|
|
53
|
+
const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
|
|
54
|
+
|
|
55
|
+
const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
|
|
56
|
+
|
|
57
|
+
// Intra-file data flow
|
|
58
|
+
const intraEdges = findIntraFileCallEdges(db, fn.file);
|
|
59
|
+
|
|
60
|
+
const dataFlowMap = new Map();
|
|
61
|
+
for (const edge of intraEdges) {
|
|
62
|
+
if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
|
|
63
|
+
dataFlowMap.get(edge.caller_name).push(edge.callee_name);
|
|
64
|
+
}
|
|
65
|
+
const dataFlow = [...dataFlowMap.entries()].map(([caller, callees]) => ({
|
|
66
|
+
caller,
|
|
67
|
+
callees,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Line count: prefer node_metrics (actual), fall back to MAX(end_line)
|
|
71
|
+
const metric = db
|
|
72
|
+
.prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`)
|
|
73
|
+
.get(fn.id);
|
|
74
|
+
let lineCount = metric?.line_count || null;
|
|
75
|
+
if (!lineCount) {
|
|
76
|
+
const maxLine = db
|
|
77
|
+
.prepare(`SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?`)
|
|
78
|
+
.get(fn.file);
|
|
79
|
+
lineCount = maxLine?.max_end || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
file: fn.file,
|
|
84
|
+
lineCount,
|
|
85
|
+
symbolCount: symbols.length,
|
|
86
|
+
publicApi,
|
|
87
|
+
internal,
|
|
88
|
+
imports,
|
|
89
|
+
importedBy,
|
|
90
|
+
dataFlow,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
96
|
+
let nodes = db
|
|
97
|
+
.prepare(
|
|
98
|
+
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module') ORDER BY file, line`,
|
|
99
|
+
)
|
|
100
|
+
.all(`%${target}%`);
|
|
101
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
102
|
+
if (nodes.length === 0) return [];
|
|
103
|
+
|
|
104
|
+
const hc = new Map();
|
|
105
|
+
return nodes.slice(0, 10).map((node) => {
|
|
106
|
+
const fileLines = getFileLines(node.file);
|
|
107
|
+
const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
|
|
108
|
+
const summary = fileLines ? extractSummary(fileLines, node.line) : null;
|
|
109
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
110
|
+
|
|
111
|
+
const callees = findCallees(db, node.id).map((c) => ({
|
|
112
|
+
name: c.name,
|
|
113
|
+
kind: c.kind,
|
|
114
|
+
file: c.file,
|
|
115
|
+
line: c.line,
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
let callers = findCallers(db, node.id).map((c) => ({
|
|
119
|
+
name: c.name,
|
|
120
|
+
kind: c.kind,
|
|
121
|
+
file: c.file,
|
|
122
|
+
line: c.line,
|
|
123
|
+
}));
|
|
124
|
+
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
125
|
+
|
|
126
|
+
const testCallerRows = findCallers(db, node.id);
|
|
127
|
+
const seenFiles = new Set();
|
|
128
|
+
const relatedTests = testCallerRows
|
|
129
|
+
.filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
|
|
130
|
+
.map((r) => ({ file: r.file }));
|
|
131
|
+
|
|
132
|
+
// Complexity metrics
|
|
133
|
+
let complexityMetrics = null;
|
|
134
|
+
try {
|
|
135
|
+
const cRow = getComplexityForNode(db, node.id);
|
|
136
|
+
if (cRow) {
|
|
137
|
+
complexityMetrics = {
|
|
138
|
+
cognitive: cRow.cognitive,
|
|
139
|
+
cyclomatic: cRow.cyclomatic,
|
|
140
|
+
maxNesting: cRow.max_nesting,
|
|
141
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
142
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
/* table may not exist */
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
...normalizeSymbol(node, db, hc),
|
|
151
|
+
lineCount,
|
|
152
|
+
summary,
|
|
153
|
+
signature,
|
|
154
|
+
complexity: complexityMetrics,
|
|
155
|
+
callees,
|
|
156
|
+
callers,
|
|
157
|
+
relatedTests,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Exported functions ──────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export function contextData(name, customDbPath, opts = {}) {
|
|
165
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
166
|
+
try {
|
|
167
|
+
const depth = opts.depth || 0;
|
|
168
|
+
const noSource = opts.noSource || false;
|
|
169
|
+
const noTests = opts.noTests || false;
|
|
170
|
+
const includeTests = opts.includeTests || false;
|
|
171
|
+
|
|
172
|
+
const dbPath = findDbPath(customDbPath);
|
|
173
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
174
|
+
|
|
175
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
176
|
+
if (nodes.length === 0) {
|
|
177
|
+
return { name, results: [] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// No hardcoded slice — pagination handles bounding via limit/offset
|
|
181
|
+
|
|
182
|
+
const getFileLines = createFileLinesReader(repoRoot);
|
|
183
|
+
|
|
184
|
+
const results = nodes.map((node) => {
|
|
185
|
+
const fileLines = getFileLines(node.file);
|
|
186
|
+
|
|
187
|
+
// Source
|
|
188
|
+
const source = noSource
|
|
189
|
+
? null
|
|
190
|
+
: readSourceRange(repoRoot, node.file, node.line, node.end_line);
|
|
191
|
+
|
|
192
|
+
// Signature
|
|
193
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
194
|
+
|
|
195
|
+
// Callees
|
|
196
|
+
const calleeRows = findCallees(db, node.id);
|
|
197
|
+
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
198
|
+
|
|
199
|
+
const callees = filteredCallees.map((c) => {
|
|
200
|
+
const cLines = getFileLines(c.file);
|
|
201
|
+
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
202
|
+
let calleeSource = null;
|
|
203
|
+
if (depth >= 1) {
|
|
204
|
+
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
name: c.name,
|
|
208
|
+
kind: c.kind,
|
|
209
|
+
file: c.file,
|
|
210
|
+
line: c.line,
|
|
211
|
+
endLine: c.end_line || null,
|
|
212
|
+
summary,
|
|
213
|
+
source: calleeSource,
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Deep callee expansion via BFS (depth > 1, capped at 5)
|
|
218
|
+
if (depth > 1) {
|
|
219
|
+
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
220
|
+
visited.add(node.id);
|
|
221
|
+
let frontier = filteredCallees.map((c) => c.id);
|
|
222
|
+
const maxDepth = Math.min(depth, 5);
|
|
223
|
+
for (let d = 2; d <= maxDepth; d++) {
|
|
224
|
+
const nextFrontier = [];
|
|
225
|
+
for (const fid of frontier) {
|
|
226
|
+
const deeper = findCallees(db, fid);
|
|
227
|
+
for (const c of deeper) {
|
|
228
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
229
|
+
visited.add(c.id);
|
|
230
|
+
nextFrontier.push(c.id);
|
|
231
|
+
const cLines = getFileLines(c.file);
|
|
232
|
+
callees.push({
|
|
233
|
+
name: c.name,
|
|
234
|
+
kind: c.kind,
|
|
235
|
+
file: c.file,
|
|
236
|
+
line: c.line,
|
|
237
|
+
endLine: c.end_line || null,
|
|
238
|
+
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
239
|
+
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
frontier = nextFrontier;
|
|
245
|
+
if (frontier.length === 0) break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Callers
|
|
250
|
+
let callerRows = findCallers(db, node.id);
|
|
251
|
+
|
|
252
|
+
// Method hierarchy resolution
|
|
253
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
254
|
+
const methodName = node.name.split('.').pop();
|
|
255
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
256
|
+
for (const rm of relatedMethods) {
|
|
257
|
+
if (rm.id === node.id) continue;
|
|
258
|
+
const extraCallers = findCallers(db, rm.id);
|
|
259
|
+
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
263
|
+
|
|
264
|
+
const callers = callerRows.map((c) => ({
|
|
265
|
+
name: c.name,
|
|
266
|
+
kind: c.kind,
|
|
267
|
+
file: c.file,
|
|
268
|
+
line: c.line,
|
|
269
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
// Related tests: callers that live in test files
|
|
273
|
+
const testCallerRows = findCallers(db, node.id);
|
|
274
|
+
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
275
|
+
|
|
276
|
+
const testsByFile = new Map();
|
|
277
|
+
for (const tc of testCallers) {
|
|
278
|
+
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
279
|
+
testsByFile.get(tc.file).push(tc);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const relatedTests = [];
|
|
283
|
+
for (const [file] of testsByFile) {
|
|
284
|
+
const tLines = getFileLines(file);
|
|
285
|
+
const testNames = [];
|
|
286
|
+
if (tLines) {
|
|
287
|
+
for (const tl of tLines) {
|
|
288
|
+
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
289
|
+
if (tm) testNames.push(tm[1]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
293
|
+
relatedTests.push({
|
|
294
|
+
file,
|
|
295
|
+
testCount: testNames.length,
|
|
296
|
+
testNames,
|
|
297
|
+
source: testSource,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Complexity metrics
|
|
302
|
+
let complexityMetrics = null;
|
|
303
|
+
try {
|
|
304
|
+
const cRow = getComplexityForNode(db, node.id);
|
|
305
|
+
if (cRow) {
|
|
306
|
+
complexityMetrics = {
|
|
307
|
+
cognitive: cRow.cognitive,
|
|
308
|
+
cyclomatic: cRow.cyclomatic,
|
|
309
|
+
maxNesting: cRow.max_nesting,
|
|
310
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
311
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
/* table may not exist */
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Children (parameters, properties, constants)
|
|
319
|
+
let nodeChildren = [];
|
|
320
|
+
try {
|
|
321
|
+
nodeChildren = findNodeChildren(db, node.id).map((c) => ({
|
|
322
|
+
name: c.name,
|
|
323
|
+
kind: c.kind,
|
|
324
|
+
line: c.line,
|
|
325
|
+
endLine: c.end_line || null,
|
|
326
|
+
}));
|
|
327
|
+
} catch {
|
|
328
|
+
/* parent_id column may not exist */
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
name: node.name,
|
|
333
|
+
kind: node.kind,
|
|
334
|
+
file: node.file,
|
|
335
|
+
line: node.line,
|
|
336
|
+
role: node.role || null,
|
|
337
|
+
endLine: node.end_line || null,
|
|
338
|
+
source,
|
|
339
|
+
signature,
|
|
340
|
+
complexity: complexityMetrics,
|
|
341
|
+
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
342
|
+
callees,
|
|
343
|
+
callers,
|
|
344
|
+
relatedTests,
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const base = { name, results };
|
|
349
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
350
|
+
} finally {
|
|
351
|
+
db.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function explainData(target, customDbPath, opts = {}) {
|
|
356
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
357
|
+
try {
|
|
358
|
+
const noTests = opts.noTests || false;
|
|
359
|
+
const depth = opts.depth || 0;
|
|
360
|
+
const kind = isFileLikeTarget(target) ? 'file' : 'function';
|
|
361
|
+
|
|
362
|
+
const dbPath = findDbPath(customDbPath);
|
|
363
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
364
|
+
|
|
365
|
+
const getFileLines = createFileLinesReader(repoRoot);
|
|
366
|
+
|
|
367
|
+
const results =
|
|
368
|
+
kind === 'file'
|
|
369
|
+
? explainFileImpl(db, target, getFileLines)
|
|
370
|
+
: explainFunctionImpl(db, target, noTests, getFileLines);
|
|
371
|
+
|
|
372
|
+
// Recursive dependency explanation for function targets
|
|
373
|
+
if (kind === 'function' && depth > 0 && results.length > 0) {
|
|
374
|
+
const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
|
|
375
|
+
|
|
376
|
+
function explainCallees(parentResults, currentDepth) {
|
|
377
|
+
if (currentDepth <= 0) return;
|
|
378
|
+
for (const r of parentResults) {
|
|
379
|
+
const newCallees = [];
|
|
380
|
+
for (const callee of r.callees) {
|
|
381
|
+
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
382
|
+
if (visited.has(key)) continue;
|
|
383
|
+
visited.add(key);
|
|
384
|
+
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
385
|
+
const exact = calleeResults.find(
|
|
386
|
+
(cr) => cr.file === callee.file && cr.line === callee.line,
|
|
387
|
+
);
|
|
388
|
+
if (exact) {
|
|
389
|
+
exact._depth = (r._depth || 0) + 1;
|
|
390
|
+
newCallees.push(exact);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (newCallees.length > 0) {
|
|
394
|
+
r.depDetails = newCallees;
|
|
395
|
+
explainCallees(newCallees, currentDepth - 1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
explainCallees(results, depth);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const base = { target, kind, results };
|
|
404
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
405
|
+
} finally {
|
|
406
|
+
db.close();
|
|
407
|
+
}
|
|
408
|
+
}
|