@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
|
@@ -6,19 +6,20 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { closeDb } from '../../../../db/index.js';
|
|
10
|
+
import { debug, info } from '../../../../infrastructure/logger.js';
|
|
11
|
+
import { normalizePath } from '../../../../shared/constants.js';
|
|
12
|
+
import { parseFilesAuto } from '../../../parser.js';
|
|
11
13
|
import { readJournal, writeJournalHeader } from '../../journal.js';
|
|
12
|
-
import { debug, info } from '../../logger.js';
|
|
13
|
-
import { parseFilesAuto } from '../../parser.js';
|
|
14
14
|
import { fileHash, fileStat, purgeFilesFromGraph, readFileSafe } from '../helpers.js';
|
|
15
15
|
|
|
16
|
+
// ── Three-tier change detection ─────────────────────────────────────────
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
19
|
* Determine which files have changed since last build.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Tier 2 — Hash comparison: O(changed) reads (fallback from Tier 1)
|
|
20
|
+
* Tier 0 — Journal: O(changed) when watcher was running
|
|
21
|
+
* Tier 1 — mtime+size: O(n) stats, O(changed) reads
|
|
22
|
+
* Tier 2 — Hash comparison: O(changed) reads (fallback from Tier 1)
|
|
22
23
|
*/
|
|
23
24
|
function getChangedFiles(db, allFiles, rootDir) {
|
|
24
25
|
let hasTable = false;
|
|
@@ -44,6 +45,17 @@ function getChangedFiles(db, allFiles, rootDir) {
|
|
|
44
45
|
.map((r) => [r.file, r]),
|
|
45
46
|
);
|
|
46
47
|
|
|
48
|
+
const removed = detectRemovedFiles(existing, allFiles, rootDir);
|
|
49
|
+
|
|
50
|
+
// Tier 0: Journal
|
|
51
|
+
const journalResult = tryJournalTier(db, existing, rootDir, removed);
|
|
52
|
+
if (journalResult) return journalResult;
|
|
53
|
+
|
|
54
|
+
// Tier 1 + 2: mtime/size fast-path → hash comparison
|
|
55
|
+
return mtimeAndHashTiers(existing, allFiles, rootDir, removed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function detectRemovedFiles(existing, allFiles, rootDir) {
|
|
47
59
|
const currentFiles = new Set();
|
|
48
60
|
for (const file of allFiles) {
|
|
49
61
|
currentFiles.add(normalizePath(path.relative(rootDir, file)));
|
|
@@ -55,51 +67,57 @@ function getChangedFiles(db, allFiles, rootDir) {
|
|
|
55
67
|
removed.push(existingFile);
|
|
56
68
|
}
|
|
57
69
|
}
|
|
70
|
+
return removed;
|
|
71
|
+
}
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
function tryJournalTier(db, existing, rootDir, removed) {
|
|
60
74
|
const journal = readJournal(rootDir);
|
|
61
|
-
if (journal.valid)
|
|
62
|
-
const dbMtimes = db.prepare('SELECT MAX(mtime) as latest FROM file_hashes').get();
|
|
63
|
-
const latestDbMtime = dbMtimes?.latest || 0;
|
|
64
|
-
const hasJournalEntries = journal.changed.length > 0 || journal.removed.length > 0;
|
|
65
|
-
|
|
66
|
-
if (hasJournalEntries && journal.timestamp >= latestDbMtime) {
|
|
67
|
-
debug(
|
|
68
|
-
`Tier 0: journal valid, ${journal.changed.length} changed, ${journal.removed.length} removed`,
|
|
69
|
-
);
|
|
70
|
-
const changed = [];
|
|
71
|
-
|
|
72
|
-
for (const relPath of journal.changed) {
|
|
73
|
-
const absPath = path.join(rootDir, relPath);
|
|
74
|
-
const stat = fileStat(absPath);
|
|
75
|
-
if (!stat) continue;
|
|
76
|
-
|
|
77
|
-
let content;
|
|
78
|
-
try {
|
|
79
|
-
content = readFileSafe(absPath);
|
|
80
|
-
} catch {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const hash = fileHash(content);
|
|
84
|
-
const record = existing.get(relPath);
|
|
85
|
-
if (!record || record.hash !== hash) {
|
|
86
|
-
changed.push({ file: absPath, content, hash, relPath, stat });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
75
|
+
if (!journal.valid) return null;
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
77
|
+
const dbMtimes = db.prepare('SELECT MAX(mtime) as latest FROM file_hashes').get();
|
|
78
|
+
const latestDbMtime = dbMtimes?.latest || 0;
|
|
79
|
+
const hasJournalEntries = journal.changed.length > 0 || journal.removed.length > 0;
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
}
|
|
81
|
+
if (!hasJournalEntries || journal.timestamp < latestDbMtime) {
|
|
97
82
|
debug(
|
|
98
83
|
`Tier 0: skipped (${hasJournalEntries ? 'timestamp stale' : 'no entries'}), falling to Tier 1`,
|
|
99
84
|
);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
debug(
|
|
89
|
+
`Tier 0: journal valid, ${journal.changed.length} changed, ${journal.removed.length} removed`,
|
|
90
|
+
);
|
|
91
|
+
const changed = [];
|
|
92
|
+
|
|
93
|
+
for (const relPath of journal.changed) {
|
|
94
|
+
const absPath = path.join(rootDir, relPath);
|
|
95
|
+
const stat = fileStat(absPath);
|
|
96
|
+
if (!stat) continue;
|
|
97
|
+
|
|
98
|
+
let content;
|
|
99
|
+
try {
|
|
100
|
+
content = readFileSafe(absPath);
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const hash = fileHash(content);
|
|
105
|
+
const record = existing.get(relPath);
|
|
106
|
+
if (!record || record.hash !== hash) {
|
|
107
|
+
changed.push({ file: absPath, content, hash, relPath, stat });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const removedSet = new Set(removed);
|
|
112
|
+
for (const relPath of journal.removed) {
|
|
113
|
+
if (existing.has(relPath)) removedSet.add(relPath);
|
|
100
114
|
}
|
|
101
115
|
|
|
102
|
-
|
|
116
|
+
return { changed, removed: [...removedSet], isFullBuild: false };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mtimeAndHashTiers(existing, allFiles, rootDir, removed) {
|
|
120
|
+
// Tier 1: mtime+size fast-path
|
|
103
121
|
const needsHash = [];
|
|
104
122
|
const skipped = [];
|
|
105
123
|
|
|
@@ -130,7 +148,7 @@ function getChangedFiles(db, allFiles, rootDir) {
|
|
|
130
148
|
debug(`Tier 1: ${skipped.length} skipped by mtime+size, ${needsHash.length} need hash check`);
|
|
131
149
|
}
|
|
132
150
|
|
|
133
|
-
//
|
|
151
|
+
// Tier 2: Hash comparison
|
|
134
152
|
const changed = [];
|
|
135
153
|
|
|
136
154
|
for (const item of needsHash) {
|
|
@@ -168,9 +186,10 @@ function getChangedFiles(db, allFiles, rootDir) {
|
|
|
168
186
|
return { changed, removed, isFullBuild: false };
|
|
169
187
|
}
|
|
170
188
|
|
|
189
|
+
// ── Pending analysis ────────────────────────────────────────────────────
|
|
190
|
+
|
|
171
191
|
/**
|
|
172
192
|
* Run pending analysis pass when no file changes but analysis tables are empty.
|
|
173
|
-
* @returns {boolean} true if analysis was run and we should early-exit
|
|
174
193
|
*/
|
|
175
194
|
async function runPendingAnalysis(ctx) {
|
|
176
195
|
const { db, opts, engineOpts, allFiles, rootDir } = ctx;
|
|
@@ -203,19 +222,18 @@ async function runPendingAnalysis(ctx) {
|
|
|
203
222
|
};
|
|
204
223
|
const analysisSymbols = await parseFilesAuto(allFiles, rootDir, analysisOpts);
|
|
205
224
|
if (needsCfg) {
|
|
206
|
-
const { buildCFGData } = await import('
|
|
225
|
+
const { buildCFGData } = await import('../../../../features/cfg.js');
|
|
207
226
|
await buildCFGData(db, analysisSymbols, rootDir, engineOpts);
|
|
208
227
|
}
|
|
209
228
|
if (needsDataflow) {
|
|
210
|
-
const { buildDataflowEdges } = await import('
|
|
229
|
+
const { buildDataflowEdges } = await import('../../../../features/dataflow.js');
|
|
211
230
|
await buildDataflowEdges(db, analysisSymbols, rootDir, engineOpts);
|
|
212
231
|
}
|
|
213
232
|
return true;
|
|
214
233
|
}
|
|
215
234
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
*/
|
|
235
|
+
// ── Metadata self-heal ──────────────────────────────────────────────────
|
|
236
|
+
|
|
219
237
|
function healMetadata(ctx) {
|
|
220
238
|
const { db, metadataUpdates } = ctx;
|
|
221
239
|
if (!metadataUpdates || metadataUpdates.length === 0) return;
|
|
@@ -237,126 +255,111 @@ function healMetadata(ctx) {
|
|
|
237
255
|
}
|
|
238
256
|
}
|
|
239
257
|
|
|
240
|
-
|
|
241
|
-
* @param {import('../context.js').PipelineContext} ctx
|
|
242
|
-
*/
|
|
243
|
-
export async function detectChanges(ctx) {
|
|
244
|
-
const { db, allFiles, rootDir, incremental, forceFullRebuild, opts } = ctx;
|
|
245
|
-
|
|
246
|
-
// Scoped builds already set parseChanges in collectFiles.
|
|
247
|
-
// Still need to purge removed files and set hasEmbeddings.
|
|
248
|
-
if (opts.scope) {
|
|
249
|
-
let hasEmbeddings = false;
|
|
250
|
-
try {
|
|
251
|
-
db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
|
|
252
|
-
hasEmbeddings = true;
|
|
253
|
-
} catch {
|
|
254
|
-
/* table doesn't exist */
|
|
255
|
-
}
|
|
256
|
-
ctx.hasEmbeddings = hasEmbeddings;
|
|
258
|
+
// ── Reverse-dependency cascade ──────────────────────────────────────────
|
|
257
259
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (!changedRelPaths.has(row.file) && !reverseDeps.has(row.file)) {
|
|
275
|
-
const absPath = path.join(rootDir, row.file);
|
|
276
|
-
if (fs.existsSync(absPath)) {
|
|
277
|
-
reverseDeps.add(row.file);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
260
|
+
function findReverseDependencies(db, changedRelPaths, rootDir) {
|
|
261
|
+
const reverseDeps = new Set();
|
|
262
|
+
if (changedRelPaths.size === 0) return reverseDeps;
|
|
263
|
+
|
|
264
|
+
const findReverseDepsStmt = db.prepare(`
|
|
265
|
+
SELECT DISTINCT n_src.file FROM edges e
|
|
266
|
+
JOIN nodes n_src ON e.source_id = n_src.id
|
|
267
|
+
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
268
|
+
WHERE n_tgt.file = ? AND n_src.file != n_tgt.file AND n_src.kind != 'directory'
|
|
269
|
+
`);
|
|
270
|
+
for (const relPath of changedRelPaths) {
|
|
271
|
+
for (const row of findReverseDepsStmt.all(relPath)) {
|
|
272
|
+
if (!changedRelPaths.has(row.file) && !reverseDeps.has(row.file)) {
|
|
273
|
+
const absPath = path.join(rootDir, row.file);
|
|
274
|
+
if (fs.existsSync(absPath)) {
|
|
275
|
+
reverseDeps.add(row.file);
|
|
281
276
|
}
|
|
282
277
|
}
|
|
283
278
|
}
|
|
284
|
-
|
|
285
|
-
// Now purge changed + removed files
|
|
286
|
-
if (changePaths.length > 0 || ctx.removed.length > 0) {
|
|
287
|
-
purgeFilesFromGraph(db, [...ctx.removed, ...changePaths], { purgeHashes: false });
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Delete outgoing edges for reverse-dep files and add to parse list
|
|
291
|
-
if (reverseDeps.size > 0) {
|
|
292
|
-
const deleteOutgoingEdgesForFile = db.prepare(
|
|
293
|
-
'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
294
|
-
);
|
|
295
|
-
for (const relPath of reverseDeps) {
|
|
296
|
-
deleteOutgoingEdgesForFile.run(relPath);
|
|
297
|
-
}
|
|
298
|
-
for (const relPath of reverseDeps) {
|
|
299
|
-
const absPath = path.join(rootDir, relPath);
|
|
300
|
-
ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
|
|
301
|
-
}
|
|
302
|
-
info(
|
|
303
|
-
`Scoped rebuild: ${changePaths.length} changed, ${ctx.removed.length} removed, ${reverseDeps.size} reverse-deps`,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
return;
|
|
307
279
|
}
|
|
280
|
+
return reverseDeps;
|
|
281
|
+
}
|
|
308
282
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
? getChangedFiles(db, allFiles, rootDir)
|
|
312
|
-
: { changed: allFiles.map((f) => ({ file: f })), removed: [], isFullBuild: true };
|
|
283
|
+
function purgeAndAddReverseDeps(ctx, changePaths, reverseDeps) {
|
|
284
|
+
const { db, rootDir } = ctx;
|
|
313
285
|
|
|
314
|
-
ctx.removed
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
ctx.metadataUpdates = increResult.changed.filter((c) => c.metadataOnly);
|
|
286
|
+
if (changePaths.length > 0 || ctx.removed.length > 0) {
|
|
287
|
+
purgeFilesFromGraph(db, [...ctx.removed, ...changePaths], { purgeHashes: false });
|
|
288
|
+
}
|
|
318
289
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
290
|
+
if (reverseDeps.size > 0) {
|
|
291
|
+
const deleteOutgoingEdgesForFile = db.prepare(
|
|
292
|
+
'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
293
|
+
);
|
|
294
|
+
for (const relPath of reverseDeps) {
|
|
295
|
+
deleteOutgoingEdgesForFile.run(relPath);
|
|
296
|
+
}
|
|
297
|
+
for (const relPath of reverseDeps) {
|
|
298
|
+
const absPath = path.join(rootDir, relPath);
|
|
299
|
+
ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
|
|
327
300
|
}
|
|
328
|
-
|
|
329
|
-
healMetadata(ctx);
|
|
330
|
-
info('No changes detected. Graph is up to date.');
|
|
331
|
-
closeDb(db);
|
|
332
|
-
writeJournalHeader(rootDir, Date.now());
|
|
333
|
-
ctx.earlyExit = true;
|
|
334
|
-
return;
|
|
335
301
|
}
|
|
302
|
+
}
|
|
336
303
|
|
|
337
|
-
|
|
338
|
-
|
|
304
|
+
// ── Shared helpers ───────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function detectHasEmbeddings(db) {
|
|
339
307
|
try {
|
|
340
308
|
db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
|
|
341
|
-
|
|
309
|
+
return true;
|
|
342
310
|
} catch {
|
|
343
|
-
|
|
311
|
+
return false;
|
|
344
312
|
}
|
|
345
|
-
|
|
313
|
+
}
|
|
346
314
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
315
|
+
// ── Scoped build path ───────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
function handleScopedBuild(ctx) {
|
|
318
|
+
const { db, rootDir, opts } = ctx;
|
|
319
|
+
|
|
320
|
+
ctx.hasEmbeddings = detectHasEmbeddings(db);
|
|
321
|
+
|
|
322
|
+
const changePaths = ctx.parseChanges.map(
|
|
323
|
+
(item) => item.relPath || normalizePath(path.relative(rootDir, item.file)),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
let reverseDeps = new Set();
|
|
327
|
+
if (!opts.noReverseDeps) {
|
|
328
|
+
const changedRelPaths = new Set([...changePaths, ...ctx.removed]);
|
|
329
|
+
reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir);
|
|
356
330
|
}
|
|
357
331
|
|
|
358
|
-
//
|
|
359
|
-
|
|
332
|
+
// Purge changed + removed files, then add reverse-deps
|
|
333
|
+
purgeAndAddReverseDeps(ctx, changePaths, reverseDeps);
|
|
334
|
+
|
|
335
|
+
info(
|
|
336
|
+
`Scoped rebuild: ${changePaths.length} changed, ${ctx.removed.length} removed, ${reverseDeps.size} reverse-deps`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Full/incremental build path ─────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
function handleFullBuild(ctx) {
|
|
343
|
+
const { db } = ctx;
|
|
344
|
+
|
|
345
|
+
const hasEmbeddings = detectHasEmbeddings(db);
|
|
346
|
+
ctx.hasEmbeddings = hasEmbeddings;
|
|
347
|
+
|
|
348
|
+
const deletions =
|
|
349
|
+
'PRAGMA foreign_keys = OFF; DELETE FROM cfg_edges; DELETE FROM cfg_blocks; DELETE FROM node_metrics; DELETE FROM edges; DELETE FROM function_complexity; DELETE FROM dataflow; DELETE FROM ast_nodes; DELETE FROM nodes; PRAGMA foreign_keys = ON;';
|
|
350
|
+
db.exec(
|
|
351
|
+
hasEmbeddings
|
|
352
|
+
? `${deletions.replace('PRAGMA foreign_keys = ON;', '')} DELETE FROM embeddings; PRAGMA foreign_keys = ON;`
|
|
353
|
+
: deletions,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function handleIncrementalBuild(ctx) {
|
|
358
|
+
const { db, rootDir, opts } = ctx;
|
|
359
|
+
|
|
360
|
+
ctx.hasEmbeddings = detectHasEmbeddings(db);
|
|
361
|
+
|
|
362
|
+
let reverseDeps = new Set();
|
|
360
363
|
if (!opts.noReverseDeps) {
|
|
361
364
|
const changedRelPaths = new Set();
|
|
362
365
|
for (const item of ctx.parseChanges) {
|
|
@@ -365,25 +368,7 @@ export async function detectChanges(ctx) {
|
|
|
365
368
|
for (const relPath of ctx.removed) {
|
|
366
369
|
changedRelPaths.add(relPath);
|
|
367
370
|
}
|
|
368
|
-
|
|
369
|
-
if (changedRelPaths.size > 0) {
|
|
370
|
-
const findReverseDeps = db.prepare(`
|
|
371
|
-
SELECT DISTINCT n_src.file FROM edges e
|
|
372
|
-
JOIN nodes n_src ON e.source_id = n_src.id
|
|
373
|
-
JOIN nodes n_tgt ON e.target_id = n_tgt.id
|
|
374
|
-
WHERE n_tgt.file = ? AND n_src.file != n_tgt.file AND n_src.kind != 'directory'
|
|
375
|
-
`);
|
|
376
|
-
for (const relPath of changedRelPaths) {
|
|
377
|
-
for (const row of findReverseDeps.all(relPath)) {
|
|
378
|
-
if (!changedRelPaths.has(row.file) && !reverseDeps.has(row.file)) {
|
|
379
|
-
const absPath = path.join(rootDir, row.file);
|
|
380
|
-
if (fs.existsSync(absPath)) {
|
|
381
|
-
reverseDeps.add(row.file);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
371
|
+
reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir);
|
|
387
372
|
}
|
|
388
373
|
|
|
389
374
|
info(
|
|
@@ -393,21 +378,57 @@ export async function detectChanges(ctx) {
|
|
|
393
378
|
debug(`Changed files: ${ctx.parseChanges.map((c) => c.relPath).join(', ')}`);
|
|
394
379
|
if (ctx.removed.length > 0) debug(`Removed files: ${ctx.removed.join(', ')}`);
|
|
395
380
|
|
|
396
|
-
// Purge changed and removed files
|
|
397
381
|
const changePaths = ctx.parseChanges.map(
|
|
398
382
|
(item) => item.relPath || normalizePath(path.relative(rootDir, item.file)),
|
|
399
383
|
);
|
|
400
|
-
|
|
384
|
+
purgeAndAddReverseDeps(ctx, changePaths, reverseDeps);
|
|
385
|
+
}
|
|
401
386
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
387
|
+
// ── Main entry point ────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {import('../context.js').PipelineContext} ctx
|
|
391
|
+
*/
|
|
392
|
+
export async function detectChanges(ctx) {
|
|
393
|
+
const { db, allFiles, rootDir, incremental, forceFullRebuild, opts } = ctx;
|
|
394
|
+
|
|
395
|
+
// Scoped builds already set parseChanges in collectFiles
|
|
396
|
+
if (opts.scope) {
|
|
397
|
+
handleScopedBuild(ctx);
|
|
398
|
+
return;
|
|
408
399
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
400
|
+
|
|
401
|
+
const increResult =
|
|
402
|
+
incremental && !forceFullRebuild
|
|
403
|
+
? getChangedFiles(db, allFiles, rootDir)
|
|
404
|
+
: { changed: allFiles.map((f) => ({ file: f })), removed: [], isFullBuild: true };
|
|
405
|
+
|
|
406
|
+
ctx.removed = increResult.removed;
|
|
407
|
+
ctx.isFullBuild = increResult.isFullBuild;
|
|
408
|
+
ctx.parseChanges = increResult.changed.filter((c) => !c.metadataOnly);
|
|
409
|
+
ctx.metadataUpdates = increResult.changed.filter((c) => c.metadataOnly);
|
|
410
|
+
|
|
411
|
+
// Early exit: no changes detected
|
|
412
|
+
if (!ctx.isFullBuild && ctx.parseChanges.length === 0 && ctx.removed.length === 0) {
|
|
413
|
+
const ranAnalysis = await runPendingAnalysis(ctx);
|
|
414
|
+
if (ranAnalysis) {
|
|
415
|
+
closeDb(db);
|
|
416
|
+
writeJournalHeader(rootDir, Date.now());
|
|
417
|
+
ctx.earlyExit = true;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
healMetadata(ctx);
|
|
422
|
+
info('No changes detected. Graph is up to date.');
|
|
423
|
+
closeDb(db);
|
|
424
|
+
writeJournalHeader(rootDir, Date.now());
|
|
425
|
+
ctx.earlyExit = true;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (ctx.isFullBuild) {
|
|
430
|
+
handleFullBuild(ctx);
|
|
431
|
+
} else {
|
|
432
|
+
handleIncrementalBuild(ctx);
|
|
412
433
|
}
|
|
413
434
|
}
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { performance } from 'node:perf_hooks';
|
|
9
|
-
import { closeDb, getBuildMeta, setBuildMeta } from '
|
|
9
|
+
import { closeDb, getBuildMeta, setBuildMeta } from '../../../../db/index.js';
|
|
10
|
+
import { debug, info, warn } from '../../../../infrastructure/logger.js';
|
|
10
11
|
import { writeJournalHeader } from '../../journal.js';
|
|
11
|
-
import { debug, info, warn } from '../../logger.js';
|
|
12
12
|
|
|
13
13
|
const __builderDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1'));
|
|
14
14
|
const CODEGRAPH_VERSION = JSON.parse(
|
|
15
|
-
fs.readFileSync(path.join(__builderDir, '..', '..', '..', 'package.json'), 'utf-8'),
|
|
15
|
+
fs.readFileSync(path.join(__builderDir, '..', '..', '..', '..', '..', 'package.json'), 'utf-8'),
|
|
16
16
|
).version;
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -127,7 +127,7 @@ export async function finalize(ctx) {
|
|
|
127
127
|
debug(`Skipping auto-registration for temp directory: ${resolvedRoot}`);
|
|
128
128
|
} else {
|
|
129
129
|
try {
|
|
130
|
-
const { registerRepo } = await import('
|
|
130
|
+
const { registerRepo } = await import('../../../../infrastructure/registry.js');
|
|
131
131
|
registerRepo(rootDir);
|
|
132
132
|
} catch (err) {
|
|
133
133
|
debug(`Auto-registration failed: ${err.message}`);
|