@colbymchenry/codegraph 0.7.10 → 0.9.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 +70 -54
- package/npm-shim.js +43 -0
- package/package.json +13 -51
- package/LICENSE +0 -21
- package/dist/bin/codegraph.d.ts +0 -21
- package/dist/bin/codegraph.d.ts.map +0 -1
- package/dist/bin/codegraph.js +0 -1232
- package/dist/bin/codegraph.js.map +0 -1
- package/dist/bin/node-version-check.d.ts +0 -20
- package/dist/bin/node-version-check.d.ts.map +0 -1
- package/dist/bin/node-version-check.js +0 -42
- package/dist/bin/node-version-check.js.map +0 -1
- package/dist/bin/uninstall.d.ts +0 -14
- package/dist/bin/uninstall.d.ts.map +0 -1
- package/dist/bin/uninstall.js +0 -36
- package/dist/bin/uninstall.js.map +0 -1
- package/dist/config.d.ts +0 -51
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -321
- package/dist/config.js.map +0 -1
- package/dist/context/formatter.d.ts +0 -30
- package/dist/context/formatter.d.ts.map +0 -1
- package/dist/context/formatter.js +0 -244
- package/dist/context/formatter.js.map +0 -1
- package/dist/context/index.d.ts +0 -97
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -1048
- package/dist/context/index.js.map +0 -1
- package/dist/db/index.d.ts +0 -72
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/index.js +0 -200
- package/dist/db/index.js.map +0 -1
- package/dist/db/migrations.d.ts +0 -44
- package/dist/db/migrations.d.ts.map +0 -1
- package/dist/db/migrations.js +0 -131
- package/dist/db/migrations.js.map +0 -1
- package/dist/db/queries.d.ts +0 -253
- package/dist/db/queries.d.ts.map +0 -1
- package/dist/db/queries.js +0 -1207
- package/dist/db/queries.js.map +0 -1
- package/dist/db/schema.sql +0 -151
- package/dist/db/sqlite-adapter.d.ts +0 -52
- package/dist/db/sqlite-adapter.d.ts.map +0 -1
- package/dist/db/sqlite-adapter.js +0 -237
- package/dist/db/sqlite-adapter.js.map +0 -1
- package/dist/directory.d.ts +0 -57
- package/dist/directory.d.ts.map +0 -1
- package/dist/directory.js +0 -264
- package/dist/directory.js.map +0 -1
- package/dist/errors.d.ts +0 -136
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -219
- package/dist/errors.js.map +0 -1
- package/dist/extraction/dfm-extractor.d.ts +0 -31
- package/dist/extraction/dfm-extractor.d.ts.map +0 -1
- package/dist/extraction/dfm-extractor.js +0 -151
- package/dist/extraction/dfm-extractor.js.map +0 -1
- package/dist/extraction/grammars.d.ts +0 -78
- package/dist/extraction/grammars.d.ts.map +0 -1
- package/dist/extraction/grammars.js +0 -322
- package/dist/extraction/grammars.js.map +0 -1
- package/dist/extraction/index.d.ts +0 -130
- package/dist/extraction/index.d.ts.map +0 -1
- package/dist/extraction/index.js +0 -1279
- package/dist/extraction/index.js.map +0 -1
- package/dist/extraction/languages/c-cpp.d.ts +0 -4
- package/dist/extraction/languages/c-cpp.d.ts.map +0 -1
- package/dist/extraction/languages/c-cpp.js +0 -126
- package/dist/extraction/languages/c-cpp.js.map +0 -1
- package/dist/extraction/languages/csharp.d.ts +0 -3
- package/dist/extraction/languages/csharp.d.ts.map +0 -1
- package/dist/extraction/languages/csharp.js +0 -72
- package/dist/extraction/languages/csharp.js.map +0 -1
- package/dist/extraction/languages/dart.d.ts +0 -3
- package/dist/extraction/languages/dart.d.ts.map +0 -1
- package/dist/extraction/languages/dart.js +0 -192
- package/dist/extraction/languages/dart.js.map +0 -1
- package/dist/extraction/languages/go.d.ts +0 -3
- package/dist/extraction/languages/go.d.ts.map +0 -1
- package/dist/extraction/languages/go.js +0 -58
- package/dist/extraction/languages/go.js.map +0 -1
- package/dist/extraction/languages/index.d.ts +0 -10
- package/dist/extraction/languages/index.d.ts.map +0 -1
- package/dist/extraction/languages/index.js +0 -45
- package/dist/extraction/languages/index.js.map +0 -1
- package/dist/extraction/languages/java.d.ts +0 -3
- package/dist/extraction/languages/java.d.ts.map +0 -1
- package/dist/extraction/languages/java.js +0 -64
- package/dist/extraction/languages/java.js.map +0 -1
- package/dist/extraction/languages/javascript.d.ts +0 -3
- package/dist/extraction/languages/javascript.d.ts.map +0 -1
- package/dist/extraction/languages/javascript.js +0 -90
- package/dist/extraction/languages/javascript.js.map +0 -1
- package/dist/extraction/languages/kotlin.d.ts +0 -3
- package/dist/extraction/languages/kotlin.d.ts.map +0 -1
- package/dist/extraction/languages/kotlin.js +0 -253
- package/dist/extraction/languages/kotlin.js.map +0 -1
- package/dist/extraction/languages/pascal.d.ts +0 -3
- package/dist/extraction/languages/pascal.d.ts.map +0 -1
- package/dist/extraction/languages/pascal.js +0 -66
- package/dist/extraction/languages/pascal.js.map +0 -1
- package/dist/extraction/languages/php.d.ts +0 -3
- package/dist/extraction/languages/php.d.ts.map +0 -1
- package/dist/extraction/languages/php.js +0 -107
- package/dist/extraction/languages/php.js.map +0 -1
- package/dist/extraction/languages/python.d.ts +0 -3
- package/dist/extraction/languages/python.d.ts.map +0 -1
- package/dist/extraction/languages/python.js +0 -56
- package/dist/extraction/languages/python.js.map +0 -1
- package/dist/extraction/languages/ruby.d.ts +0 -3
- package/dist/extraction/languages/ruby.d.ts.map +0 -1
- package/dist/extraction/languages/ruby.js +0 -114
- package/dist/extraction/languages/ruby.js.map +0 -1
- package/dist/extraction/languages/rust.d.ts +0 -3
- package/dist/extraction/languages/rust.d.ts.map +0 -1
- package/dist/extraction/languages/rust.js +0 -109
- package/dist/extraction/languages/rust.js.map +0 -1
- package/dist/extraction/languages/scala.d.ts +0 -3
- package/dist/extraction/languages/scala.d.ts.map +0 -1
- package/dist/extraction/languages/scala.js +0 -139
- package/dist/extraction/languages/scala.js.map +0 -1
- package/dist/extraction/languages/swift.d.ts +0 -3
- package/dist/extraction/languages/swift.d.ts.map +0 -1
- package/dist/extraction/languages/swift.js +0 -91
- package/dist/extraction/languages/swift.js.map +0 -1
- package/dist/extraction/languages/typescript.d.ts +0 -3
- package/dist/extraction/languages/typescript.d.ts.map +0 -1
- package/dist/extraction/languages/typescript.js +0 -129
- package/dist/extraction/languages/typescript.js.map +0 -1
- package/dist/extraction/liquid-extractor.d.ts +0 -52
- package/dist/extraction/liquid-extractor.d.ts.map +0 -1
- package/dist/extraction/liquid-extractor.js +0 -313
- package/dist/extraction/liquid-extractor.js.map +0 -1
- package/dist/extraction/parse-worker.d.ts +0 -8
- package/dist/extraction/parse-worker.d.ts.map +0 -1
- package/dist/extraction/parse-worker.js +0 -94
- package/dist/extraction/parse-worker.js.map +0 -1
- package/dist/extraction/svelte-extractor.d.ts +0 -56
- package/dist/extraction/svelte-extractor.d.ts.map +0 -1
- package/dist/extraction/svelte-extractor.js +0 -272
- package/dist/extraction/svelte-extractor.js.map +0 -1
- package/dist/extraction/tree-sitter-helpers.d.ts +0 -28
- package/dist/extraction/tree-sitter-helpers.d.ts.map +0 -1
- package/dist/extraction/tree-sitter-helpers.js +0 -103
- package/dist/extraction/tree-sitter-helpers.js.map +0 -1
- package/dist/extraction/tree-sitter-types.d.ts +0 -179
- package/dist/extraction/tree-sitter-types.d.ts.map +0 -1
- package/dist/extraction/tree-sitter-types.js +0 -10
- package/dist/extraction/tree-sitter-types.js.map +0 -1
- package/dist/extraction/tree-sitter.d.ts +0 -233
- package/dist/extraction/tree-sitter.d.ts.map +0 -1
- package/dist/extraction/tree-sitter.js +0 -2393
- package/dist/extraction/tree-sitter.js.map +0 -1
- package/dist/extraction/vue-extractor.d.ts +0 -36
- package/dist/extraction/vue-extractor.d.ts.map +0 -1
- package/dist/extraction/vue-extractor.js +0 -163
- package/dist/extraction/vue-extractor.js.map +0 -1
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/dist/graph/index.d.ts +0 -8
- package/dist/graph/index.d.ts.map +0 -1
- package/dist/graph/index.js +0 -13
- package/dist/graph/index.js.map +0 -1
- package/dist/graph/queries.d.ts +0 -106
- package/dist/graph/queries.d.ts.map +0 -1
- package/dist/graph/queries.js +0 -366
- package/dist/graph/queries.js.map +0 -1
- package/dist/graph/traversal.d.ts +0 -127
- package/dist/graph/traversal.d.ts.map +0 -1
- package/dist/graph/traversal.js +0 -493
- package/dist/graph/traversal.js.map +0 -1
- package/dist/index.d.ts +0 -447
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -825
- package/dist/index.js.map +0 -1
- package/dist/installer/claude-md-template.d.ts +0 -14
- package/dist/installer/claude-md-template.d.ts.map +0 -1
- package/dist/installer/claude-md-template.js +0 -21
- package/dist/installer/claude-md-template.js.map +0 -1
- package/dist/installer/config-writer.d.ts +0 -29
- package/dist/installer/config-writer.d.ts.map +0 -1
- package/dist/installer/config-writer.js +0 -109
- package/dist/installer/config-writer.js.map +0 -1
- package/dist/installer/index.d.ts +0 -53
- package/dist/installer/index.d.ts.map +0 -1
- package/dist/installer/index.js +0 -338
- package/dist/installer/index.js.map +0 -1
- package/dist/installer/instructions-template.d.ts +0 -28
- package/dist/installer/instructions-template.d.ts.map +0 -1
- package/dist/installer/instructions-template.js +0 -63
- package/dist/installer/instructions-template.js.map +0 -1
- package/dist/installer/targets/claude.d.ts +0 -27
- package/dist/installer/targets/claude.d.ts.map +0 -1
- package/dist/installer/targets/claude.js +0 -246
- package/dist/installer/targets/claude.js.map +0 -1
- package/dist/installer/targets/codex.d.ts +0 -18
- package/dist/installer/targets/codex.d.ts.map +0 -1
- package/dist/installer/targets/codex.js +0 -185
- package/dist/installer/targets/codex.js.map +0 -1
- package/dist/installer/targets/cursor.d.ts +0 -35
- package/dist/installer/targets/cursor.d.ts.map +0 -1
- package/dist/installer/targets/cursor.js +0 -229
- package/dist/installer/targets/cursor.js.map +0 -1
- package/dist/installer/targets/opencode.d.ts +0 -30
- package/dist/installer/targets/opencode.d.ts.map +0 -1
- package/dist/installer/targets/opencode.js +0 -235
- package/dist/installer/targets/opencode.js.map +0 -1
- package/dist/installer/targets/registry.d.ts +0 -35
- package/dist/installer/targets/registry.d.ts.map +0 -1
- package/dist/installer/targets/registry.js +0 -83
- package/dist/installer/targets/registry.js.map +0 -1
- package/dist/installer/targets/shared.d.ts +0 -77
- package/dist/installer/targets/shared.d.ts.map +0 -1
- package/dist/installer/targets/shared.js +0 -246
- package/dist/installer/targets/shared.js.map +0 -1
- package/dist/installer/targets/toml.d.ts +0 -52
- package/dist/installer/targets/toml.d.ts.map +0 -1
- package/dist/installer/targets/toml.js +0 -147
- package/dist/installer/targets/toml.js.map +0 -1
- package/dist/installer/targets/types.d.ts +0 -116
- package/dist/installer/targets/types.d.ts.map +0 -1
- package/dist/installer/targets/types.js +0 -16
- package/dist/installer/targets/types.js.map +0 -1
- package/dist/mcp/index.d.ts +0 -86
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js +0 -355
- package/dist/mcp/index.js.map +0 -1
- package/dist/mcp/server-instructions.d.ts +0 -19
- package/dist/mcp/server-instructions.d.ts.map +0 -1
- package/dist/mcp/server-instructions.js +0 -59
- package/dist/mcp/server-instructions.js.map +0 -1
- package/dist/mcp/tools.d.ts +0 -200
- package/dist/mcp/tools.d.ts.map +0 -1
- package/dist/mcp/tools.js +0 -1319
- package/dist/mcp/tools.js.map +0 -1
- package/dist/mcp/transport.d.ts +0 -89
- package/dist/mcp/transport.d.ts.map +0 -1
- package/dist/mcp/transport.js +0 -170
- package/dist/mcp/transport.js.map +0 -1
- package/dist/resolution/frameworks/cargo-workspace.d.ts +0 -18
- package/dist/resolution/frameworks/cargo-workspace.d.ts.map +0 -1
- package/dist/resolution/frameworks/cargo-workspace.js +0 -225
- package/dist/resolution/frameworks/cargo-workspace.js.map +0 -1
- package/dist/resolution/frameworks/csharp.d.ts +0 -8
- package/dist/resolution/frameworks/csharp.d.ts.map +0 -1
- package/dist/resolution/frameworks/csharp.js +0 -213
- package/dist/resolution/frameworks/csharp.js.map +0 -1
- package/dist/resolution/frameworks/express.d.ts +0 -8
- package/dist/resolution/frameworks/express.d.ts.map +0 -1
- package/dist/resolution/frameworks/express.js +0 -225
- package/dist/resolution/frameworks/express.js.map +0 -1
- package/dist/resolution/frameworks/go.d.ts +0 -8
- package/dist/resolution/frameworks/go.d.ts.map +0 -1
- package/dist/resolution/frameworks/go.js +0 -158
- package/dist/resolution/frameworks/go.js.map +0 -1
- package/dist/resolution/frameworks/index.d.ts +0 -41
- package/dist/resolution/frameworks/index.d.ts.map +0 -1
- package/dist/resolution/frameworks/index.js +0 -129
- package/dist/resolution/frameworks/index.js.map +0 -1
- package/dist/resolution/frameworks/java.d.ts +0 -8
- package/dist/resolution/frameworks/java.d.ts.map +0 -1
- package/dist/resolution/frameworks/java.js +0 -177
- package/dist/resolution/frameworks/java.js.map +0 -1
- package/dist/resolution/frameworks/laravel.d.ts +0 -13
- package/dist/resolution/frameworks/laravel.d.ts.map +0 -1
- package/dist/resolution/frameworks/laravel.js +0 -248
- package/dist/resolution/frameworks/laravel.js.map +0 -1
- package/dist/resolution/frameworks/python.d.ts +0 -10
- package/dist/resolution/frameworks/python.d.ts.map +0 -1
- package/dist/resolution/frameworks/python.js +0 -278
- package/dist/resolution/frameworks/python.js.map +0 -1
- package/dist/resolution/frameworks/react.d.ts +0 -8
- package/dist/resolution/frameworks/react.d.ts.map +0 -1
- package/dist/resolution/frameworks/react.js +0 -272
- package/dist/resolution/frameworks/react.js.map +0 -1
- package/dist/resolution/frameworks/ruby.d.ts +0 -8
- package/dist/resolution/frameworks/ruby.d.ts.map +0 -1
- package/dist/resolution/frameworks/ruby.js +0 -198
- package/dist/resolution/frameworks/ruby.js.map +0 -1
- package/dist/resolution/frameworks/rust.d.ts +0 -8
- package/dist/resolution/frameworks/rust.d.ts.map +0 -1
- package/dist/resolution/frameworks/rust.js +0 -207
- package/dist/resolution/frameworks/rust.js.map +0 -1
- package/dist/resolution/frameworks/svelte.d.ts +0 -9
- package/dist/resolution/frameworks/svelte.d.ts.map +0 -1
- package/dist/resolution/frameworks/svelte.js +0 -249
- package/dist/resolution/frameworks/svelte.js.map +0 -1
- package/dist/resolution/frameworks/swift.d.ts +0 -10
- package/dist/resolution/frameworks/swift.d.ts.map +0 -1
- package/dist/resolution/frameworks/swift.js +0 -376
- package/dist/resolution/frameworks/swift.js.map +0 -1
- package/dist/resolution/frameworks/vue.d.ts +0 -9
- package/dist/resolution/frameworks/vue.d.ts.map +0 -1
- package/dist/resolution/frameworks/vue.js +0 -306
- package/dist/resolution/frameworks/vue.js.map +0 -1
- package/dist/resolution/import-resolver.d.ts +0 -40
- package/dist/resolution/import-resolver.d.ts.map +0 -1
- package/dist/resolution/import-resolver.js +0 -663
- package/dist/resolution/import-resolver.js.map +0 -1
- package/dist/resolution/index.d.ts +0 -106
- package/dist/resolution/index.d.ts.map +0 -1
- package/dist/resolution/index.js +0 -709
- package/dist/resolution/index.js.map +0 -1
- package/dist/resolution/name-matcher.d.ts +0 -32
- package/dist/resolution/name-matcher.d.ts.map +0 -1
- package/dist/resolution/name-matcher.js +0 -384
- package/dist/resolution/name-matcher.js.map +0 -1
- package/dist/resolution/path-aliases.d.ts +0 -68
- package/dist/resolution/path-aliases.d.ts.map +0 -1
- package/dist/resolution/path-aliases.js +0 -238
- package/dist/resolution/path-aliases.js.map +0 -1
- package/dist/resolution/strip-comments.d.ts +0 -27
- package/dist/resolution/strip-comments.d.ts.map +0 -1
- package/dist/resolution/strip-comments.js +0 -441
- package/dist/resolution/strip-comments.js.map +0 -1
- package/dist/resolution/types.d.ts +0 -172
- package/dist/resolution/types.d.ts.map +0 -1
- package/dist/resolution/types.js +0 -8
- package/dist/resolution/types.js.map +0 -1
- package/dist/search/query-parser.d.ts +0 -57
- package/dist/search/query-parser.d.ts.map +0 -1
- package/dist/search/query-parser.js +0 -177
- package/dist/search/query-parser.js.map +0 -1
- package/dist/search/query-utils.d.ts +0 -53
- package/dist/search/query-utils.d.ts.map +0 -1
- package/dist/search/query-utils.js +0 -347
- package/dist/search/query-utils.js.map +0 -1
- package/dist/sync/index.d.ts +0 -13
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/index.js +0 -17
- package/dist/sync/index.js.map +0 -1
- package/dist/sync/watcher.d.ts +0 -81
- package/dist/sync/watcher.d.ts.map +0 -1
- package/dist/sync/watcher.js +0 -184
- package/dist/sync/watcher.js.map +0 -1
- package/dist/types.d.ts +0 -423
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -256
- package/dist/types.js.map +0 -1
- package/dist/ui/glyphs.d.ts +0 -42
- package/dist/ui/glyphs.d.ts.map +0 -1
- package/dist/ui/glyphs.js +0 -78
- package/dist/ui/glyphs.js.map +0 -1
- package/dist/ui/shimmer-progress.d.ts +0 -11
- package/dist/ui/shimmer-progress.d.ts.map +0 -1
- package/dist/ui/shimmer-progress.js +0 -90
- package/dist/ui/shimmer-progress.js.map +0 -1
- package/dist/ui/shimmer-worker.d.ts +0 -2
- package/dist/ui/shimmer-worker.d.ts.map +0 -1
- package/dist/ui/shimmer-worker.js +0 -118
- package/dist/ui/shimmer-worker.js.map +0 -1
- package/dist/ui/types.d.ts +0 -17
- package/dist/ui/types.d.ts.map +0 -1
- package/dist/ui/types.js +0 -3
- package/dist/ui/types.js.map +0 -1
- package/dist/utils.d.ts +0 -205
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -549
- package/dist/utils.js.map +0 -1
- package/scripts/local-install.sh +0 -41
- package/scripts/patch-tree-sitter-dart.js +0 -112
- package/scripts/release.sh +0 -70
package/dist/db/queries.js
DELETED
|
@@ -1,1207 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Database Queries
|
|
4
|
-
*
|
|
5
|
-
* Prepared statements for CRUD operations on the knowledge graph.
|
|
6
|
-
*/
|
|
7
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.QueryBuilder = void 0;
|
|
9
|
-
const utils_1 = require("../utils");
|
|
10
|
-
const query_utils_1 = require("../search/query-utils");
|
|
11
|
-
const query_parser_1 = require("../search/query-parser");
|
|
12
|
-
/**
|
|
13
|
-
* Convert database row to Node object
|
|
14
|
-
*/
|
|
15
|
-
function rowToNode(row) {
|
|
16
|
-
return {
|
|
17
|
-
id: row.id,
|
|
18
|
-
kind: row.kind,
|
|
19
|
-
name: row.name,
|
|
20
|
-
qualifiedName: row.qualified_name,
|
|
21
|
-
filePath: row.file_path,
|
|
22
|
-
language: row.language,
|
|
23
|
-
startLine: row.start_line,
|
|
24
|
-
endLine: row.end_line,
|
|
25
|
-
startColumn: row.start_column,
|
|
26
|
-
endColumn: row.end_column,
|
|
27
|
-
docstring: row.docstring ?? undefined,
|
|
28
|
-
signature: row.signature ?? undefined,
|
|
29
|
-
visibility: row.visibility,
|
|
30
|
-
isExported: row.is_exported === 1,
|
|
31
|
-
isAsync: row.is_async === 1,
|
|
32
|
-
isStatic: row.is_static === 1,
|
|
33
|
-
isAbstract: row.is_abstract === 1,
|
|
34
|
-
decorators: row.decorators ? (0, utils_1.safeJsonParse)(row.decorators, undefined) : undefined,
|
|
35
|
-
typeParameters: row.type_parameters ? (0, utils_1.safeJsonParse)(row.type_parameters, undefined) : undefined,
|
|
36
|
-
updatedAt: row.updated_at,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Convert database row to Edge object
|
|
41
|
-
*/
|
|
42
|
-
function rowToEdge(row) {
|
|
43
|
-
return {
|
|
44
|
-
source: row.source,
|
|
45
|
-
target: row.target,
|
|
46
|
-
kind: row.kind,
|
|
47
|
-
metadata: row.metadata ? (0, utils_1.safeJsonParse)(row.metadata, undefined) : undefined,
|
|
48
|
-
line: row.line ?? undefined,
|
|
49
|
-
column: row.col ?? undefined,
|
|
50
|
-
provenance: row.provenance,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Convert database row to FileRecord object
|
|
55
|
-
*/
|
|
56
|
-
function rowToFileRecord(row) {
|
|
57
|
-
return {
|
|
58
|
-
path: row.path,
|
|
59
|
-
contentHash: row.content_hash,
|
|
60
|
-
language: row.language,
|
|
61
|
-
size: row.size,
|
|
62
|
-
modifiedAt: row.modified_at,
|
|
63
|
-
indexedAt: row.indexed_at,
|
|
64
|
-
nodeCount: row.node_count,
|
|
65
|
-
errors: row.errors ? (0, utils_1.safeJsonParse)(row.errors, undefined) : undefined,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Query builder for the knowledge graph database
|
|
70
|
-
*/
|
|
71
|
-
class QueryBuilder {
|
|
72
|
-
db;
|
|
73
|
-
// Node cache for frequently accessed nodes (LRU-style, max 1000 entries)
|
|
74
|
-
nodeCache = new Map();
|
|
75
|
-
maxCacheSize = 1000;
|
|
76
|
-
// Prepared statements (lazily initialized)
|
|
77
|
-
stmts = {};
|
|
78
|
-
constructor(db) {
|
|
79
|
-
this.db = db;
|
|
80
|
-
}
|
|
81
|
-
// ===========================================================================
|
|
82
|
-
// Node Operations
|
|
83
|
-
// ===========================================================================
|
|
84
|
-
/**
|
|
85
|
-
* Insert a new node
|
|
86
|
-
*/
|
|
87
|
-
insertNode(node) {
|
|
88
|
-
if (!this.stmts.insertNode) {
|
|
89
|
-
this.stmts.insertNode = this.db.prepare(`
|
|
90
|
-
INSERT OR REPLACE INTO nodes (
|
|
91
|
-
id, kind, name, qualified_name, file_path, language,
|
|
92
|
-
start_line, end_line, start_column, end_column,
|
|
93
|
-
docstring, signature, visibility,
|
|
94
|
-
is_exported, is_async, is_static, is_abstract,
|
|
95
|
-
decorators, type_parameters, updated_at
|
|
96
|
-
) VALUES (
|
|
97
|
-
@id, @kind, @name, @qualifiedName, @filePath, @language,
|
|
98
|
-
@startLine, @endLine, @startColumn, @endColumn,
|
|
99
|
-
@docstring, @signature, @visibility,
|
|
100
|
-
@isExported, @isAsync, @isStatic, @isAbstract,
|
|
101
|
-
@decorators, @typeParameters, @updatedAt
|
|
102
|
-
)
|
|
103
|
-
`);
|
|
104
|
-
}
|
|
105
|
-
// Validate required fields to prevent SQLite bind errors
|
|
106
|
-
if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
|
|
107
|
-
console.error('[CodeGraph] Skipping node with missing required fields:', {
|
|
108
|
-
id: node.id,
|
|
109
|
-
kind: node.kind,
|
|
110
|
-
name: node.name,
|
|
111
|
-
filePath: node.filePath,
|
|
112
|
-
language: node.language,
|
|
113
|
-
});
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
this.stmts.insertNode.run({
|
|
118
|
-
id: node.id,
|
|
119
|
-
kind: node.kind,
|
|
120
|
-
name: node.name,
|
|
121
|
-
qualifiedName: node.qualifiedName ?? node.name,
|
|
122
|
-
filePath: node.filePath,
|
|
123
|
-
language: node.language,
|
|
124
|
-
startLine: node.startLine ?? 0,
|
|
125
|
-
endLine: node.endLine ?? 0,
|
|
126
|
-
startColumn: node.startColumn ?? 0,
|
|
127
|
-
endColumn: node.endColumn ?? 0,
|
|
128
|
-
docstring: node.docstring ?? null,
|
|
129
|
-
signature: node.signature ?? null,
|
|
130
|
-
visibility: node.visibility ?? null,
|
|
131
|
-
isExported: node.isExported ? 1 : 0,
|
|
132
|
-
isAsync: node.isAsync ? 1 : 0,
|
|
133
|
-
isStatic: node.isStatic ? 1 : 0,
|
|
134
|
-
isAbstract: node.isAbstract ? 1 : 0,
|
|
135
|
-
decorators: node.decorators ? JSON.stringify(node.decorators) : null,
|
|
136
|
-
typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
|
|
137
|
-
updatedAt: node.updatedAt ?? Date.now(),
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Insert multiple nodes in a transaction
|
|
146
|
-
*/
|
|
147
|
-
insertNodes(nodes) {
|
|
148
|
-
this.db.transaction(() => {
|
|
149
|
-
for (const node of nodes) {
|
|
150
|
-
this.insertNode(node);
|
|
151
|
-
}
|
|
152
|
-
})();
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Update an existing node
|
|
156
|
-
*/
|
|
157
|
-
updateNode(node) {
|
|
158
|
-
if (!this.stmts.updateNode) {
|
|
159
|
-
this.stmts.updateNode = this.db.prepare(`
|
|
160
|
-
UPDATE nodes SET
|
|
161
|
-
kind = @kind,
|
|
162
|
-
name = @name,
|
|
163
|
-
qualified_name = @qualifiedName,
|
|
164
|
-
file_path = @filePath,
|
|
165
|
-
language = @language,
|
|
166
|
-
start_line = @startLine,
|
|
167
|
-
end_line = @endLine,
|
|
168
|
-
start_column = @startColumn,
|
|
169
|
-
end_column = @endColumn,
|
|
170
|
-
docstring = @docstring,
|
|
171
|
-
signature = @signature,
|
|
172
|
-
visibility = @visibility,
|
|
173
|
-
is_exported = @isExported,
|
|
174
|
-
is_async = @isAsync,
|
|
175
|
-
is_static = @isStatic,
|
|
176
|
-
is_abstract = @isAbstract,
|
|
177
|
-
decorators = @decorators,
|
|
178
|
-
type_parameters = @typeParameters,
|
|
179
|
-
updated_at = @updatedAt
|
|
180
|
-
WHERE id = @id
|
|
181
|
-
`);
|
|
182
|
-
}
|
|
183
|
-
// Invalidate cache before update
|
|
184
|
-
this.nodeCache.delete(node.id);
|
|
185
|
-
// Validate required fields
|
|
186
|
-
if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
|
|
187
|
-
console.error('[CodeGraph] Skipping node update with missing required fields:', node.id);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
this.stmts.updateNode.run({
|
|
191
|
-
id: node.id,
|
|
192
|
-
kind: node.kind,
|
|
193
|
-
name: node.name,
|
|
194
|
-
qualifiedName: node.qualifiedName ?? node.name,
|
|
195
|
-
filePath: node.filePath,
|
|
196
|
-
language: node.language,
|
|
197
|
-
startLine: node.startLine ?? 0,
|
|
198
|
-
endLine: node.endLine ?? 0,
|
|
199
|
-
startColumn: node.startColumn ?? 0,
|
|
200
|
-
endColumn: node.endColumn ?? 0,
|
|
201
|
-
docstring: node.docstring ?? null,
|
|
202
|
-
signature: node.signature ?? null,
|
|
203
|
-
visibility: node.visibility ?? null,
|
|
204
|
-
isExported: node.isExported ? 1 : 0,
|
|
205
|
-
isAsync: node.isAsync ? 1 : 0,
|
|
206
|
-
isStatic: node.isStatic ? 1 : 0,
|
|
207
|
-
isAbstract: node.isAbstract ? 1 : 0,
|
|
208
|
-
decorators: node.decorators ? JSON.stringify(node.decorators) : null,
|
|
209
|
-
typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
|
|
210
|
-
updatedAt: node.updatedAt ?? Date.now(),
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Delete a node by ID
|
|
215
|
-
*/
|
|
216
|
-
deleteNode(id) {
|
|
217
|
-
if (!this.stmts.deleteNode) {
|
|
218
|
-
this.stmts.deleteNode = this.db.prepare('DELETE FROM nodes WHERE id = ?');
|
|
219
|
-
}
|
|
220
|
-
// Invalidate cache
|
|
221
|
-
this.nodeCache.delete(id);
|
|
222
|
-
this.stmts.deleteNode.run(id);
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Delete all nodes for a file
|
|
226
|
-
*/
|
|
227
|
-
deleteNodesByFile(filePath) {
|
|
228
|
-
if (!this.stmts.deleteNodesByFile) {
|
|
229
|
-
this.stmts.deleteNodesByFile = this.db.prepare('DELETE FROM nodes WHERE file_path = ?');
|
|
230
|
-
}
|
|
231
|
-
// Invalidate cache for nodes in this file
|
|
232
|
-
for (const [id, node] of this.nodeCache) {
|
|
233
|
-
if (node.filePath === filePath) {
|
|
234
|
-
this.nodeCache.delete(id);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
this.stmts.deleteNodesByFile.run(filePath);
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Get a node by ID
|
|
241
|
-
*/
|
|
242
|
-
getNodeById(id) {
|
|
243
|
-
// Check cache first
|
|
244
|
-
if (this.nodeCache.has(id)) {
|
|
245
|
-
const cached = this.nodeCache.get(id);
|
|
246
|
-
// Move to end to implement LRU (delete and re-add)
|
|
247
|
-
this.nodeCache.delete(id);
|
|
248
|
-
this.nodeCache.set(id, cached);
|
|
249
|
-
return cached;
|
|
250
|
-
}
|
|
251
|
-
if (!this.stmts.getNodeById) {
|
|
252
|
-
this.stmts.getNodeById = this.db.prepare('SELECT * FROM nodes WHERE id = ?');
|
|
253
|
-
}
|
|
254
|
-
const row = this.stmts.getNodeById.get(id);
|
|
255
|
-
if (!row) {
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
const node = rowToNode(row);
|
|
259
|
-
this.cacheNode(node);
|
|
260
|
-
return node;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Add a node to the cache, evicting oldest if needed
|
|
264
|
-
*/
|
|
265
|
-
cacheNode(node) {
|
|
266
|
-
if (this.nodeCache.size >= this.maxCacheSize) {
|
|
267
|
-
// Evict oldest (first) entry
|
|
268
|
-
const firstKey = this.nodeCache.keys().next().value;
|
|
269
|
-
if (firstKey) {
|
|
270
|
-
this.nodeCache.delete(firstKey);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
this.nodeCache.set(node.id, node);
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Clear the node cache
|
|
277
|
-
*/
|
|
278
|
-
clearCache() {
|
|
279
|
-
this.nodeCache.clear();
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Get all nodes in a file
|
|
283
|
-
*/
|
|
284
|
-
getNodesByFile(filePath) {
|
|
285
|
-
if (!this.stmts.getNodesByFile) {
|
|
286
|
-
this.stmts.getNodesByFile = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line');
|
|
287
|
-
}
|
|
288
|
-
const rows = this.stmts.getNodesByFile.all(filePath);
|
|
289
|
-
return rows.map(rowToNode);
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Get all nodes of a specific kind
|
|
293
|
-
*/
|
|
294
|
-
getNodesByKind(kind) {
|
|
295
|
-
if (!this.stmts.getNodesByKind) {
|
|
296
|
-
this.stmts.getNodesByKind = this.db.prepare('SELECT * FROM nodes WHERE kind = ?');
|
|
297
|
-
}
|
|
298
|
-
const rows = this.stmts.getNodesByKind.all(kind);
|
|
299
|
-
return rows.map(rowToNode);
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Get all nodes in the database
|
|
303
|
-
*/
|
|
304
|
-
getAllNodes() {
|
|
305
|
-
const rows = this.db.prepare('SELECT * FROM nodes').all();
|
|
306
|
-
return rows.map(rowToNode);
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Get nodes by exact name match (uses idx_nodes_name index)
|
|
310
|
-
*/
|
|
311
|
-
getNodesByName(name) {
|
|
312
|
-
if (!this.stmts.getNodesByName) {
|
|
313
|
-
this.stmts.getNodesByName = this.db.prepare('SELECT * FROM nodes WHERE name = ?');
|
|
314
|
-
}
|
|
315
|
-
const rows = this.stmts.getNodesByName.all(name);
|
|
316
|
-
return rows.map(rowToNode);
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Get nodes by exact qualified name match (uses idx_nodes_qualified_name index)
|
|
320
|
-
*/
|
|
321
|
-
getNodesByQualifiedNameExact(qualifiedName) {
|
|
322
|
-
if (!this.stmts.getNodesByQualifiedNameExact) {
|
|
323
|
-
this.stmts.getNodesByQualifiedNameExact = this.db.prepare('SELECT * FROM nodes WHERE qualified_name = ?');
|
|
324
|
-
}
|
|
325
|
-
const rows = this.stmts.getNodesByQualifiedNameExact.all(qualifiedName);
|
|
326
|
-
return rows.map(rowToNode);
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Get nodes by lowercase name match (uses idx_nodes_lower_name expression index)
|
|
330
|
-
*/
|
|
331
|
-
getNodesByLowerName(lowerName) {
|
|
332
|
-
if (!this.stmts.getNodesByLowerName) {
|
|
333
|
-
this.stmts.getNodesByLowerName = this.db.prepare('SELECT * FROM nodes WHERE lower(name) = ?');
|
|
334
|
-
}
|
|
335
|
-
const rows = this.stmts.getNodesByLowerName.all(lowerName);
|
|
336
|
-
return rows.map(rowToNode);
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Search nodes by name using FTS with fallback to LIKE for better matching
|
|
340
|
-
*
|
|
341
|
-
* Search strategy:
|
|
342
|
-
* 1. Try FTS5 prefix match (query*) for word-start matching
|
|
343
|
-
* 2. If no results, try LIKE for substring matching (e.g., "signIn" finds "signInWithGoogle")
|
|
344
|
-
* 3. Score results based on match quality
|
|
345
|
-
*/
|
|
346
|
-
searchNodes(query, options = {}) {
|
|
347
|
-
const { limit = 100, offset = 0 } = options;
|
|
348
|
-
// Parse field-qualified bits out of the raw query (kind:, lang:,
|
|
349
|
-
// path:, name:). Anything not recognised stays in `text` and goes
|
|
350
|
-
// to FTS unchanged. Filters compose with the SearchOptions arg —
|
|
351
|
-
// both are applied (intersection-style).
|
|
352
|
-
const parsed = (0, query_parser_1.parseQuery)(query);
|
|
353
|
-
const mergedKinds = parsed.kinds.length > 0
|
|
354
|
-
? Array.from(new Set([...(options.kinds ?? []), ...parsed.kinds]))
|
|
355
|
-
: options.kinds;
|
|
356
|
-
const mergedLanguages = parsed.languages.length > 0
|
|
357
|
-
? Array.from(new Set([...(options.languages ?? []), ...parsed.languages]))
|
|
358
|
-
: options.languages;
|
|
359
|
-
const pathFilters = parsed.pathFilters;
|
|
360
|
-
const nameFilters = parsed.nameFilters;
|
|
361
|
-
// The text portion drives FTS/LIKE; if all the user typed was
|
|
362
|
-
// filters (`kind:function`), we still need *some* candidate set,
|
|
363
|
-
// so synthesise an empty-text path that returns everything matching
|
|
364
|
-
// the filters.
|
|
365
|
-
const text = parsed.text;
|
|
366
|
-
const kinds = mergedKinds;
|
|
367
|
-
const languages = mergedLanguages;
|
|
368
|
-
// First try FTS5 with prefix matching
|
|
369
|
-
let results = text
|
|
370
|
-
? this.searchNodesFTS(text, { kinds, languages, limit, offset })
|
|
371
|
-
// Over-fetch by 5× when running filter-only (no text). The
|
|
372
|
-
// post-scoring path: + name: filters can be very selective, so
|
|
373
|
-
// a smaller multiplier risks returning fewer than `limit`
|
|
374
|
-
// results despite the DB having plenty of matches.
|
|
375
|
-
: this.searchAllByFilters({ kinds, languages, limit: limit * 5 });
|
|
376
|
-
// If no FTS results, try LIKE-based substring search
|
|
377
|
-
if (results.length === 0 && text.length >= 2) {
|
|
378
|
-
results = this.searchNodesLike(text, { kinds, languages, limit, offset });
|
|
379
|
-
}
|
|
380
|
-
// Final fuzzy fallback: scan all known names and keep those within
|
|
381
|
-
// a tight Levenshtein distance. Only fires when both FTS and LIKE
|
|
382
|
-
// returned nothing AND there's a text portion long enough to be
|
|
383
|
-
// worth fuzzing (1-char queries would match too much).
|
|
384
|
-
if (results.length === 0 && text.length >= 3) {
|
|
385
|
-
results = this.searchNodesFuzzy(text, { kinds, languages, limit });
|
|
386
|
-
}
|
|
387
|
-
// Supplement: ensure exact name matches are always candidates.
|
|
388
|
-
// BM25 can bury short exact-match names (e.g. "getBean") under hundreds of
|
|
389
|
-
// compound names (e.g. "getBeanDescriptor") in large codebases,
|
|
390
|
-
// pushing them past the FTS fetch limit before post-hoc scoring can help.
|
|
391
|
-
// Use the max BM25 score as the base so the nameMatchBonus (exact=30 vs
|
|
392
|
-
// prefix=20) actually differentiates them after rescoring.
|
|
393
|
-
if (results.length > 0 && query) {
|
|
394
|
-
const existingIds = new Set(results.map(r => r.node.id));
|
|
395
|
-
const maxFtsScore = Math.max(...results.map(r => r.score));
|
|
396
|
-
const terms = query.split(/\s+/).filter(t => t.length >= 2);
|
|
397
|
-
for (const term of terms) {
|
|
398
|
-
let sql = 'SELECT * FROM nodes WHERE name = ? COLLATE NOCASE';
|
|
399
|
-
const params = [term];
|
|
400
|
-
if (kinds && kinds.length > 0) {
|
|
401
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
402
|
-
params.push(...kinds);
|
|
403
|
-
}
|
|
404
|
-
if (languages && languages.length > 0) {
|
|
405
|
-
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
406
|
-
params.push(...languages);
|
|
407
|
-
}
|
|
408
|
-
sql += ' LIMIT 20';
|
|
409
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
410
|
-
for (const row of rows) {
|
|
411
|
-
if (!existingIds.has(row.id)) {
|
|
412
|
-
results.push({ node: rowToNode(row), score: maxFtsScore });
|
|
413
|
-
existingIds.add(row.id);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
// Apply multi-signal scoring
|
|
419
|
-
if (results.length > 0 && (text || query)) {
|
|
420
|
-
const scoringQuery = text || query;
|
|
421
|
-
results = results.map(r => ({
|
|
422
|
-
...r,
|
|
423
|
-
score: r.score
|
|
424
|
-
+ (0, query_utils_1.kindBonus)(r.node.kind)
|
|
425
|
-
+ (0, query_utils_1.scorePathRelevance)(r.node.filePath, scoringQuery)
|
|
426
|
-
+ (0, query_utils_1.nameMatchBonus)(r.node.name, scoringQuery),
|
|
427
|
-
}));
|
|
428
|
-
results.sort((a, b) => b.score - a.score);
|
|
429
|
-
// Trim to requested limit after rescoring
|
|
430
|
-
if (results.length > limit) {
|
|
431
|
-
results = results.slice(0, limit);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
// Apply path: + name: filters AFTER scoring. Scoring already uses
|
|
435
|
-
// path/name as a soft signal; the explicit filters here are a hard
|
|
436
|
-
// gate. Done last so the FTS limit fetched plenty of candidates to
|
|
437
|
-
// narrow from.
|
|
438
|
-
if (pathFilters.length > 0) {
|
|
439
|
-
const lowered = pathFilters.map((p) => p.toLowerCase());
|
|
440
|
-
results = results.filter((r) => {
|
|
441
|
-
const fp = r.node.filePath.toLowerCase();
|
|
442
|
-
return lowered.some((p) => fp.includes(p));
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
if (nameFilters.length > 0) {
|
|
446
|
-
const lowered = nameFilters.map((n) => n.toLowerCase());
|
|
447
|
-
results = results.filter((r) => {
|
|
448
|
-
const nm = r.node.name.toLowerCase();
|
|
449
|
-
return lowered.some((n) => nm.includes(n));
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
return results;
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* Match-everything path used when the user supplied only field
|
|
456
|
-
* filters (`kind:function lang:typescript`) with no text. Returns
|
|
457
|
-
* candidates ordered by name; the caller's filter pass narrows to
|
|
458
|
-
* what was asked for.
|
|
459
|
-
*/
|
|
460
|
-
searchAllByFilters(options) {
|
|
461
|
-
const { kinds, languages, limit } = options;
|
|
462
|
-
let sql = 'SELECT * FROM nodes WHERE 1=1';
|
|
463
|
-
const params = [];
|
|
464
|
-
if (kinds && kinds.length > 0) {
|
|
465
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
466
|
-
params.push(...kinds);
|
|
467
|
-
}
|
|
468
|
-
if (languages && languages.length > 0) {
|
|
469
|
-
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
470
|
-
params.push(...languages);
|
|
471
|
-
}
|
|
472
|
-
sql += ' ORDER BY name LIMIT ?';
|
|
473
|
-
params.push(limit);
|
|
474
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
475
|
-
return rows.map((row) => ({ node: rowToNode(row), score: 1 }));
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Fuzzy fallback: when zero FTS/LIKE hits, try an edit-distance
|
|
479
|
-
* sweep over the distinct symbol-name set. Caps `maxDist` at 2 so
|
|
480
|
-
* `getUssr` finds `getUser` but `process` doesn't match `prosody`.
|
|
481
|
-
* Bounded edit distance keeps each comparison cheap; the per-query
|
|
482
|
-
* scan is O(distinct-name-count) which is far smaller than total
|
|
483
|
-
* node count on any real codebase.
|
|
484
|
-
*/
|
|
485
|
-
searchNodesFuzzy(text, options) {
|
|
486
|
-
const { kinds, languages, limit } = options;
|
|
487
|
-
const lowered = text.toLowerCase();
|
|
488
|
-
const maxDist = lowered.length <= 4 ? 1 : 2;
|
|
489
|
-
// Pull the distinct name list once. The set is cached on QueryBuilder
|
|
490
|
-
// by getAllNodeNames(); even on a 200k-node project the distinct
|
|
491
|
-
// name set is typically O(10k) because most names repeat. The
|
|
492
|
-
// candidate-cap below bounds memory regardless.
|
|
493
|
-
const allNames = this.getAllNodeNames();
|
|
494
|
-
const candidates = [];
|
|
495
|
-
for (const name of allNames) {
|
|
496
|
-
const dist = (0, query_parser_1.boundedEditDistance)(name.toLowerCase(), lowered, maxDist);
|
|
497
|
-
if (dist <= maxDist)
|
|
498
|
-
candidates.push({ name, dist });
|
|
499
|
-
}
|
|
500
|
-
candidates.sort((a, b) => a.dist - b.dist);
|
|
501
|
-
// Cap the per-name follow-up queries. Each survivor triggers a
|
|
502
|
-
// separate `SELECT * FROM nodes WHERE name = ?`; without this cap
|
|
503
|
-
// a project with many similar names (`getUser1`, `getUser2`...)
|
|
504
|
-
// could fan out far beyond `limit` queries before the inner-loop
|
|
505
|
-
// limit kicks in.
|
|
506
|
-
const FUZZY_FOLLOWUP_CAP = Math.max(limit * 2, 50);
|
|
507
|
-
const cappedCandidates = candidates.slice(0, FUZZY_FOLLOWUP_CAP);
|
|
508
|
-
const results = [];
|
|
509
|
-
const seen = new Set();
|
|
510
|
-
for (const c of cappedCandidates) {
|
|
511
|
-
if (results.length >= limit)
|
|
512
|
-
break;
|
|
513
|
-
let sql = 'SELECT * FROM nodes WHERE name = ?';
|
|
514
|
-
const params = [c.name];
|
|
515
|
-
if (kinds && kinds.length > 0) {
|
|
516
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
517
|
-
params.push(...kinds);
|
|
518
|
-
}
|
|
519
|
-
if (languages && languages.length > 0) {
|
|
520
|
-
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
521
|
-
params.push(...languages);
|
|
522
|
-
}
|
|
523
|
-
sql += ' LIMIT 5';
|
|
524
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
525
|
-
for (const row of rows) {
|
|
526
|
-
if (seen.has(row.id))
|
|
527
|
-
continue;
|
|
528
|
-
seen.add(row.id);
|
|
529
|
-
// Lower the score for each edit step away from the query so
|
|
530
|
-
// exact-match fallbacks (dist 0) outrank dist-2 typos.
|
|
531
|
-
results.push({ node: rowToNode(row), score: 1 / (1 + c.dist) });
|
|
532
|
-
if (results.length >= limit)
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return results;
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* FTS5 search with prefix matching
|
|
540
|
-
*/
|
|
541
|
-
searchNodesFTS(query, options) {
|
|
542
|
-
const { kinds, languages, limit = 100, offset = 0 } = options;
|
|
543
|
-
// Add prefix wildcard for better matching (e.g., "auth" matches "AuthService", "authenticate")
|
|
544
|
-
// Escape special FTS5 characters and add prefix wildcard.
|
|
545
|
-
//
|
|
546
|
-
// `::` is a qualifier separator in Rust/C++/Ruby, not a token char,
|
|
547
|
-
// so treat it as whitespace before the strip step. Otherwise queries
|
|
548
|
-
// like `stage_apply::run` collapse to `stage_applyrun` (the colons
|
|
549
|
-
// are stripped without splitting) and find nothing. See #173.
|
|
550
|
-
const ftsQuery = query
|
|
551
|
-
.replace(/::/g, ' ') // Rust/C++/Ruby qualifier separator
|
|
552
|
-
.replace(/['"*():^]/g, '') // Remove FTS5 special chars
|
|
553
|
-
.split(/\s+/)
|
|
554
|
-
.filter(term => term.length > 0)
|
|
555
|
-
// Strip FTS5 boolean operators to prevent query manipulation
|
|
556
|
-
.filter(term => !/^(AND|OR|NOT|NEAR)$/i.test(term))
|
|
557
|
-
.map(term => `"${term}"*`) // Prefix match each term
|
|
558
|
-
.join(' OR ');
|
|
559
|
-
if (!ftsQuery) {
|
|
560
|
-
return [];
|
|
561
|
-
}
|
|
562
|
-
// BM25 column weights: id=0, name=20, qualified_name=5, docstring=1, signature=2
|
|
563
|
-
// Heavy name weight ensures exact/prefix name matches rank above incidental
|
|
564
|
-
// mentions in long docstrings or qualified names of nested symbols.
|
|
565
|
-
// Fetch 5x requested limit so post-hoc rescoring (kindBonus, pathRelevance,
|
|
566
|
-
// nameMatchBonus) can promote results that BM25 alone undervalues.
|
|
567
|
-
const ftsLimit = Math.max(limit * 5, 100);
|
|
568
|
-
let sql = `
|
|
569
|
-
SELECT nodes.*, bm25(nodes_fts, 0, 20, 5, 1, 2) as score
|
|
570
|
-
FROM nodes_fts
|
|
571
|
-
JOIN nodes ON nodes_fts.id = nodes.id
|
|
572
|
-
WHERE nodes_fts MATCH ?
|
|
573
|
-
`;
|
|
574
|
-
const params = [ftsQuery];
|
|
575
|
-
if (kinds && kinds.length > 0) {
|
|
576
|
-
sql += ` AND nodes.kind IN (${kinds.map(() => '?').join(',')})`;
|
|
577
|
-
params.push(...kinds);
|
|
578
|
-
}
|
|
579
|
-
if (languages && languages.length > 0) {
|
|
580
|
-
sql += ` AND nodes.language IN (${languages.map(() => '?').join(',')})`;
|
|
581
|
-
params.push(...languages);
|
|
582
|
-
}
|
|
583
|
-
sql += ' ORDER BY score LIMIT ? OFFSET ?';
|
|
584
|
-
params.push(ftsLimit, offset);
|
|
585
|
-
try {
|
|
586
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
587
|
-
return rows.map((row) => ({
|
|
588
|
-
node: rowToNode(row),
|
|
589
|
-
score: Math.abs(row.score), // bm25 returns negative scores
|
|
590
|
-
}));
|
|
591
|
-
}
|
|
592
|
-
catch {
|
|
593
|
-
// FTS query failed, return empty
|
|
594
|
-
return [];
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* LIKE-based substring search for cases where FTS doesn't match
|
|
599
|
-
* Useful for camelCase matching (e.g., "signIn" finds "signInWithGoogle")
|
|
600
|
-
*/
|
|
601
|
-
searchNodesLike(query, options) {
|
|
602
|
-
const { kinds, languages, limit = 100, offset = 0 } = options;
|
|
603
|
-
let sql = `
|
|
604
|
-
SELECT nodes.*,
|
|
605
|
-
CASE
|
|
606
|
-
WHEN name = ? THEN 1.0
|
|
607
|
-
WHEN name LIKE ? THEN 0.9
|
|
608
|
-
WHEN name LIKE ? THEN 0.8
|
|
609
|
-
WHEN qualified_name LIKE ? THEN 0.7
|
|
610
|
-
ELSE 0.5
|
|
611
|
-
END as score
|
|
612
|
-
FROM nodes
|
|
613
|
-
WHERE (
|
|
614
|
-
name LIKE ? OR
|
|
615
|
-
qualified_name LIKE ? OR
|
|
616
|
-
name LIKE ?
|
|
617
|
-
)
|
|
618
|
-
`;
|
|
619
|
-
// Pattern variants for better matching
|
|
620
|
-
const exactMatch = query;
|
|
621
|
-
const startsWith = `${query}%`;
|
|
622
|
-
const contains = `%${query}%`;
|
|
623
|
-
const params = [
|
|
624
|
-
exactMatch, // Exact match score
|
|
625
|
-
startsWith, // Starts with score
|
|
626
|
-
contains, // Contains score
|
|
627
|
-
contains, // Qualified name score
|
|
628
|
-
contains, // WHERE: name contains
|
|
629
|
-
contains, // WHERE: qualified_name contains
|
|
630
|
-
startsWith, // WHERE: name starts with
|
|
631
|
-
];
|
|
632
|
-
if (kinds && kinds.length > 0) {
|
|
633
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
634
|
-
params.push(...kinds);
|
|
635
|
-
}
|
|
636
|
-
if (languages && languages.length > 0) {
|
|
637
|
-
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
638
|
-
params.push(...languages);
|
|
639
|
-
}
|
|
640
|
-
sql += ' ORDER BY score DESC, length(name) ASC LIMIT ? OFFSET ?';
|
|
641
|
-
params.push(limit, offset);
|
|
642
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
643
|
-
return rows.map((row) => ({
|
|
644
|
-
node: rowToNode(row),
|
|
645
|
-
score: row.score,
|
|
646
|
-
}));
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Find nodes by exact name match
|
|
650
|
-
*
|
|
651
|
-
* Used for hybrid search - looks up symbols by exact name or case-insensitive match.
|
|
652
|
-
* Returns high-confidence matches for known symbol names extracted from query.
|
|
653
|
-
*
|
|
654
|
-
* @param names - Array of symbol names to look up
|
|
655
|
-
* @param options - Search options (kinds, languages, limit)
|
|
656
|
-
* @returns SearchResult array with exact matches scored at 1.0
|
|
657
|
-
*/
|
|
658
|
-
findNodesByExactName(names, options = {}) {
|
|
659
|
-
if (names.length === 0)
|
|
660
|
-
return [];
|
|
661
|
-
const { kinds, languages, limit = 50 } = options;
|
|
662
|
-
// Two-pass approach to handle common names (e.g., "run" has 40+ matches):
|
|
663
|
-
// Pass 1: Find which files contain distinctive (rare) symbols from the query.
|
|
664
|
-
// Pass 2: Query each name, boosting results that co-locate with distinctive symbols.
|
|
665
|
-
// Pass 1: Find files containing each queried name, identify distinctive names
|
|
666
|
-
const nameToFiles = new Map();
|
|
667
|
-
for (const name of names) {
|
|
668
|
-
let sql = 'SELECT DISTINCT file_path FROM nodes WHERE name COLLATE NOCASE = ?';
|
|
669
|
-
const params = [name];
|
|
670
|
-
if (kinds && kinds.length > 0) {
|
|
671
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
672
|
-
params.push(...kinds);
|
|
673
|
-
}
|
|
674
|
-
sql += ' LIMIT 100';
|
|
675
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
676
|
-
nameToFiles.set(name.toLowerCase(), new Set(rows.map(r => r.file_path)));
|
|
677
|
-
}
|
|
678
|
-
// Distinctive names are those with fewer than 10 file matches (e.g., "scrapeLoop" = 1 file)
|
|
679
|
-
const distinctiveFiles = new Set();
|
|
680
|
-
for (const [, files] of nameToFiles) {
|
|
681
|
-
if (files.size > 0 && files.size < 10) {
|
|
682
|
-
for (const f of files)
|
|
683
|
-
distinctiveFiles.add(f);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
// Pass 2: Query each name with per-name limit, scoring by co-location
|
|
687
|
-
const perNameLimit = Math.max(8, Math.ceil(limit / names.length));
|
|
688
|
-
const allResults = [];
|
|
689
|
-
const seenIds = new Set();
|
|
690
|
-
for (const name of names) {
|
|
691
|
-
let sql = `
|
|
692
|
-
SELECT nodes.*, 1.0 as score
|
|
693
|
-
FROM nodes
|
|
694
|
-
WHERE name COLLATE NOCASE = ?
|
|
695
|
-
`;
|
|
696
|
-
const params = [name];
|
|
697
|
-
if (kinds && kinds.length > 0) {
|
|
698
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
699
|
-
params.push(...kinds);
|
|
700
|
-
}
|
|
701
|
-
if (languages && languages.length > 0) {
|
|
702
|
-
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
703
|
-
params.push(...languages);
|
|
704
|
-
}
|
|
705
|
-
// Fetch enough to find co-located results among common names
|
|
706
|
-
sql += ' LIMIT ?';
|
|
707
|
-
params.push(Math.max(perNameLimit * 3, 50));
|
|
708
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
709
|
-
const nameResults = [];
|
|
710
|
-
for (const row of rows) {
|
|
711
|
-
const node = rowToNode(row);
|
|
712
|
-
if (seenIds.has(node.id))
|
|
713
|
-
continue;
|
|
714
|
-
// Boost results in files that also contain distinctive symbols
|
|
715
|
-
const coLocationBoost = distinctiveFiles.has(node.filePath) ? 20 : 0;
|
|
716
|
-
nameResults.push({ node, score: row.score + coLocationBoost });
|
|
717
|
-
}
|
|
718
|
-
// Sort by score (co-located first), take per-name limit
|
|
719
|
-
nameResults.sort((a, b) => b.score - a.score);
|
|
720
|
-
for (const r of nameResults.slice(0, perNameLimit)) {
|
|
721
|
-
seenIds.add(r.node.id);
|
|
722
|
-
allResults.push(r);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
// Sort all results by score so co-located results bubble up
|
|
726
|
-
allResults.sort((a, b) => b.score - a.score);
|
|
727
|
-
return allResults.slice(0, limit);
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Find nodes whose name contains a substring (LIKE-based).
|
|
731
|
-
* Useful for CamelCase-part matching where FTS fails because
|
|
732
|
-
* e.g. "TransportSearchAction" is one FTS token, not matchable by "Search"*.
|
|
733
|
-
*
|
|
734
|
-
* Results are ordered by name length (shorter = more likely to be the core type).
|
|
735
|
-
*/
|
|
736
|
-
findNodesByNameSubstring(substring, options = {}) {
|
|
737
|
-
const { kinds, languages, limit = 30, excludePrefix } = options;
|
|
738
|
-
let sql = `
|
|
739
|
-
SELECT nodes.*, 1.0 as score
|
|
740
|
-
FROM nodes
|
|
741
|
-
WHERE name LIKE ?
|
|
742
|
-
`;
|
|
743
|
-
const params = [`%${substring}%`];
|
|
744
|
-
// Exclude prefix matches (handled by FTS-based prefix search in Step 2b)
|
|
745
|
-
if (excludePrefix) {
|
|
746
|
-
sql += ` AND name NOT LIKE ?`;
|
|
747
|
-
params.push(`${substring}%`);
|
|
748
|
-
}
|
|
749
|
-
if (kinds && kinds.length > 0) {
|
|
750
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
751
|
-
params.push(...kinds);
|
|
752
|
-
}
|
|
753
|
-
if (languages && languages.length > 0) {
|
|
754
|
-
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
755
|
-
params.push(...languages);
|
|
756
|
-
}
|
|
757
|
-
sql += ' ORDER BY length(name) ASC LIMIT ?';
|
|
758
|
-
params.push(limit);
|
|
759
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
760
|
-
return rows.map((row) => ({
|
|
761
|
-
node: rowToNode(row),
|
|
762
|
-
score: row.score,
|
|
763
|
-
}));
|
|
764
|
-
}
|
|
765
|
-
// ===========================================================================
|
|
766
|
-
// Edge Operations
|
|
767
|
-
// ===========================================================================
|
|
768
|
-
/**
|
|
769
|
-
* Insert a new edge
|
|
770
|
-
*/
|
|
771
|
-
insertEdge(edge) {
|
|
772
|
-
if (!this.stmts.insertEdge) {
|
|
773
|
-
this.stmts.insertEdge = this.db.prepare(`
|
|
774
|
-
INSERT OR IGNORE INTO edges (source, target, kind, metadata, line, col, provenance)
|
|
775
|
-
VALUES (@source, @target, @kind, @metadata, @line, @col, @provenance)
|
|
776
|
-
`);
|
|
777
|
-
}
|
|
778
|
-
this.stmts.insertEdge.run({
|
|
779
|
-
source: edge.source,
|
|
780
|
-
target: edge.target,
|
|
781
|
-
kind: edge.kind,
|
|
782
|
-
metadata: edge.metadata ? JSON.stringify(edge.metadata) : null,
|
|
783
|
-
line: edge.line ?? null,
|
|
784
|
-
col: edge.column ?? null,
|
|
785
|
-
provenance: edge.provenance ?? null,
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* Insert multiple edges in a transaction
|
|
790
|
-
*/
|
|
791
|
-
insertEdges(edges) {
|
|
792
|
-
this.db.transaction(() => {
|
|
793
|
-
for (const edge of edges) {
|
|
794
|
-
this.insertEdge(edge);
|
|
795
|
-
}
|
|
796
|
-
})();
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Delete all edges from a source node
|
|
800
|
-
*/
|
|
801
|
-
deleteEdgesBySource(sourceId) {
|
|
802
|
-
if (!this.stmts.deleteEdgesBySource) {
|
|
803
|
-
this.stmts.deleteEdgesBySource = this.db.prepare('DELETE FROM edges WHERE source = ?');
|
|
804
|
-
}
|
|
805
|
-
this.stmts.deleteEdgesBySource.run(sourceId);
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Get outgoing edges from a node
|
|
809
|
-
*/
|
|
810
|
-
getOutgoingEdges(sourceId, kinds, provenance) {
|
|
811
|
-
if ((kinds && kinds.length > 0) || provenance) {
|
|
812
|
-
let sql = 'SELECT * FROM edges WHERE source = ?';
|
|
813
|
-
const params = [sourceId];
|
|
814
|
-
if (kinds && kinds.length > 0) {
|
|
815
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
816
|
-
params.push(...kinds);
|
|
817
|
-
}
|
|
818
|
-
if (provenance) {
|
|
819
|
-
sql += ' AND provenance = ?';
|
|
820
|
-
params.push(provenance);
|
|
821
|
-
}
|
|
822
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
823
|
-
return rows.map(rowToEdge);
|
|
824
|
-
}
|
|
825
|
-
if (!this.stmts.getEdgesBySource) {
|
|
826
|
-
this.stmts.getEdgesBySource = this.db.prepare('SELECT * FROM edges WHERE source = ?');
|
|
827
|
-
}
|
|
828
|
-
const rows = this.stmts.getEdgesBySource.all(sourceId);
|
|
829
|
-
return rows.map(rowToEdge);
|
|
830
|
-
}
|
|
831
|
-
/**
|
|
832
|
-
* Get incoming edges to a node
|
|
833
|
-
*/
|
|
834
|
-
getIncomingEdges(targetId, kinds) {
|
|
835
|
-
if (kinds && kinds.length > 0) {
|
|
836
|
-
const sql = `SELECT * FROM edges WHERE target = ? AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
837
|
-
const rows = this.db.prepare(sql).all(targetId, ...kinds);
|
|
838
|
-
return rows.map(rowToEdge);
|
|
839
|
-
}
|
|
840
|
-
if (!this.stmts.getEdgesByTarget) {
|
|
841
|
-
this.stmts.getEdgesByTarget = this.db.prepare('SELECT * FROM edges WHERE target = ?');
|
|
842
|
-
}
|
|
843
|
-
const rows = this.stmts.getEdgesByTarget.all(targetId);
|
|
844
|
-
return rows.map(rowToEdge);
|
|
845
|
-
}
|
|
846
|
-
/**
|
|
847
|
-
* Find all edges where both source and target are in the given node set.
|
|
848
|
-
* Useful for recovering inter-node connectivity after BFS.
|
|
849
|
-
*/
|
|
850
|
-
findEdgesBetweenNodes(nodeIds, kinds) {
|
|
851
|
-
if (nodeIds.length === 0)
|
|
852
|
-
return [];
|
|
853
|
-
const idsJson = JSON.stringify(nodeIds);
|
|
854
|
-
let sql = `SELECT * FROM edges WHERE source IN (SELECT value FROM json_each(?)) AND target IN (SELECT value FROM json_each(?))`;
|
|
855
|
-
const params = [idsJson, idsJson];
|
|
856
|
-
if (kinds && kinds.length > 0) {
|
|
857
|
-
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
858
|
-
params.push(...kinds);
|
|
859
|
-
}
|
|
860
|
-
const rows = this.db.prepare(sql).all(...params);
|
|
861
|
-
return rows.map(rowToEdge);
|
|
862
|
-
}
|
|
863
|
-
// ===========================================================================
|
|
864
|
-
// File Operations
|
|
865
|
-
// ===========================================================================
|
|
866
|
-
/**
|
|
867
|
-
* Insert or update a file record
|
|
868
|
-
*/
|
|
869
|
-
upsertFile(file) {
|
|
870
|
-
if (!this.stmts.upsertFile) {
|
|
871
|
-
this.stmts.upsertFile = this.db.prepare(`
|
|
872
|
-
INSERT INTO files (path, content_hash, language, size, modified_at, indexed_at, node_count, errors)
|
|
873
|
-
VALUES (@path, @contentHash, @language, @size, @modifiedAt, @indexedAt, @nodeCount, @errors)
|
|
874
|
-
ON CONFLICT(path) DO UPDATE SET
|
|
875
|
-
content_hash = @contentHash,
|
|
876
|
-
language = @language,
|
|
877
|
-
size = @size,
|
|
878
|
-
modified_at = @modifiedAt,
|
|
879
|
-
indexed_at = @indexedAt,
|
|
880
|
-
node_count = @nodeCount,
|
|
881
|
-
errors = @errors
|
|
882
|
-
`);
|
|
883
|
-
}
|
|
884
|
-
this.stmts.upsertFile.run({
|
|
885
|
-
path: file.path,
|
|
886
|
-
contentHash: file.contentHash,
|
|
887
|
-
language: file.language,
|
|
888
|
-
size: file.size,
|
|
889
|
-
modifiedAt: file.modifiedAt,
|
|
890
|
-
indexedAt: file.indexedAt,
|
|
891
|
-
nodeCount: file.nodeCount,
|
|
892
|
-
errors: file.errors ? JSON.stringify(file.errors) : null,
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* Delete a file record and its nodes
|
|
897
|
-
*/
|
|
898
|
-
deleteFile(filePath) {
|
|
899
|
-
this.db.transaction(() => {
|
|
900
|
-
this.deleteNodesByFile(filePath);
|
|
901
|
-
if (!this.stmts.deleteFile) {
|
|
902
|
-
this.stmts.deleteFile = this.db.prepare('DELETE FROM files WHERE path = ?');
|
|
903
|
-
}
|
|
904
|
-
this.stmts.deleteFile.run(filePath);
|
|
905
|
-
})();
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* Get a file record by path
|
|
909
|
-
*/
|
|
910
|
-
getFileByPath(filePath) {
|
|
911
|
-
if (!this.stmts.getFileByPath) {
|
|
912
|
-
this.stmts.getFileByPath = this.db.prepare('SELECT * FROM files WHERE path = ?');
|
|
913
|
-
}
|
|
914
|
-
const row = this.stmts.getFileByPath.get(filePath);
|
|
915
|
-
return row ? rowToFileRecord(row) : null;
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* Get all tracked files
|
|
919
|
-
*/
|
|
920
|
-
getAllFiles() {
|
|
921
|
-
if (!this.stmts.getAllFiles) {
|
|
922
|
-
this.stmts.getAllFiles = this.db.prepare('SELECT * FROM files ORDER BY path');
|
|
923
|
-
}
|
|
924
|
-
const rows = this.stmts.getAllFiles.all();
|
|
925
|
-
return rows.map(rowToFileRecord);
|
|
926
|
-
}
|
|
927
|
-
/**
|
|
928
|
-
* Get files that need re-indexing (hash changed)
|
|
929
|
-
*/
|
|
930
|
-
getStaleFiles(currentHashes) {
|
|
931
|
-
const files = this.getAllFiles();
|
|
932
|
-
return files.filter((f) => {
|
|
933
|
-
const currentHash = currentHashes.get(f.path);
|
|
934
|
-
return currentHash && currentHash !== f.contentHash;
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
// ===========================================================================
|
|
938
|
-
// Unresolved References
|
|
939
|
-
// ===========================================================================
|
|
940
|
-
/**
|
|
941
|
-
* Insert an unresolved reference
|
|
942
|
-
*/
|
|
943
|
-
insertUnresolvedRef(ref) {
|
|
944
|
-
if (!this.stmts.insertUnresolved) {
|
|
945
|
-
this.stmts.insertUnresolved = this.db.prepare(`
|
|
946
|
-
INSERT INTO unresolved_refs (from_node_id, reference_name, reference_kind, line, col, candidates, file_path, language)
|
|
947
|
-
VALUES (@fromNodeId, @referenceName, @referenceKind, @line, @col, @candidates, @filePath, @language)
|
|
948
|
-
`);
|
|
949
|
-
}
|
|
950
|
-
this.stmts.insertUnresolved.run({
|
|
951
|
-
fromNodeId: ref.fromNodeId,
|
|
952
|
-
referenceName: ref.referenceName,
|
|
953
|
-
referenceKind: ref.referenceKind,
|
|
954
|
-
line: ref.line,
|
|
955
|
-
col: ref.column,
|
|
956
|
-
candidates: ref.candidates ? JSON.stringify(ref.candidates) : null,
|
|
957
|
-
filePath: ref.filePath ?? '',
|
|
958
|
-
language: ref.language ?? 'unknown',
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Insert multiple unresolved references in a transaction
|
|
963
|
-
*/
|
|
964
|
-
insertUnresolvedRefsBatch(refs) {
|
|
965
|
-
if (refs.length === 0)
|
|
966
|
-
return;
|
|
967
|
-
const insert = this.db.transaction(() => {
|
|
968
|
-
for (const ref of refs) {
|
|
969
|
-
this.insertUnresolvedRef(ref);
|
|
970
|
-
}
|
|
971
|
-
});
|
|
972
|
-
insert();
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* Delete unresolved references from a node
|
|
976
|
-
*/
|
|
977
|
-
deleteUnresolvedByNode(nodeId) {
|
|
978
|
-
if (!this.stmts.deleteUnresolvedByNode) {
|
|
979
|
-
this.stmts.deleteUnresolvedByNode = this.db.prepare('DELETE FROM unresolved_refs WHERE from_node_id = ?');
|
|
980
|
-
}
|
|
981
|
-
this.stmts.deleteUnresolvedByNode.run(nodeId);
|
|
982
|
-
}
|
|
983
|
-
/**
|
|
984
|
-
* Get unresolved references by name (for resolution)
|
|
985
|
-
*/
|
|
986
|
-
getUnresolvedByName(name) {
|
|
987
|
-
if (!this.stmts.getUnresolvedByName) {
|
|
988
|
-
this.stmts.getUnresolvedByName = this.db.prepare('SELECT * FROM unresolved_refs WHERE reference_name = ?');
|
|
989
|
-
}
|
|
990
|
-
const rows = this.stmts.getUnresolvedByName.all(name);
|
|
991
|
-
return rows.map((row) => ({
|
|
992
|
-
fromNodeId: row.from_node_id,
|
|
993
|
-
referenceName: row.reference_name,
|
|
994
|
-
referenceKind: row.reference_kind,
|
|
995
|
-
line: row.line,
|
|
996
|
-
column: row.col,
|
|
997
|
-
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
998
|
-
filePath: row.file_path,
|
|
999
|
-
language: row.language,
|
|
1000
|
-
}));
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Get all unresolved references
|
|
1004
|
-
*/
|
|
1005
|
-
getUnresolvedReferences() {
|
|
1006
|
-
const rows = this.db.prepare('SELECT * FROM unresolved_refs').all();
|
|
1007
|
-
return rows.map((row) => ({
|
|
1008
|
-
fromNodeId: row.from_node_id,
|
|
1009
|
-
referenceName: row.reference_name,
|
|
1010
|
-
referenceKind: row.reference_kind,
|
|
1011
|
-
line: row.line,
|
|
1012
|
-
column: row.col,
|
|
1013
|
-
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
1014
|
-
filePath: row.file_path,
|
|
1015
|
-
language: row.language,
|
|
1016
|
-
}));
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Get the count of unresolved references without loading them into memory
|
|
1020
|
-
*/
|
|
1021
|
-
getUnresolvedReferencesCount() {
|
|
1022
|
-
if (!this.stmts.getUnresolvedCount) {
|
|
1023
|
-
this.stmts.getUnresolvedCount = this.db.prepare('SELECT COUNT(*) as count FROM unresolved_refs');
|
|
1024
|
-
}
|
|
1025
|
-
const row = this.stmts.getUnresolvedCount.get();
|
|
1026
|
-
return row.count;
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Get a batch of unresolved references using LIMIT/OFFSET pagination.
|
|
1030
|
-
* Used to process references in bounded memory chunks.
|
|
1031
|
-
*/
|
|
1032
|
-
getUnresolvedReferencesBatch(offset, limit) {
|
|
1033
|
-
if (!this.stmts.getUnresolvedBatch) {
|
|
1034
|
-
this.stmts.getUnresolvedBatch = this.db.prepare('SELECT * FROM unresolved_refs LIMIT ? OFFSET ?');
|
|
1035
|
-
}
|
|
1036
|
-
const rows = this.stmts.getUnresolvedBatch.all(limit, offset);
|
|
1037
|
-
return rows.map((row) => ({
|
|
1038
|
-
fromNodeId: row.from_node_id,
|
|
1039
|
-
referenceName: row.reference_name,
|
|
1040
|
-
referenceKind: row.reference_kind,
|
|
1041
|
-
line: row.line,
|
|
1042
|
-
column: row.col,
|
|
1043
|
-
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
1044
|
-
filePath: row.file_path,
|
|
1045
|
-
language: row.language,
|
|
1046
|
-
}));
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Get all tracked file paths (lightweight — no full FileRecord objects)
|
|
1050
|
-
*/
|
|
1051
|
-
getAllFilePaths() {
|
|
1052
|
-
if (!this.stmts.getAllFilePaths) {
|
|
1053
|
-
this.stmts.getAllFilePaths = this.db.prepare('SELECT path FROM files ORDER BY path');
|
|
1054
|
-
}
|
|
1055
|
-
const rows = this.stmts.getAllFilePaths.all();
|
|
1056
|
-
return rows.map((r) => r.path);
|
|
1057
|
-
}
|
|
1058
|
-
/**
|
|
1059
|
-
* Get all distinct node names (lightweight — just name strings for pre-filtering)
|
|
1060
|
-
*/
|
|
1061
|
-
getAllNodeNames() {
|
|
1062
|
-
if (!this.stmts.getAllNodeNames) {
|
|
1063
|
-
this.stmts.getAllNodeNames = this.db.prepare('SELECT DISTINCT name FROM nodes');
|
|
1064
|
-
}
|
|
1065
|
-
const rows = this.stmts.getAllNodeNames.all();
|
|
1066
|
-
return rows.map((r) => r.name);
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Get unresolved references scoped to specific file paths.
|
|
1070
|
-
* Uses the idx_unresolved_file_path index for efficient lookup.
|
|
1071
|
-
*/
|
|
1072
|
-
getUnresolvedReferencesByFiles(filePaths) {
|
|
1073
|
-
if (filePaths.length === 0)
|
|
1074
|
-
return [];
|
|
1075
|
-
const placeholders = filePaths.map(() => '?').join(',');
|
|
1076
|
-
const rows = this.db
|
|
1077
|
-
.prepare(`SELECT * FROM unresolved_refs WHERE file_path IN (${placeholders})`)
|
|
1078
|
-
.all(...filePaths);
|
|
1079
|
-
return rows.map((row) => ({
|
|
1080
|
-
fromNodeId: row.from_node_id,
|
|
1081
|
-
referenceName: row.reference_name,
|
|
1082
|
-
referenceKind: row.reference_kind,
|
|
1083
|
-
line: row.line,
|
|
1084
|
-
column: row.col,
|
|
1085
|
-
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
1086
|
-
filePath: row.file_path,
|
|
1087
|
-
language: row.language,
|
|
1088
|
-
}));
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Delete all unresolved references (after resolution)
|
|
1092
|
-
*/
|
|
1093
|
-
clearUnresolvedReferences() {
|
|
1094
|
-
this.db.exec('DELETE FROM unresolved_refs');
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Delete resolved references by their IDs
|
|
1098
|
-
*/
|
|
1099
|
-
deleteResolvedReferences(fromNodeIds) {
|
|
1100
|
-
if (fromNodeIds.length === 0)
|
|
1101
|
-
return;
|
|
1102
|
-
const placeholders = fromNodeIds.map(() => '?').join(',');
|
|
1103
|
-
this.db.prepare(`DELETE FROM unresolved_refs WHERE from_node_id IN (${placeholders})`).run(...fromNodeIds);
|
|
1104
|
-
}
|
|
1105
|
-
/**
|
|
1106
|
-
* Delete specific resolved references by (fromNodeId, referenceName, referenceKind) tuples.
|
|
1107
|
-
* More precise than deleteResolvedReferences — only removes refs that were actually resolved.
|
|
1108
|
-
*/
|
|
1109
|
-
deleteSpecificResolvedReferences(refs) {
|
|
1110
|
-
if (refs.length === 0)
|
|
1111
|
-
return;
|
|
1112
|
-
const stmt = this.db.prepare('DELETE FROM unresolved_refs WHERE from_node_id = ? AND reference_name = ? AND reference_kind = ?');
|
|
1113
|
-
const deleteMany = this.db.transaction((items) => {
|
|
1114
|
-
for (const ref of items) {
|
|
1115
|
-
stmt.run(ref.fromNodeId, ref.referenceName, ref.referenceKind);
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
deleteMany(refs);
|
|
1119
|
-
}
|
|
1120
|
-
// ===========================================================================
|
|
1121
|
-
// Statistics
|
|
1122
|
-
// ===========================================================================
|
|
1123
|
-
/**
|
|
1124
|
-
* Get graph statistics
|
|
1125
|
-
*/
|
|
1126
|
-
getStats() {
|
|
1127
|
-
// Single query for all three aggregate counts
|
|
1128
|
-
const counts = this.db.prepare(`
|
|
1129
|
-
SELECT
|
|
1130
|
-
(SELECT COUNT(*) FROM nodes) AS node_count,
|
|
1131
|
-
(SELECT COUNT(*) FROM edges) AS edge_count,
|
|
1132
|
-
(SELECT COUNT(*) FROM files) AS file_count
|
|
1133
|
-
`).get();
|
|
1134
|
-
const nodesByKind = {};
|
|
1135
|
-
const nodeKindRows = this.db
|
|
1136
|
-
.prepare('SELECT kind, COUNT(*) as count FROM nodes GROUP BY kind')
|
|
1137
|
-
.all();
|
|
1138
|
-
for (const row of nodeKindRows) {
|
|
1139
|
-
nodesByKind[row.kind] = row.count;
|
|
1140
|
-
}
|
|
1141
|
-
const edgesByKind = {};
|
|
1142
|
-
const edgeKindRows = this.db
|
|
1143
|
-
.prepare('SELECT kind, COUNT(*) as count FROM edges GROUP BY kind')
|
|
1144
|
-
.all();
|
|
1145
|
-
for (const row of edgeKindRows) {
|
|
1146
|
-
edgesByKind[row.kind] = row.count;
|
|
1147
|
-
}
|
|
1148
|
-
const filesByLanguage = {};
|
|
1149
|
-
const languageRows = this.db
|
|
1150
|
-
.prepare('SELECT language, COUNT(*) as count FROM files GROUP BY language')
|
|
1151
|
-
.all();
|
|
1152
|
-
for (const row of languageRows) {
|
|
1153
|
-
filesByLanguage[row.language] = row.count;
|
|
1154
|
-
}
|
|
1155
|
-
return {
|
|
1156
|
-
nodeCount: counts.node_count,
|
|
1157
|
-
edgeCount: counts.edge_count,
|
|
1158
|
-
fileCount: counts.file_count,
|
|
1159
|
-
nodesByKind,
|
|
1160
|
-
edgesByKind,
|
|
1161
|
-
filesByLanguage,
|
|
1162
|
-
dbSizeBytes: 0, // Set by caller using DatabaseConnection.getSize()
|
|
1163
|
-
lastUpdated: Date.now(),
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
// ===========================================================================
|
|
1167
|
-
// Project Metadata
|
|
1168
|
-
// ===========================================================================
|
|
1169
|
-
/**
|
|
1170
|
-
* Get a metadata value by key
|
|
1171
|
-
*/
|
|
1172
|
-
getMetadata(key) {
|
|
1173
|
-
const row = this.db.prepare('SELECT value FROM project_metadata WHERE key = ?').get(key);
|
|
1174
|
-
return row?.value ?? null;
|
|
1175
|
-
}
|
|
1176
|
-
/**
|
|
1177
|
-
* Set a metadata key-value pair (upsert)
|
|
1178
|
-
*/
|
|
1179
|
-
setMetadata(key, value) {
|
|
1180
|
-
this.db.prepare('INSERT INTO project_metadata (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at').run(key, value, Date.now());
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* Get all metadata as a key-value record
|
|
1184
|
-
*/
|
|
1185
|
-
getAllMetadata() {
|
|
1186
|
-
const rows = this.db.prepare('SELECT key, value FROM project_metadata').all();
|
|
1187
|
-
const result = {};
|
|
1188
|
-
for (const row of rows) {
|
|
1189
|
-
result[row.key] = row.value;
|
|
1190
|
-
}
|
|
1191
|
-
return result;
|
|
1192
|
-
}
|
|
1193
|
-
/**
|
|
1194
|
-
* Clear all data from the database
|
|
1195
|
-
*/
|
|
1196
|
-
clear() {
|
|
1197
|
-
this.nodeCache.clear();
|
|
1198
|
-
this.db.transaction(() => {
|
|
1199
|
-
this.db.exec('DELETE FROM unresolved_refs');
|
|
1200
|
-
this.db.exec('DELETE FROM edges');
|
|
1201
|
-
this.db.exec('DELETE FROM nodes');
|
|
1202
|
-
this.db.exec('DELETE FROM files');
|
|
1203
|
-
})();
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
exports.QueryBuilder = QueryBuilder;
|
|
1207
|
-
//# sourceMappingURL=queries.js.map
|