@optave/codegraph 3.10.0 → 3.11.1
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 +40 -33
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +91 -60
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/rules/index.d.ts.map +1 -1
- package/dist/ast-analysis/rules/index.js +77 -0
- package/dist/ast-analysis/rules/index.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +3 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +83 -49
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/audit.js +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +2 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/check.js +1 -1
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/children.js +1 -1
- package/dist/cli/commands/children.js.map +1 -1
- package/dist/cli/commands/diff-impact.js +1 -1
- package/dist/cli/commands/diff-impact.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +49 -4
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/cli/commands/roles.js +1 -1
- package/dist/cli/commands/roles.js.map +1 -1
- package/dist/cli/commands/structure.js +1 -1
- package/dist/cli/commands/structure.js.map +1 -1
- package/dist/cli/shared/options.js +1 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +8 -0
- package/dist/db/connection.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +106 -80
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +77 -52
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +132 -121
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +4 -4
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +47 -33
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +6 -6
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +148 -99
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -0
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +23 -637
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +141 -98
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +82 -65
- package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +60 -51
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/cycles.d.ts +6 -4
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +50 -55
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +89 -70
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +28 -20
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -23
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +153 -80
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts +3 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +68 -45
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +18 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +72 -4
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/search/search/hybrid.d.ts.map +1 -1
- package/dist/domain/search/search/hybrid.js +49 -40
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/domain/search/search/semantic.d.ts.map +1 -1
- package/dist/domain/search/search/semantic.js +69 -49
- package/dist/domain/search/search/semantic.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +209 -137
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/c.js +25 -6
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/cpp.js +47 -6
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/cuda.js +90 -14
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +108 -4
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/erlang.js +56 -20
- package/dist/extractors/erlang.js.map +1 -1
- package/dist/extractors/fsharp.d.ts +7 -0
- package/dist/extractors/fsharp.d.ts.map +1 -1
- package/dist/extractors/fsharp.js +94 -0
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +29 -33
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/groovy.js +41 -1
- package/dist/extractors/groovy.js.map +1 -1
- package/dist/extractors/haskell.js +48 -4
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/helpers.d.ts +79 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +137 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +37 -49
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +44 -44
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/julia.js +198 -74
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/kotlin.js +4 -0
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/objc.js +184 -47
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/python.js +7 -4
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +103 -87
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/scala.d.ts.map +1 -1
- package/dist/extractors/scala.js +18 -32
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +55 -69
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/verilog.js +80 -15
- package/dist/extractors/verilog.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +49 -39
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +90 -63
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +43 -34
- package/dist/features/check.js.map +1 -1
- package/dist/features/cochange.d.ts.map +1 -1
- package/dist/features/cochange.js +68 -56
- package/dist/features/cochange.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +105 -75
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +37 -29
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +31 -22
- package/dist/features/flow.js.map +1 -1
- package/dist/features/graph-enrichment.d.ts.map +1 -1
- package/dist/features/graph-enrichment.js +77 -70
- package/dist/features/graph-enrichment.js.map +1 -1
- package/dist/features/owners.d.ts +17 -26
- package/dist/features/owners.d.ts.map +1 -1
- package/dist/features/owners.js +120 -109
- package/dist/features/owners.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +59 -54
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +60 -60
- package/dist/features/structure-query.js.map +1 -1
- package/dist/features/structure.js +28 -36
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +100 -69
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +63 -59
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -1
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +4 -0
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/mcp/tools/semantic-search.d.ts +1 -0
- package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
- package/dist/mcp/tools/semantic-search.js +1 -0
- package/dist/mcp/tools/semantic-search.js.map +1 -1
- package/dist/presentation/cfg.d.ts.map +1 -1
- package/dist/presentation/cfg.js +44 -29
- package/dist/presentation/cfg.js.map +1 -1
- package/dist/presentation/flow.d.ts.map +1 -1
- package/dist/presentation/flow.js +58 -38
- package/dist/presentation/flow.js.map +1 -1
- package/dist/types.d.ts +16 -2
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-fsharp.wasm +0 -0
- package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +10 -10
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/rules/index.ts +87 -0
- package/src/ast-analysis/visitor-utils.ts +86 -46
- package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
- package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.ts +2 -0
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/children.ts +1 -1
- package/src/cli/commands/diff-impact.ts +1 -1
- package/src/cli/commands/embed.ts +54 -4
- package/src/cli/commands/roles.ts +1 -1
- package/src/cli/commands/structure.ts +1 -1
- package/src/cli/shared/options.ts +1 -1
- package/src/db/connection.ts +8 -0
- package/src/domain/analysis/dependencies.ts +166 -85
- package/src/domain/analysis/fn-impact.ts +120 -50
- package/src/domain/analysis/module-map.ts +175 -140
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +223 -131
- package/src/domain/graph/builder/pipeline.ts +32 -785
- package/src/domain/graph/builder/stages/build-edges.ts +207 -142
- package/src/domain/graph/builder/stages/build-structure.ts +115 -82
- package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
- package/src/domain/graph/builder/stages/finalize.ts +72 -70
- package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
- package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
- package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +29 -25
- package/src/domain/parser.ts +170 -67
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +75 -4
- package/src/domain/search/search/hybrid.ts +53 -42
- package/src/domain/search/search/semantic.ts +105 -65
- package/src/domain/wasm-worker-entry.ts +243 -153
- package/src/extractors/c.ts +27 -8
- package/src/extractors/cpp.ts +50 -8
- package/src/extractors/cuda.ts +90 -16
- package/src/extractors/elixir.ts +103 -4
- package/src/extractors/erlang.ts +63 -20
- package/src/extractors/fsharp.ts +104 -0
- package/src/extractors/gleam.ts +40 -39
- package/src/extractors/groovy.ts +45 -1
- package/src/extractors/haskell.ts +45 -4
- package/src/extractors/helpers.ts +205 -1
- package/src/extractors/java.ts +42 -45
- package/src/extractors/javascript.ts +44 -43
- package/src/extractors/julia.ts +191 -77
- package/src/extractors/kotlin.ts +4 -0
- package/src/extractors/objc.ts +171 -47
- package/src/extractors/python.ts +5 -3
- package/src/extractors/r.ts +104 -82
- package/src/extractors/scala.ts +24 -36
- package/src/extractors/solidity.ts +59 -78
- package/src/extractors/verilog.ts +83 -15
- package/src/features/boundaries.ts +64 -46
- package/src/features/cfg.ts +145 -74
- package/src/features/check.ts +60 -43
- package/src/features/cochange.ts +95 -72
- package/src/features/complexity.ts +134 -79
- package/src/features/dataflow.ts +57 -34
- package/src/features/flow.ts +48 -24
- package/src/features/graph-enrichment.ts +105 -70
- package/src/features/owners.ts +186 -146
- package/src/features/sequence.ts +99 -69
- package/src/features/structure-query.ts +94 -79
- package/src/features/structure.ts +56 -56
- package/src/graph/algorithms/leiden/optimiser.ts +142 -87
- package/src/graph/classifiers/roles.ts +64 -54
- package/src/infrastructure/config.ts +1 -1
- package/src/mcp/tool-registry.ts +5 -0
- package/src/mcp/tools/semantic-search.ts +2 -0
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- package/src/types.ts +16 -1
|
@@ -33,15 +33,23 @@ function buildReexportMap(ctx: PipelineContext): void {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Find barrel files related to
|
|
37
|
-
* For small
|
|
38
|
-
* or are imported by
|
|
36
|
+
* Find barrel files related to `fromRelPaths` for scoped re-parsing.
|
|
37
|
+
* For small frontiers (<=smallFilesThreshold files), only barrels that re-export from
|
|
38
|
+
* or are imported by `fromRelPaths`. For larger frontiers, all barrels.
|
|
39
|
+
*
|
|
40
|
+
* `firstPass` gates the reexport-from DB scan: re-parsed barrels haven't
|
|
41
|
+
* changed content, so subsequent passes can't surface new reexport-from
|
|
42
|
+
* candidates and only need to follow imports of newly-merged barrels
|
|
43
|
+
* (mirrors the Rust orchestrator's seed-only `collect_reexport_from_barrels`).
|
|
39
44
|
*/
|
|
40
|
-
function findBarrelCandidates(
|
|
45
|
+
function findBarrelCandidates(
|
|
46
|
+
ctx: PipelineContext,
|
|
47
|
+
fromRelPaths: readonly string[],
|
|
48
|
+
firstPass: boolean,
|
|
49
|
+
): Array<{ file: string }> {
|
|
41
50
|
const { db, fileSymbols, rootDir, aliases } = ctx;
|
|
42
|
-
const changedRelPaths = new Set<string>(fileSymbols.keys());
|
|
43
51
|
|
|
44
|
-
if (
|
|
52
|
+
if (fromRelPaths.length <= ctx.config.build.smallFilesThreshold) {
|
|
45
53
|
const allBarrelFiles = new Set(
|
|
46
54
|
(
|
|
47
55
|
db
|
|
@@ -56,9 +64,9 @@ function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
|
|
|
56
64
|
|
|
57
65
|
const barrels = new Set<string>();
|
|
58
66
|
|
|
59
|
-
// Find barrels imported by
|
|
67
|
+
// Find barrels imported by `fromRelPaths` using parsed import data
|
|
60
68
|
// (can't query DB edges -- they were purged for the changed files).
|
|
61
|
-
for (const relPath of
|
|
69
|
+
for (const relPath of fromRelPaths) {
|
|
62
70
|
const symbols = fileSymbols.get(relPath);
|
|
63
71
|
if (!symbols) continue;
|
|
64
72
|
for (const imp of symbols.imports) {
|
|
@@ -71,16 +79,17 @@ function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
|
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
if (firstPass) {
|
|
83
|
+
const reexportSourceStmt = db.prepare(
|
|
84
|
+
`SELECT DISTINCT n1.file FROM edges e
|
|
85
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
86
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
87
|
+
WHERE e.kind = 'reexports' AND n1.kind = 'file' AND n2.file = ?`,
|
|
88
|
+
);
|
|
89
|
+
for (const relPath of fromRelPaths) {
|
|
90
|
+
for (const row of reexportSourceStmt.all(relPath) as Array<{ file: string }>) {
|
|
91
|
+
barrels.add(row.file);
|
|
92
|
+
}
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
return [...barrels].map((file) => ({ file }));
|
|
@@ -95,11 +104,22 @@ function findBarrelCandidates(ctx: PipelineContext): Array<{ file: string }> {
|
|
|
95
104
|
.all() as Array<{ file: string }>;
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Re-parse barrel files and update fileSymbols/reexportMap with fresh data.
|
|
109
|
+
* Returns the relative paths of newly-merged files so the caller can scan
|
|
110
|
+
* them for the next level of barrel candidates.
|
|
111
|
+
*
|
|
112
|
+
* A re-parsed file is marked `barrel-only` only when it really is one (the
|
|
113
|
+
* `isBarrelFile` check — reexports >= ownDefs). The previous unconditional
|
|
114
|
+
* `.add(relPath)` caused hybrid barrels with many local defs (e.g. a file
|
|
115
|
+
* with one `export type ... from` and dozens of internal functions) to drop
|
|
116
|
+
* all their non-reexport imports in build-edges, since the barrel-only branch
|
|
117
|
+
* skips them (#1174).
|
|
118
|
+
*/
|
|
99
119
|
async function reparseBarrelFiles(
|
|
100
120
|
ctx: PipelineContext,
|
|
101
121
|
barrelCandidates: Array<{ file: string }>,
|
|
102
|
-
): Promise<
|
|
122
|
+
): Promise<string[]> {
|
|
103
123
|
const { db, fileSymbols, rootDir, engineOpts } = ctx;
|
|
104
124
|
|
|
105
125
|
const barrelPaths: string[] = [];
|
|
@@ -109,18 +129,27 @@ async function reparseBarrelFiles(
|
|
|
109
129
|
}
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
if (barrelPaths.length === 0) return;
|
|
132
|
+
if (barrelPaths.length === 0) return [];
|
|
113
133
|
|
|
134
|
+
// Preserve `contains` and `parameter_of` — those are emitted by insertNodes,
|
|
135
|
+
// which only runs on the original (changed + reverse-dep) fileSymbols. Barrel
|
|
136
|
+
// candidates are merged here *after* insertNodes, so wiping those kinds
|
|
137
|
+
// would permanently drop them (mirrors the Rust orchestrator's Stage 6b
|
|
138
|
+
// delete in build_pipeline.rs).
|
|
114
139
|
const deleteOutgoingEdges = db.prepare(
|
|
115
|
-
|
|
140
|
+
`DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)
|
|
141
|
+
AND kind NOT IN ('contains', 'parameter_of')`,
|
|
116
142
|
);
|
|
117
143
|
|
|
144
|
+
const added: string[] = [];
|
|
118
145
|
try {
|
|
119
146
|
const barrelSymbols = await parseFilesAuto(barrelPaths, rootDir, engineOpts);
|
|
120
147
|
for (const [relPath, fileSym] of barrelSymbols) {
|
|
121
148
|
deleteOutgoingEdges.run(relPath);
|
|
122
149
|
fileSymbols.set(relPath, fileSym);
|
|
123
|
-
ctx
|
|
150
|
+
if (isBarrelFile(ctx, relPath)) {
|
|
151
|
+
ctx.barrelOnlyFiles.add(relPath);
|
|
152
|
+
}
|
|
124
153
|
const reexports = fileSym.imports.filter((imp: Import) => imp.reexport);
|
|
125
154
|
if (reexports.length > 0) {
|
|
126
155
|
ctx.reexportMap.set(
|
|
@@ -132,10 +161,12 @@ async function reparseBarrelFiles(
|
|
|
132
161
|
})),
|
|
133
162
|
);
|
|
134
163
|
}
|
|
164
|
+
added.push(relPath);
|
|
135
165
|
}
|
|
136
166
|
} catch (e: unknown) {
|
|
137
167
|
debug(`Barrel re-parse failed (non-fatal): ${(e as Error).message}`);
|
|
138
168
|
}
|
|
169
|
+
return added;
|
|
139
170
|
}
|
|
140
171
|
|
|
141
172
|
export async function resolveImports(ctx: PipelineContext): Promise<void> {
|
|
@@ -156,8 +187,31 @@ export async function resolveImports(ctx: PipelineContext): Promise<void> {
|
|
|
156
187
|
|
|
157
188
|
ctx.barrelOnlyFiles = new Set<string>();
|
|
158
189
|
if (!isFullBuild) {
|
|
159
|
-
|
|
160
|
-
|
|
190
|
+
// Iteratively discover and re-parse barrel chains. A barrel that imports
|
|
191
|
+
// another barrel (e.g. `parser.ts → extractors/index.ts → extractors/<lang>.ts`)
|
|
192
|
+
// needs both loaded so build-edges can emit the barrel-through edges from
|
|
193
|
+
// the first barrel to the leaf targets. Without iteration, only the first
|
|
194
|
+
// level of barrels gets merged into fileSymbols; the deeper chain has no
|
|
195
|
+
// entry in reexportMap and the resolver silently drops the affected edges
|
|
196
|
+
// on every incremental rebuild (#1174).
|
|
197
|
+
//
|
|
198
|
+
// Convergence is guaranteed because fileSymbols grows monotonically and
|
|
199
|
+
// is bounded by the set of barrel files in the project — each iteration
|
|
200
|
+
// either adds a previously-unseen barrel or terminates.
|
|
201
|
+
//
|
|
202
|
+
// Subsequent passes only walk newly-merged barrels' imports (`frontier`
|
|
203
|
+
// = paths returned by reparseBarrelFiles), matching the Rust
|
|
204
|
+
// orchestrator's `&newly_added` slice. Without this, every pass would
|
|
205
|
+
// re-query the DB for every key in `fileSymbols`.
|
|
206
|
+
let frontier: readonly string[] = [...fileSymbols.keys()];
|
|
207
|
+
let firstPass = true;
|
|
208
|
+
while (frontier.length > 0) {
|
|
209
|
+
const barrelCandidates = findBarrelCandidates(ctx, frontier, firstPass);
|
|
210
|
+
const added = await reparseBarrelFiles(ctx, barrelCandidates);
|
|
211
|
+
if (added.length === 0) break;
|
|
212
|
+
frontier = added;
|
|
213
|
+
firstPass = false;
|
|
214
|
+
}
|
|
161
215
|
}
|
|
162
216
|
}
|
|
163
217
|
|
|
@@ -3,6 +3,45 @@ import { loadNative } from '../../infrastructure/native.js';
|
|
|
3
3
|
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
4
4
|
import type { BetterSqlite3Database } from '../../types.js';
|
|
5
5
|
|
|
6
|
+
type Edge = { source: string; target: string };
|
|
7
|
+
type DbEdge = { source_id: number; target_id: number };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a label-based edge list from DB rows, filtering to known nodes and
|
|
11
|
+
* deduplicating. Self-loops are skipped (Tarjan treats them as trivial SCCs).
|
|
12
|
+
*/
|
|
13
|
+
function buildLabelEdges(dbEdges: DbEdge[], idToLabel: Map<number, string>): Edge[] {
|
|
14
|
+
const edges: Edge[] = [];
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
for (const e of dbEdges) {
|
|
17
|
+
if (e.source_id === e.target_id) continue;
|
|
18
|
+
const src = idToLabel.get(e.source_id);
|
|
19
|
+
const tgt = idToLabel.get(e.target_id);
|
|
20
|
+
if (src === undefined || tgt === undefined) continue;
|
|
21
|
+
const key = `${src}\0${tgt}`;
|
|
22
|
+
if (seen.has(key)) continue;
|
|
23
|
+
seen.add(key);
|
|
24
|
+
edges.push({ source: src, target: tgt });
|
|
25
|
+
}
|
|
26
|
+
return edges;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildFileLevelEdges(db: BetterSqlite3Database, noTests: boolean): Edge[] {
|
|
30
|
+
let nodes = getFileNodesAll(db);
|
|
31
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
32
|
+
const idToLabel = new Map<number, string>();
|
|
33
|
+
for (const n of nodes) idToLabel.set(n.id, n.file);
|
|
34
|
+
return buildLabelEdges(getImportEdges(db), idToLabel);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildCallableEdges(db: BetterSqlite3Database, noTests: boolean): Edge[] {
|
|
38
|
+
let nodes = getCallableNodes(db);
|
|
39
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
40
|
+
const idToLabel = new Map<number, string>();
|
|
41
|
+
for (const n of nodes) idToLabel.set(n.id, `${n.name}|${n.file}`);
|
|
42
|
+
return buildLabelEdges(getCallEdges(db), idToLabel);
|
|
43
|
+
}
|
|
44
|
+
|
|
6
45
|
/**
|
|
7
46
|
* Find cycles using Tarjan's SCC algorithm.
|
|
8
47
|
*
|
|
@@ -16,66 +55,20 @@ export function findCycles(
|
|
|
16
55
|
const fileLevel = opts.fileLevel !== false;
|
|
17
56
|
const noTests = opts.noTests || false;
|
|
18
57
|
|
|
19
|
-
const edges
|
|
20
|
-
const seen = new Set<string>();
|
|
21
|
-
|
|
22
|
-
if (fileLevel) {
|
|
23
|
-
let nodes = getFileNodesAll(db);
|
|
24
|
-
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
25
|
-
const nodeIds = new Set<number>();
|
|
26
|
-
const idToFile = new Map<number, string>();
|
|
27
|
-
for (const n of nodes) {
|
|
28
|
-
nodeIds.add(n.id);
|
|
29
|
-
idToFile.set(n.id, n.file);
|
|
30
|
-
}
|
|
31
|
-
for (const e of getImportEdges(db)) {
|
|
32
|
-
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
33
|
-
if (e.source_id === e.target_id) continue;
|
|
34
|
-
const src = idToFile.get(e.source_id)!;
|
|
35
|
-
const tgt = idToFile.get(e.target_id)!;
|
|
36
|
-
const key = `${src}\0${tgt}`;
|
|
37
|
-
if (seen.has(key)) continue;
|
|
38
|
-
seen.add(key);
|
|
39
|
-
edges.push({ source: src, target: tgt });
|
|
40
|
-
}
|
|
41
|
-
} else {
|
|
42
|
-
let nodes = getCallableNodes(db);
|
|
43
|
-
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
44
|
-
const nodeIds = new Set<number>();
|
|
45
|
-
const idToLabel = new Map<number, string>();
|
|
46
|
-
for (const n of nodes) {
|
|
47
|
-
nodeIds.add(n.id);
|
|
48
|
-
idToLabel.set(n.id, `${n.name}|${n.file}`);
|
|
49
|
-
}
|
|
50
|
-
for (const e of getCallEdges(db)) {
|
|
51
|
-
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
52
|
-
if (e.source_id === e.target_id) continue;
|
|
53
|
-
const src = idToLabel.get(e.source_id)!;
|
|
54
|
-
const tgt = idToLabel.get(e.target_id)!;
|
|
55
|
-
const key = `${src}\0${tgt}`;
|
|
56
|
-
if (seen.has(key)) continue;
|
|
57
|
-
seen.add(key);
|
|
58
|
-
edges.push({ source: src, target: tgt });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
58
|
+
const edges = fileLevel ? buildFileLevelEdges(db, noTests) : buildCallableEdges(db, noTests);
|
|
61
59
|
|
|
62
60
|
const native = loadNative();
|
|
63
61
|
if (native) {
|
|
64
62
|
return native.detectCycles(edges) as string[][];
|
|
65
63
|
}
|
|
66
|
-
|
|
67
64
|
return tarjanFromEdges(edges);
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
export function findCyclesJS(edges:
|
|
67
|
+
export function findCyclesJS(edges: Edge[]): string[][] {
|
|
71
68
|
return tarjanFromEdges(edges);
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
* Run Tarjan's SCC on a flat edge list. Returns SCCs with length > 1 (cycles).
|
|
76
|
-
* Uses a simple adjacency-list Map instead of a full CodeGraph.
|
|
77
|
-
*/
|
|
78
|
-
function tarjanFromEdges(edges: Array<{ source: string; target: string }>): string[][] {
|
|
71
|
+
function buildAdjacency(edges: Edge[]): { adj: Map<string, string[]>; allNodes: Set<string> } {
|
|
79
72
|
const adj = new Map<string, string[]>();
|
|
80
73
|
const allNodes = new Set<string>();
|
|
81
74
|
for (const { source, target } of edges) {
|
|
@@ -88,6 +81,15 @@ function tarjanFromEdges(edges: Array<{ source: string; target: string }>): stri
|
|
|
88
81
|
}
|
|
89
82
|
list.push(target);
|
|
90
83
|
}
|
|
84
|
+
return { adj, allNodes };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run Tarjan's SCC on a flat edge list. Returns SCCs with length > 1 (cycles).
|
|
89
|
+
* Uses a simple adjacency-list Map instead of a full CodeGraph.
|
|
90
|
+
*/
|
|
91
|
+
function tarjanFromEdges(edges: Edge[]): string[][] {
|
|
92
|
+
const { adj, allNodes } = buildAdjacency(edges);
|
|
91
93
|
|
|
92
94
|
let index = 0;
|
|
93
95
|
const stack: string[] = [];
|
|
@@ -91,62 +91,69 @@ function trySteal(lockPath: string): AcquiredLock | null {
|
|
|
91
91
|
return { fd, nonce };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Try to create the lockfile fresh via `wx`. Returns the acquired lock on
|
|
96
|
+
* success, `null` if another holder exists, or throws on unexpected errors.
|
|
97
|
+
*
|
|
98
|
+
* If the stamp write fails (ENOSPC, I/O error) we release the empty file —
|
|
99
|
+
* leaving it would look stale to concurrent waiters and admit double-acquire.
|
|
100
|
+
*/
|
|
101
|
+
function tryFreshAcquire(lockPath: string): AcquiredLock | null {
|
|
102
|
+
const nonce = `${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
|
|
103
|
+
let fd: number;
|
|
104
|
+
try {
|
|
105
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if ((e as NodeJS.ErrnoException).code === 'EEXIST') return null;
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
fs.writeSync(fd, `${process.pid}\n${nonce}\n`);
|
|
112
|
+
} catch {
|
|
98
113
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
} catch {
|
|
103
|
-
// Stamp write failed (ENOSPC, I/O error). An empty lockfile would
|
|
104
|
-
// look stale to concurrent waiters (Number('') === 0, isPidAlive(0)
|
|
105
|
-
// returns false), so they'd steal our live lock. Release and retry.
|
|
106
|
-
try {
|
|
107
|
-
fs.closeSync(fd);
|
|
108
|
-
} catch {
|
|
109
|
-
/* ignore */
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
fs.unlinkSync(lockPath);
|
|
113
|
-
} catch {
|
|
114
|
-
/* ignore */
|
|
115
|
-
}
|
|
116
|
-
if (Date.now() - start > LOCK_TIMEOUT_MS) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`Failed to acquire journal lock at ${lockPath} within ${LOCK_TIMEOUT_MS}ms`,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
sleepSync(LOCK_RETRY_MS);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
return { fd, nonce };
|
|
125
|
-
} catch (e) {
|
|
126
|
-
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
|
|
114
|
+
fs.closeSync(fd);
|
|
115
|
+
} catch {
|
|
116
|
+
/* ignore */
|
|
127
117
|
}
|
|
128
|
-
|
|
129
|
-
let holderAlive = true;
|
|
130
118
|
try {
|
|
131
|
-
|
|
132
|
-
holderAlive = isPidAlive(Number(pidContent));
|
|
119
|
+
fs.unlinkSync(lockPath);
|
|
133
120
|
} catch {
|
|
134
|
-
/*
|
|
121
|
+
/* ignore */
|
|
135
122
|
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return { fd, nonce };
|
|
126
|
+
}
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Decide whether the current lock holder is stale and should be stolen.
|
|
130
|
+
* Returns true if the PID is dead, or if the lockfile mtime exceeds the
|
|
131
|
+
* staleness threshold.
|
|
132
|
+
*/
|
|
133
|
+
function isLockStale(lockPath: string): boolean {
|
|
134
|
+
let holderAlive = true;
|
|
135
|
+
try {
|
|
136
|
+
const pidContent = fs.readFileSync(lockPath, 'utf-8').split('\n')[0]!.trim();
|
|
137
|
+
holderAlive = isPidAlive(Number(pidContent));
|
|
138
|
+
} catch {
|
|
139
|
+
/* unreadable — fall through to age check */
|
|
140
|
+
}
|
|
141
|
+
if (!holderAlive) return true;
|
|
142
|
+
try {
|
|
143
|
+
const stat = fs.statSync(lockPath);
|
|
144
|
+
return Date.now() - stat.mtimeMs > LOCK_STALE_MS;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
+
function acquireJournalLock(lockPath: string): AcquiredLock {
|
|
151
|
+
const start = Date.now();
|
|
152
|
+
for (;;) {
|
|
153
|
+
const fresh = tryFreshAcquire(lockPath);
|
|
154
|
+
if (fresh) return fresh;
|
|
155
|
+
|
|
156
|
+
if (isLockStale(lockPath)) {
|
|
150
157
|
const stolen = trySteal(lockPath);
|
|
151
158
|
if (stolen) return stolen;
|
|
152
159
|
// Steal failed or lost the race — fall through to timeout check & retry.
|
|
@@ -227,27 +234,20 @@ interface JournalResult {
|
|
|
227
234
|
removed?: string[];
|
|
228
235
|
}
|
|
229
236
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
let content: string;
|
|
233
|
-
try {
|
|
234
|
-
content = fs.readFileSync(journalPath, 'utf-8');
|
|
235
|
-
} catch {
|
|
236
|
-
return { valid: false };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const lines = content.split('\n');
|
|
240
|
-
if (lines.length === 0 || !lines[0]!.startsWith(HEADER_PREFIX)) {
|
|
237
|
+
function parseJournalHeader(firstLine: string | undefined): number | null {
|
|
238
|
+
if (!firstLine || !firstLine.startsWith(HEADER_PREFIX)) {
|
|
241
239
|
debug('Journal has malformed or missing header');
|
|
242
|
-
return
|
|
240
|
+
return null;
|
|
243
241
|
}
|
|
244
|
-
|
|
245
|
-
const timestamp = Number(lines[0]!.slice(HEADER_PREFIX.length).trim());
|
|
242
|
+
const timestamp = Number(firstLine.slice(HEADER_PREFIX.length).trim());
|
|
246
243
|
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
|
247
244
|
debug('Journal has invalid timestamp');
|
|
248
|
-
return
|
|
245
|
+
return null;
|
|
249
246
|
}
|
|
247
|
+
return timestamp;
|
|
248
|
+
}
|
|
250
249
|
|
|
250
|
+
function parseJournalBody(lines: string[]): { changed: string[]; removed: string[] } {
|
|
251
251
|
const changed: string[] = [];
|
|
252
252
|
const removed: string[] = [];
|
|
253
253
|
const seenChanged = new Set<string>();
|
|
@@ -263,14 +263,29 @@ export function readJournal(rootDir: string): JournalResult {
|
|
|
263
263
|
seenRemoved.add(filePath);
|
|
264
264
|
removed.push(filePath);
|
|
265
265
|
}
|
|
266
|
-
} else {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
changed.push(line);
|
|
270
|
-
}
|
|
266
|
+
} else if (!seenChanged.has(line)) {
|
|
267
|
+
seenChanged.add(line);
|
|
268
|
+
changed.push(line);
|
|
271
269
|
}
|
|
272
270
|
}
|
|
273
271
|
|
|
272
|
+
return { changed, removed };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function readJournal(rootDir: string): JournalResult {
|
|
276
|
+
const journalPath = path.join(rootDir, '.codegraph', JOURNAL_FILENAME);
|
|
277
|
+
let content: string;
|
|
278
|
+
try {
|
|
279
|
+
content = fs.readFileSync(journalPath, 'utf-8');
|
|
280
|
+
} catch {
|
|
281
|
+
return { valid: false };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const lines = content.split('\n');
|
|
285
|
+
const timestamp = parseJournalHeader(lines[0]);
|
|
286
|
+
if (timestamp === null) return { valid: false };
|
|
287
|
+
|
|
288
|
+
const { changed, removed } = parseJournalBody(lines);
|
|
274
289
|
return { valid: true, timestamp, changed, removed };
|
|
275
290
|
}
|
|
276
291
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
|
|
4
|
-
import { debug, info } from '../../infrastructure/logger.js';
|
|
4
|
+
import { debug, info, warn } from '../../infrastructure/logger.js';
|
|
5
5
|
import { isSupportedFile, normalizePath, shouldIgnore } from '../../shared/constants.js';
|
|
6
6
|
import { DbError } from '../../shared/errors.js';
|
|
7
7
|
import { createParseTreeCache, getActiveEngine } from '../parser.js';
|
|
@@ -16,12 +16,13 @@ function shouldIgnorePath(filePath: string): boolean {
|
|
|
16
16
|
|
|
17
17
|
/** Prepare all SQL statements needed by the watcher's incremental rebuild. */
|
|
18
18
|
function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStmts {
|
|
19
|
-
|
|
19
|
+
return {
|
|
20
20
|
insertNode: db.prepare(
|
|
21
21
|
'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
|
|
22
22
|
),
|
|
23
23
|
getNodeId: {
|
|
24
|
-
get: (
|
|
24
|
+
get: (...params: unknown[]) => {
|
|
25
|
+
const [name, kind, file, line] = params as [string, string, string, number];
|
|
25
26
|
const id = getNodeIdQuery(db, name, kind, file, line);
|
|
26
27
|
return id != null ? { id } : undefined;
|
|
27
28
|
},
|
|
@@ -29,10 +30,10 @@ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStm
|
|
|
29
30
|
insertEdge: db.prepare(
|
|
30
31
|
'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
|
|
31
32
|
),
|
|
32
|
-
deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
|
|
33
|
-
deleteEdgesForFile: null as { run: (f: string) => void } | null,
|
|
34
33
|
countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
|
|
35
|
-
|
|
34
|
+
countEdges: db.prepare(
|
|
35
|
+
'SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
36
|
+
),
|
|
36
37
|
findNodeInFile: db.prepare(
|
|
37
38
|
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
|
|
38
39
|
),
|
|
@@ -41,19 +42,6 @@ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStm
|
|
|
41
42
|
),
|
|
42
43
|
listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
|
|
43
44
|
};
|
|
44
|
-
|
|
45
|
-
const origDeleteEdges = db.prepare(
|
|
46
|
-
`DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
|
|
47
|
-
);
|
|
48
|
-
const origCountEdges = db.prepare(
|
|
49
|
-
`SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
|
|
50
|
-
);
|
|
51
|
-
stmts.deleteEdgesForFile = { run: (f: string) => origDeleteEdges.run({ f }) };
|
|
52
|
-
stmts.countEdgesForFile = {
|
|
53
|
-
get: (f: string) => origCountEdges.get({ f }) as { c: number } | undefined,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
return stmts as IncrementalStmts;
|
|
57
45
|
}
|
|
58
46
|
|
|
59
47
|
/** Rebuild result shape from rebuildFile. */
|
|
@@ -67,6 +55,7 @@ interface RebuildResult {
|
|
|
67
55
|
nodesAdded: number;
|
|
68
56
|
nodesRemoved: number;
|
|
69
57
|
edgesAdded: number;
|
|
58
|
+
edgesBefore: number;
|
|
70
59
|
}
|
|
71
60
|
|
|
72
61
|
/** Process a batch of pending file changes: rebuild, journal, and log. */
|
|
@@ -80,10 +69,23 @@ async function processPendingFiles(
|
|
|
80
69
|
): Promise<void> {
|
|
81
70
|
const results: RebuildResult[] = [];
|
|
82
71
|
for (const filePath of files) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
72
|
+
// Per-file try/catch so one bad rebuild doesn't crash the watcher loop.
|
|
73
|
+
// The watcher is a long-running session — any SQLite error, parse failure,
|
|
74
|
+
// or filesystem race must be reported and skipped, not propagated. Issue #1176.
|
|
75
|
+
try {
|
|
76
|
+
const result = (await rebuildFile(db, rootDir, filePath, stmts, engineOpts, cache, {
|
|
77
|
+
diffSymbols: diffSymbols as (old: unknown[], new_: unknown[]) => unknown,
|
|
78
|
+
})) as RebuildResult | null;
|
|
79
|
+
if (result) results.push(result);
|
|
80
|
+
} catch (err: unknown) {
|
|
81
|
+
const relPath = normalizePath(path.relative(rootDir, filePath));
|
|
82
|
+
// Narrow with `instanceof` instead of casting: a non-Error throw (a plain
|
|
83
|
+
// string, `null`, or any value a third-party dependency throws) would log
|
|
84
|
+
// `(err as Error).message` as `undefined`. See Greptile review on #1182.
|
|
85
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
86
|
+
warn(`Failed to rebuild ${relPath}: ${message} — skipping`);
|
|
87
|
+
debug(err instanceof Error ? (err.stack ?? message) : String(err));
|
|
88
|
+
}
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
if (results.length > 0) {
|
|
@@ -109,7 +111,7 @@ function writeJournalAndChangeEvents(rootDir: string, updates: RebuildResult[]):
|
|
|
109
111
|
buildChangeEvent(r.file, r.event, r.symbolDiff, {
|
|
110
112
|
nodesBefore: r.nodesBefore,
|
|
111
113
|
nodesAfter: r.nodesAfter,
|
|
112
|
-
edgesAdded: r.edgesAdded,
|
|
114
|
+
edgesAdded: r.edgesAdded - r.edgesBefore,
|
|
113
115
|
}),
|
|
114
116
|
);
|
|
115
117
|
try {
|
|
@@ -127,7 +129,9 @@ function logRebuildResults(updates: RebuildResult[]): void {
|
|
|
127
129
|
if (r.deleted) {
|
|
128
130
|
info(`Removed: ${r.file} (-${r.nodesRemoved} nodes)`);
|
|
129
131
|
} else {
|
|
130
|
-
|
|
132
|
+
const edgeDelta = r.edgesAdded - r.edgesBefore;
|
|
133
|
+
const edgeStr = edgeDelta >= 0 ? `+${edgeDelta}` : `${edgeDelta}`;
|
|
134
|
+
info(`Updated: ${r.file} (${nodeStr} nodes, ${edgeStr} edges)`);
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
}
|