@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,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage: insertNodes
|
|
3
|
+
*
|
|
4
|
+
* Batch-inserts file nodes, definitions, exports, children, and contains/parameter_of edges.
|
|
5
|
+
* Updates file hashes for incremental builds.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { performance } from 'node:perf_hooks';
|
|
9
|
+
import { bulkNodeIdsByFile } from '../../../../db/index.js';
|
|
10
|
+
import {
|
|
11
|
+
batchInsertEdges,
|
|
12
|
+
batchInsertNodes,
|
|
13
|
+
fileHash,
|
|
14
|
+
fileStat,
|
|
15
|
+
readFileSafe,
|
|
16
|
+
} from '../helpers.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('../context.js').PipelineContext} ctx
|
|
20
|
+
*/
|
|
21
|
+
export async function insertNodes(ctx) {
|
|
22
|
+
const { db, allSymbols, filesToParse, metadataUpdates, rootDir, removed } = ctx;
|
|
23
|
+
|
|
24
|
+
// Build lookup from incremental data (pre-computed hashes + stats)
|
|
25
|
+
const precomputedData = new Map();
|
|
26
|
+
for (const item of filesToParse) {
|
|
27
|
+
if (item.relPath) {
|
|
28
|
+
precomputedData.set(item.relPath, item);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const bulkGetNodeIds = { all: (file) => bulkNodeIdsByFile(db, file) };
|
|
33
|
+
|
|
34
|
+
// Prepare hash upsert
|
|
35
|
+
let upsertHash;
|
|
36
|
+
try {
|
|
37
|
+
upsertHash = db.prepare(
|
|
38
|
+
'INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)',
|
|
39
|
+
);
|
|
40
|
+
} catch {
|
|
41
|
+
upsertHash = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Populate fileSymbols before the transaction so it is a pure input
|
|
45
|
+
// to (rather than a side-effect of) the DB write — avoids partial
|
|
46
|
+
// population if the transaction rolls back.
|
|
47
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
48
|
+
ctx.fileSymbols.set(relPath, symbols);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const insertAll = db.transaction(() => {
|
|
52
|
+
// Phase 1: Batch insert all file nodes + definitions + exports
|
|
53
|
+
// Row format: [name, kind, file, line, end_line, parent_id, qualified_name, scope, visibility]
|
|
54
|
+
const phase1Rows = [];
|
|
55
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
56
|
+
phase1Rows.push([relPath, 'file', relPath, 0, null, null, null, null, null]);
|
|
57
|
+
for (const def of symbols.definitions) {
|
|
58
|
+
// Methods already have 'Class.method' as name — use as qualified_name.
|
|
59
|
+
// For methods, scope is the class portion; for top-level defs, scope is null.
|
|
60
|
+
const dotIdx = def.name.lastIndexOf('.');
|
|
61
|
+
const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;
|
|
62
|
+
phase1Rows.push([
|
|
63
|
+
def.name,
|
|
64
|
+
def.kind,
|
|
65
|
+
relPath,
|
|
66
|
+
def.line,
|
|
67
|
+
def.endLine || null,
|
|
68
|
+
null,
|
|
69
|
+
def.name,
|
|
70
|
+
scope,
|
|
71
|
+
def.visibility || null,
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
for (const exp of symbols.exports) {
|
|
75
|
+
phase1Rows.push([exp.name, exp.kind, relPath, exp.line, null, null, exp.name, null, null]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
batchInsertNodes(db, phase1Rows);
|
|
79
|
+
|
|
80
|
+
// Phase 1b: Mark exported symbols
|
|
81
|
+
const markExported = db.prepare(
|
|
82
|
+
'UPDATE nodes SET exported = 1 WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
83
|
+
);
|
|
84
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
85
|
+
for (const exp of symbols.exports) {
|
|
86
|
+
markExported.run(exp.name, exp.kind, relPath, exp.line);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Phase 3: Batch insert children (needs parent IDs from Phase 2)
|
|
91
|
+
const childRows = [];
|
|
92
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
93
|
+
const nodeIdMap = new Map();
|
|
94
|
+
for (const row of bulkGetNodeIds.all(relPath)) {
|
|
95
|
+
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
96
|
+
}
|
|
97
|
+
for (const def of symbols.definitions) {
|
|
98
|
+
if (!def.children?.length) continue;
|
|
99
|
+
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
100
|
+
if (!defId) continue;
|
|
101
|
+
for (const child of def.children) {
|
|
102
|
+
const qualifiedName = `${def.name}.${child.name}`;
|
|
103
|
+
childRows.push([
|
|
104
|
+
child.name,
|
|
105
|
+
child.kind,
|
|
106
|
+
relPath,
|
|
107
|
+
child.line,
|
|
108
|
+
child.endLine || null,
|
|
109
|
+
defId,
|
|
110
|
+
qualifiedName,
|
|
111
|
+
def.name,
|
|
112
|
+
child.visibility || null,
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
batchInsertNodes(db, childRows);
|
|
118
|
+
|
|
119
|
+
// Phase 5: Batch insert contains/parameter_of edges
|
|
120
|
+
const edgeRows = [];
|
|
121
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
122
|
+
const nodeIdMap = new Map();
|
|
123
|
+
for (const row of bulkGetNodeIds.all(relPath)) {
|
|
124
|
+
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
125
|
+
}
|
|
126
|
+
const fileId = nodeIdMap.get(`${relPath}|file|0`);
|
|
127
|
+
for (const def of symbols.definitions) {
|
|
128
|
+
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
129
|
+
if (fileId && defId) {
|
|
130
|
+
edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
|
|
131
|
+
}
|
|
132
|
+
if (def.children?.length && defId) {
|
|
133
|
+
for (const child of def.children) {
|
|
134
|
+
const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
|
|
135
|
+
if (childId) {
|
|
136
|
+
edgeRows.push([defId, childId, 'contains', 1.0, 0]);
|
|
137
|
+
if (child.kind === 'parameter') {
|
|
138
|
+
edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Update file hash — skip reverse-dep files (unchanged)
|
|
146
|
+
if (upsertHash) {
|
|
147
|
+
const precomputed = precomputedData.get(relPath);
|
|
148
|
+
if (precomputed?._reverseDepOnly) {
|
|
149
|
+
// no-op: file unchanged, hash already correct
|
|
150
|
+
} else if (precomputed?.hash) {
|
|
151
|
+
const stat = precomputed.stat || fileStat(path.join(rootDir, relPath));
|
|
152
|
+
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
153
|
+
const size = stat ? stat.size : 0;
|
|
154
|
+
upsertHash.run(relPath, precomputed.hash, mtime, size);
|
|
155
|
+
} else {
|
|
156
|
+
const absPath = path.join(rootDir, relPath);
|
|
157
|
+
let code;
|
|
158
|
+
try {
|
|
159
|
+
code = readFileSafe(absPath);
|
|
160
|
+
} catch {
|
|
161
|
+
code = null;
|
|
162
|
+
}
|
|
163
|
+
if (code !== null) {
|
|
164
|
+
const stat = fileStat(absPath);
|
|
165
|
+
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
166
|
+
const size = stat ? stat.size : 0;
|
|
167
|
+
upsertHash.run(relPath, fileHash(code), mtime, size);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
batchInsertEdges(db, edgeRows);
|
|
173
|
+
|
|
174
|
+
// Also update metadata-only entries (self-heal mtime/size without re-parse)
|
|
175
|
+
if (upsertHash) {
|
|
176
|
+
for (const item of metadataUpdates) {
|
|
177
|
+
const mtime = item.stat ? Math.floor(item.stat.mtimeMs) : 0;
|
|
178
|
+
const size = item.stat ? item.stat.size : 0;
|
|
179
|
+
upsertHash.run(item.relPath, item.hash, mtime, size);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const t0 = performance.now();
|
|
185
|
+
insertAll();
|
|
186
|
+
ctx.timing.insertMs = performance.now() - t0;
|
|
187
|
+
|
|
188
|
+
// Clean up removed file hashes
|
|
189
|
+
if (upsertHash && removed.length > 0) {
|
|
190
|
+
const deleteHash = db.prepare('DELETE FROM file_hashes WHERE file = ?');
|
|
191
|
+
for (const relPath of removed) {
|
|
192
|
+
deleteHash.run(relPath);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage: parseFiles
|
|
3
|
+
*
|
|
4
|
+
* Parses source files via parseFilesAuto (native or WASM engine).
|
|
5
|
+
* Populates ctx.allSymbols, ctx.fileSymbols, ctx.filesToParse.
|
|
6
|
+
*/
|
|
7
|
+
import { performance } from 'node:perf_hooks';
|
|
8
|
+
import { info } from '../../../../infrastructure/logger.js';
|
|
9
|
+
import { parseFilesAuto } from '../../../parser.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('../context.js').PipelineContext} ctx
|
|
13
|
+
*/
|
|
14
|
+
export async function parseFiles(ctx) {
|
|
15
|
+
const { allFiles, parseChanges, isFullBuild, engineOpts, rootDir } = ctx;
|
|
16
|
+
|
|
17
|
+
ctx.filesToParse = isFullBuild ? allFiles.map((f) => ({ file: f })) : parseChanges;
|
|
18
|
+
ctx.fileSymbols = new Map();
|
|
19
|
+
|
|
20
|
+
const filePaths = ctx.filesToParse.map((item) => item.file);
|
|
21
|
+
const t0 = performance.now();
|
|
22
|
+
ctx.allSymbols = await parseFilesAuto(filePaths, rootDir, engineOpts);
|
|
23
|
+
ctx.timing.parseMs = performance.now() - t0;
|
|
24
|
+
|
|
25
|
+
const parsed = ctx.allSymbols.size;
|
|
26
|
+
const skipped = ctx.filesToParse.length - parsed;
|
|
27
|
+
info(`Parsed ${parsed} files (${skipped} skipped)`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage: resolveImports
|
|
3
|
+
*
|
|
4
|
+
* Batch import resolution + barrel/re-export map construction.
|
|
5
|
+
* For incremental builds, loads unchanged barrel files for resolution.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { performance } from 'node:perf_hooks';
|
|
9
|
+
import { parseFilesAuto } from '../../../parser.js';
|
|
10
|
+
import { resolveImportPath, resolveImportsBatch } from '../../resolve.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {import('../context.js').PipelineContext} ctx
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveImports(ctx) {
|
|
16
|
+
const { db, fileSymbols, rootDir, aliases, allFiles, isFullBuild, engineOpts } = ctx;
|
|
17
|
+
|
|
18
|
+
// Collect all (fromFile, importSource) pairs and resolve in one native call
|
|
19
|
+
const t0 = performance.now();
|
|
20
|
+
const batchInputs = [];
|
|
21
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
22
|
+
const absFile = path.join(rootDir, relPath);
|
|
23
|
+
for (const imp of symbols.imports) {
|
|
24
|
+
batchInputs.push({ fromFile: absFile, importSource: imp.source });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
ctx.batchResolved = resolveImportsBatch(batchInputs, rootDir, aliases, allFiles);
|
|
28
|
+
ctx.timing.resolveMs = performance.now() - t0;
|
|
29
|
+
|
|
30
|
+
// Build re-export map for barrel resolution
|
|
31
|
+
ctx.reexportMap = new Map();
|
|
32
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
33
|
+
const reexports = symbols.imports.filter((imp) => imp.reexport);
|
|
34
|
+
if (reexports.length > 0) {
|
|
35
|
+
ctx.reexportMap.set(
|
|
36
|
+
relPath,
|
|
37
|
+
reexports.map((imp) => ({
|
|
38
|
+
source: getResolved(ctx, path.join(rootDir, relPath), imp.source),
|
|
39
|
+
names: imp.names,
|
|
40
|
+
wildcardReexport: imp.wildcardReexport || false,
|
|
41
|
+
})),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// For incremental builds, load unchanged barrel files into reexportMap
|
|
47
|
+
ctx.barrelOnlyFiles = new Set();
|
|
48
|
+
if (!isFullBuild) {
|
|
49
|
+
const barrelCandidates = db
|
|
50
|
+
.prepare(
|
|
51
|
+
`SELECT DISTINCT n1.file FROM edges e
|
|
52
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
53
|
+
WHERE e.kind = 'reexports' AND n1.kind = 'file'`,
|
|
54
|
+
)
|
|
55
|
+
.all();
|
|
56
|
+
for (const { file: relPath } of barrelCandidates) {
|
|
57
|
+
if (fileSymbols.has(relPath)) continue;
|
|
58
|
+
const absPath = path.join(rootDir, relPath);
|
|
59
|
+
try {
|
|
60
|
+
const symbols = await parseFilesAuto([absPath], rootDir, engineOpts);
|
|
61
|
+
const fileSym = symbols.get(relPath);
|
|
62
|
+
if (fileSym) {
|
|
63
|
+
fileSymbols.set(relPath, fileSym);
|
|
64
|
+
ctx.barrelOnlyFiles.add(relPath);
|
|
65
|
+
const reexports = fileSym.imports.filter((imp) => imp.reexport);
|
|
66
|
+
if (reexports.length > 0) {
|
|
67
|
+
ctx.reexportMap.set(
|
|
68
|
+
relPath,
|
|
69
|
+
reexports.map((imp) => ({
|
|
70
|
+
source: getResolved(ctx, absPath, imp.source),
|
|
71
|
+
names: imp.names,
|
|
72
|
+
wildcardReexport: imp.wildcardReexport || false,
|
|
73
|
+
})),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
/* skip if unreadable */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve an import source, preferring batch results.
|
|
86
|
+
* Exported so other stages (build-edges) can reuse it.
|
|
87
|
+
*/
|
|
88
|
+
export function getResolved(ctx, absFile, importSource) {
|
|
89
|
+
if (ctx.batchResolved) {
|
|
90
|
+
const key = `${absFile}|${importSource}`;
|
|
91
|
+
const hit = ctx.batchResolved.get(key);
|
|
92
|
+
if (hit !== undefined) return hit;
|
|
93
|
+
}
|
|
94
|
+
return resolveImportPath(absFile, importSource, ctx.rootDir, ctx.aliases);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a file is a barrel (re-export hub).
|
|
99
|
+
*/
|
|
100
|
+
export function isBarrelFile(ctx, relPath) {
|
|
101
|
+
const symbols = ctx.fileSymbols.get(relPath);
|
|
102
|
+
if (!symbols) return false;
|
|
103
|
+
const reexports = symbols.imports.filter((imp) => imp.reexport);
|
|
104
|
+
if (reexports.length === 0) return false;
|
|
105
|
+
const ownDefs = symbols.definitions.length;
|
|
106
|
+
return reexports.length >= ownDefs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a symbol through barrel re-export chains.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveBarrelExport(ctx, barrelPath, symbolName, visited = new Set()) {
|
|
113
|
+
if (visited.has(barrelPath)) return null;
|
|
114
|
+
visited.add(barrelPath);
|
|
115
|
+
const reexports = ctx.reexportMap.get(barrelPath);
|
|
116
|
+
if (!reexports) return null;
|
|
117
|
+
|
|
118
|
+
for (const re of reexports) {
|
|
119
|
+
if (re.names.length > 0 && !re.wildcardReexport) {
|
|
120
|
+
if (re.names.includes(symbolName)) {
|
|
121
|
+
const targetSymbols = ctx.fileSymbols.get(re.source);
|
|
122
|
+
if (targetSymbols) {
|
|
123
|
+
const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
|
|
124
|
+
if (hasDef) return re.source;
|
|
125
|
+
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
126
|
+
if (deeper) return deeper;
|
|
127
|
+
}
|
|
128
|
+
return re.source;
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (re.wildcardReexport || re.names.length === 0) {
|
|
133
|
+
const targetSymbols = ctx.fileSymbols.get(re.source);
|
|
134
|
+
if (targetSymbols) {
|
|
135
|
+
const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
|
|
136
|
+
if (hasDef) return re.source;
|
|
137
|
+
const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
|
|
138
|
+
if (deeper) return deeper;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage: runAnalyses
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to the unified AST analysis engine (AST nodes, complexity, CFG, dataflow).
|
|
5
|
+
* Filters out reverse-dep files for incremental builds.
|
|
6
|
+
*/
|
|
7
|
+
import { debug, warn } from '../../../../infrastructure/logger.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('../context.js').PipelineContext} ctx
|
|
11
|
+
*/
|
|
12
|
+
export async function runAnalyses(ctx) {
|
|
13
|
+
const { db, allSymbols, rootDir, opts, engineOpts, isFullBuild, filesToParse } = ctx;
|
|
14
|
+
|
|
15
|
+
// For incremental builds, exclude reverse-dep-only files
|
|
16
|
+
let astComplexitySymbols = allSymbols;
|
|
17
|
+
if (!isFullBuild) {
|
|
18
|
+
const reverseDepFiles = new Set(
|
|
19
|
+
filesToParse.filter((item) => item._reverseDepOnly).map((item) => item.relPath),
|
|
20
|
+
);
|
|
21
|
+
if (reverseDepFiles.size > 0) {
|
|
22
|
+
astComplexitySymbols = new Map();
|
|
23
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
24
|
+
if (!reverseDepFiles.has(relPath)) {
|
|
25
|
+
astComplexitySymbols.set(relPath, symbols);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
debug(
|
|
29
|
+
`AST/complexity/CFG/dataflow: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { runAnalyses: runAnalysesFn } = await import('../../../../ast-analysis/engine.js');
|
|
35
|
+
try {
|
|
36
|
+
const analysisTiming = await runAnalysesFn(db, astComplexitySymbols, rootDir, opts, engineOpts);
|
|
37
|
+
ctx.timing.astMs = analysisTiming.astMs;
|
|
38
|
+
ctx.timing.complexityMs = analysisTiming.complexityMs;
|
|
39
|
+
ctx.timing.cfgMs = analysisTiming.cfgMs;
|
|
40
|
+
ctx.timing.dataflowMs = analysisTiming.dataflowMs;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
warn(`Analysis engine failed (AST/complexity/CFG/dataflow may be incomplete): ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Barrel re-export — keeps all existing `import { ... } from './builder.js'` working.
|
|
2
|
+
// See src/builder/ for the pipeline implementation (ROADMAP 3.9).
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
collectFiles,
|
|
6
|
+
loadPathAliases,
|
|
7
|
+
purgeFilesFromGraph,
|
|
8
|
+
readFileSafe,
|
|
9
|
+
} from './builder/helpers.js';
|
|
10
|
+
export { buildGraph } from './builder/pipeline.js';
|
|
11
|
+
export { resolveImportPath } from './resolve.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { debug, warn } from '
|
|
3
|
+
import { debug, warn } from '../../infrastructure/logger.js';
|
|
4
4
|
|
|
5
5
|
export const CHANGE_EVENTS_FILENAME = 'change-events.ndjson';
|
|
6
6
|
export const DEFAULT_MAX_BYTES = 1024 * 1024; // 1 MB
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { tarjan } from '../../graph/algorithms/tarjan.js';
|
|
2
|
+
import { buildDependencyGraph } from '../../graph/builders/dependency.js';
|
|
3
|
+
import { CodeGraph } from '../../graph/model.js';
|
|
4
|
+
import { loadNative } from '../../infrastructure/native.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect circular dependencies in the codebase using Tarjan's SCC algorithm.
|
|
8
|
+
* Dispatches to native Rust implementation when available, falls back to JS.
|
|
9
|
+
* @param {object} db - Open SQLite database
|
|
10
|
+
* @param {object} opts - { fileLevel: true, noTests: false }
|
|
11
|
+
* @returns {string[][]} Array of cycles, each cycle is an array of file paths
|
|
12
|
+
*/
|
|
13
|
+
export function findCycles(db, opts = {}) {
|
|
14
|
+
const fileLevel = opts.fileLevel !== false;
|
|
15
|
+
const noTests = opts.noTests || false;
|
|
16
|
+
|
|
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}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
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
|
+
|
|
36
|
+
// Try native Rust implementation
|
|
37
|
+
const native = loadNative();
|
|
38
|
+
if (native) {
|
|
39
|
+
return native.detectCycles(edges);
|
|
40
|
+
}
|
|
41
|
+
|
|
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);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Pure-JS Tarjan's SCC implementation.
|
|
53
|
+
* Kept for backward compatibility — accepts raw {source, target}[] edges.
|
|
54
|
+
*/
|
|
55
|
+
export function findCyclesJS(edges) {
|
|
56
|
+
const graph = new CodeGraph();
|
|
57
|
+
for (const { source, target } of edges) {
|
|
58
|
+
graph.addEdge(source, target);
|
|
59
|
+
}
|
|
60
|
+
return tarjan(graph);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format cycles for human-readable output.
|
|
65
|
+
*/
|
|
66
|
+
export function formatCycles(cycles) {
|
|
67
|
+
if (cycles.length === 0) {
|
|
68
|
+
return 'No circular dependencies detected.';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines = [`Found ${cycles.length} circular dependency cycle(s):\n`];
|
|
72
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
73
|
+
const cycle = cycles[i];
|
|
74
|
+
lines.push(` Cycle ${i + 1} (${cycle.length} files):`);
|
|
75
|
+
for (const file of cycle) {
|
|
76
|
+
lines.push(` -> ${file}`);
|
|
77
|
+
}
|
|
78
|
+
lines.push(` -> ${cycle[0]} (back to start)`);
|
|
79
|
+
lines.push('');
|
|
80
|
+
}
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { debug, warn } from '
|
|
3
|
+
import { debug, warn } from '../../infrastructure/logger.js';
|
|
4
4
|
|
|
5
5
|
export const JOURNAL_FILENAME = 'changes.journal';
|
|
6
6
|
const HEADER_PREFIX = '# codegraph-journal v1 ';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { loadNative } from '../../infrastructure/native.js';
|
|
4
|
+
import { normalizePath } from '../../shared/constants.js';
|
|
5
5
|
|
|
6
6
|
// ── Alias format conversion ─────────────────────────────────────────
|
|
7
7
|
|
|
@@ -178,4 +178,4 @@ export function resolveImportsBatch(inputs, rootDir, aliases, knownFiles) {
|
|
|
178
178
|
|
|
179
179
|
// ── Exported for testing ────────────────────────────────────────────
|
|
180
180
|
|
|
181
|
-
export {
|
|
181
|
+
export { computeConfidenceJS, resolveImportPathJS };
|