gitnexus 1.6.3-rc.8 → 1.6.3
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 +21 -5
- package/dist/_shared/graph/types.d.ts +16 -0
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +20 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +11 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/def-index.js +2 -2
- package/dist/_shared/scope-resolution/def-index.js.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +8 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.js +2 -2
- package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
- package/dist/_shared/scope-resolution/module-scope-index.d.ts +8 -0
- package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/module-scope-index.js +10 -2
- package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -1
- package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/parsed-file.js +54 -0
- package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
- package/dist/_shared/scope-resolution/position-index.d.ts +12 -0
- package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/position-index.js +2 -2
- package/dist/_shared/scope-resolution/position-index.js.map +1 -1
- package/dist/_shared/scope-resolution/qualified-name-index.js +2 -2
- package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -1
- package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/reference-site.js +24 -0
- package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.js +44 -0
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
- package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +1 -10
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/resolve-type-ref.js +6 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.d.ts +4 -4
- package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.js +3 -2
- package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +6 -2
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/shadow/aggregate.js +5 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -1
- package/dist/_shared/scope-resolution/types.d.ts +11 -0
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
- package/dist/cli/ai-context.js +35 -4
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +31 -1
- package/dist/cli/clean.js +19 -1
- package/dist/cli/group.js +73 -0
- package/dist/cli/index-repo.js +8 -1
- package/dist/cli/index.js +26 -1
- package/dist/cli/list.js +11 -1
- package/dist/cli/remove.d.ts +30 -0
- package/dist/cli/remove.js +99 -0
- package/dist/cli/setup.js +185 -57
- package/dist/cli/tool.d.ts +5 -0
- package/dist/cli/tool.js +42 -0
- package/dist/config/ignore-service.d.ts +9 -0
- package/dist/config/ignore-service.js +80 -13
- package/dist/core/embedding-mode.d.ts +30 -0
- package/dist/core/embedding-mode.js +30 -0
- package/dist/core/embeddings/ast-utils.js +22 -22
- package/dist/core/embeddings/chunker.js +30 -25
- package/dist/core/embeddings/embedding-pipeline.d.ts +6 -0
- package/dist/core/embeddings/embedding-pipeline.js +15 -6
- package/dist/core/embeddings/text-generator.d.ts +1 -1
- package/dist/core/embeddings/text-generator.js +33 -24
- package/dist/core/embeddings/types.d.ts +43 -1
- package/dist/core/embeddings/types.js +101 -29
- package/dist/core/git-staleness.d.ts +18 -0
- package/dist/core/git-staleness.js +108 -0
- package/dist/core/graph/graph.js +115 -20
- package/dist/core/graph/types.d.ts +12 -1
- package/dist/core/group/config-parser.d.ts +4 -0
- package/dist/core/group/config-parser.js +18 -1
- package/dist/core/group/cross-impact.d.ts +41 -0
- package/dist/core/group/cross-impact.js +441 -0
- package/dist/core/group/extractors/http-patterns/php.js +126 -18
- package/dist/core/group/group-path-utils.d.ts +17 -0
- package/dist/core/group/group-path-utils.js +40 -0
- package/dist/core/group/resolve-at-member.d.ts +10 -0
- package/dist/core/group/resolve-at-member.js +31 -0
- package/dist/core/group/service.d.ts +9 -0
- package/dist/core/group/service.js +259 -25
- package/dist/core/group/types.d.ts +30 -0
- package/dist/core/ingestion/ast-cache.d.ts +16 -1
- package/dist/core/ingestion/ast-cache.js +14 -2
- package/dist/core/ingestion/call-processor.js +9 -0
- package/dist/core/ingestion/emit-references.d.ts +88 -0
- package/dist/core/ingestion/emit-references.js +229 -0
- package/dist/core/ingestion/filesystem-walker.js +6 -4
- package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
- package/dist/core/ingestion/finalize-orchestrator.js +139 -0
- package/dist/core/ingestion/framework-detection.js +6 -2
- package/dist/core/ingestion/import-processor.js +4 -0
- package/dist/core/ingestion/import-resolvers/python.js +9 -6
- package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
- package/dist/core/ingestion/import-target-adapter.js +95 -0
- package/dist/core/ingestion/language-provider.d.ts +36 -33
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
- package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
- package/dist/core/ingestion/languages/csharp/arity.js +37 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/captures.js +249 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
- package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
- package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
- package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
- package/dist/core/ingestion/languages/csharp/index.js +82 -0
- package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +50 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +374 -0
- package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
- package/dist/core/ingestion/languages/csharp/query.js +515 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
- package/dist/core/ingestion/languages/csharp.js +14 -0
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
- package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
- package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
- package/dist/core/ingestion/languages/python/arity.js +38 -0
- package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
- package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
- package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/python/captures.js +106 -0
- package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
- package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
- package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
- package/dist/core/ingestion/languages/python/import-target.js +99 -0
- package/dist/core/ingestion/languages/python/index.d.ts +80 -0
- package/dist/core/ingestion/languages/python/index.js +80 -0
- package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/python/interpret.js +191 -0
- package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
- package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
- package/dist/core/ingestion/languages/python/query.d.ts +9 -0
- package/dist/core/ingestion/languages/python/query.js +267 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
- package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
- package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
- package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
- package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
- package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
- package/dist/core/ingestion/languages/python.js +14 -0
- package/dist/core/ingestion/model/method-registry.d.ts +9 -0
- package/dist/core/ingestion/model/method-registry.js +4 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +64 -0
- package/dist/core/ingestion/model/semantic-model.js +55 -0
- package/dist/core/ingestion/mro-processor.js +38 -22
- package/dist/core/ingestion/parsing-processor.d.ts +18 -1
- package/dist/core/ingestion/parsing-processor.js +45 -11
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
- package/dist/core/ingestion/pipeline.js +2 -1
- package/dist/core/ingestion/registry-primary-flag.d.ts +86 -0
- package/dist/core/ingestion/registry-primary-flag.js +111 -0
- package/dist/core/ingestion/resolve-references.d.ts +63 -0
- package/dist/core/ingestion/resolve-references.js +175 -0
- package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
- package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
- package/dist/core/ingestion/scope-extractor.d.ts +86 -0
- package/dist/core/ingestion/scope-extractor.js +758 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +372 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +212 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +112 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +113 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +198 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +27 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +131 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +26 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +61 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +327 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +130 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +21 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +66 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +157 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +127 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +349 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
- package/dist/core/ingestion/shadow-harness.d.ts +113 -0
- package/dist/core/ingestion/shadow-harness.js +148 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
- package/dist/core/ingestion/utils/ast-helpers.js +70 -0
- package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
- package/dist/core/ingestion/utils/max-file-size.js +52 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
- package/dist/core/ingestion/workers/parse-worker.js +57 -21
- package/dist/core/lbug/lbug-adapter.d.ts +22 -2
- package/dist/core/lbug/lbug-adapter.js +58 -14
- package/dist/core/lbug/pool-adapter.d.ts +17 -0
- package/dist/core/lbug/pool-adapter.js +24 -14
- package/dist/core/run-analyze.d.ts +32 -0
- package/dist/core/run-analyze.js +74 -19
- package/dist/core/search/bm25-index.d.ts +18 -0
- package/dist/core/search/bm25-index.js +125 -12
- package/dist/core/tree-sitter/parser-loader.js +6 -1
- package/dist/mcp/local/local-backend.d.ts +67 -3
- package/dist/mcp/local/local-backend.js +296 -34
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +100 -17
- package/dist/mcp/tools.d.ts +4 -1
- package/dist/mcp/tools.js +75 -54
- package/dist/server/api.js +6 -2
- package/dist/storage/git.d.ts +49 -0
- package/dist/storage/git.js +111 -0
- package/dist/storage/repo-manager.d.ts +246 -1
- package/dist/storage/repo-manager.js +391 -9
- package/package.json +7 -6
- package/scripts/bench-scope-resolution.ts +134 -0
- package/scripts/ci-list-migrated-languages.ts +24 -0
- package/skills/gitnexus-cli.md +1 -0
|
@@ -110,6 +110,17 @@ let conn = null;
|
|
|
110
110
|
let currentDbPath = null;
|
|
111
111
|
let ftsLoaded = false;
|
|
112
112
|
let vectorExtensionLoaded = false;
|
|
113
|
+
/**
|
|
114
|
+
* In-process cache of FTS indexes that have been ensured against the current
|
|
115
|
+
* connection. Prevents repeated `CALL CREATE_FTS_INDEX` round-trips inside a
|
|
116
|
+
* single CLI/MCP session — the first call to `ensureFTSIndex` for a given
|
|
117
|
+
* `(tableName, indexName)` pays the LadybugDB cost (~440 ms even when the
|
|
118
|
+
* index already exists on disk), subsequent calls are a Set lookup. Cleared
|
|
119
|
+
* by `closeLbug` so a re-init starts fresh.
|
|
120
|
+
*
|
|
121
|
+
* Key format: `${tableName}:${indexName}`.
|
|
122
|
+
*/
|
|
123
|
+
const ensuredFTSIndexes = new Set();
|
|
113
124
|
/**
|
|
114
125
|
* Check if an error indicates a missing column or table (schema-level problem)
|
|
115
126
|
* rather than a transient/connection error. Used for legacy DB fallback logic.
|
|
@@ -935,6 +946,7 @@ export const closeLbug = async () => {
|
|
|
935
946
|
currentDbPath = null;
|
|
936
947
|
ftsLoaded = false;
|
|
937
948
|
vectorExtensionLoaded = false;
|
|
949
|
+
ensuredFTSIndexes.clear();
|
|
938
950
|
};
|
|
939
951
|
export const isLbugReady = () => conn !== null && db !== null;
|
|
940
952
|
/**
|
|
@@ -1014,36 +1026,50 @@ export const getEmbeddingTableName = () => EMBEDDING_TABLE_NAME;
|
|
|
1014
1026
|
// ============================================================================
|
|
1015
1027
|
/**
|
|
1016
1028
|
* Load the FTS extension (required before using FTS functions).
|
|
1017
|
-
*
|
|
1029
|
+
*
|
|
1030
|
+
* Safe to call multiple times — when invoked without arguments, tracks loaded
|
|
1031
|
+
* state via module-level `ftsLoaded`. When invoked with an explicit
|
|
1032
|
+
* connection, loads on that connection and returns whether the load
|
|
1033
|
+
* succeeded — letting callers (e.g. the pool adapter) track their own state.
|
|
1034
|
+
*
|
|
1035
|
+
* Tries `LOAD EXTENSION fts` first so previously-cached installs skip the
|
|
1036
|
+
* network entirely; falls back to `INSTALL` + `LOAD` only when the extension
|
|
1037
|
+
* hasn't been cached yet.
|
|
1018
1038
|
*/
|
|
1019
|
-
export const loadFTSExtension = async () => {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1039
|
+
export const loadFTSExtension = async (targetConn) => {
|
|
1040
|
+
const useModuleState = targetConn === undefined;
|
|
1041
|
+
if (useModuleState && ftsLoaded)
|
|
1042
|
+
return true;
|
|
1043
|
+
const c = targetConn ?? conn;
|
|
1044
|
+
if (!c) {
|
|
1023
1045
|
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
1024
1046
|
}
|
|
1047
|
+
const markLoaded = () => {
|
|
1048
|
+
if (useModuleState)
|
|
1049
|
+
ftsLoaded = true;
|
|
1050
|
+
return true;
|
|
1051
|
+
};
|
|
1025
1052
|
try {
|
|
1026
1053
|
// Try loading locally first (no network required)
|
|
1027
|
-
await
|
|
1028
|
-
|
|
1054
|
+
await c.query('LOAD EXTENSION fts');
|
|
1055
|
+
return markLoaded();
|
|
1029
1056
|
}
|
|
1030
1057
|
catch {
|
|
1031
1058
|
// Fall back to install + load (requires network)
|
|
1032
1059
|
try {
|
|
1033
|
-
await
|
|
1034
|
-
await
|
|
1035
|
-
|
|
1060
|
+
await c.query('INSTALL fts');
|
|
1061
|
+
await c.query('LOAD EXTENSION fts');
|
|
1062
|
+
return markLoaded();
|
|
1036
1063
|
}
|
|
1037
1064
|
catch (err) {
|
|
1038
1065
|
const msg = err?.message || '';
|
|
1039
1066
|
if (msg.includes('already loaded') ||
|
|
1040
1067
|
msg.includes('already installed') ||
|
|
1041
1068
|
msg.includes('already exists')) {
|
|
1042
|
-
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
console.error('GitNexus: FTS extension load failed:', msg);
|
|
1069
|
+
return markLoaded();
|
|
1046
1070
|
}
|
|
1071
|
+
console.error('GitNexus: FTS extension load failed:', msg);
|
|
1072
|
+
return false;
|
|
1047
1073
|
}
|
|
1048
1074
|
}
|
|
1049
1075
|
};
|
|
@@ -1097,6 +1123,24 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
|
|
|
1097
1123
|
}
|
|
1098
1124
|
}
|
|
1099
1125
|
};
|
|
1126
|
+
/**
|
|
1127
|
+
* Lazy-create an FTS index, caching the fact in-process.
|
|
1128
|
+
*
|
|
1129
|
+
* Used by `queryFTS` so that `analyze` doesn't pay the ~440 ms × 5 fixed
|
|
1130
|
+
* LadybugDB cost up-front (it dominates analyze on small repos). Instead,
|
|
1131
|
+
* the cost is moved to the first `query`/`context` call in a session,
|
|
1132
|
+
* where it's amortised across many lookups.
|
|
1133
|
+
*
|
|
1134
|
+
* Safe to call repeatedly — the in-process Set guarantees only the first
|
|
1135
|
+
* call hits LadybugDB. `closeLbug` clears the cache so re-init starts fresh.
|
|
1136
|
+
*/
|
|
1137
|
+
export const ensureFTSIndex = async (tableName, indexName, properties, stemmer = 'porter') => {
|
|
1138
|
+
const key = `${tableName}:${indexName}`;
|
|
1139
|
+
if (ensuredFTSIndexes.has(key))
|
|
1140
|
+
return;
|
|
1141
|
+
await createFTSIndex(tableName, indexName, properties, stemmer);
|
|
1142
|
+
ensuredFTSIndexes.add(key);
|
|
1143
|
+
};
|
|
1100
1144
|
/**
|
|
1101
1145
|
* Query a full-text search index
|
|
1102
1146
|
* @param tableName - The node table name
|
|
@@ -15,6 +15,22 @@
|
|
|
15
15
|
* from the same Database is the officially supported concurrency pattern.
|
|
16
16
|
*/
|
|
17
17
|
import lbug from '@ladybugdb/core';
|
|
18
|
+
/**
|
|
19
|
+
* Listeners notified when a pool entry is torn down (LRU eviction, idle
|
|
20
|
+
* timeout, explicit close). Used by upper layers (e.g. the BM25 search
|
|
21
|
+
* module) to invalidate per-repo caches that must not outlive the pool
|
|
22
|
+
* entry that produced them.
|
|
23
|
+
*
|
|
24
|
+
* Listeners run synchronously inside `closeOne` after the pool entry has
|
|
25
|
+
* been removed; throwing listeners are isolated so one bad listener does
|
|
26
|
+
* not prevent others from firing or break teardown.
|
|
27
|
+
*/
|
|
28
|
+
type PoolCloseListener = (repoId: string) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to pool-close events. Returns a disposer that removes the
|
|
31
|
+
* listener (handy for tests).
|
|
32
|
+
*/
|
|
33
|
+
export declare function addPoolCloseListener(listener: PoolCloseListener): () => void;
|
|
18
34
|
/** Saved real stdout/stderr write — used to silence native module output without race conditions */
|
|
19
35
|
export declare const realStdoutWrite: any;
|
|
20
36
|
export declare const realStderrWrite: any;
|
|
@@ -74,3 +90,4 @@ export declare const isLbugReady: (repoId: string) => boolean;
|
|
|
74
90
|
export declare const CYPHER_WRITE_RE: RegExp;
|
|
75
91
|
/** Check if a Cypher query contains write operations */
|
|
76
92
|
export declare function isWriteQuery(query: string): boolean;
|
|
93
|
+
export {};
|
|
@@ -16,7 +16,19 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'fs/promises';
|
|
18
18
|
import lbug from '@ladybugdb/core';
|
|
19
|
+
import { loadFTSExtension } from './lbug-adapter.js';
|
|
19
20
|
const pool = new Map();
|
|
21
|
+
const poolCloseListeners = new Set();
|
|
22
|
+
/**
|
|
23
|
+
* Subscribe to pool-close events. Returns a disposer that removes the
|
|
24
|
+
* listener (handy for tests).
|
|
25
|
+
*/
|
|
26
|
+
export function addPoolCloseListener(listener) {
|
|
27
|
+
poolCloseListeners.add(listener);
|
|
28
|
+
return () => {
|
|
29
|
+
poolCloseListeners.delete(listener);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
20
32
|
const dbCache = new Map();
|
|
21
33
|
/** Max repos in the pool (LRU eviction) */
|
|
22
34
|
const MAX_POOL_SIZE = 5;
|
|
@@ -119,6 +131,16 @@ function closeOne(repoId) {
|
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
133
|
pool.delete(repoId);
|
|
134
|
+
// Notify listeners AFTER the pool entry is gone so any cache-invalidation
|
|
135
|
+
// they perform is consistent with `isLbugReady(repoId) === false`.
|
|
136
|
+
for (const listener of poolCloseListeners) {
|
|
137
|
+
try {
|
|
138
|
+
listener(repoId);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Isolate listener failures — teardown must complete.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
122
144
|
}
|
|
123
145
|
/**
|
|
124
146
|
* Create a new Connection from a repo's Database.
|
|
@@ -263,13 +285,7 @@ async function doInitLbug(repoId, dbPath) {
|
|
|
263
285
|
// Done BEFORE pool registration so no concurrent checkout can grab
|
|
264
286
|
// the connection while the async FTS load is in progress.
|
|
265
287
|
if (!shared.ftsLoaded) {
|
|
266
|
-
|
|
267
|
-
await available[0].query('LOAD EXTENSION fts');
|
|
268
|
-
shared.ftsLoaded = true;
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
// Extension may not be installed — FTS queries will fail gracefully
|
|
272
|
-
}
|
|
288
|
+
shared.ftsLoaded = await loadFTSExtension(available[0]);
|
|
273
289
|
}
|
|
274
290
|
// Load VECTOR extension once per shared Database for semantic search support.
|
|
275
291
|
if (!shared.vectorLoaded) {
|
|
@@ -335,13 +351,7 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
|
|
|
335
351
|
}
|
|
336
352
|
// Load FTS extension if not already loaded on this Database
|
|
337
353
|
if (!shared.ftsLoaded) {
|
|
338
|
-
|
|
339
|
-
await available[0].query('LOAD EXTENSION fts');
|
|
340
|
-
shared.ftsLoaded = true;
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// Extension may already be loaded or not installed
|
|
344
|
-
}
|
|
354
|
+
shared.ftsLoaded = await loadFTSExtension(available[0]);
|
|
345
355
|
}
|
|
346
356
|
// Load VECTOR extension for semantic search support
|
|
347
357
|
if (!shared.vectorLoaded) {
|
|
@@ -13,13 +13,43 @@ export interface AnalyzeCallbacks {
|
|
|
13
13
|
onLog?: (message: string) => void;
|
|
14
14
|
}
|
|
15
15
|
export interface AnalyzeOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Force a full re-index of the pipeline. Callers may OR this with
|
|
18
|
+
* other flags that imply re-analysis (e.g. `--skills`), so the value
|
|
19
|
+
* here is the PIPELINE-force signal, NOT the registry-collision
|
|
20
|
+
* bypass. See `allowDuplicateName` below.
|
|
21
|
+
*/
|
|
16
22
|
force?: boolean;
|
|
17
23
|
embeddings?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Explicitly drop any embeddings present in the existing index instead of
|
|
26
|
+
* preserving them. Only meaningful when `embeddings` is false/undefined:
|
|
27
|
+
* the default behavior in that case is to load the previously generated
|
|
28
|
+
* embeddings and re-insert them after the rebuild so a routine
|
|
29
|
+
* re-analyze does not silently wipe a long embedding pass (#issue: analyze
|
|
30
|
+
* silently wipes existing embeddings when run without --embeddings).
|
|
31
|
+
*/
|
|
32
|
+
dropEmbeddings?: boolean;
|
|
18
33
|
skipGit?: boolean;
|
|
19
34
|
/** Skip AGENTS.md and CLAUDE.md gitnexus block updates. */
|
|
20
35
|
skipAgentsMd?: boolean;
|
|
21
36
|
/** Omit volatile symbol/relationship counts from AGENTS.md and CLAUDE.md. */
|
|
22
37
|
noStats?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* User-provided alias for the registry `name` (#829). When set,
|
|
40
|
+
* forwarded to `registerRepo` so the indexed repo is stored under
|
|
41
|
+
* this alias instead of the path-derived basename.
|
|
42
|
+
*/
|
|
43
|
+
registryName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Bypass the `RegistryNameCollisionError` guard and allow two paths
|
|
46
|
+
* to register under the same `name` (#829). Controlled by the
|
|
47
|
+
* dedicated `--allow-duplicate-name` CLI flag, intentionally
|
|
48
|
+
* independent from `--force` — users who hit the collision guard
|
|
49
|
+
* should be able to accept the duplicate without paying the cost
|
|
50
|
+
* of a pipeline re-index.
|
|
51
|
+
*/
|
|
52
|
+
allowDuplicateName?: boolean;
|
|
23
53
|
}
|
|
24
54
|
export interface AnalyzeResult {
|
|
25
55
|
repoName: string;
|
|
@@ -36,6 +66,8 @@ export interface AnalyzeResult {
|
|
|
36
66
|
/** The raw pipeline result — only populated when needed by callers (e.g. skill generation). */
|
|
37
67
|
pipelineResult?: any;
|
|
38
68
|
}
|
|
69
|
+
export { deriveEmbeddingMode } from './embedding-mode.js';
|
|
70
|
+
export type { EmbeddingMode } from './embedding-mode.js';
|
|
39
71
|
export declare const PHASE_LABELS: Record<string, string>;
|
|
40
72
|
/**
|
|
41
73
|
* Run the full GitNexus analysis pipeline.
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import fs from 'fs/promises';
|
|
13
13
|
import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
14
|
-
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug,
|
|
14
|
+
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, loadCachedEmbeddings, } from './lbug/lbug-adapter.js';
|
|
15
15
|
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, cleanupOldKuzuFiles, } from '../storage/repo-manager.js';
|
|
16
|
-
import { getCurrentCommit, hasGitDir } from '../storage/git.js';
|
|
16
|
+
import { getCurrentCommit, getRemoteUrl, hasGitDir, getInferredRepoName } from '../storage/git.js';
|
|
17
17
|
import { generateAIContextFiles } from '../cli/ai-context.js';
|
|
18
18
|
import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
|
|
19
19
|
import { STALE_HASH_SENTINEL } from './lbug/schema.js';
|
|
20
20
|
/** Threshold: auto-skip embeddings for repos with more nodes than this */
|
|
21
21
|
const EMBEDDING_NODE_LIMIT = 50_000;
|
|
22
|
+
// Re-export the pure flag-derivation helper so external callers (and tests)
|
|
23
|
+
// keep importing from this module's stable surface.
|
|
24
|
+
export { deriveEmbeddingMode } from './embedding-mode.js';
|
|
25
|
+
import { deriveEmbeddingMode as _deriveEmbeddingMode } from './embedding-mode.js';
|
|
22
26
|
export const PHASE_LABELS = {
|
|
23
27
|
extracting: 'Scanning files',
|
|
24
28
|
structure: 'Building structure',
|
|
@@ -65,7 +69,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
65
69
|
// Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
|
|
66
70
|
if (currentCommit !== '') {
|
|
67
71
|
return {
|
|
68
|
-
repoName: path.basename(repoPath),
|
|
72
|
+
repoName: options.registryName ?? getInferredRepoName(repoPath) ?? path.basename(repoPath),
|
|
69
73
|
repoPath,
|
|
70
74
|
stats: existingMeta.stats ?? {},
|
|
71
75
|
alreadyUpToDate: true,
|
|
@@ -73,9 +77,39 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
// ── Cache embeddings from existing index before rebuild ────────────
|
|
80
|
+
// Four modes:
|
|
81
|
+
// --embeddings -> load cache, restore, then generate any new ones
|
|
82
|
+
// --force (with existing
|
|
83
|
+
// embeddings) -> auto-imply --embeddings: load cache, restore,
|
|
84
|
+
// regenerate embeddings for new/changed nodes
|
|
85
|
+
// (a forced re-index of an embedded repo
|
|
86
|
+
// shouldn't quietly downgrade to "preserve only")
|
|
87
|
+
// (default) -> if existing index has embeddings, preserve them
|
|
88
|
+
// (load + restore, but do not generate); otherwise no-op
|
|
89
|
+
// --drop-embeddings -> skip cache load entirely; rebuild wipes embeddings
|
|
90
|
+
//
|
|
91
|
+
// The default-preserve branch is what makes a routine `analyze` (e.g. a
|
|
92
|
+
// post-commit hook) safe: a multi-minute embedding pass is no longer
|
|
93
|
+
// silently dropped just because the caller omitted `--embeddings`.
|
|
76
94
|
let cachedEmbeddingNodeIds = new Set();
|
|
77
95
|
let cachedEmbeddings = [];
|
|
78
|
-
|
|
96
|
+
const existingEmbeddingCount = existingMeta?.stats?.embeddings ?? 0;
|
|
97
|
+
const { forceRegenerateEmbeddings, preserveExistingEmbeddings, shouldGenerateEmbeddings, shouldLoadCache, } = _deriveEmbeddingMode(options, existingEmbeddingCount);
|
|
98
|
+
if (options.dropEmbeddings && existingEmbeddingCount > 0) {
|
|
99
|
+
log(`Dropping ${existingEmbeddingCount} existing embeddings (--drop-embeddings). ` +
|
|
100
|
+
`Re-run with --embeddings to regenerate.`);
|
|
101
|
+
}
|
|
102
|
+
else if (forceRegenerateEmbeddings) {
|
|
103
|
+
log(`--force on a repo with ${existingEmbeddingCount} existing embeddings: ` +
|
|
104
|
+
`regenerating embeddings for new/changed nodes. ` +
|
|
105
|
+
`Pass --drop-embeddings to wipe them instead.`);
|
|
106
|
+
}
|
|
107
|
+
else if (preserveExistingEmbeddings) {
|
|
108
|
+
log(`Preserving ${existingEmbeddingCount} existing embeddings. ` +
|
|
109
|
+
`Pass --embeddings to also generate embeddings for new/changed nodes, ` +
|
|
110
|
+
`or --drop-embeddings to wipe them.`);
|
|
111
|
+
}
|
|
112
|
+
if (shouldLoadCache && existingMeta) {
|
|
79
113
|
try {
|
|
80
114
|
progress('embeddings', 0, 'Caching embeddings...');
|
|
81
115
|
await initLbug(lbugPath);
|
|
@@ -84,7 +118,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
84
118
|
cachedEmbeddings = cached.embeddings;
|
|
85
119
|
await closeLbug();
|
|
86
120
|
}
|
|
87
|
-
catch {
|
|
121
|
+
catch (err) {
|
|
122
|
+
// Surface cache-load failures explicitly: silently swallowing here would
|
|
123
|
+
// re-introduce the original silent-data-loss symptom (embeddings end up
|
|
124
|
+
// at 0 in meta.json with no diagnostic) through a different door.
|
|
125
|
+
log(`Warning: could not load cached embeddings ` +
|
|
126
|
+
`(${err?.message ?? String(err)}). ` +
|
|
127
|
+
`Embeddings will not be preserved on this run.`);
|
|
128
|
+
cachedEmbeddingNodeIds = new Set();
|
|
129
|
+
cachedEmbeddings = [];
|
|
88
130
|
try {
|
|
89
131
|
await closeLbug();
|
|
90
132
|
}
|
|
@@ -123,17 +165,12 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
123
165
|
progress('lbug', pct, msg);
|
|
124
166
|
});
|
|
125
167
|
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// Non-fatal — FTS is best-effort
|
|
136
|
-
}
|
|
168
|
+
// FTS indexes are created lazily on first `query`/`context` call instead
|
|
169
|
+
// of eagerly here. On small repos / CI runners the LadybugDB
|
|
170
|
+
// CREATE_FTS_INDEX cost is ~440 ms × 5 (≈2 s) regardless of table size,
|
|
171
|
+
// which dominated `analyze` runtime and pushed Windows CI past its
|
|
172
|
+
// 30 s test budget. Lazy creation is implemented in
|
|
173
|
+
// `core/search/bm25-index.ts` via `ensureFTSIndex`.
|
|
137
174
|
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
138
175
|
if (cachedEmbeddings.length > 0) {
|
|
139
176
|
const cachedDims = cachedEmbeddings[0].embedding.length;
|
|
@@ -162,7 +199,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
162
199
|
// ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
|
|
163
200
|
const stats = await getLbugStats();
|
|
164
201
|
let embeddingSkipped = true;
|
|
165
|
-
if (
|
|
202
|
+
if (shouldGenerateEmbeddings) {
|
|
166
203
|
if (stats.nodes <= EMBEDDING_NODE_LIMIT) {
|
|
167
204
|
embeddingSkipped = false;
|
|
168
205
|
}
|
|
@@ -208,6 +245,13 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
208
245
|
repoPath,
|
|
209
246
|
lastCommit: currentCommit,
|
|
210
247
|
indexedAt: new Date().toISOString(),
|
|
248
|
+
// Captured here (not at registration) so it travels with the
|
|
249
|
+
// on-disk meta.json — sibling-clone fingerprinting works for
|
|
250
|
+
// out-of-tree consumers (group-status, future tooling) without
|
|
251
|
+
// a second git shellout. `undefined` when the repo has no
|
|
252
|
+
// origin remote, which is fine: paths-only repos behave as
|
|
253
|
+
// before.
|
|
254
|
+
remoteUrl: hasGitDir(repoPath) ? getRemoteUrl(repoPath) : undefined,
|
|
211
255
|
stats: {
|
|
212
256
|
files: pipelineResult.totalFileCount,
|
|
213
257
|
nodes: stats.nodes,
|
|
@@ -218,12 +262,23 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
218
262
|
},
|
|
219
263
|
};
|
|
220
264
|
await saveMeta(storagePath, meta);
|
|
221
|
-
|
|
265
|
+
// Forward the --name alias and the registry-collision bypass bit.
|
|
266
|
+
// `allowDuplicateName` is its own concern — independent from the
|
|
267
|
+
// pipeline `force` above. The CLI maps it from
|
|
268
|
+
// `--allow-duplicate-name` only; `--force` and `--skills` both
|
|
269
|
+
// trigger pipeline re-run but never bypass the registry guard.
|
|
270
|
+
// The returned name is the one actually written to the registry
|
|
271
|
+
// (after applying the precedence chain in registerRepo) — reuse it
|
|
272
|
+
// so AGENTS.md / skill files reference the same name MCP clients
|
|
273
|
+
// will look up (#979).
|
|
274
|
+
const projectName = await registerRepo(repoPath, meta, {
|
|
275
|
+
name: options.registryName,
|
|
276
|
+
allowDuplicateName: options.allowDuplicateName,
|
|
277
|
+
});
|
|
222
278
|
// Only attempt to update .gitignore when a .git directory is present.
|
|
223
279
|
if (hasGitDir(repoPath)) {
|
|
224
280
|
await addToGitignore(repoPath);
|
|
225
281
|
}
|
|
226
|
-
const projectName = path.basename(repoPath);
|
|
227
282
|
// ── Generate AI context files (best-effort) ───────────────────────
|
|
228
283
|
let aggregatedClusterCount = 0;
|
|
229
284
|
if (pipelineResult.communityResult?.communities) {
|
|
@@ -3,12 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Uses LadybugDB's built-in full-text search indexes for keyword-based search.
|
|
5
5
|
* Always reads from the database (no cached state to drift).
|
|
6
|
+
*
|
|
7
|
+
* FTS indexes are created lazily on first query (via `ensureFTSIndex`) — see
|
|
8
|
+
* `lbug-adapter.ts` for the rationale. This keeps `analyze` fast (the
|
|
9
|
+
* ~440 ms × 5 LadybugDB CREATE_FTS_INDEX cost dominates pipeline time on
|
|
10
|
+
* small repos / CI runners) at the cost of paying that overhead on the
|
|
11
|
+
* first `query`/`context` call in a session.
|
|
6
12
|
*/
|
|
7
13
|
export interface BM25SearchResult {
|
|
8
14
|
filePath: string;
|
|
9
15
|
score: number;
|
|
10
16
|
rank: number;
|
|
17
|
+
nodeIds?: string[];
|
|
11
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Drop all ensured-FTS cache entries for a given repoId.
|
|
21
|
+
*
|
|
22
|
+
* Called from the pool-close listener so that a pool teardown / recreation
|
|
23
|
+
* forces the next `searchFTSFromLbug` call to re-issue `CREATE_FTS_INDEX`
|
|
24
|
+
* against the fresh connection rather than trust stale ensure-state from a
|
|
25
|
+
* previous pool lifetime.
|
|
26
|
+
*
|
|
27
|
+
* Exported for tests; the listener wiring is internal.
|
|
28
|
+
*/
|
|
29
|
+
export declare function invalidateEnsuredFTSForRepo(repoId: string): void;
|
|
12
30
|
/**
|
|
13
31
|
* Search using LadybugDB's built-in FTS (always fresh, reads from disk)
|
|
14
32
|
*
|
|
@@ -3,8 +3,96 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Uses LadybugDB's built-in full-text search indexes for keyword-based search.
|
|
5
5
|
* Always reads from the database (no cached state to drift).
|
|
6
|
+
*
|
|
7
|
+
* FTS indexes are created lazily on first query (via `ensureFTSIndex`) — see
|
|
8
|
+
* `lbug-adapter.ts` for the rationale. This keeps `analyze` fast (the
|
|
9
|
+
* ~440 ms × 5 LadybugDB CREATE_FTS_INDEX cost dominates pipeline time on
|
|
10
|
+
* small repos / CI runners) at the cost of paying that overhead on the
|
|
11
|
+
* first `query`/`context` call in a session.
|
|
12
|
+
*/
|
|
13
|
+
import { queryFTS, ensureFTSIndex } from '../lbug/lbug-adapter.js';
|
|
14
|
+
/**
|
|
15
|
+
* FTS schema served by `searchFTSFromLbug`. Centralised so that both the
|
|
16
|
+
* CLI/pipeline path and the MCP pool path use identical (table, index,
|
|
17
|
+
* properties) tuples and the lazy-create logic stays in one place.
|
|
18
|
+
*/
|
|
19
|
+
const FTS_INDEXES = [
|
|
20
|
+
{ table: 'File', indexName: 'file_fts', properties: ['name', 'content'] },
|
|
21
|
+
{ table: 'Function', indexName: 'function_fts', properties: ['name', 'content'] },
|
|
22
|
+
{ table: 'Class', indexName: 'class_fts', properties: ['name', 'content'] },
|
|
23
|
+
{ table: 'Method', indexName: 'method_fts', properties: ['name', 'content'] },
|
|
24
|
+
{ table: 'Interface', indexName: 'interface_fts', properties: ['name', 'content'] },
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Per-process cache for the MCP pool path: tracks which `(repoId, table)`
|
|
28
|
+
* pairs have been ensured. The CLI/pipeline path gets its own cache inside
|
|
29
|
+
* `lbug-adapter.ts` keyed by table/index, scoped to the singleton connection.
|
|
30
|
+
*
|
|
31
|
+
* IMPORTANT: an entry is added ONLY when the index was confirmed to exist
|
|
32
|
+
* (CREATE_FTS_INDEX succeeded, or failed with `'already exists'`). Other
|
|
33
|
+
* failures (transient lock errors, missing extension, etc.) leave the key
|
|
34
|
+
* unset so the next query retries instead of silently caching the failure.
|
|
35
|
+
*
|
|
36
|
+
* Entries for a given repoId are invalidated when its pool is closed —
|
|
37
|
+
* see the `addPoolCloseListener` registration in `searchFTSFromLbug`.
|
|
38
|
+
*/
|
|
39
|
+
const ensuredPoolFTS = new Set();
|
|
40
|
+
/**
|
|
41
|
+
* Drop all ensured-FTS cache entries for a given repoId.
|
|
42
|
+
*
|
|
43
|
+
* Called from the pool-close listener so that a pool teardown / recreation
|
|
44
|
+
* forces the next `searchFTSFromLbug` call to re-issue `CREATE_FTS_INDEX`
|
|
45
|
+
* against the fresh connection rather than trust stale ensure-state from a
|
|
46
|
+
* previous pool lifetime.
|
|
47
|
+
*
|
|
48
|
+
* Exported for tests; the listener wiring is internal.
|
|
49
|
+
*/
|
|
50
|
+
export function invalidateEnsuredFTSForRepo(repoId) {
|
|
51
|
+
const prefix = `${repoId}:`;
|
|
52
|
+
for (const key of ensuredPoolFTS) {
|
|
53
|
+
if (key.startsWith(prefix))
|
|
54
|
+
ensuredPoolFTS.delete(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Tracks whether we've already wired the pool-close listener for this
|
|
59
|
+
* process. The pool adapter is dynamically imported, so registration
|
|
60
|
+
* happens lazily on the first MCP-pool-backed FTS query.
|
|
6
61
|
*/
|
|
7
|
-
|
|
62
|
+
let poolCloseListenerRegistered = false;
|
|
63
|
+
function registerPoolCloseListenerOnce(addPoolCloseListener) {
|
|
64
|
+
if (poolCloseListenerRegistered)
|
|
65
|
+
return;
|
|
66
|
+
poolCloseListenerRegistered = true;
|
|
67
|
+
addPoolCloseListener((repoId) => invalidateEnsuredFTSForRepo(repoId));
|
|
68
|
+
}
|
|
69
|
+
async function ensureFTSIndexViaExecutor(executor, repoId, table, indexName, properties) {
|
|
70
|
+
const key = `${repoId}:${table}:${indexName}`;
|
|
71
|
+
if (ensuredPoolFTS.has(key))
|
|
72
|
+
return;
|
|
73
|
+
const propList = properties.map((p) => `'${p}'`).join(', ');
|
|
74
|
+
try {
|
|
75
|
+
await executor(`CALL CREATE_FTS_INDEX('${table}', '${indexName}', [${propList}], stemmer := 'porter')`);
|
|
76
|
+
// Index was created successfully — safe to cache.
|
|
77
|
+
ensuredPoolFTS.add(key);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
// 'already exists' is the happy path (index persists on disk between
|
|
81
|
+
// process invocations) — cache it. Anything else is treated as a
|
|
82
|
+
// transient failure: surface a one-time warning and leave the key
|
|
83
|
+
// unset so the NEXT query retries rather than silently using a
|
|
84
|
+
// cached failure (which previously disabled BM25 for the whole
|
|
85
|
+
// process for that repo).
|
|
86
|
+
const msg = String(e?.message ?? '');
|
|
87
|
+
if (msg.includes('already exists')) {
|
|
88
|
+
ensuredPoolFTS.add(key);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.warn(`[gitnexus] FTS index ensure failed for repo "${repoId}" table "${table}" ` +
|
|
92
|
+
`(index "${indexName}"): ${msg || e}. Will retry on next query.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
8
96
|
/**
|
|
9
97
|
* Execute a single FTS query via a custom executor (for MCP connection pool).
|
|
10
98
|
* Returns the same shape as core queryFTS (from LadybugDB adapter).
|
|
@@ -26,6 +114,7 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
|
|
|
26
114
|
return {
|
|
27
115
|
filePath: node.filePath || '',
|
|
28
116
|
score: typeof score === 'number' ? score : parseFloat(score) || 0,
|
|
117
|
+
nodeId: node.nodeId || node.id || '',
|
|
29
118
|
};
|
|
30
119
|
});
|
|
31
120
|
}
|
|
@@ -50,8 +139,19 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
50
139
|
// Use MCP connection pool via dynamic import
|
|
51
140
|
// IMPORTANT: FTS queries run sequentially to avoid connection contention.
|
|
52
141
|
// The MCP pool supports multiple connections, but FTS is best run serially.
|
|
53
|
-
const
|
|
142
|
+
const poolMod = await import('../lbug/pool-adapter.js');
|
|
143
|
+
const { executeQuery, addPoolCloseListener } = poolMod;
|
|
144
|
+
// Register the pool-close listener lazily on first use so a teardown of
|
|
145
|
+
// the pool entry (LRU eviction, idle timeout, explicit close) drops the
|
|
146
|
+
// matching `ensuredPoolFTS` entries. Without this, stale ensure-state
|
|
147
|
+
// can outlive the pool that produced it.
|
|
148
|
+
registerPoolCloseListenerOnce(addPoolCloseListener);
|
|
54
149
|
const executor = (cypher) => executeQuery(repoId, cypher);
|
|
150
|
+
// Lazy-create FTS indexes on first query for this repo (analyze no longer
|
|
151
|
+
// creates them up-front, so we ensure them here). Cached per-process.
|
|
152
|
+
for (const { table, indexName, properties } of FTS_INDEXES) {
|
|
153
|
+
await ensureFTSIndexViaExecutor(executor, repoId, table, indexName, properties);
|
|
154
|
+
}
|
|
55
155
|
fileResults = await queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit);
|
|
56
156
|
functionResults = await queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit);
|
|
57
157
|
classResults = await queryFTSViaExecutor(executor, 'Class', 'class_fts', query, limit);
|
|
@@ -59,24 +159,24 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
59
159
|
interfaceResults = await queryFTSViaExecutor(executor, 'Interface', 'interface_fts', query, limit);
|
|
60
160
|
}
|
|
61
161
|
else {
|
|
62
|
-
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety
|
|
162
|
+
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety.
|
|
163
|
+
// Lazy-create FTS indexes on first query (analyze no longer does it).
|
|
164
|
+
for (const { table, indexName, properties } of FTS_INDEXES) {
|
|
165
|
+
await ensureFTSIndex(table, indexName, [...properties]).catch(() => { });
|
|
166
|
+
}
|
|
63
167
|
fileResults = await queryFTS('File', 'file_fts', query, limit, false).catch(() => []);
|
|
64
168
|
functionResults = await queryFTS('Function', 'function_fts', query, limit, false).catch(() => []);
|
|
65
169
|
classResults = await queryFTS('Class', 'class_fts', query, limit, false).catch(() => []);
|
|
66
170
|
methodResults = await queryFTS('Method', 'method_fts', query, limit, false).catch(() => []);
|
|
67
171
|
interfaceResults = await queryFTS('Interface', 'interface_fts', query, limit, false).catch(() => []);
|
|
68
172
|
}
|
|
69
|
-
//
|
|
70
|
-
const
|
|
173
|
+
// Collect all node scores per filePath to track which nodes actually matched
|
|
174
|
+
const fileNodeScores = new Map();
|
|
71
175
|
const addResults = (results) => {
|
|
72
176
|
for (const r of results) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
merged.set(r.filePath, { filePath: r.filePath, score: r.score });
|
|
79
|
-
}
|
|
177
|
+
if (!fileNodeScores.has(r.filePath))
|
|
178
|
+
fileNodeScores.set(r.filePath, []);
|
|
179
|
+
fileNodeScores.get(r.filePath).push({ score: r.score, nodeId: r.nodeId });
|
|
80
180
|
}
|
|
81
181
|
};
|
|
82
182
|
addResults(fileResults);
|
|
@@ -84,6 +184,18 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
84
184
|
addResults(classResults);
|
|
85
185
|
addResults(methodResults);
|
|
86
186
|
addResults(interfaceResults);
|
|
187
|
+
// Sum the top-3 highest-scoring nodes per file and collect their nodeIds.
|
|
188
|
+
// Summing all nodes naively inflates scores for files with many mediocre
|
|
189
|
+
// matches (e.g. test files) over files with a single highly-relevant symbol.
|
|
190
|
+
const merged = new Map();
|
|
191
|
+
for (const [filePath, entries] of fileNodeScores) {
|
|
192
|
+
const top3 = [...entries].sort((a, b) => b.score - a.score).slice(0, 3);
|
|
193
|
+
merged.set(filePath, {
|
|
194
|
+
filePath,
|
|
195
|
+
score: top3.reduce((acc, e) => acc + e.score, 0),
|
|
196
|
+
nodeIds: top3.map((e) => e.nodeId).filter((id) => id),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
87
199
|
// Sort by score descending and add rank
|
|
88
200
|
const sorted = Array.from(merged.values())
|
|
89
201
|
.sort((a, b) => b.score - a.score)
|
|
@@ -92,5 +204,6 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
92
204
|
filePath: r.filePath,
|
|
93
205
|
score: r.score,
|
|
94
206
|
rank: index + 1,
|
|
207
|
+
nodeIds: r.nodeIds,
|
|
95
208
|
}));
|
|
96
209
|
};
|
|
@@ -5,7 +5,12 @@ import Python from 'tree-sitter-python';
|
|
|
5
5
|
import Java from 'tree-sitter-java';
|
|
6
6
|
import C from 'tree-sitter-c';
|
|
7
7
|
import CPP from 'tree-sitter-cpp';
|
|
8
|
-
|
|
8
|
+
// Explicit subpath import: tree-sitter-c-sharp declares `type: "module"` with
|
|
9
|
+
// `main: "bindings/node"` (no extension) and no `exports` field, which triggers
|
|
10
|
+
// Node 22's DEP0151 deprecation warning on the bare-package import. Importing
|
|
11
|
+
// the built entrypoint directly bypasses the deprecated ESM main-field
|
|
12
|
+
// resolution. (#1013)
|
|
13
|
+
import CSharp from 'tree-sitter-c-sharp/bindings/node/index.js';
|
|
9
14
|
import Go from 'tree-sitter-go';
|
|
10
15
|
import Rust from 'tree-sitter-rust';
|
|
11
16
|
import PHP from 'tree-sitter-php';
|