gitnexus 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/_shared/graph/types.d.ts +1 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +2 -0
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/languages.d.ts +1 -0
- package/dist/_shared/languages.d.ts.map +1 -1
- package/dist/_shared/languages.js +1 -0
- package/dist/_shared/languages.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +3 -1
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/mro-strategy.d.ts +19 -0
- package/dist/_shared/mro-strategy.d.ts.map +1 -0
- package/dist/_shared/mro-strategy.js +2 -0
- package/dist/_shared/mro-strategy.js.map +1 -0
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +28 -4
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +30 -4
- package/dist/cli/group.d.ts +2 -0
- package/dist/cli/group.js +233 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/serve.js +4 -1
- package/dist/cli/setup.js +34 -3
- package/dist/config/ignore-service.js +8 -3
- package/dist/core/augmentation/engine.js +1 -1
- package/dist/core/git-staleness.d.ts +13 -0
- package/dist/core/git-staleness.js +29 -0
- package/dist/core/group/bridge-db.d.ts +82 -0
- package/dist/core/group/bridge-db.js +460 -0
- package/dist/core/group/bridge-schema.d.ts +27 -0
- package/dist/core/group/bridge-schema.js +55 -0
- package/dist/core/group/config-parser.d.ts +3 -0
- package/dist/core/group/config-parser.js +83 -0
- package/dist/core/group/contract-extractor.d.ts +7 -0
- package/dist/core/group/contract-extractor.js +1 -0
- package/dist/core/group/extractors/fs-utils.d.ts +10 -0
- package/dist/core/group/extractors/fs-utils.js +24 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +25 -0
- package/dist/core/group/extractors/grpc-extractor.js +386 -0
- package/dist/core/group/extractors/grpc-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/go.js +97 -0
- package/dist/core/group/extractors/grpc-patterns/index.d.ts +19 -0
- package/dist/core/group/extractors/grpc-patterns/index.js +46 -0
- package/dist/core/group/extractors/grpc-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/java.js +173 -0
- package/dist/core/group/extractors/grpc-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/grpc-patterns/node.js +290 -0
- package/dist/core/group/extractors/grpc-patterns/proto.d.ts +9 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +134 -0
- package/dist/core/group/extractors/grpc-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/python.js +67 -0
- package/dist/core/group/extractors/grpc-patterns/types.d.ts +50 -0
- package/dist/core/group/extractors/grpc-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/go.js +215 -0
- package/dist/core/group/extractors/http-patterns/index.d.ts +17 -0
- package/dist/core/group/extractors/http-patterns/index.js +44 -0
- package/dist/core/group/extractors/http-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/java.js +253 -0
- package/dist/core/group/extractors/http-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/http-patterns/node.js +354 -0
- package/dist/core/group/extractors/http-patterns/php.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/php.js +70 -0
- package/dist/core/group/extractors/http-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/python.js +133 -0
- package/dist/core/group/extractors/http-patterns/types.d.ts +61 -0
- package/dist/core/group/extractors/http-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +21 -0
- package/dist/core/group/extractors/http-route-extractor.js +391 -0
- package/dist/core/group/extractors/manifest-extractor.d.ts +54 -0
- package/dist/core/group/extractors/manifest-extractor.js +235 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +8 -0
- package/dist/core/group/extractors/topic-extractor.js +97 -0
- package/dist/core/group/extractors/topic-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/go.js +120 -0
- package/dist/core/group/extractors/topic-patterns/index.d.ts +14 -0
- package/dist/core/group/extractors/topic-patterns/index.js +38 -0
- package/dist/core/group/extractors/topic-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/java.js +80 -0
- package/dist/core/group/extractors/topic-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/topic-patterns/node.js +155 -0
- package/dist/core/group/extractors/topic-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/python.js +116 -0
- package/dist/core/group/extractors/topic-patterns/types.d.ts +25 -0
- package/dist/core/group/extractors/topic-patterns/types.js +10 -0
- package/dist/core/group/extractors/tree-sitter-scanner.d.ts +113 -0
- package/dist/core/group/extractors/tree-sitter-scanner.js +94 -0
- package/dist/core/group/matching.d.ts +13 -0
- package/dist/core/group/matching.js +198 -0
- package/dist/core/group/normalization.d.ts +3 -0
- package/dist/core/group/normalization.js +115 -0
- package/dist/core/group/service-boundary-detector.d.ts +8 -0
- package/dist/core/group/service-boundary-detector.js +155 -0
- package/dist/core/group/service.d.ts +46 -0
- package/dist/core/group/service.js +160 -0
- package/dist/core/group/storage.d.ts +9 -0
- package/dist/core/group/storage.js +91 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +148 -0
- package/dist/core/group/types.d.ts +130 -0
- package/dist/core/group/types.js +1 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +212 -0
- package/dist/core/ingestion/binding-accumulator.js +336 -0
- package/dist/core/ingestion/call-processor.d.ts +155 -24
- package/dist/core/ingestion/call-processor.js +1129 -247
- package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/generic.js +135 -0
- package/dist/core/ingestion/class-types.d.ts +34 -0
- package/dist/core/ingestion/class-types.js +1 -0
- package/dist/core/ingestion/cobol-processor.d.ts +1 -1
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
- package/dist/core/ingestion/entry-point-scoring.js +1 -0
- package/dist/core/ingestion/field-types.d.ts +2 -2
- package/dist/core/ingestion/filesystem-walker.js +8 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +1 -0
- package/dist/core/ingestion/heritage-processor.d.ts +8 -15
- package/dist/core/ingestion/heritage-processor.js +15 -28
- package/dist/core/ingestion/import-processor.d.ts +1 -11
- package/dist/core/ingestion/import-processor.js +1 -13
- package/dist/core/ingestion/import-resolvers/utils.js +1 -0
- package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/vue.js +9 -0
- package/dist/core/ingestion/language-config.js +1 -1
- package/dist/core/ingestion/language-provider.d.ts +14 -3
- package/dist/core/ingestion/languages/c-cpp.js +168 -1
- package/dist/core/ingestion/languages/csharp.js +20 -0
- package/dist/core/ingestion/languages/dart.js +26 -4
- package/dist/core/ingestion/languages/go.js +22 -0
- package/dist/core/ingestion/languages/index.d.ts +1 -0
- package/dist/core/ingestion/languages/index.js +2 -0
- package/dist/core/ingestion/languages/java.js +17 -0
- package/dist/core/ingestion/languages/kotlin.js +24 -1
- package/dist/core/ingestion/languages/php.js +23 -11
- package/dist/core/ingestion/languages/python.js +9 -0
- package/dist/core/ingestion/languages/ruby.js +43 -0
- package/dist/core/ingestion/languages/rust.js +38 -0
- package/dist/core/ingestion/languages/swift.js +31 -0
- package/dist/core/ingestion/languages/typescript.d.ts +1 -0
- package/dist/core/ingestion/languages/typescript.js +52 -3
- package/dist/core/ingestion/languages/vue.d.ts +13 -0
- package/dist/core/ingestion/languages/vue.js +81 -0
- package/dist/core/ingestion/markdown-processor.d.ts +1 -1
- package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
- package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
- package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +14 -4
- package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
- package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +286 -0
- package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
- package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +85 -8
- package/dist/core/ingestion/method-extractors/generic.d.ts +6 -0
- package/dist/core/ingestion/method-extractors/generic.js +84 -17
- package/dist/core/ingestion/method-types.d.ts +29 -0
- package/dist/core/ingestion/model/field-registry.d.ts +18 -0
- package/dist/core/ingestion/model/field-registry.js +22 -0
- package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
- package/dist/core/ingestion/model/heritage-map.js +159 -0
- package/dist/core/ingestion/model/index.d.ts +20 -0
- package/dist/core/ingestion/model/index.js +41 -0
- package/dist/core/ingestion/model/method-registry.d.ts +62 -0
- package/dist/core/ingestion/model/method-registry.js +130 -0
- package/dist/core/ingestion/model/registration-table.d.ts +139 -0
- package/dist/core/ingestion/model/registration-table.js +224 -0
- package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
- package/dist/core/ingestion/model/resolution-context.js +337 -0
- package/dist/core/ingestion/model/resolve.d.ts +56 -0
- package/dist/core/ingestion/model/resolve.js +297 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
- package/dist/core/ingestion/model/semantic-model.js +120 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
- package/dist/core/ingestion/model/symbol-table.js +206 -0
- package/dist/core/ingestion/model/type-registry.d.ts +39 -0
- package/dist/core/ingestion/model/type-registry.js +62 -0
- package/dist/core/ingestion/mro-processor.d.ts +5 -4
- package/dist/core/ingestion/mro-processor.js +311 -107
- package/dist/core/ingestion/parsing-processor.d.ts +5 -4
- package/dist/core/ingestion/parsing-processor.js +224 -87
- package/dist/core/ingestion/pipeline-phases/cobol.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/cobol.js +45 -0
- package/dist/core/ingestion/pipeline-phases/communities.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/communities.js +62 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +156 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.d.ts +37 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.js +63 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/index.js +22 -0
- package/dist/core/ingestion/pipeline-phases/markdown.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/markdown.js +33 -0
- package/dist/core/ingestion/pipeline-phases/mro.d.ts +18 -0
- package/dist/core/ingestion/pipeline-phases/mro.js +36 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.js +92 -0
- package/dist/core/ingestion/pipeline-phases/orm.d.ts +15 -0
- package/dist/core/ingestion/pipeline-phases/orm.js +74 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +47 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +437 -0
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +49 -0
- package/dist/core/ingestion/pipeline-phases/parse.js +33 -0
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/processes.js +143 -0
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +243 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/runner.js +203 -0
- package/dist/core/ingestion/pipeline-phases/scan.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/scan.js +46 -0
- package/dist/core/ingestion/pipeline-phases/structure.d.ts +27 -0
- package/dist/core/ingestion/pipeline-phases/structure.js +35 -0
- package/dist/core/ingestion/pipeline-phases/tools.d.ts +20 -0
- package/dist/core/ingestion/pipeline-phases/tools.js +79 -0
- package/dist/core/ingestion/pipeline-phases/types.d.ts +79 -0
- package/dist/core/ingestion/pipeline-phases/types.js +37 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.d.ts +35 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.js +174 -0
- package/dist/core/ingestion/pipeline.d.ts +18 -10
- package/dist/core/ingestion/pipeline.js +66 -1410
- package/dist/core/ingestion/process-processor.js +1 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +5 -5
- package/dist/core/ingestion/tree-sitter-queries.js +90 -0
- package/dist/core/ingestion/type-env.d.ts +15 -2
- package/dist/core/ingestion/type-env.js +163 -102
- package/dist/core/ingestion/type-extractors/csharp.js +17 -0
- package/dist/core/ingestion/type-extractors/jvm.js +11 -0
- package/dist/core/ingestion/type-extractors/php.js +0 -55
- package/dist/core/ingestion/type-extractors/ruby.js +0 -32
- package/dist/core/ingestion/type-extractors/swift.js +13 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
- package/dist/core/ingestion/type-extractors/typescript.js +66 -69
- package/dist/core/ingestion/utils/ast-helpers.d.ts +32 -44
- package/dist/core/ingestion/utils/ast-helpers.js +157 -573
- package/dist/core/ingestion/utils/env.d.ts +10 -0
- package/dist/core/ingestion/utils/env.js +10 -0
- package/dist/core/ingestion/utils/graph-sort.d.ts +58 -0
- package/dist/core/ingestion/utils/graph-sort.js +100 -0
- package/dist/core/ingestion/utils/method-props.d.ts +32 -0
- package/dist/core/ingestion/utils/method-props.js +147 -0
- package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
- package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
- package/dist/core/ingestion/workers/parse-worker.js +469 -200
- package/dist/core/lbug/lbug-adapter.d.ts +6 -0
- package/dist/core/lbug/lbug-adapter.js +134 -27
- package/dist/core/lbug/pool-adapter.d.ts +76 -0
- package/dist/core/lbug/pool-adapter.js +522 -0
- package/dist/core/run-analyze.d.ts +2 -0
- package/dist/core/run-analyze.js +1 -1
- package/dist/core/search/bm25-index.js +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -0
- package/dist/core/wiki/graph-queries.js +1 -1
- package/dist/mcp/core/embedder.js +6 -5
- package/dist/mcp/core/lbug-adapter.d.ts +3 -63
- package/dist/mcp/core/lbug-adapter.js +3 -484
- package/dist/mcp/local/local-backend.d.ts +31 -2
- package/dist/mcp/local/local-backend.js +255 -46
- package/dist/mcp/resources.js +5 -4
- package/dist/mcp/staleness.d.ts +3 -13
- package/dist/mcp/staleness.js +2 -31
- package/dist/mcp/tools.js +80 -4
- package/dist/server/analyze-job.d.ts +2 -0
- package/dist/server/analyze-job.js +4 -0
- package/dist/server/api.d.ts +20 -1
- package/dist/server/api.js +306 -71
- package/dist/server/git-clone.d.ts +2 -1
- package/dist/server/git-clone.js +98 -5
- package/dist/storage/git.d.ts +13 -0
- package/dist/storage/git.js +25 -0
- package/dist/storage/repo-manager.js +1 -1
- package/package.json +9 -3
- package/scripts/patch-tree-sitter-swift.cjs +78 -0
- package/vendor/tree-sitter-proto/binding.gyp +30 -0
- package/vendor/tree-sitter-proto/bindings/node/binding.cc +20 -0
- package/vendor/tree-sitter-proto/bindings/node/index.d.ts +28 -0
- package/vendor/tree-sitter-proto/bindings/node/index.js +7 -0
- package/vendor/tree-sitter-proto/package.json +18 -0
- package/vendor/tree-sitter-proto/src/node-types.json +1145 -0
- package/vendor/tree-sitter-proto/src/parser.c +10149 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/alloc.h +54 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/array.h +291 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/parser.h +266 -0
- package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
- package/dist/core/ingestion/named-binding-processor.js +0 -42
- package/dist/core/ingestion/resolution-context.d.ts +0 -58
- package/dist/core/ingestion/resolution-context.js +0 -135
- package/dist/core/ingestion/symbol-table.d.ts +0 -79
- package/dist/core/ingestion/symbol-table.js +0 -115
package/dist/cli/setup.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import os from 'os';
|
|
11
|
-
import { execFile } from 'child_process';
|
|
11
|
+
import { execFile, execFileSync } from 'child_process';
|
|
12
12
|
import { promisify } from 'util';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { glob } from 'glob';
|
|
@@ -16,11 +16,42 @@ import { getGlobalDir } from '../storage/repo-manager.js';
|
|
|
16
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
17
|
const __dirname = path.dirname(__filename);
|
|
18
18
|
const execFileAsync = promisify(execFile);
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the absolute path to the `gitnexus` binary if it's installed
|
|
21
|
+
* globally (or via npm -g / yarn global). Returns null when not found.
|
|
22
|
+
*/
|
|
23
|
+
function resolveGitnexusBin() {
|
|
24
|
+
try {
|
|
25
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
26
|
+
const resolved = execFileSync(cmd, ['gitnexus'], {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
timeout: 5000,
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
})
|
|
31
|
+
.split('\n')[0]
|
|
32
|
+
.trim();
|
|
33
|
+
return resolved || null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
19
39
|
/**
|
|
20
40
|
* The MCP server entry for all editors.
|
|
21
|
-
*
|
|
41
|
+
*
|
|
42
|
+
* Prefers the globally-installed `gitnexus` binary (starts in ~1 s) over
|
|
43
|
+
* `npx -y gitnexus@latest` (cold-cache install of native deps can take
|
|
44
|
+
* >60 s, exceeding Claude Code's 30 s MCP connection timeout).
|
|
45
|
+
*
|
|
46
|
+
* Falls back to npx when the binary isn't on PATH — e.g. first-time
|
|
47
|
+
* users who ran `npx gitnexus analyze` but haven't done `npm i -g`.
|
|
22
48
|
*/
|
|
23
49
|
function getMcpEntry() {
|
|
50
|
+
const bin = resolveGitnexusBin();
|
|
51
|
+
if (bin) {
|
|
52
|
+
return { command: bin, args: ['mcp'] };
|
|
53
|
+
}
|
|
54
|
+
// Fallback: npx (works without a global install, but slow cold-start)
|
|
24
55
|
if (process.platform === 'win32') {
|
|
25
56
|
return {
|
|
26
57
|
command: 'cmd',
|
|
@@ -193,7 +224,7 @@ async function setupOpenCode(result) {
|
|
|
193
224
|
result.skipped.push('OpenCode (not installed)');
|
|
194
225
|
return;
|
|
195
226
|
}
|
|
196
|
-
const configPath = path.join(opencodeDir, '
|
|
227
|
+
const configPath = path.join(opencodeDir, 'opencode.json');
|
|
197
228
|
try {
|
|
198
229
|
const existing = await readJsonFile(configPath);
|
|
199
230
|
const config = existing || {};
|
|
@@ -351,11 +351,16 @@ export const createIgnoreFilter = async (repoPath, options) => {
|
|
|
351
351
|
if (DEFAULT_IGNORE_LIST.has(p.name))
|
|
352
352
|
return true;
|
|
353
353
|
// Check against .gitignore / .gitnexusignore patterns.
|
|
354
|
-
//
|
|
355
|
-
//
|
|
354
|
+
// Since childrenIgnored is only called for directories, always test with
|
|
355
|
+
// a trailing slash. This ensures directory-only negation patterns (e.g.
|
|
356
|
+
// `!iOS/`) are applied correctly — without the slash, `ig.ignores('iOS')`
|
|
357
|
+
// treats the path as a file and misses the negation.
|
|
358
|
+
// Bare-name patterns (e.g. `local`) still match `local/` per gitignore spec:
|
|
359
|
+
// the `ignore` package normalizes `dir` and `dir/` to match directories.
|
|
360
|
+
// See: https://github.com/kaelzhang/node-ignore#2-filenames-and-dirnames
|
|
356
361
|
if (ig) {
|
|
357
362
|
const rel = p.relative();
|
|
358
|
-
if (rel &&
|
|
363
|
+
if (rel && ig.ignores(rel + '/'))
|
|
359
364
|
return true;
|
|
360
365
|
}
|
|
361
366
|
return false;
|
|
@@ -82,7 +82,7 @@ export async function augment(pattern, cwd) {
|
|
|
82
82
|
if (!repo)
|
|
83
83
|
return '';
|
|
84
84
|
// Lazy-load lbug adapter (skip unnecessary init)
|
|
85
|
-
const { initLbug, executeQuery, isLbugReady } = await import('
|
|
85
|
+
const { initLbug, executeQuery, isLbugReady } = await import('../lbug/pool-adapter.js');
|
|
86
86
|
const { searchFTSFromLbug } = await import('../search/bm25-index.js');
|
|
87
87
|
const repoId = repo.name.toLowerCase();
|
|
88
88
|
// Init LadybugDB if not already
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git working tree vs index commit staleness (used by MCP resources, group status, etc.).
|
|
3
|
+
* Lives in core/ so application code does not depend on the MCP package layer.
|
|
4
|
+
*/
|
|
5
|
+
export interface StalenessInfo {
|
|
6
|
+
isStale: boolean;
|
|
7
|
+
commitsBehind: number;
|
|
8
|
+
hint?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check how many commits the index is behind HEAD (synchronous; uses git CLI).
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkStaleness(repoPath: string, lastCommit: string): StalenessInfo;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git working tree vs index commit staleness (used by MCP resources, group status, etc.).
|
|
3
|
+
* Lives in core/ so application code does not depend on the MCP package layer.
|
|
4
|
+
*/
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Check how many commits the index is behind HEAD (synchronous; uses git CLI).
|
|
8
|
+
*/
|
|
9
|
+
export function checkStaleness(repoPath, lastCommit) {
|
|
10
|
+
try {
|
|
11
|
+
const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}..HEAD`], {
|
|
12
|
+
cwd: repoPath,
|
|
13
|
+
encoding: 'utf-8',
|
|
14
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
15
|
+
}).trim();
|
|
16
|
+
const commitsBehind = parseInt(result, 10) || 0;
|
|
17
|
+
if (commitsBehind > 0) {
|
|
18
|
+
return {
|
|
19
|
+
isStale: true,
|
|
20
|
+
commitsBehind,
|
|
21
|
+
hint: `⚠️ Index is ${commitsBehind} commit${commitsBehind > 1 ? 's' : ''} behind HEAD. Run analyze tool to update.`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { isStale: false, commitsBehind: 0 };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { isStale: false, commitsBehind: 0 };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { LbugValue } from '@ladybugdb/core';
|
|
2
|
+
import type { BridgeHandle, BridgeMeta, StoredContract, CrossLink, RepoSnapshot } from './types.js';
|
|
3
|
+
export declare function contractNodeId(repo: string, contractId: string, role: string, filePath: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* In-memory index of contract node IDs keyed three ways, mirroring the
|
|
6
|
+
* three-tier fallback lookup in {@link findContractNode}. Built once per
|
|
7
|
+
* `writeBridge` call after all contracts are successfully inserted, then
|
|
8
|
+
* consulted for every cross-link — which eliminates the former N+1 query
|
|
9
|
+
* pattern (up to `6 × cross-links` DB round-trips) and turns cross-link
|
|
10
|
+
* resolution into constant-time per link.
|
|
11
|
+
*
|
|
12
|
+
* Keys are deliberately flat strings (not tuples) so `Map<string, ...>`
|
|
13
|
+
* works; the separator `\0` can't occur in any legal repo path / file
|
|
14
|
+
* path / symbol identifier, which makes the encoding injection-safe.
|
|
15
|
+
*/
|
|
16
|
+
export interface ContractLookupIndex {
|
|
17
|
+
/** tier 1: `repo + role + symbolUid` → contract node id */
|
|
18
|
+
byUid: Map<string, string>;
|
|
19
|
+
/** tier 2: `repo + role + filePath + symbolName` → contract node id */
|
|
20
|
+
byRef: Map<string, string>;
|
|
21
|
+
/** tier 3: `repo + role + filePath` → list of contract node ids in that file */
|
|
22
|
+
byFile: Map<string, string[]>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createContractLookupIndex(): ContractLookupIndex;
|
|
25
|
+
/**
|
|
26
|
+
* Add a successfully-inserted contract to the lookup index. Must be called
|
|
27
|
+
* AFTER the DB insert succeeds (not before) so failed inserts don't poison
|
|
28
|
+
* the index and cause cross-links to point at non-existent rows.
|
|
29
|
+
*/
|
|
30
|
+
export declare function indexContract(index: ContractLookupIndex, contract: StoredContract, nodeId: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a cross-link endpoint (consumer or provider reference) to an
|
|
33
|
+
* already-inserted contract node id. Returns `null` if no match — the
|
|
34
|
+
* caller is expected to count that as a dropped link in `WriteBridgeReport`.
|
|
35
|
+
*
|
|
36
|
+
* The resolution order matches the pre-cache DB-query behavior:
|
|
37
|
+
* 1. exact `symbolUid` match in the same `(repo, role)` scope
|
|
38
|
+
* 2. exact `(filePath, symbolName)` match
|
|
39
|
+
* 3. if exactly one contract lives in the file → that one (fallback for
|
|
40
|
+
* legacy graph-assisted extractors that couldn't resolve a symbol name)
|
|
41
|
+
*
|
|
42
|
+
* This is a pure function — no I/O, no DB — so it's trivial to unit-test
|
|
43
|
+
* in isolation (which was the reviewer's main clean-code concern on the
|
|
44
|
+
* original 35-line inner closure in `writeBridge`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function findContractNode(index: ContractLookupIndex, repo: string, role: 'consumer' | 'provider', symbolUid: string, filePath: string, symbolName: string): string | null;
|
|
47
|
+
export declare function openBridgeDb(dbPath: string): Promise<BridgeHandle>;
|
|
48
|
+
export declare function ensureBridgeSchema(handle: BridgeHandle): Promise<void>;
|
|
49
|
+
export declare function queryBridge<T>(handle: BridgeHandle, cypher: string, params?: Record<string, LbugValue>): Promise<T[]>;
|
|
50
|
+
export declare function closeBridgeDb(handle: BridgeHandle): Promise<void>;
|
|
51
|
+
export declare function retryRename(src: string, dst: string, attempts?: number): Promise<void>;
|
|
52
|
+
export declare function writeBridgeMeta(groupDir: string, meta: BridgeMeta): Promise<void>;
|
|
53
|
+
export declare function readBridgeMeta(groupDir: string): Promise<BridgeMeta>;
|
|
54
|
+
export interface WriteBridgeInput {
|
|
55
|
+
contracts: StoredContract[];
|
|
56
|
+
crossLinks: CrossLink[];
|
|
57
|
+
repoSnapshots: Record<string, RepoSnapshot>;
|
|
58
|
+
missingRepos: string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Non-fatal issues encountered during writeBridge. Callers can log these to
|
|
62
|
+
* surface partial-success state without aborting the whole sync.
|
|
63
|
+
* `sampleErrors` is capped at MAX_SAMPLE_ERRORS per category to bound memory.
|
|
64
|
+
*/
|
|
65
|
+
export interface WriteBridgeReport {
|
|
66
|
+
contractsInserted: number;
|
|
67
|
+
contractsFailed: number;
|
|
68
|
+
snapshotsInserted: number;
|
|
69
|
+
snapshotsFailed: number;
|
|
70
|
+
linksInserted: number;
|
|
71
|
+
linksFailed: number;
|
|
72
|
+
/** Cross-links skipped because their from/to contract nodes weren't found. */
|
|
73
|
+
linksDroppedMissingNode: number;
|
|
74
|
+
sampleErrors: Array<{
|
|
75
|
+
kind: 'contract' | 'snapshot' | 'link';
|
|
76
|
+
id: string;
|
|
77
|
+
message: string;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
export declare function writeBridge(groupDir: string, input: WriteBridgeInput): Promise<WriteBridgeReport>;
|
|
81
|
+
export declare function openBridgeDbReadOnly(groupDir: string): Promise<BridgeHandle | null>;
|
|
82
|
+
export declare function bridgeExists(groupDir: string): Promise<boolean>;
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import lbug from '@ladybugdb/core';
|
|
5
|
+
import { BRIDGE_SCHEMA_QUERIES, BRIDGE_SCHEMA_VERSION } from './bridge-schema.js';
|
|
6
|
+
import { dedupeContracts, dedupeCrossLinks } from './normalization.js';
|
|
7
|
+
export function contractNodeId(repo, contractId, role, filePath) {
|
|
8
|
+
return createHash('sha256').update(`${repo}\0${contractId}\0${role}\0${filePath}`).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
export function createContractLookupIndex() {
|
|
11
|
+
return {
|
|
12
|
+
byUid: new Map(),
|
|
13
|
+
byRef: new Map(),
|
|
14
|
+
byFile: new Map(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function uidKey(repo, role, symbolUid) {
|
|
18
|
+
return `${repo}\0${role}\0${symbolUid}`;
|
|
19
|
+
}
|
|
20
|
+
function refKey(repo, role, filePath, symbolName) {
|
|
21
|
+
return `${repo}\0${role}\0${filePath}\0${symbolName}`;
|
|
22
|
+
}
|
|
23
|
+
function fileKey(repo, role, filePath) {
|
|
24
|
+
return `${repo}\0${role}\0${filePath}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Add a successfully-inserted contract to the lookup index. Must be called
|
|
28
|
+
* AFTER the DB insert succeeds (not before) so failed inserts don't poison
|
|
29
|
+
* the index and cause cross-links to point at non-existent rows.
|
|
30
|
+
*/
|
|
31
|
+
export function indexContract(index, contract, nodeId) {
|
|
32
|
+
if (contract.symbolUid) {
|
|
33
|
+
index.byUid.set(uidKey(contract.repo, contract.role, contract.symbolUid), nodeId);
|
|
34
|
+
}
|
|
35
|
+
index.byRef.set(refKey(contract.repo, contract.role, contract.symbolRef.filePath, contract.symbolRef.name), nodeId);
|
|
36
|
+
const fk = fileKey(contract.repo, contract.role, contract.symbolRef.filePath);
|
|
37
|
+
const existing = index.byFile.get(fk);
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.push(nodeId);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
index.byFile.set(fk, [nodeId]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a cross-link endpoint (consumer or provider reference) to an
|
|
47
|
+
* already-inserted contract node id. Returns `null` if no match — the
|
|
48
|
+
* caller is expected to count that as a dropped link in `WriteBridgeReport`.
|
|
49
|
+
*
|
|
50
|
+
* The resolution order matches the pre-cache DB-query behavior:
|
|
51
|
+
* 1. exact `symbolUid` match in the same `(repo, role)` scope
|
|
52
|
+
* 2. exact `(filePath, symbolName)` match
|
|
53
|
+
* 3. if exactly one contract lives in the file → that one (fallback for
|
|
54
|
+
* legacy graph-assisted extractors that couldn't resolve a symbol name)
|
|
55
|
+
*
|
|
56
|
+
* This is a pure function — no I/O, no DB — so it's trivial to unit-test
|
|
57
|
+
* in isolation (which was the reviewer's main clean-code concern on the
|
|
58
|
+
* original 35-line inner closure in `writeBridge`).
|
|
59
|
+
*/
|
|
60
|
+
export function findContractNode(index, repo, role, symbolUid, filePath, symbolName) {
|
|
61
|
+
if (symbolUid) {
|
|
62
|
+
const uidHit = index.byUid.get(uidKey(repo, role, symbolUid));
|
|
63
|
+
if (uidHit !== undefined)
|
|
64
|
+
return uidHit;
|
|
65
|
+
}
|
|
66
|
+
const refHit = index.byRef.get(refKey(repo, role, filePath, symbolName));
|
|
67
|
+
if (refHit !== undefined)
|
|
68
|
+
return refHit;
|
|
69
|
+
const fileCandidates = index.byFile.get(fileKey(repo, role, filePath));
|
|
70
|
+
if (fileCandidates && fileCandidates.length === 1)
|
|
71
|
+
return fileCandidates[0];
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
export async function openBridgeDb(dbPath) {
|
|
75
|
+
const parentDir = path.dirname(dbPath);
|
|
76
|
+
await fsp.mkdir(parentDir, { recursive: true });
|
|
77
|
+
const db = new lbug.Database(dbPath, 0, false, false); // writable
|
|
78
|
+
const conn = new lbug.Connection(db);
|
|
79
|
+
return { _db: db, _conn: conn, groupDir: parentDir };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* LadybugDB returns an error whose message contains this substring when a
|
|
83
|
+
* CREATE NODE TABLE or CREATE REL TABLE statement hits an already-existing
|
|
84
|
+
* table. LadybugDB DDL doesn't support IF NOT EXISTS, and its JS driver
|
|
85
|
+
* doesn't expose typed error codes, so we match on the message substring —
|
|
86
|
+
* the same pattern used by `core/lbug/lbug-adapter.ts`. If a future
|
|
87
|
+
* LadybugDB release changes the wording, update this constant.
|
|
88
|
+
*/
|
|
89
|
+
const LBUG_ALREADY_EXISTS_MSG = 'already exists';
|
|
90
|
+
export async function ensureBridgeSchema(handle) {
|
|
91
|
+
const conn = handle._conn;
|
|
92
|
+
for (const q of BRIDGE_SCHEMA_QUERIES) {
|
|
93
|
+
try {
|
|
94
|
+
await conn.query(q);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
if (!msg.includes(LBUG_ALREADY_EXISTS_MSG))
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function queryBridge(handle, cypher, params) {
|
|
104
|
+
const conn = handle._conn;
|
|
105
|
+
if (params && Object.keys(params).length > 0) {
|
|
106
|
+
const stmt = await conn.prepare(cypher);
|
|
107
|
+
if (!stmt.isSuccess()) {
|
|
108
|
+
const errMsg = await stmt.getErrorMessage();
|
|
109
|
+
throw new Error(`Bridge query prepare failed: ${errMsg}`);
|
|
110
|
+
}
|
|
111
|
+
const queryResult = await conn.execute(stmt, params);
|
|
112
|
+
const result = unwrapQueryResult(queryResult);
|
|
113
|
+
return (await result.getAll());
|
|
114
|
+
}
|
|
115
|
+
const queryResult = await conn.query(cypher);
|
|
116
|
+
const result = unwrapQueryResult(queryResult);
|
|
117
|
+
return (await result.getAll());
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* LadybugDB's `conn.query` / `conn.execute` can return either a single
|
|
121
|
+
* `QueryResult` (for a single statement) or an array of them (when a
|
|
122
|
+
* multi-statement script is dispatched). We always pass a single statement,
|
|
123
|
+
* so the array form is a wrapper we unwrap here — but an empty top-level
|
|
124
|
+
* array would cause `.getAll()` on `undefined` and crash with a confusing
|
|
125
|
+
* stack. Throwing an explicit error makes a driver-contract regression
|
|
126
|
+
* visible immediately instead of masking it.
|
|
127
|
+
*/
|
|
128
|
+
function unwrapQueryResult(queryResult) {
|
|
129
|
+
if (Array.isArray(queryResult)) {
|
|
130
|
+
if (queryResult.length === 0) {
|
|
131
|
+
throw new Error('Bridge query returned an empty QueryResult array');
|
|
132
|
+
}
|
|
133
|
+
return queryResult[0];
|
|
134
|
+
}
|
|
135
|
+
return queryResult;
|
|
136
|
+
}
|
|
137
|
+
export async function closeBridgeDb(handle) {
|
|
138
|
+
try {
|
|
139
|
+
await handle._conn.close();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* ignore */
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await handle._db.close();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* ignore */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/* ------------------------------------------------------------------ */
|
|
152
|
+
/* retryRename — handles transient EBUSY/EPERM/EACCES on Windows */
|
|
153
|
+
/* ------------------------------------------------------------------ */
|
|
154
|
+
const RETRY_CODES = new Set(['EBUSY', 'EPERM', 'EACCES']);
|
|
155
|
+
export async function retryRename(src, dst, attempts = 3) {
|
|
156
|
+
for (let i = 1; i <= attempts; i++) {
|
|
157
|
+
try {
|
|
158
|
+
await fsp.rename(src, dst);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const code = err.code;
|
|
163
|
+
if (!code || !RETRY_CODES.has(code) || i === attempts)
|
|
164
|
+
throw err;
|
|
165
|
+
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i - 1)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/* ------------------------------------------------------------------ */
|
|
170
|
+
/* writeBridgeMeta / readBridgeMeta */
|
|
171
|
+
/* ------------------------------------------------------------------ */
|
|
172
|
+
export async function writeBridgeMeta(groupDir, meta) {
|
|
173
|
+
const target = path.join(groupDir, 'meta.json');
|
|
174
|
+
const tmp = `${target}.tmp.${Date.now()}`;
|
|
175
|
+
await fsp.writeFile(tmp, JSON.stringify(meta, null, 2), 'utf-8');
|
|
176
|
+
// Use retryRename for consistency with writeBridge's atomic swap — on
|
|
177
|
+
// Windows a concurrent reader can cause EBUSY/EPERM even on a tiny
|
|
178
|
+
// meta.json, and we don't want meta write to be less robust than the
|
|
179
|
+
// bridge.lbug swap it accompanies.
|
|
180
|
+
await retryRename(tmp, target);
|
|
181
|
+
}
|
|
182
|
+
export async function readBridgeMeta(groupDir) {
|
|
183
|
+
try {
|
|
184
|
+
const content = await fsp.readFile(path.join(groupDir, 'meta.json'), 'utf-8');
|
|
185
|
+
return JSON.parse(content);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return { version: 0, generatedAt: '', missingRepos: [] };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const MAX_SAMPLE_ERRORS = 10;
|
|
192
|
+
function errMessage(err) {
|
|
193
|
+
if (err instanceof Error)
|
|
194
|
+
return err.message;
|
|
195
|
+
try {
|
|
196
|
+
return String(err);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return 'unknown error';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export async function writeBridge(groupDir, input) {
|
|
203
|
+
await fsp.mkdir(groupDir, { recursive: true });
|
|
204
|
+
const contracts = dedupeContracts(input.contracts);
|
|
205
|
+
const crossLinks = dedupeCrossLinks(input.crossLinks);
|
|
206
|
+
const finalPath = path.join(groupDir, 'bridge.lbug');
|
|
207
|
+
const tmpPath = path.join(groupDir, 'bridge.lbug.tmp');
|
|
208
|
+
const bakPath = path.join(groupDir, 'bridge.lbug.bak');
|
|
209
|
+
const report = {
|
|
210
|
+
contractsInserted: 0,
|
|
211
|
+
contractsFailed: 0,
|
|
212
|
+
snapshotsInserted: 0,
|
|
213
|
+
snapshotsFailed: 0,
|
|
214
|
+
linksInserted: 0,
|
|
215
|
+
linksFailed: 0,
|
|
216
|
+
linksDroppedMissingNode: 0,
|
|
217
|
+
sampleErrors: [],
|
|
218
|
+
};
|
|
219
|
+
const recordError = (kind, id, err) => {
|
|
220
|
+
if (report.sampleErrors.length < MAX_SAMPLE_ERRORS) {
|
|
221
|
+
report.sampleErrors.push({ kind, id, message: errMessage(err) });
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
// Clean up any leftover tmp
|
|
225
|
+
try {
|
|
226
|
+
await fsp.rm(tmpPath, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
/* ignore */
|
|
230
|
+
}
|
|
231
|
+
// 1. Create temp DB, insert all data.
|
|
232
|
+
//
|
|
233
|
+
// Everything after `openBridgeDb` must run inside a try/finally so that
|
|
234
|
+
// if ANY step before the explicit `closeBridgeDb` throws — schema
|
|
235
|
+
// creation, a contract insert loop that rethrows, a snapshot write, the
|
|
236
|
+
// cross-link loop, or anything else — the handle is still released. A
|
|
237
|
+
// leaked handle holds the native LadybugDB file lock on tmpPath, which
|
|
238
|
+
// (a) leaks a FD and (b) prevents the next writeBridge call from
|
|
239
|
+
// reusing the same tmp slot.
|
|
240
|
+
const handle = await openBridgeDb(tmpPath);
|
|
241
|
+
let handleClosed = false;
|
|
242
|
+
try {
|
|
243
|
+
await ensureBridgeSchema(handle);
|
|
244
|
+
// Build the lookup index incrementally as contracts are inserted, so
|
|
245
|
+
// failed inserts are never in the index (and therefore never resolved
|
|
246
|
+
// by the cross-link loop below). This replaces a previous N+1 query
|
|
247
|
+
// pattern where each link made up to 6 DB round-trips to find its
|
|
248
|
+
// endpoints — see ContractLookupIndex.
|
|
249
|
+
const lookupIndex = createContractLookupIndex();
|
|
250
|
+
// Insert contracts — tolerate individual failures (e.g., a corrupt meta
|
|
251
|
+
// that can't be serialized). The whole sync must not fail because one
|
|
252
|
+
// contract is broken.
|
|
253
|
+
for (const c of contracts) {
|
|
254
|
+
const id = contractNodeId(c.repo, c.contractId, c.role, c.symbolRef.filePath);
|
|
255
|
+
try {
|
|
256
|
+
await queryBridge(handle, `CREATE (n:Contract {
|
|
257
|
+
id: $id,
|
|
258
|
+
contractId: $contractId,
|
|
259
|
+
type: $type,
|
|
260
|
+
role: $role,
|
|
261
|
+
repo: $repo,
|
|
262
|
+
service: $service,
|
|
263
|
+
symbolUid: $symbolUid,
|
|
264
|
+
filePath: $filePath,
|
|
265
|
+
symbolName: $symbolName,
|
|
266
|
+
confidence: $confidence,
|
|
267
|
+
meta: $meta
|
|
268
|
+
})`, {
|
|
269
|
+
id,
|
|
270
|
+
contractId: c.contractId,
|
|
271
|
+
type: c.type,
|
|
272
|
+
role: c.role,
|
|
273
|
+
repo: c.repo,
|
|
274
|
+
service: c.service ?? '',
|
|
275
|
+
symbolUid: c.symbolUid,
|
|
276
|
+
filePath: c.symbolRef.filePath,
|
|
277
|
+
symbolName: c.symbolName,
|
|
278
|
+
confidence: c.confidence,
|
|
279
|
+
meta: JSON.stringify(c.meta),
|
|
280
|
+
});
|
|
281
|
+
report.contractsInserted++;
|
|
282
|
+
// Only index on successful insert — the cross-link loop must never
|
|
283
|
+
// resolve to a row that isn't actually in the DB.
|
|
284
|
+
indexContract(lookupIndex, c, id);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
report.contractsFailed++;
|
|
288
|
+
recordError('contract', id, err);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Insert repo snapshots
|
|
292
|
+
for (const [repoId, snap] of Object.entries(input.repoSnapshots)) {
|
|
293
|
+
try {
|
|
294
|
+
await queryBridge(handle, `CREATE (s:RepoSnapshot {
|
|
295
|
+
id: $id,
|
|
296
|
+
indexedAt: $indexedAt,
|
|
297
|
+
lastCommit: $lastCommit
|
|
298
|
+
})`, {
|
|
299
|
+
id: repoId,
|
|
300
|
+
indexedAt: snap.indexedAt,
|
|
301
|
+
lastCommit: snap.lastCommit,
|
|
302
|
+
});
|
|
303
|
+
report.snapshotsInserted++;
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
report.snapshotsFailed++;
|
|
307
|
+
recordError('snapshot', repoId, err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Insert cross-links (tolerating missing nodes).
|
|
311
|
+
//
|
|
312
|
+
// `findContractNode` consults the in-memory lookup index built above,
|
|
313
|
+
// not the DB — that's an O(1) pure-function lookup per endpoint instead
|
|
314
|
+
// of the previous 2-3 DB queries. For M cross-links, the previous code
|
|
315
|
+
// issued up to 6M round-trips; this version issues zero.
|
|
316
|
+
//
|
|
317
|
+
// `link.contractId` may differ between the consumer and provider sides
|
|
318
|
+
// (e.g. wildcard consumer `grpc::Service/*` → method-level provider
|
|
319
|
+
// `grpc::Service/Method`) — that's why we resolve each endpoint
|
|
320
|
+
// independently via its own `(repo, role, symbolUid, filePath, symbolName)`
|
|
321
|
+
// tuple rather than matching on contractId.
|
|
322
|
+
for (const link of crossLinks) {
|
|
323
|
+
const linkId = `${link.from.repo}::${link.contractId}->${link.to.repo}::${link.contractId}`;
|
|
324
|
+
try {
|
|
325
|
+
const fromId = findContractNode(lookupIndex, link.from.repo, 'consumer', link.from.symbolUid, link.from.symbolRef.filePath, link.from.symbolRef.name);
|
|
326
|
+
const toId = findContractNode(lookupIndex, link.to.repo, 'provider', link.to.symbolUid, link.to.symbolRef.filePath, link.to.symbolRef.name);
|
|
327
|
+
if (!fromId || !toId) {
|
|
328
|
+
report.linksDroppedMissingNode++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
await queryBridge(handle, `
|
|
332
|
+
MATCH (a:Contract), (b:Contract)
|
|
333
|
+
WHERE a.id = $fromId AND b.id = $toId
|
|
334
|
+
CREATE (a)-[:ContractLink {
|
|
335
|
+
matchType: $matchType,
|
|
336
|
+
confidence: $confidence,
|
|
337
|
+
contractId: $contractId,
|
|
338
|
+
fromRepo: $fromRepo,
|
|
339
|
+
toRepo: $toRepo
|
|
340
|
+
}]->(b)
|
|
341
|
+
`, {
|
|
342
|
+
fromId,
|
|
343
|
+
toId,
|
|
344
|
+
matchType: link.matchType,
|
|
345
|
+
confidence: link.confidence,
|
|
346
|
+
contractId: link.contractId,
|
|
347
|
+
fromRepo: link.from.repo,
|
|
348
|
+
toRepo: link.to.repo,
|
|
349
|
+
});
|
|
350
|
+
report.linksInserted++;
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
report.linksFailed++;
|
|
354
|
+
recordError('link', linkId, err);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// 2. Close temp DB (happy path). The finally block also calls
|
|
358
|
+
// closeBridgeDb if we threw above; `handleClosed` prevents a
|
|
359
|
+
// double-close on the native handle.
|
|
360
|
+
await closeBridgeDb(handle);
|
|
361
|
+
handleClosed = true;
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (!handleClosed) {
|
|
365
|
+
await closeBridgeDb(handle).catch(() => {
|
|
366
|
+
/* ignore: cleanup path, best effort */
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// 3. Atomic swap: old→.bak, tmp→final, rm .bak
|
|
371
|
+
try {
|
|
372
|
+
await fsp.access(finalPath);
|
|
373
|
+
await retryRename(finalPath, bakPath);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
/* no existing db */
|
|
377
|
+
}
|
|
378
|
+
await retryRename(tmpPath, finalPath);
|
|
379
|
+
try {
|
|
380
|
+
await fsp.rm(bakPath, { recursive: true, force: true });
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
/* ignore */
|
|
384
|
+
}
|
|
385
|
+
// 4. Write meta.json
|
|
386
|
+
await writeBridgeMeta(groupDir, {
|
|
387
|
+
version: BRIDGE_SCHEMA_VERSION,
|
|
388
|
+
generatedAt: new Date().toISOString(),
|
|
389
|
+
missingRepos: input.missingRepos,
|
|
390
|
+
});
|
|
391
|
+
return report;
|
|
392
|
+
}
|
|
393
|
+
/* ------------------------------------------------------------------ */
|
|
394
|
+
/* openBridgeDbReadOnly */
|
|
395
|
+
/* ------------------------------------------------------------------ */
|
|
396
|
+
export async function openBridgeDbReadOnly(groupDir) {
|
|
397
|
+
const dbPath = path.join(groupDir, 'bridge.lbug');
|
|
398
|
+
try {
|
|
399
|
+
await fsp.access(dbPath);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// Check for .bak recovery. Use `retryRename` (not `fsp.rename`) for the
|
|
403
|
+
// exact same reason the rest of this file does: the scenario that
|
|
404
|
+
// triggers bak recovery is an interrupted writer, which on Windows may
|
|
405
|
+
// still be holding an open handle on `.bak` for a few milliseconds when
|
|
406
|
+
// a reader races in. EBUSY/EPERM retries recover that case silently.
|
|
407
|
+
const bakPath = path.join(groupDir, 'bridge.lbug.bak');
|
|
408
|
+
try {
|
|
409
|
+
await fsp.access(bakPath);
|
|
410
|
+
await retryRename(bakPath, dbPath);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Version gate: check meta.json version compatibility
|
|
417
|
+
const meta = await readBridgeMeta(groupDir);
|
|
418
|
+
if (meta.version > 0 && meta.version !== BRIDGE_SCHEMA_VERSION) {
|
|
419
|
+
return null; // incompatible schema version — fallback to JSON or re-sync
|
|
420
|
+
}
|
|
421
|
+
// Open the native handle. If Connection construction throws AFTER
|
|
422
|
+
// Database was successfully allocated, we'd leak the native Database
|
|
423
|
+
// object. Wrap each step separately and tear down the partial handle.
|
|
424
|
+
let db;
|
|
425
|
+
let conn;
|
|
426
|
+
try {
|
|
427
|
+
db = new lbug.Database(dbPath, 0, false, true); // readOnly
|
|
428
|
+
conn = new lbug.Connection(db);
|
|
429
|
+
return { _db: db, _conn: conn, groupDir };
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
if (conn) {
|
|
433
|
+
try {
|
|
434
|
+
await conn.close();
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
/* ignore */
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (db) {
|
|
441
|
+
try {
|
|
442
|
+
await db.close();
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
/* ignore */
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/* ------------------------------------------------------------------ */
|
|
452
|
+
/* bridgeExists */
|
|
453
|
+
/* ------------------------------------------------------------------ */
|
|
454
|
+
export async function bridgeExists(groupDir) {
|
|
455
|
+
const handle = await openBridgeDbReadOnly(groupDir);
|
|
456
|
+
if (!handle)
|
|
457
|
+
return false;
|
|
458
|
+
await closeBridgeDb(handle);
|
|
459
|
+
return true;
|
|
460
|
+
}
|