codesearch-mcp 0.2.3-alpha → 0.4.0-alpha
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/CHANGELOG.md +88 -5
- package/LICENSE +661 -21
- package/README.md +76 -40
- package/dist/app.js +17 -38
- package/dist/src/ast/CallExtractor.d.ts +8 -0
- package/dist/src/ast/CallExtractor.js +68 -0
- package/dist/src/ast/GraphqlNameExtractor.d.ts +22 -0
- package/dist/src/ast/GraphqlNameExtractor.js +240 -0
- package/dist/src/ast/ImportEdgeExtractor.d.ts +11 -0
- package/dist/src/ast/ImportEdgeExtractor.js +129 -0
- package/dist/src/ast/IndexedNodeResolver.d.ts +14 -0
- package/dist/src/ast/IndexedNodeResolver.js +217 -0
- package/dist/src/ast/MemberExtractor.d.ts +8 -0
- package/dist/src/ast/MemberExtractor.js +129 -0
- package/dist/src/ast/TopLevelSymbolExtractor.d.ts +5 -0
- package/dist/src/ast/TopLevelSymbolExtractor.js +86 -0
- package/dist/src/ast/graphql-grammar.d.ts +24 -0
- package/dist/src/ast/graphql-grammar.js +93 -0
- package/dist/src/ast/node-helpers.d.ts +11 -0
- package/dist/src/ast/node-helpers.js +55 -0
- package/dist/src/chunking/AstChunker.d.ts +12 -0
- package/dist/src/chunking/AstChunker.js +30 -0
- package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
- package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
- package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
- package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
- package/dist/src/chunking/FragmentMerger.d.ts +11 -0
- package/dist/src/chunking/FragmentMerger.js +61 -0
- package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
- package/dist/src/chunking/ImportOnlyFilter.js +14 -0
- package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
- package/dist/src/chunking/LangchainTextSplitter.js +133 -0
- package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
- package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
- package/dist/src/chunking/RunPacker.d.ts +12 -0
- package/dist/src/chunking/RunPacker.js +84 -0
- package/dist/src/chunking/create-chunker.d.ts +3 -0
- package/dist/src/chunking/create-chunker.js +15 -0
- package/dist/src/chunking/interfaces.d.ts +27 -0
- package/dist/src/commands/ConfigCommand.js +1 -9
- package/dist/src/commands/EvalCommand.d.ts +10 -5
- package/dist/src/commands/EvalCommand.js +60 -149
- package/dist/src/commands/IndexCommand.d.ts +10 -4
- package/dist/src/commands/IndexCommand.js +52 -146
- package/dist/src/commands/McpCommand.d.ts +6 -3
- package/dist/src/commands/McpCommand.js +19 -20
- package/dist/src/commands/WebCommand.d.ts +9 -3
- package/dist/src/commands/WebCommand.js +17 -11
- package/dist/src/commands/eval/baseline-comparison.d.ts +6 -0
- package/dist/src/commands/eval/baseline-comparison.js +54 -0
- package/dist/src/commands/eval/eval-args.d.ts +6 -0
- package/dist/src/commands/eval/eval-args.js +71 -0
- package/dist/src/commands/eval/eval-corpora.d.ts +6 -0
- package/dist/src/commands/eval/eval-corpora.js +76 -0
- package/dist/src/commands/eval/eval-metadata.d.ts +17 -0
- package/dist/src/commands/eval/eval-metadata.js +75 -0
- package/dist/src/commands/eval/index-inspector.d.ts +3 -0
- package/dist/src/commands/eval/index-inspector.js +21 -0
- package/dist/src/commands/eval/staged-search.d.ts +3 -0
- package/dist/src/commands/eval/staged-search.js +20 -0
- package/dist/src/commands/eval/suite-loader.d.ts +8 -0
- package/dist/src/commands/eval/suite-loader.js +52 -0
- package/dist/src/commands/index/backend-probe.d.ts +2 -0
- package/dist/src/commands/index/backend-probe.js +15 -0
- package/dist/src/commands/index/embedding-setup.d.ts +3 -0
- package/dist/src/commands/index/embedding-setup.js +16 -0
- package/dist/src/commands/index/index-summary.d.ts +2 -0
- package/dist/src/commands/index/index-summary.js +22 -0
- package/dist/src/commands/index/legacy-ollama-guard.d.ts +3 -0
- package/dist/src/commands/index/legacy-ollama-guard.js +23 -0
- package/dist/src/commands/index/prune-step.d.ts +3 -0
- package/dist/src/commands/index/prune-step.js +22 -0
- package/dist/src/commands/index/scan-targets.d.ts +8 -0
- package/dist/src/commands/index/scan-targets.js +107 -0
- package/dist/src/config-error.d.ts +0 -4
- package/dist/src/config-error.js +0 -4
- package/dist/src/config-store.d.ts +1 -55
- package/dist/src/config-store.js +11 -73
- package/dist/src/config.d.ts +9 -18
- package/dist/src/config.js +36 -35
- package/dist/src/constants.d.ts +17 -28
- package/dist/src/constants.js +35 -29
- package/dist/src/container.d.ts +10 -2
- package/dist/src/container.js +19 -8
- package/dist/src/db/drizzle-types.d.ts +7 -0
- package/dist/src/db/drizzle-types.js +2 -0
- package/dist/src/db/index.d.ts +1 -0
- package/dist/src/db/schema-steps.d.ts +8 -0
- package/dist/src/db/schema-steps.js +105 -0
- package/dist/src/db/schema.d.ts +17 -0
- package/dist/src/db/schema.js +1 -6
- package/dist/src/declarations/DeclarationTree.d.ts +19 -0
- package/dist/src/declarations/DeclarationTree.js +66 -0
- package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
- package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
- package/dist/src/declarations/SourceLines.d.ts +37 -0
- package/dist/src/declarations/SourceLines.js +415 -0
- package/dist/src/dependency-traversal.d.ts +0 -6
- package/dist/src/dependency-traversal.js +0 -6
- package/dist/src/enriched-prompt.d.ts +0 -6
- package/dist/src/enriched-prompt.js +0 -6
- package/dist/src/error-record.d.ts +12 -0
- package/dist/src/error-record.js +248 -0
- package/dist/src/eval-corpus.d.ts +16 -0
- package/dist/src/eval-corpus.js +169 -0
- package/dist/src/eval-results.d.ts +6 -3
- package/dist/src/eval-results.js +323 -26
- package/dist/src/eval-vector-index.d.ts +5 -0
- package/dist/src/eval-vector-index.js +60 -0
- package/dist/src/file-utils.d.ts +0 -5
- package/dist/src/file-utils.js +0 -5
- package/dist/src/graph.d.ts +0 -25
- package/dist/src/graph.js +0 -28
- package/dist/src/help.js +9 -1
- package/dist/src/hnsw-settings.d.ts +3 -0
- package/dist/src/hnsw-settings.js +24 -0
- package/dist/src/index-args.d.ts +13 -0
- package/dist/src/index-args.js +48 -0
- package/dist/src/indexing/AnalysisBatcher.d.ts +12 -0
- package/dist/src/indexing/AnalysisBatcher.js +28 -0
- package/dist/src/indexing/ChunkEmbedder.d.ts +24 -0
- package/dist/src/indexing/ChunkEmbedder.js +37 -0
- package/dist/src/indexing/ConsoleScanReporter.d.ts +10 -0
- package/dist/src/indexing/ConsoleScanReporter.js +73 -0
- package/dist/src/indexing/EmbeddingSchemaPreparer.d.ts +14 -0
- package/dist/src/indexing/EmbeddingSchemaPreparer.js +66 -0
- package/dist/src/indexing/FileIngestor.d.ts +22 -0
- package/dist/src/indexing/FileIngestor.js +116 -0
- package/dist/src/indexing/IndexStore.d.ts +24 -0
- package/dist/src/indexing/IndexStore.js +69 -0
- package/dist/src/indexing/StaleFilePruner.d.ts +8 -0
- package/dist/src/indexing/StaleFilePruner.js +55 -0
- package/dist/src/indexing/analysis-rows.d.ts +4 -0
- package/dist/src/indexing/analysis-rows.js +65 -0
- package/dist/src/indexing/project-notices.d.ts +2 -0
- package/dist/src/indexing/project-notices.js +63 -0
- package/dist/src/interfaces/IAstParser.d.ts +4 -40
- package/dist/src/interfaces/ICallGraph.d.ts +28 -0
- package/dist/src/interfaces/ICallGraph.js +2 -0
- package/dist/src/interfaces/IChunkSearch.d.ts +42 -0
- package/dist/src/interfaces/IChunkSearch.js +2 -0
- package/dist/src/interfaces/IChunker.d.ts +3 -12
- package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
- package/dist/src/interfaces/ICorpusInspector.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +8 -24
- package/dist/src/interfaces/IDependencyGraph.d.ts +24 -0
- package/dist/src/interfaces/IDependencyGraph.js +2 -0
- package/dist/src/interfaces/IEmbedder.d.ts +0 -9
- package/dist/src/interfaces/IIndexIdentityGuard.d.ts +3 -0
- package/dist/src/interfaces/IIndexIdentityGuard.js +2 -0
- package/dist/src/interfaces/IIndexedNodeResolver.d.ts +10 -0
- package/dist/src/interfaces/IIndexedNodeResolver.js +2 -0
- package/dist/src/interfaces/ILogger.d.ts +1 -11
- package/dist/src/interfaces/IMcpReadiness.d.ts +12 -0
- package/dist/src/interfaces/IMcpReadiness.js +2 -0
- package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
- package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
- package/dist/src/interfaces/IProjectCatalog.d.ts +13 -0
- package/dist/src/interfaces/IProjectCatalog.js +2 -0
- package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
- package/dist/src/interfaces/IProjectRoots.d.ts +3 -0
- package/dist/src/interfaces/IProjectRoots.js +2 -0
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +153 -12
- package/dist/src/interfaces/IScanReporter.d.ts +13 -0
- package/dist/src/interfaces/IScanReporter.js +2 -0
- package/dist/src/interfaces/ISymbolLookup.d.ts +49 -0
- package/dist/src/interfaces/ISymbolLookup.js +2 -0
- package/dist/src/interfaces/IToolPaths.d.ts +62 -0
- package/dist/src/interfaces/IToolPaths.js +2 -0
- package/dist/src/interfaces/IWorkspaceFiles.d.ts +28 -0
- package/dist/src/interfaces/IWorkspaceFiles.js +2 -0
- package/dist/src/interfaces/SymbolKind.d.ts +1 -0
- package/dist/src/interfaces/SymbolKind.js +2 -0
- package/dist/src/interfaces/index.d.ts +12 -1
- package/dist/src/interrupt.js +0 -2
- package/dist/src/owner-path.d.ts +9 -10
- package/dist/src/owner-path.js +11 -23
- package/dist/src/project-scope.d.ts +1 -12
- package/dist/src/project-scope.js +5 -12
- package/dist/src/project-selector.d.ts +0 -5
- package/dist/src/project-selector.js +0 -11
- package/dist/src/retrieval-eval.d.ts +15 -6
- package/dist/src/retrieval-eval.js +388 -59
- package/dist/src/search/CallGraph.d.ts +9 -0
- package/dist/src/search/CallGraph.js +84 -0
- package/dist/src/search/ChunkSearch.d.ts +23 -0
- package/dist/src/search/ChunkSearch.js +115 -0
- package/dist/src/search/DependencyGraph.d.ts +19 -0
- package/dist/src/search/DependencyGraph.js +180 -0
- package/dist/src/search/IndexIdentityGuard.d.ts +11 -0
- package/dist/src/search/IndexIdentityGuard.js +37 -0
- package/dist/src/search/ProjectCatalog.d.ts +11 -0
- package/dist/src/search/ProjectCatalog.js +38 -0
- package/dist/src/search/SymbolLookup.d.ts +24 -0
- package/dist/src/search/SymbolLookup.js +97 -0
- package/dist/src/search/chunks/CandidateFetcher.d.ts +19 -0
- package/dist/src/search/chunks/CandidateFetcher.js +40 -0
- package/dist/src/search/chunks/IterativeScanGate.d.ts +10 -0
- package/dist/src/search/chunks/IterativeScanGate.js +42 -0
- package/dist/src/search/chunks/RerankStage.d.ts +15 -0
- package/dist/src/search/chunks/RerankStage.js +42 -0
- package/dist/src/search/chunks/search-metrics.d.ts +18 -0
- package/dist/src/search/chunks/search-metrics.js +32 -0
- package/dist/src/search/chunks/search-plan.d.ts +18 -0
- package/dist/src/search/chunks/search-plan.js +19 -0
- package/dist/src/search/create-search-services.d.ts +26 -0
- package/dist/src/search/create-search-services.js +29 -0
- package/dist/src/search/index-paths.d.ts +1 -0
- package/dist/src/search/index-paths.js +5 -0
- package/dist/src/search/result-shaping.d.ts +4 -0
- package/dist/src/search/result-shaping.js +19 -0
- package/dist/src/search/symbol-sql.d.ts +13 -0
- package/dist/src/search/symbol-sql.js +66 -0
- package/dist/src/search/symbols/SymbolGrepFallback.d.ts +19 -0
- package/dist/src/search/symbols/SymbolGrepFallback.js +106 -0
- package/dist/src/search/symbols/SymbolIndexQuery.d.ts +14 -0
- package/dist/src/search/symbols/SymbolIndexQuery.js +52 -0
- package/dist/src/search/symbols/SymbolTextSearch.d.ts +13 -0
- package/dist/src/search/symbols/SymbolTextSearch.js +84 -0
- package/dist/src/search/symbols/TargetColumnGate.d.ts +7 -0
- package/dist/src/search/symbols/TargetColumnGate.js +23 -0
- package/dist/src/search/symbols/find-symbol-constants.d.ts +4 -0
- package/dist/src/search/symbols/find-symbol-constants.js +8 -0
- package/dist/src/search/symbols/grep-args.d.ts +3 -0
- package/dist/src/search/symbols/grep-args.js +7 -0
- package/dist/src/search/symbols/grep-patterns.d.ts +4 -0
- package/dist/src/search/symbols/grep-patterns.js +60 -0
- package/dist/src/services/AstParserService.d.ts +10 -106
- package/dist/src/services/AstParserService.js +37 -1068
- package/dist/src/services/DatabaseService.d.ts +10 -17
- package/dist/src/services/DatabaseService.js +71 -115
- package/dist/src/services/EmbedderFactory.d.ts +0 -4
- package/dist/src/services/EmbedderFactory.js +0 -5
- package/dist/src/services/IndexerService.d.ts +7 -26
- package/dist/src/services/IndexerService.js +38 -286
- package/dist/src/services/InfinityEmbedder.d.ts +0 -21
- package/dist/src/services/InfinityEmbedder.js +0 -15
- package/dist/src/services/LoggerService.d.ts +7 -5
- package/dist/src/services/LoggerService.js +37 -41
- package/dist/src/services/McpReadiness.d.ts +18 -0
- package/dist/src/services/McpReadiness.js +73 -0
- package/dist/src/services/McpServer.d.ts +50 -19
- package/dist/src/services/McpServer.js +132 -62
- package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
- package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
- package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
- package/dist/src/services/NxMonorepoResolver.js +5 -56
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
- package/dist/src/services/OpenAICompatibleEmbedder.js +0 -14
- package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
- package/dist/src/services/ProjectDiscoveryService.js +0 -4
- package/dist/src/services/VoyageEmbedder.d.ts +0 -6
- package/dist/src/services/VoyageEmbedder.js +0 -6
- package/dist/src/services/WebServerService.d.ts +5 -56
- package/dist/src/services/WebServerService.js +25 -63
- package/dist/src/services/http-retry.d.ts +0 -7
- package/dist/src/services/http-retry.js +1 -10
- package/dist/src/services/infinity-provisioning.d.ts +0 -74
- package/dist/src/services/infinity-provisioning.js +5 -88
- package/dist/src/services/legacy-ollama-index.d.ts +0 -16
- package/dist/src/services/legacy-ollama-index.js +0 -23
- package/dist/src/services/reranker.d.ts +0 -9
- package/dist/src/services/reranker.js +46 -12
- package/dist/src/setup.d.ts +24 -24
- package/dist/src/setup.js +205 -103
- package/dist/src/symbol-name.d.ts +1 -12
- package/dist/src/symbol-name.js +16 -25
- package/dist/src/symbol-rows.d.ts +0 -14
- package/dist/src/symbol-rows.js +0 -14
- package/dist/src/tool-call-log.d.ts +2 -0
- package/dist/src/tool-call-log.js +24 -0
- package/dist/src/tools/FindSymbolTool.d.ts +17 -7
- package/dist/src/tools/FindSymbolTool.js +54 -35
- package/dist/src/tools/GetCallersTool.d.ts +16 -8
- package/dist/src/tools/GetCallersTool.js +65 -35
- package/dist/src/tools/GetDependenciesTool.d.ts +16 -8
- package/dist/src/tools/GetDependenciesTool.js +36 -29
- package/dist/src/tools/GetDependentsTool.d.ts +20 -8
- package/dist/src/tools/GetDependentsTool.js +48 -34
- package/dist/src/tools/ListFilesTool.d.ts +16 -8
- package/dist/src/tools/ListFilesTool.js +20 -26
- package/dist/src/tools/ListProjectsTool.d.ts +11 -7
- package/dist/src/tools/ListProjectsTool.js +13 -11
- package/dist/src/tools/McpTool.d.ts +17 -0
- package/dist/src/tools/McpTool.js +28 -0
- package/dist/src/tools/ReadFileRangeTool.d.ts +16 -8
- package/dist/src/tools/ReadFileRangeTool.js +23 -25
- package/dist/src/tools/ReadFileTool.d.ts +15 -8
- package/dist/src/tools/ReadFileTool.js +19 -24
- package/dist/src/tools/SearchCodebaseTool.d.ts +22 -10
- package/dist/src/tools/SearchCodebaseTool.js +71 -50
- package/dist/src/tools/format-helpers.d.ts +9 -9
- package/dist/src/tools/format-helpers.js +146 -28
- package/dist/src/tools/tool-args.d.ts +38 -0
- package/dist/src/tools/tool-args.js +95 -0
- package/dist/src/web/dashboard.d.ts +0 -8
- package/dist/src/web/dashboard.js +7 -10
- package/dist/src/workspace/GrepRunner.d.ts +15 -0
- package/dist/src/workspace/GrepRunner.js +104 -0
- package/dist/src/workspace/ProjectRoots.d.ts +22 -0
- package/dist/src/workspace/ProjectRoots.js +102 -0
- package/dist/src/workspace/ToolPaths.d.ts +31 -0
- package/dist/src/workspace/ToolPaths.js +312 -0
- package/dist/src/workspace/WorkspaceFiles.d.ts +19 -0
- package/dist/src/workspace/WorkspaceFiles.js +285 -0
- package/dist/src/workspace/fs-access.d.ts +9 -0
- package/dist/src/workspace/fs-access.js +47 -0
- package/dist/src/workspace/path-helpers.d.ts +3 -0
- package/dist/src/workspace/path-helpers.js +43 -0
- package/package.json +5 -4
- package/dist/src/interfaces/ISearcher.d.ts +0 -35
- package/dist/src/services/AstChunker.d.ts +0 -42
- package/dist/src/services/AstChunker.js +0 -158
- package/dist/src/services/SearcherService.d.ts +0 -342
- package/dist/src/services/SearcherService.js +0 -1199
- package/docs/web-dashboard.md +0 -206
- /package/dist/src/{interfaces/ISearcher.js → chunking/interfaces.js} +0 -0
|
@@ -1,1199 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.SearcherService = void 0;
|
|
37
|
-
const fs = __importStar(require("fs"));
|
|
38
|
-
const path = __importStar(require("path"));
|
|
39
|
-
const readline = __importStar(require("readline"));
|
|
40
|
-
const child_process_1 = require("child_process");
|
|
41
|
-
const file_utils_1 = require("../file-utils");
|
|
42
|
-
const MonorepoAwareDiscoveryService_1 = require("./MonorepoAwareDiscoveryService");
|
|
43
|
-
const legacy_ollama_index_1 = require("./legacy-ollama-index");
|
|
44
|
-
const symbol_name_1 = require("../symbol-name");
|
|
45
|
-
const dependency_traversal_1 = require("../dependency-traversal");
|
|
46
|
-
const SILENT_LOGGER = {
|
|
47
|
-
debug() { },
|
|
48
|
-
info() { },
|
|
49
|
-
warn() { },
|
|
50
|
-
error() { },
|
|
51
|
-
metric() { },
|
|
52
|
-
};
|
|
53
|
-
const NODE_FS = {
|
|
54
|
-
realpath: p => fs.promises.realpath(p),
|
|
55
|
-
stat: p => fs.promises.stat(p),
|
|
56
|
-
readdir: p => fs.promises.readdir(p, { withFileTypes: true }),
|
|
57
|
-
createReadStream: p => fs.createReadStream(p, { encoding: 'utf-8' }),
|
|
58
|
-
};
|
|
59
|
-
const GRAPHQL_KINDS = new Set([
|
|
60
|
-
'graphql_query', 'graphql_mutation', 'graphql_subscription', 'graphql_field', 'graphql_type',
|
|
61
|
-
]);
|
|
62
|
-
const EMPTY_META = { description: null, stack: null, notes: null, relations: null };
|
|
63
|
-
const GREP_MAX_RESULTS = 200;
|
|
64
|
-
const GREP_RAW_LIMIT = GREP_MAX_RESULTS * 5;
|
|
65
|
-
const GREP_EXCLUDE_DIRS = [...file_utils_1.IGNORE_DIRS].map(d => `--exclude-dir=${d}`);
|
|
66
|
-
const FIND_SYMBOL_LIMIT = 50;
|
|
67
|
-
const FIND_SYMBOL_RAW_LIMIT = FIND_SYMBOL_LIMIT * 10;
|
|
68
|
-
const PATH_CANDIDATE_LIMIT = 3;
|
|
69
|
-
/** Every path column of the index; `content` marks those whose file the indexer read. */
|
|
70
|
-
const INDEX_PATH_COLUMNS = [
|
|
71
|
-
{ table: 'code_chunk', column: '"filePath"', content: true },
|
|
72
|
-
{ table: 'code_symbol', column: '"filePath"', content: true },
|
|
73
|
-
{ table: 'code_dependency', column: '"fromFile"', content: true },
|
|
74
|
-
{ table: 'code_call', column: '"fromFile"', content: true },
|
|
75
|
-
{ table: 'code_dependency', column: '"toFile"', content: false },
|
|
76
|
-
{ table: 'code_call', column: '"calleeFile"', content: false },
|
|
77
|
-
];
|
|
78
|
-
const OUTSIDE_WORKSPACE_VALUE = (col) => `(${col} = '..' OR ${col} LIKE '../%')`;
|
|
79
|
-
function stripTrailingSlash(p) {
|
|
80
|
-
return p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p;
|
|
81
|
-
}
|
|
82
|
-
/** Segment-boundary containment: `/ws-other` is not within `/ws`; `.` contains every relative path. */
|
|
83
|
-
function isWithin(p, root) {
|
|
84
|
-
if (root === '.')
|
|
85
|
-
return true;
|
|
86
|
-
if (root === '/')
|
|
87
|
-
return p.startsWith('/');
|
|
88
|
-
return p === root || p.startsWith(root + '/');
|
|
89
|
-
}
|
|
90
|
-
function errorCode(err) {
|
|
91
|
-
return err?.code ?? 'EIO';
|
|
92
|
-
}
|
|
93
|
-
class SearcherService {
|
|
94
|
-
db;
|
|
95
|
-
embedder;
|
|
96
|
-
discovery;
|
|
97
|
-
config;
|
|
98
|
-
logger;
|
|
99
|
-
fs;
|
|
100
|
-
constructor(db, embedder, discovery, config, logger = SILENT_LOGGER, fsDeps = {}) {
|
|
101
|
-
this.db = db;
|
|
102
|
-
this.embedder = embedder;
|
|
103
|
-
this.discovery = discovery;
|
|
104
|
-
this.config = config;
|
|
105
|
-
this.logger = logger;
|
|
106
|
-
this.fs = { ...NODE_FS, ...fsDeps };
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Whether `code_symbol.target` exists — `true` once a check finds it (never
|
|
110
|
-
* re-checked again in this process), `false` until then (re-checked on
|
|
111
|
-
* every lookup, so a `rag-index` run that adds the column takes effect
|
|
112
|
-
* without restarting `rag-mcp`). See "find_symbol works before the target
|
|
113
|
-
* column exists" (`graphql-symbols`).
|
|
114
|
-
*/
|
|
115
|
-
targetColumnPresent = false;
|
|
116
|
-
/** Real path of `workspaceRoot`; cached only once resolved, so a transient failure is retried. */
|
|
117
|
-
realRootCache;
|
|
118
|
-
/** Lazily-built, cached map of project name → real filesystem root. */
|
|
119
|
-
rootMap;
|
|
120
|
-
/** Cached, lazily-built map of leaf project name → real filesystem root (via discovery). */
|
|
121
|
-
projectRootMap() {
|
|
122
|
-
if (!this.rootMap) {
|
|
123
|
-
this.rootMap = new Map((0, MonorepoAwareDiscoveryService_1.toLeafProjects)(this.discovery.discover(this.config.workspaceRoot)).map(p => [p.name, p.path]));
|
|
124
|
-
}
|
|
125
|
-
return this.rootMap;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Resolve a project name to its real filesystem root. Uses discovery (leaf
|
|
129
|
-
* projects) so nested-monorepo names like `repo/app` map to their true path;
|
|
130
|
-
* falls back to `join(workspaceRoot, name)` when the name is unknown (e.g. a
|
|
131
|
-
* project since removed from disk, or a flat project not surfaced by discovery).
|
|
132
|
-
*/
|
|
133
|
-
resolveProjectRoot(name) {
|
|
134
|
-
let root = this.projectRootMap().get(name);
|
|
135
|
-
if (root === undefined) {
|
|
136
|
-
// Cache miss — the project may have been added/renamed on disk since the
|
|
137
|
-
// map was built (mcp/web servers are long-lived). Rebuild once and retry
|
|
138
|
-
// before the flat fallback, which is wrong for nested-monorepo names
|
|
139
|
-
// (`repo/app` lives at `repo/apps/app`, not `repo/app`).
|
|
140
|
-
this.rootMap = undefined;
|
|
141
|
-
root = this.projectRootMap().get(name);
|
|
142
|
-
}
|
|
143
|
-
return root ?? path.join(this.config.workspaceRoot, name);
|
|
144
|
-
}
|
|
145
|
-
toRel(absPath) {
|
|
146
|
-
return path.relative(this.config.workspaceRoot, absPath) || '.';
|
|
147
|
-
}
|
|
148
|
-
toAbs(relPath) {
|
|
149
|
-
return relPath === '.' ? this.workspaceRoot() : path.join(this.workspaceRoot(), relPath);
|
|
150
|
-
}
|
|
151
|
-
workspaceRoot() {
|
|
152
|
-
return stripTrailingSlash(path.posix.normalize(this.config.workspaceRoot));
|
|
153
|
-
}
|
|
154
|
-
async realWorkspaceRoot() {
|
|
155
|
-
if (this.realRootCache !== undefined)
|
|
156
|
-
return this.realRootCache;
|
|
157
|
-
try {
|
|
158
|
-
this.realRootCache = await this.fs.realpath(this.workspaceRoot());
|
|
159
|
-
return this.realRootCache;
|
|
160
|
-
}
|
|
161
|
-
catch (err) {
|
|
162
|
-
this.logger.warn('SearcherService: workspaceRoot real path unavailable', { reason: errorCode(err) });
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Lexical validation of a tool path argument: refusals for an empty value,
|
|
168
|
-
* `\`, an absolute path and a `..` escape; otherwise the canonical
|
|
169
|
-
* workspace-relative form. The only filesystem access is the cached real path
|
|
170
|
-
* of `workspaceRoot`.
|
|
171
|
-
*/
|
|
172
|
-
async resolveToolPath(input) {
|
|
173
|
-
if (input == null || input === '')
|
|
174
|
-
return { ok: false, reason: 'invalid', missing: true };
|
|
175
|
-
if (input.includes('\\'))
|
|
176
|
-
return { ok: false, reason: 'invalid', missing: false };
|
|
177
|
-
if (path.posix.isAbsolute(input)) {
|
|
178
|
-
const abs = stripTrailingSlash(path.posix.normalize(input));
|
|
179
|
-
const realRoot = await this.realWorkspaceRoot();
|
|
180
|
-
for (const root of realRoot ? [this.workspaceRoot(), realRoot] : [this.workspaceRoot()]) {
|
|
181
|
-
if (!isWithin(abs, root))
|
|
182
|
-
continue;
|
|
183
|
-
const relative = abs === root ? '.' : abs.slice(root === '/' ? 1 : root.length + 1);
|
|
184
|
-
return { ok: false, reason: 'absolute', relative };
|
|
185
|
-
}
|
|
186
|
-
return { ok: false, reason: 'outside_workspace' };
|
|
187
|
-
}
|
|
188
|
-
const relPath = stripTrailingSlash(path.posix.normalize(input));
|
|
189
|
-
if (relPath === '..' || relPath.startsWith('../'))
|
|
190
|
-
return { ok: false, reason: 'outside_workspace' };
|
|
191
|
-
return { ok: true, relPath, absPath: this.toAbs(relPath) };
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Whether the real location of `absPath` — of the target, else of its nearest
|
|
195
|
-
* existing ancestor plus the remaining segments — lies under the real
|
|
196
|
-
* workspace root.
|
|
197
|
-
*/
|
|
198
|
-
async realLocation(absPath) {
|
|
199
|
-
const root = (await this.realWorkspaceRoot()) ?? this.workspaceRoot();
|
|
200
|
-
let existing = absPath;
|
|
201
|
-
const rest = [];
|
|
202
|
-
for (;;) {
|
|
203
|
-
try {
|
|
204
|
-
const real = path.join(await this.fs.realpath(existing), ...rest);
|
|
205
|
-
return { ok: true, inside: isWithin(real, root) };
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
if (errorCode(err) !== 'ENOENT')
|
|
209
|
-
return { ok: false, code: errorCode(err) };
|
|
210
|
-
const parent = path.dirname(existing);
|
|
211
|
-
if (parent === existing)
|
|
212
|
-
return { ok: true, inside: false };
|
|
213
|
-
rest.unshift(path.basename(existing));
|
|
214
|
-
existing = parent;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Workspace boundary of the disk tools. A real location outside the workspace
|
|
220
|
-
* is allowed only for content the indexer already read (a symlinked share):
|
|
221
|
-
* the file itself for `file`, any file below it for `dir`.
|
|
222
|
-
*/
|
|
223
|
-
async checkRealLocation(relPath, absPath, kind) {
|
|
224
|
-
const loc = await this.realLocation(absPath);
|
|
225
|
-
if (!loc.ok)
|
|
226
|
-
return { ok: false, reason: 'unreadable', code: loc.code };
|
|
227
|
-
if (loc.inside)
|
|
228
|
-
return { ok: true };
|
|
229
|
-
const exempt = kind === 'file' ? await this.isContentPath(relPath) : await this.hasContentUnder(relPath);
|
|
230
|
-
return exempt ? { ok: true } : { ok: false, reason: 'outside_workspace' };
|
|
231
|
-
}
|
|
232
|
-
async pathExists(columns, predicate, param) {
|
|
233
|
-
const branches = columns.map(c => `SELECT 1 FROM ${c.table} WHERE ${predicate.replace(/\$col/g, c.column)}`);
|
|
234
|
-
const [{ exists }] = await this.db.query(`SELECT EXISTS (${branches.join(' UNION ALL ')}) AS exists`, [param]);
|
|
235
|
-
return exists;
|
|
236
|
-
}
|
|
237
|
-
/** Whether any table of the index holds `relPath` (a canonical path, never `../…`). */
|
|
238
|
-
isIndexedPath(relPath) {
|
|
239
|
-
return this.pathExists(INDEX_PATH_COLUMNS, '$col = $1', relPath);
|
|
240
|
-
}
|
|
241
|
-
/** Whether the indexer read the file at `relPath`. */
|
|
242
|
-
isContentPath(relPath) {
|
|
243
|
-
return this.pathExists(INDEX_PATH_COLUMNS.filter(c => c.content), '$col = $1', relPath);
|
|
244
|
-
}
|
|
245
|
-
hasContentUnder(relPath) {
|
|
246
|
-
const prefix = relPath === '.' ? '' : (0, symbol_name_1.escapeLike)(relPath) + '/';
|
|
247
|
-
return this.pathExists(INDEX_PATH_COLUMNS.filter(c => c.content), `$col LIKE $1 ESCAPE '\\'`, prefix + '%');
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Up to three index paths ending in `/<relPath>` (`file`) or directories
|
|
251
|
-
* ending in `/<relPath>` that hold one (`dir`), in byte order. For `dir`, the
|
|
252
|
-
* root of a project named `relPath` comes first.
|
|
253
|
-
*/
|
|
254
|
-
async findPathCandidates(relPath, kind) {
|
|
255
|
-
const union = INDEX_PATH_COLUMNS.map(c => `SELECT ${c.column} AS p FROM ${c.table}`).join(' UNION ALL ');
|
|
256
|
-
const notOutside = `NOT ${OUTSIDE_WORKSPACE_VALUE('p')}`;
|
|
257
|
-
if (kind === 'file') {
|
|
258
|
-
const substituted = await this.substituteProjectName(relPath);
|
|
259
|
-
const rows = await this.db.query(`SELECT p FROM (${union}) s
|
|
260
|
-
WHERE p LIKE $1 ESCAPE '\\' AND ${notOutside}
|
|
261
|
-
GROUP BY p ORDER BY p COLLATE "C" LIMIT ${PATH_CANDIDATE_LIMIT + 1}`, ['%/' + (0, symbol_name_1.escapeLike)(relPath)]);
|
|
262
|
-
const first = substituted ? [substituted] : [];
|
|
263
|
-
return [...first, ...rows.map(r => r.p).filter(p => p !== substituted)].slice(0, PATH_CANDIDATE_LIMIT);
|
|
264
|
-
}
|
|
265
|
-
const projectRoot = this.findProjectRootByName(relPath);
|
|
266
|
-
const first = projectRoot && projectRoot !== relPath ? [projectRoot] : [];
|
|
267
|
-
const rows = await this.db.query(`SELECT d AS p FROM (
|
|
268
|
-
SELECT substring(p FROM 1 FOR position($2::text IN p) + length($3::text)) AS d
|
|
269
|
-
FROM (${union}) s
|
|
270
|
-
WHERE p LIKE $1 ESCAPE '\\' AND ${notOutside}
|
|
271
|
-
) dirs
|
|
272
|
-
GROUP BY d ORDER BY d COLLATE "C" LIMIT ${PATH_CANDIDATE_LIMIT + 1}`, ['%/' + (0, symbol_name_1.escapeLike)(relPath) + '/%', '/' + relPath + '/', relPath]);
|
|
273
|
-
const rest = rows.map(r => r.p).filter(p => !first.includes(p));
|
|
274
|
-
return [...first, ...rest].slice(0, PATH_CANDIDATE_LIMIT);
|
|
275
|
-
}
|
|
276
|
-
/** `findPathCandidates`, with a failed lookup logged and answered by no candidates. */
|
|
277
|
-
async candidatesOrNone(relPath, kind) {
|
|
278
|
-
try {
|
|
279
|
-
return await this.findPathCandidates(relPath, kind);
|
|
280
|
-
}
|
|
281
|
-
catch (err) {
|
|
282
|
-
this.logger.error('SearcherService: path candidate lookup failed', { relPath, reason: err.message });
|
|
283
|
-
return [];
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* `relPath` with a leading project name replaced by that project's root
|
|
288
|
-
* (`repo/app/src/x.ts` → `repo/apps/app/src/x.ts`), or null when no name
|
|
289
|
-
* prefixes it, the root equals the name, or the result names nothing.
|
|
290
|
-
*/
|
|
291
|
-
async substituteProjectName(relPath) {
|
|
292
|
-
let named = [];
|
|
293
|
-
for (let attempt = 0; attempt < 2 && named.length === 0; attempt++) {
|
|
294
|
-
if (attempt === 1)
|
|
295
|
-
this.rootMap = undefined;
|
|
296
|
-
named = [...this.projectRootMap().entries()].filter(([name]) => relPath.startsWith(name + '/'));
|
|
297
|
-
}
|
|
298
|
-
const longest = named.sort((a, b) => b[0].length - a[0].length)[0];
|
|
299
|
-
if (!longest)
|
|
300
|
-
return null;
|
|
301
|
-
const [name, absRoot] = longest;
|
|
302
|
-
const root = this.toRel(absRoot);
|
|
303
|
-
if (root === name)
|
|
304
|
-
return null;
|
|
305
|
-
const candidate = (root === '.' ? '' : root + '/') + relPath.slice(name.length + 1);
|
|
306
|
-
if (await this.isIndexedPath(candidate))
|
|
307
|
-
return candidate;
|
|
308
|
-
try {
|
|
309
|
-
return (await this.fs.stat(this.toAbs(candidate))).isFile() ? candidate : null;
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
findProjectRootByName(name) {
|
|
316
|
-
let root = this.projectRootMap().get(name);
|
|
317
|
-
if (root === undefined) {
|
|
318
|
-
this.rootMap = undefined;
|
|
319
|
-
root = this.projectRootMap().get(name);
|
|
320
|
-
}
|
|
321
|
-
return root === undefined ? null : this.toRel(root);
|
|
322
|
-
}
|
|
323
|
-
/** Longest workspace-relative project root accepted by `match`; discovery is re-run once before giving up. */
|
|
324
|
-
findProjectRoot(match) {
|
|
325
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
326
|
-
if (attempt === 1)
|
|
327
|
-
this.rootMap = undefined;
|
|
328
|
-
const best = [...this.projectRootMap().entries()]
|
|
329
|
-
.map(([name, abs]) => ({ name, rel: this.toRel(abs) }))
|
|
330
|
-
.filter(r => match(r.rel))
|
|
331
|
-
.sort((a, b) => b.rel.length - a.rel.length)[0];
|
|
332
|
-
if (best)
|
|
333
|
-
return best;
|
|
334
|
-
}
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
/** Root directory (workspace-relative) of the project the text-search fallback for `relPath` runs in. */
|
|
338
|
-
fallbackRoot(relPath) {
|
|
339
|
-
return this.findProjectRoot(r => isWithin(relPath, r))?.rel ?? null;
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Why a graph tool has no data for `relPath`, or null when the path is in the
|
|
343
|
-
* index path set. Touches the disk only for an unknown path, and answers a
|
|
344
|
-
* real location outside the workspace as `not_found` without looking at it.
|
|
345
|
-
*/
|
|
346
|
-
async checkGraphPath(relPath) {
|
|
347
|
-
if (await this.isIndexedPath(relPath))
|
|
348
|
-
return null;
|
|
349
|
-
const absPath = this.toAbs(relPath);
|
|
350
|
-
const loc = await this.realLocation(absPath);
|
|
351
|
-
if (!loc.ok)
|
|
352
|
-
return { ok: false, reason: 'unreadable', code: loc.code };
|
|
353
|
-
if (!loc.inside)
|
|
354
|
-
return { ok: false, reason: 'not_found', candidates: await this.candidatesOrNone(relPath, 'file') };
|
|
355
|
-
let stat;
|
|
356
|
-
try {
|
|
357
|
-
stat = await this.fs.stat(absPath);
|
|
358
|
-
}
|
|
359
|
-
catch (err) {
|
|
360
|
-
if (errorCode(err) !== 'ENOENT')
|
|
361
|
-
return { ok: false, reason: 'unreadable', code: errorCode(err) };
|
|
362
|
-
return { ok: false, reason: 'not_found', candidates: await this.candidatesOrNone(relPath, 'file') };
|
|
363
|
-
}
|
|
364
|
-
if (!stat.isFile())
|
|
365
|
-
return { ok: false, reason: 'not_a_file' };
|
|
366
|
-
return { ok: false, reason: 'not_indexed', explanation: await this.explainNotIndexed(relPath) };
|
|
367
|
-
}
|
|
368
|
-
async explainNotIndexed(relPath) {
|
|
369
|
-
const owner = this.findProjectRoot(r => r !== relPath && isWithin(relPath, r));
|
|
370
|
-
if (!owner)
|
|
371
|
-
return { kind: 'no_project' };
|
|
372
|
-
const inProject = owner.rel === '.' ? relPath : relPath.slice(owner.rel.length + 1);
|
|
373
|
-
const exclusion = (0, file_utils_1.indexingExclusion)(inProject, (0, file_utils_1.loadIgnore)(this.toAbs(owner.rel)));
|
|
374
|
-
if (exclusion)
|
|
375
|
-
return exclusion;
|
|
376
|
-
const [{ exists }] = await this.db.query(`SELECT EXISTS (
|
|
377
|
-
SELECT 1 FROM code_chunk WHERE "serviceName" = $1
|
|
378
|
-
UNION ALL SELECT 1 FROM code_symbol WHERE project = $1
|
|
379
|
-
UNION ALL SELECT 1 FROM code_dependency WHERE project = $1
|
|
380
|
-
UNION ALL SELECT 1 FROM code_call WHERE project = $1
|
|
381
|
-
) AS exists`, [owner.name]);
|
|
382
|
-
return exists ? { kind: 'unknown' } : { kind: 'project_not_indexed', project: owner.name };
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Project-level dependency edges aggregated from `code_dependency`: an edge
|
|
386
|
-
* `A → B` means some file in project A imports a file owned by project B.
|
|
387
|
-
* Used by the graph to draw links between a monorepo's sub-projects. Only
|
|
388
|
-
* edges whose target file belongs to an indexed project are returned (targets
|
|
389
|
-
* outside any indexed project — e.g. unindexed libs — are skipped).
|
|
390
|
-
*/
|
|
391
|
-
async getProjectEdges() {
|
|
392
|
-
const rows = await this.db.query(`SELECT DISTINCT project, "toFile" FROM code_dependency`);
|
|
393
|
-
// Reverse map: workspace-relative project root → name, longest first so the
|
|
394
|
-
// most specific owner wins (e.g. repo/apps/account before repo).
|
|
395
|
-
const byRelPath = [...this.projectRootMap().entries()]
|
|
396
|
-
.map(([name, abs]) => ({ name, rel: path.relative(this.config.workspaceRoot, abs) }))
|
|
397
|
-
.sort((a, b) => b.rel.length - a.rel.length);
|
|
398
|
-
const ownerOf = (toFile) => {
|
|
399
|
-
for (const { name, rel } of byRelPath) {
|
|
400
|
-
if (toFile === rel || toFile.startsWith(rel + path.sep) || toFile.startsWith(rel + '/'))
|
|
401
|
-
return name;
|
|
402
|
-
}
|
|
403
|
-
return null;
|
|
404
|
-
};
|
|
405
|
-
const seen = new Set();
|
|
406
|
-
const edges = [];
|
|
407
|
-
for (const { project, toFile } of rows) {
|
|
408
|
-
const to = ownerOf(toFile);
|
|
409
|
-
if (!to || to === project)
|
|
410
|
-
continue;
|
|
411
|
-
const key = project + ' ' + to;
|
|
412
|
-
if (seen.has(key))
|
|
413
|
-
continue;
|
|
414
|
-
seen.add(key);
|
|
415
|
-
edges.push({ from: project, to });
|
|
416
|
-
}
|
|
417
|
-
return edges;
|
|
418
|
-
}
|
|
419
|
-
// Query translation was removed (legacy). The query is embedded as-is; the
|
|
420
|
-
// method is kept so the tool layer needs no change and `translated` is always
|
|
421
|
-
// false.
|
|
422
|
-
async prepareQuery(raw) {
|
|
423
|
-
return { query: raw, translated: false };
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Refuse when the configured embedding model disagrees with the one that built
|
|
427
|
-
* the index — embedding a query into a foreign vector space returns noise. A
|
|
428
|
-
* missing `index_meta` row with an empty (or altogether absent) `code_chunk`
|
|
429
|
-
* does not block — but a *populated* `code_chunk` with no `index_meta` row is a
|
|
430
|
-
* pre-`index_meta` install, treated as the legacy `ollama`/`bge-m3` index (same
|
|
431
|
-
* interpretation as the `rag-index` legacy-Ollama guard), read-only — this check
|
|
432
|
-
* never writes `index_meta`.
|
|
433
|
-
*/
|
|
434
|
-
async assertIndexMatches() {
|
|
435
|
-
const meta = await this.db.readIndexMeta();
|
|
436
|
-
if (meta) {
|
|
437
|
-
this.compareIndexed(meta.provider, meta.model, meta.dimensions);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
// No row — a missing code_chunk table counts as empty; any other DB error
|
|
441
|
-
// while counting propagates rather than being read as "not indexed".
|
|
442
|
-
if (await (0, legacy_ollama_index_1.hasIndexedChunks)(this.db)) {
|
|
443
|
-
this.compareIndexed(legacy_ollama_index_1.LEGACY_OLLAMA_IDENTITY.provider, legacy_ollama_index_1.LEGACY_OLLAMA_IDENTITY.model, null);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
compareIndexed(indexedProvider, indexedModel, indexedDimensions) {
|
|
447
|
-
const { provider, model } = this.config.embedding;
|
|
448
|
-
if (indexedProvider !== provider || indexedModel !== model) {
|
|
449
|
-
throw new Error(`Index was built with ${indexedProvider}/${indexedModel} but the config selects ${provider}/${model}. ` +
|
|
450
|
-
`Run \`rag-index --reindex\` to rebuild the index for the new model.`);
|
|
451
|
-
}
|
|
452
|
-
// Same model but a changed dimension (e.g. a Matryoshka `embedding.dimensions`
|
|
453
|
-
// override) would embed the query at a width the column can't hold — pgvector
|
|
454
|
-
// would throw an opaque "different vector dimensions" error mid-query. Catch
|
|
455
|
-
// it here with the friendly hint. `embedder.dimensions` is 0 when the width is
|
|
456
|
-
// only knowable by probing, in which case we skip (can't compare cheaply).
|
|
457
|
-
// `indexedDimensions` is null for the legacy-inferred identity, whose provider
|
|
458
|
-
// can never match a configured provider (ollama is no longer selectable), so
|
|
459
|
-
// this branch is unreached in that case.
|
|
460
|
-
const dim = this.embedder.dimensions;
|
|
461
|
-
if (indexedDimensions != null && dim > 0 && dim !== indexedDimensions) {
|
|
462
|
-
throw new Error(`Index was built at ${indexedDimensions} dimensions but ${provider}/${model} now resolves to ${dim}. ` +
|
|
463
|
-
`Run \`rag-index --reindex\` to rebuild the index at the new dimension.`);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
async listProjects() {
|
|
467
|
-
const rows = await this.db.query(`SELECT "serviceName", COUNT(*)::int AS chunks
|
|
468
|
-
FROM code_chunk
|
|
469
|
-
GROUP BY "serviceName"
|
|
470
|
-
ORDER BY "serviceName"`);
|
|
471
|
-
return Promise.all(rows.map(async (r) => {
|
|
472
|
-
const projectRoot = this.resolveProjectRoot(r.serviceName);
|
|
473
|
-
const meta = (0, file_utils_1.readRagMeta)(projectRoot);
|
|
474
|
-
let rootError = null;
|
|
475
|
-
try {
|
|
476
|
-
await this.fs.stat(projectRoot);
|
|
477
|
-
}
|
|
478
|
-
catch (err) {
|
|
479
|
-
rootError = errorCode(err);
|
|
480
|
-
}
|
|
481
|
-
return {
|
|
482
|
-
...r, path: projectRoot, relPath: this.toRel(projectRoot), rootError,
|
|
483
|
-
...(typeof meta === 'object' ? meta : EMPTY_META),
|
|
484
|
-
};
|
|
485
|
-
}));
|
|
486
|
-
}
|
|
487
|
-
async searchChunks(query, limit, project, threshold, rerankOverride) {
|
|
488
|
-
const t0 = Date.now();
|
|
489
|
-
await this.assertIndexMatches();
|
|
490
|
-
const effectiveThreshold = threshold ?? this.config.search.similarityThreshold;
|
|
491
|
-
const reranking = rerankOverride === false ? false : !!this.embedder.rerank;
|
|
492
|
-
// Candidate floor: decouple the pool size from `limit` so a small `limit`
|
|
493
|
-
// (e.g. the default 5) still gives the reranker a large-enough menu. Without
|
|
494
|
-
// a reranker the extra candidates are never reordered, so keep `limit`.
|
|
495
|
-
const fetchLimit = reranking
|
|
496
|
-
? Math.min(Math.max(limit * this.config.reranker.overfetch, this.config.reranker.candidateFloor), 200)
|
|
497
|
-
: limit;
|
|
498
|
-
// With metrics on, fetch one extra row to detect a saturated pool (more
|
|
499
|
-
// under-threshold candidates existed than the window took). The extra row is
|
|
500
|
-
// dropped before reranking so it never affects results.
|
|
501
|
-
const metricsOn = this.config.logging.searchMetrics;
|
|
502
|
-
const dbLimit = metricsOn ? fetchLimit + 1 : fetchLimit;
|
|
503
|
-
this.logger.debug('SearcherService: searchChunks', {
|
|
504
|
-
query: query.slice(0, 120), limit, fetchLimit, project, threshold: effectiveThreshold,
|
|
505
|
-
});
|
|
506
|
-
const tEmbed0 = Date.now();
|
|
507
|
-
const embedding = await this.embedder.embed(query, 'query');
|
|
508
|
-
const embedMs = Date.now() - tEmbed0;
|
|
509
|
-
const params = [`[${embedding}]`, effectiveThreshold, dbLimit];
|
|
510
|
-
let projectFilter = '';
|
|
511
|
-
if (project) {
|
|
512
|
-
params.push(project);
|
|
513
|
-
projectFilter = `AND "serviceName" = $${params.length}`;
|
|
514
|
-
}
|
|
515
|
-
const tDb0 = Date.now();
|
|
516
|
-
const fetched = await this.db.query(`SELECT "serviceName", "filePath", "startLine", content, "ownerPath",
|
|
517
|
-
ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
|
|
518
|
-
FROM code_chunk
|
|
519
|
-
WHERE embedding <=> $1 < $2
|
|
520
|
-
${projectFilter}
|
|
521
|
-
ORDER BY embedding <=> $1
|
|
522
|
-
LIMIT $3`, params);
|
|
523
|
-
const dbMs = Date.now() - tDb0;
|
|
524
|
-
// Saturated iff the extra probe row came back — more candidates were under
|
|
525
|
-
// the threshold than `fetchLimit`. Trim the probe row before reranking.
|
|
526
|
-
const poolSaturated = metricsOn && fetched.length > fetchLimit;
|
|
527
|
-
const candidates = fetched.slice(0, fetchLimit);
|
|
528
|
-
this.logger.debug('SearcherService: searchChunks candidates', {
|
|
529
|
-
count: candidates.length,
|
|
530
|
-
scores: candidates.map(r => r.score),
|
|
531
|
-
files: candidates.map(r => r.filePath),
|
|
532
|
-
ms: Date.now() - t0,
|
|
533
|
-
});
|
|
534
|
-
let results;
|
|
535
|
-
let rerankMs = 0;
|
|
536
|
-
if (!reranking || candidates.length === 0) {
|
|
537
|
-
results = candidates.slice(0, limit);
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
const tRerank0 = Date.now();
|
|
541
|
-
try {
|
|
542
|
-
const indices = await this.embedder.rerank(query, candidates.map(r => r.content), limit);
|
|
543
|
-
rerankMs = Date.now() - tRerank0;
|
|
544
|
-
this.logger.debug('SearcherService: searchChunks rerank done', { indices, ms: Date.now() - t0 });
|
|
545
|
-
// Cap at `limit`: some rerank servers ignore `top_n` and echo back every
|
|
546
|
-
// candidate, so slicing here keeps the result count within the request.
|
|
547
|
-
results = indices.filter(i => i >= 0 && i < candidates.length).slice(0, limit).map(i => candidates[i]);
|
|
548
|
-
}
|
|
549
|
-
catch (err) {
|
|
550
|
-
rerankMs = Date.now() - tRerank0;
|
|
551
|
-
this.logger.warn('SearcherService: rerank failed, falling back to vector order', {
|
|
552
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
553
|
-
ms: Date.now() - t0,
|
|
554
|
-
});
|
|
555
|
-
results = candidates.slice(0, limit);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
if (metricsOn) {
|
|
559
|
-
this.emitSearchMetrics(query, {
|
|
560
|
-
fetchLimit, candidates, results, poolSaturated, embedMs, dbMs, rerankMs, totalMs: Date.now() - t0,
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
return results;
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Emit one structured search-tuning line (via `logger.metric`, at `info`
|
|
567
|
-
* regardless of `logging.level`). `candidates` are in cosine order, so a
|
|
568
|
-
* candidate's array index is its cosine rank — `maxCosineRankSurvived` is the
|
|
569
|
-
* deepest cosine position that survived reranking into the returned set. Read
|
|
570
|
-
* it alongside `poolSaturated` to tune `candidateFloor`/`overfetch`/threshold
|
|
571
|
-
* per hardware. Never logs chunk content, embeddings, or credentials.
|
|
572
|
-
*/
|
|
573
|
-
emitSearchMetrics(query, m) {
|
|
574
|
-
const { candidates, results } = m;
|
|
575
|
-
const contentIndex = new Map(candidates.map((c, i) => [c, i]));
|
|
576
|
-
const maxCosineRankSurvived = results.reduce((max, r) => Math.max(max, contentIndex.get(r) ?? -1), -1);
|
|
577
|
-
const fileCounts = new Map();
|
|
578
|
-
for (const r of results)
|
|
579
|
-
fileCounts.set(r.filePath, (fileCounts.get(r.filePath) ?? 0) + 1);
|
|
580
|
-
const topFileCount = results.length ? Math.max(...fileCounts.values()) : 0;
|
|
581
|
-
this.logger.metric('search metrics', {
|
|
582
|
-
query: query.slice(0, 120),
|
|
583
|
-
fetchLimit: m.fetchLimit,
|
|
584
|
-
candidatesReturned: candidates.length,
|
|
585
|
-
poolSaturated: m.poolSaturated,
|
|
586
|
-
maxCosineRankSurvived,
|
|
587
|
-
resultCount: results.length,
|
|
588
|
-
distinctFilesInResult: fileCounts.size,
|
|
589
|
-
topFileShare: results.length ? +(topFileCount / results.length).toFixed(3) : 0,
|
|
590
|
-
bestResultScore: results.length ? Math.min(...results.map(r => r.score)) : null,
|
|
591
|
-
worstResultScore: results.length ? Math.max(...results.map(r => r.score)) : null,
|
|
592
|
-
embedMs: m.embedMs,
|
|
593
|
-
rerankMs: m.rerankMs,
|
|
594
|
-
dbMs: m.dbMs,
|
|
595
|
-
totalMs: m.totalMs,
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
async fetchNeighbors(results) {
|
|
599
|
-
const t0 = Date.now();
|
|
600
|
-
if (results.length === 0)
|
|
601
|
-
return [];
|
|
602
|
-
this.logger.debug('SearcherService: fetchNeighbors', { count: results.length });
|
|
603
|
-
const values = results.map((_, i) => `(${i}, $${i * 2 + 1}, $${i * 2 + 2}::int)`).join(',');
|
|
604
|
-
const params = results.flatMap(r => [r.filePath, r.startLine]);
|
|
605
|
-
const rows = await this.db.query(`SELECT r.idx,
|
|
606
|
-
prev."serviceName" AS prev_sn, prev."filePath" AS prev_fp, prev."startLine" AS prev_sl, prev.content AS prev_c,
|
|
607
|
-
next."serviceName" AS next_sn, next."filePath" AS next_fp, next."startLine" AS next_sl, next.content AS next_c
|
|
608
|
-
FROM (VALUES ${values}) r(idx, fp, sl)
|
|
609
|
-
LEFT JOIN LATERAL (
|
|
610
|
-
SELECT "serviceName", "filePath", "startLine", content
|
|
611
|
-
FROM code_chunk WHERE "filePath" = r.fp AND "startLine" < r.sl
|
|
612
|
-
ORDER BY "startLine" DESC LIMIT 1
|
|
613
|
-
) prev ON true
|
|
614
|
-
LEFT JOIN LATERAL (
|
|
615
|
-
SELECT "serviceName", "filePath", "startLine", content
|
|
616
|
-
FROM code_chunk WHERE "filePath" = r.fp AND "startLine" > r.sl
|
|
617
|
-
ORDER BY "startLine" ASC LIMIT 1
|
|
618
|
-
) next ON true
|
|
619
|
-
ORDER BY r.idx`, params);
|
|
620
|
-
const neighbors = rows.map((row, i) => ({
|
|
621
|
-
result: results[i],
|
|
622
|
-
prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
|
|
623
|
-
nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
|
|
624
|
-
}));
|
|
625
|
-
this.logger.debug('SearcherService: fetchNeighbors result', {
|
|
626
|
-
withPrev: neighbors.filter(n => n.prevChunk).length,
|
|
627
|
-
withNext: neighbors.filter(n => n.nextChunk).length,
|
|
628
|
-
ms: Date.now() - t0,
|
|
629
|
-
});
|
|
630
|
-
return neighbors;
|
|
631
|
-
}
|
|
632
|
-
async fetchGraphContext(results) {
|
|
633
|
-
const t0 = Date.now();
|
|
634
|
-
const files = [...new Set(results.map(r => r.filePath))];
|
|
635
|
-
this.logger.debug('SearcherService: fetchGraphContext', { files: files.length });
|
|
636
|
-
if (files.length === 0)
|
|
637
|
-
return new Map();
|
|
638
|
-
// Use proportional limit: 50 imports + 50 usedBy per file
|
|
639
|
-
const proportionalLimit = Math.max(2000, files.length * 100);
|
|
640
|
-
const values = files.map((_, i) => `($${i + 1}::text)`).join(', ');
|
|
641
|
-
const rows = await this.db.query(`SELECT r.fp AS "filePath", 'import' AS direction, d."toFile" AS related
|
|
642
|
-
FROM (VALUES ${values}) AS r(fp)
|
|
643
|
-
JOIN code_dependency d ON d."fromFile" = r.fp
|
|
644
|
-
WHERE NOT ${OUTSIDE_WORKSPACE_VALUE('d."toFile"')}
|
|
645
|
-
|
|
646
|
-
UNION ALL
|
|
647
|
-
|
|
648
|
-
SELECT r.fp AS "filePath", 'usedby' AS direction, d."fromFile" AS related
|
|
649
|
-
FROM (VALUES ${values}) AS r(fp)
|
|
650
|
-
JOIN code_dependency d ON d."toFile" = r.fp
|
|
651
|
-
WHERE NOT ${OUTSIDE_WORKSPACE_VALUE('d."fromFile"')}
|
|
652
|
-
|
|
653
|
-
LIMIT ${proportionalLimit}`, files);
|
|
654
|
-
const ctx = new Map();
|
|
655
|
-
for (const filePath of files)
|
|
656
|
-
ctx.set(filePath, { imports: new Set(), usedBy: new Set() });
|
|
657
|
-
for (const row of rows) {
|
|
658
|
-
const g = ctx.get(row.filePath);
|
|
659
|
-
if (row.direction === 'import')
|
|
660
|
-
g.imports.add(row.related);
|
|
661
|
-
else
|
|
662
|
-
g.usedBy.add(row.related);
|
|
663
|
-
}
|
|
664
|
-
this.logger.debug('SearcherService: fetchGraphContext result', { files: files.length, rows: rows.length, ms: Date.now() - t0 });
|
|
665
|
-
const result = new Map();
|
|
666
|
-
for (const [fp, g] of ctx)
|
|
667
|
-
result.set(fp, {
|
|
668
|
-
imports: [...g.imports].slice(0, 50),
|
|
669
|
-
usedBy: [...g.usedBy].slice(0, 50),
|
|
670
|
-
});
|
|
671
|
-
return result;
|
|
672
|
-
}
|
|
673
|
-
async getDependencies(filePath, symbol = null, transitive = false) {
|
|
674
|
-
const t0 = Date.now();
|
|
675
|
-
this.logger.debug('SearcherService: getDependencies', { filePath, symbol, transitive });
|
|
676
|
-
const params = [filePath];
|
|
677
|
-
let symbolFilter = '';
|
|
678
|
-
if (symbol) {
|
|
679
|
-
params.push(symbol);
|
|
680
|
-
symbolFilter = `AND "toSymbol" = $${params.length}`;
|
|
681
|
-
}
|
|
682
|
-
const direct = await this.db.query(`SELECT DISTINCT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
|
|
683
|
-
FROM code_dependency
|
|
684
|
-
WHERE "fromFile" = $1 AND NOT ${OUTSIDE_WORKSPACE_VALUE('"toFile"')}
|
|
685
|
-
${symbolFilter}
|
|
686
|
-
ORDER BY "toFile"`, params);
|
|
687
|
-
if (!transitive) {
|
|
688
|
-
this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, ms: Date.now() - t0 });
|
|
689
|
-
return direct;
|
|
690
|
-
}
|
|
691
|
-
const neighbours = async (frontier) => {
|
|
692
|
-
const rows = await this.db.query(`SELECT DISTINCT "fromFile", "toFile" FROM code_dependency
|
|
693
|
-
WHERE "fromFile" = ANY($1::text[]) AND NOT ${OUTSIDE_WORKSPACE_VALUE('"toFile"')}`, [frontier]);
|
|
694
|
-
return rows.map(r => ({ from: r.fromFile, to: r.toFile }));
|
|
695
|
-
};
|
|
696
|
-
const indirect = (await (0, dependency_traversal_1.traverse)(filePath, neighbours)).map(r => ({
|
|
697
|
-
toFile: r.file, depth: r.depth, via: r.via,
|
|
698
|
-
fromFile: null, fromSymbol: null, toSymbol: null, depType: null,
|
|
699
|
-
}));
|
|
700
|
-
this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, indirect: indirect.length, ms: Date.now() - t0 });
|
|
701
|
-
return [...direct, ...indirect];
|
|
702
|
-
}
|
|
703
|
-
async getDependents(filePath, symbol = null, transitive = false) {
|
|
704
|
-
const t0 = Date.now();
|
|
705
|
-
this.logger.debug('SearcherService: getDependents', { filePath, symbol, transitive });
|
|
706
|
-
const params = [filePath];
|
|
707
|
-
let symbolFilter = '';
|
|
708
|
-
if (symbol) {
|
|
709
|
-
params.push(symbol);
|
|
710
|
-
symbolFilter = `AND "toSymbol" = $${params.length}`;
|
|
711
|
-
}
|
|
712
|
-
const direct = await this.db.query(`SELECT DISTINCT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
|
|
713
|
-
FROM code_dependency
|
|
714
|
-
WHERE "toFile" = $1 AND NOT ${OUTSIDE_WORKSPACE_VALUE('"fromFile"')}
|
|
715
|
-
${symbolFilter}
|
|
716
|
-
ORDER BY "fromFile"`, params);
|
|
717
|
-
if (!transitive) {
|
|
718
|
-
this.logger.debug('SearcherService: getDependents result', { direct: direct.length, ms: Date.now() - t0 });
|
|
719
|
-
return direct;
|
|
720
|
-
}
|
|
721
|
-
const neighbours = async (frontier) => {
|
|
722
|
-
const rows = await this.db.query(`SELECT DISTINCT "fromFile", "toFile" FROM code_dependency
|
|
723
|
-
WHERE "toFile" = ANY($1::text[]) AND NOT ${OUTSIDE_WORKSPACE_VALUE('"fromFile"')}`, [frontier]);
|
|
724
|
-
return rows.map(r => ({ from: r.toFile, to: r.fromFile }));
|
|
725
|
-
};
|
|
726
|
-
const indirect = (await (0, dependency_traversal_1.traverse)(filePath, neighbours)).map(r => ({
|
|
727
|
-
fromFile: r.file, depth: r.depth, via: r.via,
|
|
728
|
-
toFile: null, fromSymbol: null, toSymbol: null, depType: null,
|
|
729
|
-
}));
|
|
730
|
-
this.logger.debug('SearcherService: getDependents result', { direct: direct.length, indirect: indirect.length, ms: Date.now() - t0 });
|
|
731
|
-
return [...direct, ...indirect];
|
|
732
|
-
}
|
|
733
|
-
async getCallers(symbol, project, calleeFile = null) {
|
|
734
|
-
const t0 = Date.now();
|
|
735
|
-
this.logger.debug('SearcherService: getCallers', { symbol, project, calleeFile });
|
|
736
|
-
const parsed = (0, symbol_name_1.parseCallerSymbol)(symbol);
|
|
737
|
-
if (!parsed)
|
|
738
|
-
throw new Error('Invalid symbol name');
|
|
739
|
-
const params = [project];
|
|
740
|
-
let nameFilter;
|
|
741
|
-
if (parsed.form === 'bare') {
|
|
742
|
-
params.push(parsed.name);
|
|
743
|
-
nameFilter = `"calleeName" = $${params.length}`;
|
|
744
|
-
}
|
|
745
|
-
else {
|
|
746
|
-
params.push(`${parsed.container}.${parsed.name}`);
|
|
747
|
-
const exactParam = params.length;
|
|
748
|
-
params.push(`%.${(0, symbol_name_1.escapeLike)(parsed.container)}.${(0, symbol_name_1.escapeLike)(parsed.name)}`);
|
|
749
|
-
const suffixParam = params.length;
|
|
750
|
-
nameFilter = `("calleeSymbol" = $${exactParam} OR "calleeSymbol" LIKE $${suffixParam} ESCAPE '\\')`;
|
|
751
|
-
}
|
|
752
|
-
let fileFilter = '';
|
|
753
|
-
if (calleeFile) {
|
|
754
|
-
params.push(calleeFile);
|
|
755
|
-
fileFilter = `AND "calleeFile" = $${params.length}`;
|
|
756
|
-
}
|
|
757
|
-
const rows = await this.db.query(`SELECT "fromFile", "fromSymbol", "calleeName", "calleeFile", line
|
|
758
|
-
FROM code_call
|
|
759
|
-
WHERE project = $1 AND ${nameFilter}
|
|
760
|
-
${fileFilter}
|
|
761
|
-
ORDER BY "fromFile", line
|
|
762
|
-
LIMIT 200`, params);
|
|
763
|
-
this.logger.debug('SearcherService: getCallers result', { count: rows.length, ms: Date.now() - t0 });
|
|
764
|
-
return rows;
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Whole-word text search for `symbol` under `rootDir` (workspace-relative).
|
|
768
|
-
* grep exiting ≥ 2 after printing matches is a partial result; without
|
|
769
|
-
* matches, or when grep cannot be spawned, the search failed.
|
|
770
|
-
*/
|
|
771
|
-
async grepSymbol(symbol, rootDir, excludeFile) {
|
|
772
|
-
const t0 = Date.now();
|
|
773
|
-
this.logger.debug('SearcherService: grepSymbol', { symbol, rootDir, excludeFile });
|
|
774
|
-
const result = await this.spawnGrep([
|
|
775
|
-
'-rn', '-F', '-w',
|
|
776
|
-
'-m', String(GREP_RAW_LIMIT),
|
|
777
|
-
'--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
|
|
778
|
-
...GREP_EXCLUDE_DIRS,
|
|
779
|
-
symbol,
|
|
780
|
-
this.toAbs(rootDir),
|
|
781
|
-
]);
|
|
782
|
-
if (!result.ok && result.output === null)
|
|
783
|
-
return { ok: false };
|
|
784
|
-
const excludeAbsPath = path.join(this.config.workspaceRoot, excludeFile);
|
|
785
|
-
const all = (result.output ?? '').split('\n').flatMap(line => {
|
|
786
|
-
const match = line.match(/^(.+?):(\d+):/);
|
|
787
|
-
if (!match)
|
|
788
|
-
return [];
|
|
789
|
-
const [, absFile, lineNum] = match;
|
|
790
|
-
if (absFile === excludeAbsPath)
|
|
791
|
-
return [];
|
|
792
|
-
return [{ filePath: path.relative(this.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
|
|
793
|
-
});
|
|
794
|
-
const truncated = all.length > GREP_MAX_RESULTS;
|
|
795
|
-
this.logger.debug('SearcherService: grepSymbol result', { matches: Math.min(all.length, GREP_MAX_RESULTS), truncated, partial: !result.ok, ms: Date.now() - t0 });
|
|
796
|
-
return { ok: true, matches: all.slice(0, GREP_MAX_RESULTS), truncated, partial: !result.ok };
|
|
797
|
-
}
|
|
798
|
-
// Sync wrapper: validates name (throws synchronously), then returns Promise.
|
|
799
|
-
// Every kind, including 'method', is looked up in the index first.
|
|
800
|
-
findSymbol(name, project, kind) {
|
|
801
|
-
const parsed = (0, symbol_name_1.parseFindSymbolName)(name);
|
|
802
|
-
if (!parsed)
|
|
803
|
-
throw new Error('Invalid symbol name');
|
|
804
|
-
return this.findSymbolFromIndex(parsed, project, kind);
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Whether `code_symbol.target` exists. A present result is cached for the
|
|
808
|
-
* process; an absent one is re-checked on every call. Propagates on a
|
|
809
|
-
* failing check, same as any other failed index query.
|
|
810
|
-
*/
|
|
811
|
-
async hasTargetColumn() {
|
|
812
|
-
if (this.targetColumnPresent)
|
|
813
|
-
return true;
|
|
814
|
-
const rows = await this.db.query(`SELECT EXISTS (
|
|
815
|
-
SELECT 1 FROM information_schema.columns
|
|
816
|
-
WHERE table_schema = 'public' AND table_name = 'code_symbol' AND column_name = 'target'
|
|
817
|
-
) AS present`);
|
|
818
|
-
const present = !!rows[0]?.present;
|
|
819
|
-
if (present)
|
|
820
|
-
this.targetColumnPresent = true;
|
|
821
|
-
return present;
|
|
822
|
-
}
|
|
823
|
-
async findSymbolFromIndex(parsed, project, kind) {
|
|
824
|
-
const t0 = Date.now();
|
|
825
|
-
this.logger.debug('SearcherService: findSymbol', { parsed, project, kind, source: 'index' });
|
|
826
|
-
const container = 'container' in parsed ? parsed.container : null;
|
|
827
|
-
const isGraphqlKind = GRAPHQL_KINDS.has(kind);
|
|
828
|
-
try {
|
|
829
|
-
const hasTarget = await this.hasTargetColumn();
|
|
830
|
-
const params = [parsed.name];
|
|
831
|
-
let nameFilter = 'name = $1';
|
|
832
|
-
let orderPrefix = '';
|
|
833
|
-
if (container !== null) {
|
|
834
|
-
params.push(container);
|
|
835
|
-
const exactParam = params.length;
|
|
836
|
-
params.push(`%.${(0, symbol_name_1.escapeLike)(container)}`);
|
|
837
|
-
const suffixParam = params.length;
|
|
838
|
-
nameFilter += ` AND (container = $${exactParam} OR container LIKE $${suffixParam} ESCAPE '\\')`;
|
|
839
|
-
orderPrefix = `(CASE WHEN container = $${exactParam} THEN 0 ELSE 1 END), `;
|
|
840
|
-
}
|
|
841
|
-
let projectFilter = '';
|
|
842
|
-
if (project) {
|
|
843
|
-
params.push(project);
|
|
844
|
-
projectFilter = `AND project = $${params.length}`;
|
|
845
|
-
}
|
|
846
|
-
let kindFilter = '';
|
|
847
|
-
if (kind !== 'all') {
|
|
848
|
-
params.push(kind);
|
|
849
|
-
kindFilter = `AND kind = $${params.length}`;
|
|
850
|
-
}
|
|
851
|
-
params.push(FIND_SYMBOL_LIMIT + 1);
|
|
852
|
-
const targetSelect = hasTarget ? '"target"' : 'NULL AS target';
|
|
853
|
-
const rows = await this.db.query(`SELECT "filePath", name, kind, signature, line, col, container, "isStatic", ${targetSelect}
|
|
854
|
-
FROM code_symbol
|
|
855
|
-
WHERE ${nameFilter} ${projectFilter} ${kindFilter}
|
|
856
|
-
ORDER BY ${orderPrefix}project, "filePath", line, col, kind COLLATE "C", container COLLATE "C" NULLS FIRST
|
|
857
|
-
LIMIT $${params.length}`, params);
|
|
858
|
-
const truncated = rows.length > FIND_SYMBOL_LIMIT;
|
|
859
|
-
const hits = rows.slice(0, FIND_SYMBOL_LIMIT).map(row => ({
|
|
860
|
-
filePath: row.filePath,
|
|
861
|
-
line: row.line ?? 1,
|
|
862
|
-
column: row.col ?? 1,
|
|
863
|
-
kind: row.kind,
|
|
864
|
-
signature: row.signature ?? row.name,
|
|
865
|
-
qualified: row.container ? `${row.container}.${row.name}` : row.name,
|
|
866
|
-
isStatic: !!row.isStatic,
|
|
867
|
-
source: 'index',
|
|
868
|
-
target: row.target ?? null,
|
|
869
|
-
}));
|
|
870
|
-
this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated, ms: Date.now() - t0 });
|
|
871
|
-
if (hits.length > 0)
|
|
872
|
-
return { hits, truncated, grepFailed: false };
|
|
873
|
-
// GraphQL kinds never grep — an empty result just means no row yet (stale index).
|
|
874
|
-
if (isGraphqlKind)
|
|
875
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
876
|
-
if (parsed.form === 'quoted' || parsed.form === 'computed')
|
|
877
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
878
|
-
return this.grepFindSymbol(parsed, project, kind);
|
|
879
|
-
}
|
|
880
|
-
catch (err) {
|
|
881
|
-
this.logger.warn('SearcherService: symbol index unavailable', { reason: err.message, ms: Date.now() - t0 });
|
|
882
|
-
// GraphQL kinds never grep — a failing index query propagates as an error response instead.
|
|
883
|
-
if (isGraphqlKind)
|
|
884
|
-
throw err;
|
|
885
|
-
if (parsed.form === 'quoted' || parsed.form === 'computed')
|
|
886
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
887
|
-
return this.grepFindSymbol(parsed, project, kind);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
async grepFindSymbol(parsed, project, kind) {
|
|
891
|
-
const t0 = Date.now();
|
|
892
|
-
const grepName = parsed.name; // qualified names are grepped by their last segment, no container filter
|
|
893
|
-
this.logger.debug('SearcherService: findSymbol', { grepName, project, kind, source: 'grep' });
|
|
894
|
-
const pattern = this.buildFindSymbolPattern(grepName, kind);
|
|
895
|
-
if (pattern === null)
|
|
896
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
897
|
-
const searchDir = project
|
|
898
|
-
? this.resolveProjectRoot(project)
|
|
899
|
-
: this.config.workspaceRoot;
|
|
900
|
-
const result = await this.spawnGrep([
|
|
901
|
-
'-rn', '-E',
|
|
902
|
-
'-m', String(FIND_SYMBOL_RAW_LIMIT),
|
|
903
|
-
'--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
|
|
904
|
-
...GREP_EXCLUDE_DIRS,
|
|
905
|
-
pattern,
|
|
906
|
-
searchDir,
|
|
907
|
-
]);
|
|
908
|
-
if (!result.ok) {
|
|
909
|
-
this.logger.warn('SearcherService: findSymbol grep failed', { grepName, project });
|
|
910
|
-
return { hits: [], truncated: false, grepFailed: true };
|
|
911
|
-
}
|
|
912
|
-
if (result.output === null)
|
|
913
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
914
|
-
const parsedHits = [];
|
|
915
|
-
for (const raw of result.output.split('\n')) {
|
|
916
|
-
const m = raw.match(/^(.+?):(\d+):(.*)$/);
|
|
917
|
-
if (!m)
|
|
918
|
-
continue;
|
|
919
|
-
const [, absFile, lineNumStr, matchedLine] = m;
|
|
920
|
-
const lineNum = parseInt(lineNumStr, 10);
|
|
921
|
-
if (isNaN(lineNum))
|
|
922
|
-
continue;
|
|
923
|
-
const col = SearcherService.wholeWordIndex(matchedLine, grepName);
|
|
924
|
-
if (col === -1)
|
|
925
|
-
continue;
|
|
926
|
-
parsedHits.push({
|
|
927
|
-
filePath: path.relative(this.config.workspaceRoot, absFile),
|
|
928
|
-
line: lineNum,
|
|
929
|
-
column: col + 1,
|
|
930
|
-
kind: this.inferKind(matchedLine, grepName),
|
|
931
|
-
signature: matchedLine.trim(),
|
|
932
|
-
qualified: grepName,
|
|
933
|
-
isStatic: false,
|
|
934
|
-
source: 'grep',
|
|
935
|
-
target: null,
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
const truncated = parsedHits.length > FIND_SYMBOL_LIMIT;
|
|
939
|
-
this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: Math.min(parsedHits.length, FIND_SYMBOL_LIMIT), truncated, ms: Date.now() - t0 });
|
|
940
|
-
return { hits: parsedHits.slice(0, FIND_SYMBOL_LIMIT), truncated, grepFailed: false };
|
|
941
|
-
}
|
|
942
|
-
async listFiles(relPath, depth, pattern) {
|
|
943
|
-
const t0 = Date.now();
|
|
944
|
-
this.logger.debug('SearcherService: listFiles', { relPath, depth, pattern });
|
|
945
|
-
const absPath = this.toAbs(relPath);
|
|
946
|
-
const loc = await this.checkRealLocation(relPath, absPath, 'dir');
|
|
947
|
-
if (!loc.ok)
|
|
948
|
-
return loc;
|
|
949
|
-
let stat;
|
|
950
|
-
try {
|
|
951
|
-
stat = await this.fs.stat(absPath);
|
|
952
|
-
}
|
|
953
|
-
catch (err) {
|
|
954
|
-
if (errorCode(err) !== 'ENOENT')
|
|
955
|
-
return { ok: false, reason: 'unreadable', code: errorCode(err) };
|
|
956
|
-
return { ok: false, reason: 'not_found', candidates: await this.candidatesOrNone(relPath, 'dir') };
|
|
957
|
-
}
|
|
958
|
-
if (!stat.isDirectory())
|
|
959
|
-
return { ok: false, reason: 'not_a_directory' };
|
|
960
|
-
const clampedDepth = Math.min(5, Math.max(1, depth));
|
|
961
|
-
const lines = [];
|
|
962
|
-
try {
|
|
963
|
-
await this.walkDir(absPath, 0, clampedDepth - 1, pattern, lines);
|
|
964
|
-
}
|
|
965
|
-
catch (err) {
|
|
966
|
-
return { ok: false, reason: 'unreadable', code: errorCode(err) };
|
|
967
|
-
}
|
|
968
|
-
this.logger.debug('SearcherService: listFiles result', { entries: lines.length, ms: Date.now() - t0 });
|
|
969
|
-
if (lines.length === 0) {
|
|
970
|
-
return { ok: true, tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
|
|
971
|
-
}
|
|
972
|
-
return { ok: true, tree: lines.join('\n') };
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* The read tools' common front: workspace boundary, then existence and type.
|
|
976
|
-
* A missing file still in the index is `deleted`; otherwise `not_found`.
|
|
977
|
-
*/
|
|
978
|
-
async openableFile(relPath, absPath) {
|
|
979
|
-
const loc = await this.checkRealLocation(relPath, absPath, 'file');
|
|
980
|
-
if (!loc.ok)
|
|
981
|
-
return loc;
|
|
982
|
-
let stat;
|
|
983
|
-
try {
|
|
984
|
-
stat = await this.fs.stat(absPath);
|
|
985
|
-
}
|
|
986
|
-
catch (err) {
|
|
987
|
-
if (errorCode(err) !== 'ENOENT')
|
|
988
|
-
return { ok: false, reason: 'unreadable', code: errorCode(err) };
|
|
989
|
-
if (await this.isIndexedPath(relPath))
|
|
990
|
-
return { ok: false, reason: 'deleted' };
|
|
991
|
-
return { ok: false, reason: 'not_found', candidates: await this.candidatesOrNone(relPath, 'file') };
|
|
992
|
-
}
|
|
993
|
-
// Checked before opening: a FIFO or device would block the reader.
|
|
994
|
-
return stat.isFile() ? null : { ok: false, reason: 'not_a_file' };
|
|
995
|
-
}
|
|
996
|
-
/**
|
|
997
|
-
* Streams the file's lines (as `readline` yields them) to `onLine` until it
|
|
998
|
-
* returns false. An open or read error discards everything read so far.
|
|
999
|
-
*/
|
|
1000
|
-
async readLines(absPath, onLine) {
|
|
1001
|
-
const stream = this.fs.createReadStream(absPath);
|
|
1002
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1003
|
-
let lineNo = 0;
|
|
1004
|
-
try {
|
|
1005
|
-
for await (const line of rl) {
|
|
1006
|
-
lineNo++;
|
|
1007
|
-
if (!onLine(line, lineNo))
|
|
1008
|
-
break;
|
|
1009
|
-
}
|
|
1010
|
-
return { ok: true, lineCount: lineNo };
|
|
1011
|
-
}
|
|
1012
|
-
catch (err) {
|
|
1013
|
-
return { ok: false, code: errorCode(err) };
|
|
1014
|
-
}
|
|
1015
|
-
finally {
|
|
1016
|
-
rl.close();
|
|
1017
|
-
stream.destroy?.();
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
async readFile(relPath, maxLines) {
|
|
1021
|
-
const t0 = Date.now();
|
|
1022
|
-
this.logger.debug('SearcherService: readFile', { relPath, maxLines });
|
|
1023
|
-
const absPath = this.toAbs(relPath);
|
|
1024
|
-
const failure = await this.openableFile(relPath, absPath);
|
|
1025
|
-
if (failure)
|
|
1026
|
-
return failure;
|
|
1027
|
-
const lines = [];
|
|
1028
|
-
let hasMore = false;
|
|
1029
|
-
const read = await this.readLines(absPath, line => {
|
|
1030
|
-
if (lines.length < maxLines) {
|
|
1031
|
-
lines.push(line);
|
|
1032
|
-
return true;
|
|
1033
|
-
}
|
|
1034
|
-
hasMore = true;
|
|
1035
|
-
return false;
|
|
1036
|
-
});
|
|
1037
|
-
if (!read.ok)
|
|
1038
|
-
return { ok: false, reason: 'unreadable', code: read.code };
|
|
1039
|
-
this.logger.debug('SearcherService: readFile result', { linesRead: lines.length, hasMore, ms: Date.now() - t0 });
|
|
1040
|
-
return { ok: true, filePath: relPath, startLine: 1, endLine: lines.length, lines, hasMore };
|
|
1041
|
-
}
|
|
1042
|
-
async readFileRange(relPath, startLine, endLine) {
|
|
1043
|
-
const t0 = Date.now();
|
|
1044
|
-
this.logger.debug('SearcherService: readFileRange', { relPath, startLine, endLine });
|
|
1045
|
-
if (endLine < startLine)
|
|
1046
|
-
return { ok: false, reason: 'bad_range', startLine, endLine };
|
|
1047
|
-
const absPath = this.toAbs(relPath);
|
|
1048
|
-
const failure = await this.openableFile(relPath, absPath);
|
|
1049
|
-
if (failure)
|
|
1050
|
-
return failure;
|
|
1051
|
-
const lines = [];
|
|
1052
|
-
let lastRead = 0;
|
|
1053
|
-
let hasMore = false;
|
|
1054
|
-
const read = await this.readLines(absPath, (line, lineNo) => {
|
|
1055
|
-
if (lineNo > endLine) {
|
|
1056
|
-
hasMore = true;
|
|
1057
|
-
return false;
|
|
1058
|
-
}
|
|
1059
|
-
if (lineNo >= startLine)
|
|
1060
|
-
lines.push(line);
|
|
1061
|
-
lastRead = lineNo;
|
|
1062
|
-
return true;
|
|
1063
|
-
});
|
|
1064
|
-
if (!read.ok)
|
|
1065
|
-
return { ok: false, reason: 'unreadable', code: read.code };
|
|
1066
|
-
if (!hasMore && startLine > Math.max(read.lineCount, 1)) {
|
|
1067
|
-
return { ok: false, reason: 'past_end', lineCount: read.lineCount, startLine };
|
|
1068
|
-
}
|
|
1069
|
-
this.logger.debug('SearcherService: readFileRange result', { linesRead: lines.length, hasMore, ms: Date.now() - t0 });
|
|
1070
|
-
return { ok: true, filePath: relPath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
|
|
1071
|
-
}
|
|
1072
|
-
/** Throws when `absDir` itself cannot be read; an unreadable subdirectory is marked in place. */
|
|
1073
|
-
async walkDir(absDir, currentDepth, maxDepth, pattern, lines) {
|
|
1074
|
-
const entries = await this.fs.readdir(absDir);
|
|
1075
|
-
entries.sort((a, b) => {
|
|
1076
|
-
const ad = a.isDirectory() ? 0 : 1;
|
|
1077
|
-
const bd = b.isDirectory() ? 0 : 1;
|
|
1078
|
-
if (ad !== bd)
|
|
1079
|
-
return ad - bd;
|
|
1080
|
-
return a.name.localeCompare(b.name);
|
|
1081
|
-
});
|
|
1082
|
-
for (const entry of entries) {
|
|
1083
|
-
if (file_utils_1.IGNORE_DIRS.has(entry.name))
|
|
1084
|
-
continue;
|
|
1085
|
-
const indent = ' '.repeat(currentDepth);
|
|
1086
|
-
if (entry.isDirectory()) {
|
|
1087
|
-
const at = lines.push(`${indent}${entry.name}/`) - 1;
|
|
1088
|
-
if (currentDepth < maxDepth) {
|
|
1089
|
-
try {
|
|
1090
|
-
await this.walkDir(path.join(absDir, entry.name), currentDepth + 1, maxDepth, pattern, lines);
|
|
1091
|
-
}
|
|
1092
|
-
catch {
|
|
1093
|
-
lines[at] += ' (unreadable)';
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
else {
|
|
1098
|
-
if (pattern && !entry.name.includes(pattern))
|
|
1099
|
-
continue;
|
|
1100
|
-
lines.push(`${indent}${entry.name}`);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* ERE pattern for a grep-fallback kind, or null when that kind is never
|
|
1106
|
-
* grepped (`function_property`/`method_signature` are index-only; a bare
|
|
1107
|
-
* `constructor` search only makes sense when the name itself is `constructor`).
|
|
1108
|
-
*/
|
|
1109
|
-
buildFindSymbolPattern(name, kind) {
|
|
1110
|
-
if (kind === 'function_property' || kind === 'method_signature')
|
|
1111
|
-
return null;
|
|
1112
|
-
if (GRAPHQL_KINDS.has(kind))
|
|
1113
|
-
return null;
|
|
1114
|
-
if (kind === 'constructor')
|
|
1115
|
-
return name === 'constructor' ? `(^|[[:space:]])constructor[[:space:]]*\\(` : null;
|
|
1116
|
-
const esc = name.replace(/\$/g, '\\$'); // ERE end-of-line metachar
|
|
1117
|
-
const MODIFIER = '(public|private|protected|static|abstract|async|override)';
|
|
1118
|
-
const BOUNDARY = '(^|[^[:alnum:]_$])';
|
|
1119
|
-
const patterns = {
|
|
1120
|
-
function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${esc}[[:space:](<]`,
|
|
1121
|
-
class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${esc}[[:space:]{<]`,
|
|
1122
|
-
type: `(export[[:space:]]+)?type[[:space:]]+${esc}[[:space:]=<]`,
|
|
1123
|
-
interface: `(export[[:space:]]+)?interface[[:space:]]+${esc}[[:space:]{<]`,
|
|
1124
|
-
variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${esc}[[:space:]=:]`,
|
|
1125
|
-
method: `${BOUNDARY}(${MODIFIER}[[:space:]]+)+\\*?[[:space:]]*${esc}[[:space:]]*[(<]`,
|
|
1126
|
-
get_accessor: `(^|[[:space:]])get[[:space:]]+${esc}[[:space:]]*\\(`,
|
|
1127
|
-
set_accessor: `(^|[[:space:]])set[[:space:]]+${esc}[[:space:]]*\\(`,
|
|
1128
|
-
};
|
|
1129
|
-
if (kind === 'all')
|
|
1130
|
-
return Object.values(patterns).join('|');
|
|
1131
|
-
return patterns[kind];
|
|
1132
|
-
}
|
|
1133
|
-
/** 1-based index of the first whole-word occurrence of `name` in `line`, or -1. */
|
|
1134
|
-
static wholeWordIndex(line, name) {
|
|
1135
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1136
|
-
const re = new RegExp(`(?<![\\w$#])${escaped}(?![\\w$#])`);
|
|
1137
|
-
const m = re.exec(line);
|
|
1138
|
-
return m ? m.index : -1;
|
|
1139
|
-
}
|
|
1140
|
-
inferKind(line, name) {
|
|
1141
|
-
if (name === 'constructor')
|
|
1142
|
-
return 'constructor';
|
|
1143
|
-
const idx = SearcherService.wholeWordIndex(line, name);
|
|
1144
|
-
const before = idx === -1 ? line : line.slice(0, idx);
|
|
1145
|
-
const tokens = before.trim().split(/\s+/).filter(Boolean);
|
|
1146
|
-
const last = (tokens[tokens.length - 1] ?? '').replace(/\*$/, '');
|
|
1147
|
-
if (last === 'get')
|
|
1148
|
-
return 'get_accessor';
|
|
1149
|
-
if (last === 'set')
|
|
1150
|
-
return 'set_accessor';
|
|
1151
|
-
if (last === 'function')
|
|
1152
|
-
return 'function';
|
|
1153
|
-
if (last === 'class')
|
|
1154
|
-
return 'class';
|
|
1155
|
-
if (last === 'interface')
|
|
1156
|
-
return 'interface';
|
|
1157
|
-
if (last === 'type')
|
|
1158
|
-
return 'type';
|
|
1159
|
-
if (['public', 'private', 'protected', 'static', 'abstract', 'async', 'override'].includes(last))
|
|
1160
|
-
return 'method';
|
|
1161
|
-
return 'variable';
|
|
1162
|
-
}
|
|
1163
|
-
/**
|
|
1164
|
-
* Async wrapper around grep to avoid blocking the event loop. `ok: false`
|
|
1165
|
-
* means grep could not be spawned (`output: null`) or exited with status ≥ 2,
|
|
1166
|
-
* keeping whatever it printed — distinguishable from a clean "no matches",
|
|
1167
|
-
* which is `ok: true, output: null`.
|
|
1168
|
-
*/
|
|
1169
|
-
spawnGrep(args) {
|
|
1170
|
-
return new Promise((resolve) => {
|
|
1171
|
-
const proc = (0, child_process_1.spawn)('grep', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1172
|
-
const chunks = [];
|
|
1173
|
-
let totalBytes = 0;
|
|
1174
|
-
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
1175
|
-
proc.stdout.on('data', (chunk) => {
|
|
1176
|
-
totalBytes += chunk.length;
|
|
1177
|
-
if (totalBytes <= MAX_BUFFER) {
|
|
1178
|
-
chunks.push(chunk);
|
|
1179
|
-
}
|
|
1180
|
-
});
|
|
1181
|
-
proc.stderr.on('data', () => { });
|
|
1182
|
-
proc.on('error', (err) => {
|
|
1183
|
-
this.logger.error('SearcherService: grep spawn failed', { reason: err.message });
|
|
1184
|
-
resolve({ ok: false, output: null });
|
|
1185
|
-
});
|
|
1186
|
-
proc.on('close', (code) => {
|
|
1187
|
-
const output = chunks.length === 0 ? null : Buffer.concat(chunks).toString('utf-8');
|
|
1188
|
-
// grep returns 1 for "no matches" — not an error; 2+ is a real failure
|
|
1189
|
-
if (code !== null && code >= 2) {
|
|
1190
|
-
this.logger.warn('SearcherService: grep exited with code', { code });
|
|
1191
|
-
resolve({ ok: false, output });
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
resolve({ ok: true, output });
|
|
1195
|
-
});
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
exports.SearcherService = SearcherService;
|