codesearch-mcp 0.3.0-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 +59 -6
- package/LICENSE +661 -21
- package/README.md +33 -17
- package/dist/app.js +13 -20
- 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/commands/EvalCommand.d.ts +7 -3
- package/dist/src/commands/EvalCommand.js +36 -158
- package/dist/src/commands/IndexCommand.d.ts +10 -4
- package/dist/src/commands/IndexCommand.js +52 -124
- package/dist/src/commands/McpCommand.d.ts +6 -3
- package/dist/src/commands/McpCommand.js +19 -14
- package/dist/src/commands/WebCommand.d.ts +9 -3
- package/dist/src/commands/WebCommand.js +17 -8
- 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-store.d.ts +0 -1
- package/dist/src/config-store.js +3 -4
- package/dist/src/config.d.ts +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/constants.d.ts +15 -0
- package/dist/src/constants.js +34 -6
- package/dist/src/container.d.ts +4 -2
- package/dist/src/container.js +7 -5
- package/dist/src/db/drizzle-types.d.ts +7 -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/declarations/DeclarationTree.d.ts +1 -1
- package/dist/src/eval-results.js +16 -2
- package/dist/src/eval-vector-index.d.ts +5 -0
- package/dist/src/eval-vector-index.js +60 -0
- package/dist/src/help.js +9 -1
- 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 +2 -1
- 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/IDatabase.d.ts +6 -2
- package/dist/src/interfaces/IDependencyGraph.d.ts +24 -0
- package/dist/src/interfaces/IDependencyGraph.js +2 -0
- 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/IMcpReadiness.d.ts +12 -0
- package/dist/src/interfaces/IMcpReadiness.js +2 -0
- package/dist/src/interfaces/IProjectCatalog.d.ts +13 -0
- package/dist/src/interfaces/IProjectCatalog.js +2 -0
- package/dist/src/interfaces/IProjectRoots.d.ts +3 -0
- package/dist/src/interfaces/IProjectRoots.js +2 -0
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +21 -2
- 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/project-scope.d.ts +1 -0
- package/dist/src/project-scope.js +5 -0
- package/dist/src/retrieval-eval.d.ts +2 -1
- package/dist/src/retrieval-eval.js +45 -2
- 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/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 +8 -44
- package/dist/src/services/AstParserService.js +19 -938
- package/dist/src/services/DatabaseService.d.ts +8 -10
- package/dist/src/services/DatabaseService.js +29 -86
- package/dist/src/services/IndexerService.d.ts +7 -10
- package/dist/src/services/IndexerService.js +36 -255
- package/dist/src/services/McpReadiness.d.ts +18 -0
- package/dist/src/services/McpReadiness.js +73 -0
- package/dist/src/services/McpServer.d.ts +32 -9
- package/dist/src/services/McpServer.js +78 -41
- package/dist/src/services/WebServerService.d.ts +4 -3
- package/dist/src/services/WebServerService.js +16 -8
- package/dist/src/setup.d.ts +16 -4
- package/dist/src/setup.js +115 -40
- package/dist/src/symbol-name.d.ts +1 -0
- package/dist/src/symbol-name.js +16 -12
- 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 +35 -28
- package/dist/src/tools/GetDependentsTool.d.ts +20 -8
- package/dist/src/tools/GetDependentsTool.js +47 -33
- 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 +69 -51
- package/dist/src/tools/format-helpers.d.ts +9 -3
- package/dist/src/tools/format-helpers.js +59 -14
- package/dist/src/tools/tool-args.d.ts +38 -0
- package/dist/src/tools/tool-args.js +95 -0
- 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 +4 -3
- package/dist/src/interfaces/ISearcher.d.ts +0 -46
- package/dist/src/services/SearcherService.d.ts +0 -235
- package/dist/src/services/SearcherService.js +0 -1118
- /package/dist/src/{interfaces/ISearcher.js → db/drizzle-types.js} +0 -0
|
@@ -1,1118 +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
|
-
exports.rerankDocument = rerankDocument;
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
|
-
const path = __importStar(require("path"));
|
|
40
|
-
const readline = __importStar(require("readline"));
|
|
41
|
-
const child_process_1 = require("child_process");
|
|
42
|
-
const file_utils_1 = require("../file-utils");
|
|
43
|
-
const MonorepoAwareDiscoveryService_1 = require("./MonorepoAwareDiscoveryService");
|
|
44
|
-
const legacy_ollama_index_1 = require("./legacy-ollama-index");
|
|
45
|
-
const symbol_name_1 = require("../symbol-name");
|
|
46
|
-
const dependency_traversal_1 = require("../dependency-traversal");
|
|
47
|
-
const result_shaping_1 = require("../search/result-shaping");
|
|
48
|
-
const hnsw_settings_1 = require("../hnsw-settings");
|
|
49
|
-
const SILENT_LOGGER = {
|
|
50
|
-
debug() { },
|
|
51
|
-
info() { },
|
|
52
|
-
warn() { },
|
|
53
|
-
error() { },
|
|
54
|
-
withContext(_, fn) { return fn(); },
|
|
55
|
-
};
|
|
56
|
-
const NODE_FS = {
|
|
57
|
-
realpath: p => fs.promises.realpath(p),
|
|
58
|
-
stat: p => fs.promises.stat(p),
|
|
59
|
-
readdir: p => fs.promises.readdir(p, { withFileTypes: true }),
|
|
60
|
-
createReadStream: p => fs.createReadStream(p, { encoding: 'utf-8' }),
|
|
61
|
-
};
|
|
62
|
-
const GRAPHQL_KINDS = new Set([
|
|
63
|
-
'graphql_query', 'graphql_mutation', 'graphql_subscription', 'graphql_field', 'graphql_type',
|
|
64
|
-
]);
|
|
65
|
-
const EMPTY_META = { description: null, stack: null, notes: null, relations: null };
|
|
66
|
-
const GREP_MAX_RESULTS = 200;
|
|
67
|
-
const GREP_RAW_LIMIT = GREP_MAX_RESULTS * 5;
|
|
68
|
-
const GREP_EXCLUDE_DIRS = [...file_utils_1.IGNORE_DIRS].map(d => `--exclude-dir=${d}`);
|
|
69
|
-
const FIND_SYMBOL_LIMIT = 50;
|
|
70
|
-
const FIND_SYMBOL_RAW_LIMIT = FIND_SYMBOL_LIMIT * 10;
|
|
71
|
-
const PATH_CANDIDATE_LIMIT = 3;
|
|
72
|
-
const INDEX_PATH_COLUMNS = [
|
|
73
|
-
{ table: 'code_chunk', column: '"filePath"', content: true },
|
|
74
|
-
{ table: 'code_symbol', column: '"filePath"', content: true },
|
|
75
|
-
{ table: 'code_dependency', column: '"fromFile"', content: true },
|
|
76
|
-
{ table: 'code_call', column: '"fromFile"', content: true },
|
|
77
|
-
{ table: 'code_dependency', column: '"toFile"', content: false },
|
|
78
|
-
{ table: 'code_call', column: '"calleeFile"', content: false },
|
|
79
|
-
];
|
|
80
|
-
const OUTSIDE_WORKSPACE_VALUE = (col) => `(${col} = '..' OR ${col} LIKE '../%')`;
|
|
81
|
-
function stripTrailingSlash(p) {
|
|
82
|
-
return p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p;
|
|
83
|
-
}
|
|
84
|
-
function isWithin(p, root) {
|
|
85
|
-
if (root === '.')
|
|
86
|
-
return true;
|
|
87
|
-
if (root === '/')
|
|
88
|
-
return p.startsWith('/');
|
|
89
|
-
return p === root || p.startsWith(root + '/');
|
|
90
|
-
}
|
|
91
|
-
function errorCode(err) {
|
|
92
|
-
return err?.code ?? 'EIO';
|
|
93
|
-
}
|
|
94
|
-
function rerankDocument(row) {
|
|
95
|
-
const symbol = row.ownerPath ? ` | Symbol: ${oneLine(row.ownerPath)}` : '';
|
|
96
|
-
return `File: ${oneLine(row.filePath)}${symbol}\n${row.content}`;
|
|
97
|
-
}
|
|
98
|
-
function oneLine(text) {
|
|
99
|
-
return text.replace(/\s+/g, ' ').trim();
|
|
100
|
-
}
|
|
101
|
-
class SearcherService {
|
|
102
|
-
db;
|
|
103
|
-
embedder;
|
|
104
|
-
discovery;
|
|
105
|
-
config;
|
|
106
|
-
logger;
|
|
107
|
-
fs;
|
|
108
|
-
constructor(db, embedder, discovery, config, logger = SILENT_LOGGER, fsDeps = {}) {
|
|
109
|
-
this.db = db;
|
|
110
|
-
this.embedder = embedder;
|
|
111
|
-
this.discovery = discovery;
|
|
112
|
-
this.config = config;
|
|
113
|
-
this.logger = logger;
|
|
114
|
-
this.fs = { ...NODE_FS, ...fsDeps };
|
|
115
|
-
}
|
|
116
|
-
targetColumnPresent = false;
|
|
117
|
-
realRootCache;
|
|
118
|
-
rootMap;
|
|
119
|
-
pgvectorVersionRead;
|
|
120
|
-
degradedScanWarned = false;
|
|
121
|
-
async iterativeScanEnabled() {
|
|
122
|
-
const read = this.pgvectorVersionRead ??= this.db.pgvectorVersion().then(v => {
|
|
123
|
-
if (v === null)
|
|
124
|
-
throw new Error('vector extension not found in pg_extension');
|
|
125
|
-
return v;
|
|
126
|
-
});
|
|
127
|
-
let version;
|
|
128
|
-
try {
|
|
129
|
-
version = await read;
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
if (this.pgvectorVersionRead === read)
|
|
133
|
-
this.pgvectorVersionRead = undefined;
|
|
134
|
-
this.warnDegradedScan('unknown', err);
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
if ((0, hnsw_settings_1.supportsIterativeScan)(version))
|
|
138
|
-
return true;
|
|
139
|
-
this.warnDegradedScan(version);
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
warnDegradedScan(version, err) {
|
|
143
|
-
if (this.degradedScanWarned)
|
|
144
|
-
return;
|
|
145
|
-
this.degradedScanWarned = true;
|
|
146
|
-
this.logger.warn(`SearcherService: pgvector ${version} lacks hnsw.iterative_scan — filtered searches on large projects may return fewer candidates than requested; upgrade to pgvector ≥ 0.8`, err ? { err } : undefined);
|
|
147
|
-
}
|
|
148
|
-
projectRootMap() {
|
|
149
|
-
if (!this.rootMap) {
|
|
150
|
-
this.rootMap = new Map((0, MonorepoAwareDiscoveryService_1.toLeafProjects)(this.discovery.discover(this.config.workspaceRoot)).map(p => [p.name, p.path]));
|
|
151
|
-
}
|
|
152
|
-
return this.rootMap;
|
|
153
|
-
}
|
|
154
|
-
resolveProjectRoot(name) {
|
|
155
|
-
let root = this.projectRootMap().get(name);
|
|
156
|
-
if (root === undefined) {
|
|
157
|
-
this.rootMap = undefined;
|
|
158
|
-
root = this.projectRootMap().get(name);
|
|
159
|
-
}
|
|
160
|
-
return root ?? path.join(this.config.workspaceRoot, name);
|
|
161
|
-
}
|
|
162
|
-
toRel(absPath) {
|
|
163
|
-
return path.relative(this.config.workspaceRoot, absPath) || '.';
|
|
164
|
-
}
|
|
165
|
-
toAbs(relPath) {
|
|
166
|
-
return relPath === '.' ? this.workspaceRoot() : path.join(this.workspaceRoot(), relPath);
|
|
167
|
-
}
|
|
168
|
-
workspaceRoot() {
|
|
169
|
-
return stripTrailingSlash(path.posix.normalize(this.config.workspaceRoot));
|
|
170
|
-
}
|
|
171
|
-
async realWorkspaceRoot() {
|
|
172
|
-
if (this.realRootCache !== undefined)
|
|
173
|
-
return this.realRootCache;
|
|
174
|
-
try {
|
|
175
|
-
this.realRootCache = await this.fs.realpath(this.workspaceRoot());
|
|
176
|
-
return this.realRootCache;
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
this.logger.warn('SearcherService: workspaceRoot real path unavailable', { reason: errorCode(err) });
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
async resolveToolPath(input) {
|
|
184
|
-
if (input == null || input === '')
|
|
185
|
-
return { ok: false, reason: 'invalid', missing: true };
|
|
186
|
-
if (input.includes('\\'))
|
|
187
|
-
return { ok: false, reason: 'invalid', missing: false };
|
|
188
|
-
if (path.posix.isAbsolute(input)) {
|
|
189
|
-
const abs = stripTrailingSlash(path.posix.normalize(input));
|
|
190
|
-
const realRoot = await this.realWorkspaceRoot();
|
|
191
|
-
for (const root of realRoot ? [this.workspaceRoot(), realRoot] : [this.workspaceRoot()]) {
|
|
192
|
-
if (!isWithin(abs, root))
|
|
193
|
-
continue;
|
|
194
|
-
const relative = abs === root ? '.' : abs.slice(root === '/' ? 1 : root.length + 1);
|
|
195
|
-
return { ok: false, reason: 'absolute', relative };
|
|
196
|
-
}
|
|
197
|
-
return { ok: false, reason: 'outside_workspace' };
|
|
198
|
-
}
|
|
199
|
-
const relPath = stripTrailingSlash(path.posix.normalize(input));
|
|
200
|
-
if (relPath === '..' || relPath.startsWith('../'))
|
|
201
|
-
return { ok: false, reason: 'outside_workspace' };
|
|
202
|
-
return { ok: true, relPath, absPath: this.toAbs(relPath) };
|
|
203
|
-
}
|
|
204
|
-
async realLocation(absPath) {
|
|
205
|
-
const root = (await this.realWorkspaceRoot()) ?? this.workspaceRoot();
|
|
206
|
-
let existing = absPath;
|
|
207
|
-
const rest = [];
|
|
208
|
-
for (;;) {
|
|
209
|
-
try {
|
|
210
|
-
const real = path.join(await this.fs.realpath(existing), ...rest);
|
|
211
|
-
return { ok: true, inside: isWithin(real, root) };
|
|
212
|
-
}
|
|
213
|
-
catch (err) {
|
|
214
|
-
if (errorCode(err) !== 'ENOENT')
|
|
215
|
-
return { ok: false, code: errorCode(err) };
|
|
216
|
-
const parent = path.dirname(existing);
|
|
217
|
-
if (parent === existing)
|
|
218
|
-
return { ok: true, inside: false };
|
|
219
|
-
rest.unshift(path.basename(existing));
|
|
220
|
-
existing = parent;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
async checkRealLocation(relPath, absPath, kind) {
|
|
225
|
-
const loc = await this.realLocation(absPath);
|
|
226
|
-
if (!loc.ok)
|
|
227
|
-
return { ok: false, reason: 'unreadable', code: loc.code };
|
|
228
|
-
if (loc.inside)
|
|
229
|
-
return { ok: true };
|
|
230
|
-
const exempt = kind === 'file' ? await this.isContentPath(relPath) : await this.hasContentUnder(relPath);
|
|
231
|
-
return exempt ? { ok: true } : { ok: false, reason: 'outside_workspace' };
|
|
232
|
-
}
|
|
233
|
-
async pathExists(columns, predicate, param) {
|
|
234
|
-
const branches = columns.map(c => `SELECT 1 FROM ${c.table} WHERE ${predicate.replace(/\$col/g, c.column)}`);
|
|
235
|
-
const [{ exists }] = await this.db.query(`SELECT EXISTS (${branches.join(' UNION ALL ')}) AS exists`, [param]);
|
|
236
|
-
return exists;
|
|
237
|
-
}
|
|
238
|
-
isIndexedPath(relPath) {
|
|
239
|
-
return this.pathExists(INDEX_PATH_COLUMNS, '$col = $1', relPath);
|
|
240
|
-
}
|
|
241
|
-
isContentPath(relPath) {
|
|
242
|
-
return this.pathExists(INDEX_PATH_COLUMNS.filter(c => c.content), '$col = $1', relPath);
|
|
243
|
-
}
|
|
244
|
-
hasContentUnder(relPath) {
|
|
245
|
-
const prefix = relPath === '.' ? '' : (0, symbol_name_1.escapeLike)(relPath) + '/';
|
|
246
|
-
return this.pathExists(INDEX_PATH_COLUMNS.filter(c => c.content), `$col LIKE $1 ESCAPE '\\'`, prefix + '%');
|
|
247
|
-
}
|
|
248
|
-
async findPathCandidates(relPath, kind) {
|
|
249
|
-
const union = INDEX_PATH_COLUMNS.map(c => `SELECT ${c.column} AS p FROM ${c.table}`).join(' UNION ALL ');
|
|
250
|
-
const notOutside = `NOT ${OUTSIDE_WORKSPACE_VALUE('p')}`;
|
|
251
|
-
if (kind === 'file') {
|
|
252
|
-
const substituted = await this.substituteProjectName(relPath);
|
|
253
|
-
const rows = await this.db.query(`SELECT p FROM (${union}) s
|
|
254
|
-
WHERE p LIKE $1 ESCAPE '\\' AND ${notOutside}
|
|
255
|
-
GROUP BY p ORDER BY p COLLATE "C" LIMIT ${PATH_CANDIDATE_LIMIT + 1}`, ['%/' + (0, symbol_name_1.escapeLike)(relPath)]);
|
|
256
|
-
const first = substituted ? [substituted] : [];
|
|
257
|
-
return [...first, ...rows.map(r => r.p).filter(p => p !== substituted)].slice(0, PATH_CANDIDATE_LIMIT);
|
|
258
|
-
}
|
|
259
|
-
const projectRoot = this.findProjectRootByName(relPath);
|
|
260
|
-
const first = projectRoot && projectRoot !== relPath ? [projectRoot] : [];
|
|
261
|
-
const rows = await this.db.query(`SELECT d AS p FROM (
|
|
262
|
-
SELECT substring(p FROM 1 FOR position($2::text IN p) + length($3::text)) AS d
|
|
263
|
-
FROM (${union}) s
|
|
264
|
-
WHERE p LIKE $1 ESCAPE '\\' AND ${notOutside}
|
|
265
|
-
) dirs
|
|
266
|
-
GROUP BY d ORDER BY d COLLATE "C" LIMIT ${PATH_CANDIDATE_LIMIT + 1}`, ['%/' + (0, symbol_name_1.escapeLike)(relPath) + '/%', '/' + relPath + '/', relPath]);
|
|
267
|
-
const rest = rows.map(r => r.p).filter(p => !first.includes(p));
|
|
268
|
-
return [...first, ...rest].slice(0, PATH_CANDIDATE_LIMIT);
|
|
269
|
-
}
|
|
270
|
-
async candidatesOrNone(relPath, kind) {
|
|
271
|
-
try {
|
|
272
|
-
return await this.findPathCandidates(relPath, kind);
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
this.logger.error('SearcherService: path candidate lookup failed', { relPath, err });
|
|
276
|
-
return [];
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
async substituteProjectName(relPath) {
|
|
280
|
-
let named = [];
|
|
281
|
-
for (let attempt = 0; attempt < 2 && named.length === 0; attempt++) {
|
|
282
|
-
if (attempt === 1)
|
|
283
|
-
this.rootMap = undefined;
|
|
284
|
-
named = [...this.projectRootMap().entries()].filter(([name]) => relPath.startsWith(name + '/'));
|
|
285
|
-
}
|
|
286
|
-
const longest = named.sort((a, b) => b[0].length - a[0].length)[0];
|
|
287
|
-
if (!longest)
|
|
288
|
-
return null;
|
|
289
|
-
const [name, absRoot] = longest;
|
|
290
|
-
const root = this.toRel(absRoot);
|
|
291
|
-
if (root === name)
|
|
292
|
-
return null;
|
|
293
|
-
const candidate = (root === '.' ? '' : root + '/') + relPath.slice(name.length + 1);
|
|
294
|
-
if (await this.isIndexedPath(candidate))
|
|
295
|
-
return candidate;
|
|
296
|
-
try {
|
|
297
|
-
return (await this.fs.stat(this.toAbs(candidate))).isFile() ? candidate : null;
|
|
298
|
-
}
|
|
299
|
-
catch {
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
findProjectRootByName(name) {
|
|
304
|
-
let root = this.projectRootMap().get(name);
|
|
305
|
-
if (root === undefined) {
|
|
306
|
-
this.rootMap = undefined;
|
|
307
|
-
root = this.projectRootMap().get(name);
|
|
308
|
-
}
|
|
309
|
-
return root === undefined ? null : this.toRel(root);
|
|
310
|
-
}
|
|
311
|
-
findProjectRoot(match) {
|
|
312
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
313
|
-
if (attempt === 1)
|
|
314
|
-
this.rootMap = undefined;
|
|
315
|
-
const best = [...this.projectRootMap().entries()]
|
|
316
|
-
.map(([name, abs]) => ({ name, rel: this.toRel(abs) }))
|
|
317
|
-
.filter(r => match(r.rel))
|
|
318
|
-
.sort((a, b) => b.rel.length - a.rel.length)[0];
|
|
319
|
-
if (best)
|
|
320
|
-
return best;
|
|
321
|
-
}
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
fallbackRoot(relPath) {
|
|
325
|
-
return this.findProjectRoot(r => isWithin(relPath, r))?.rel ?? null;
|
|
326
|
-
}
|
|
327
|
-
async checkGraphPath(relPath) {
|
|
328
|
-
if (await this.isIndexedPath(relPath))
|
|
329
|
-
return null;
|
|
330
|
-
const absPath = this.toAbs(relPath);
|
|
331
|
-
const loc = await this.realLocation(absPath);
|
|
332
|
-
if (!loc.ok)
|
|
333
|
-
return { ok: false, reason: 'unreadable', code: loc.code };
|
|
334
|
-
if (!loc.inside)
|
|
335
|
-
return { ok: false, reason: 'not_found', candidates: await this.candidatesOrNone(relPath, 'file') };
|
|
336
|
-
let stat;
|
|
337
|
-
try {
|
|
338
|
-
stat = await this.fs.stat(absPath);
|
|
339
|
-
}
|
|
340
|
-
catch (err) {
|
|
341
|
-
if (errorCode(err) !== 'ENOENT')
|
|
342
|
-
return { ok: false, reason: 'unreadable', code: errorCode(err) };
|
|
343
|
-
return { ok: false, reason: 'not_found', candidates: await this.candidatesOrNone(relPath, 'file') };
|
|
344
|
-
}
|
|
345
|
-
if (!stat.isFile())
|
|
346
|
-
return { ok: false, reason: 'not_a_file' };
|
|
347
|
-
return { ok: false, reason: 'not_indexed', explanation: await this.explainNotIndexed(relPath) };
|
|
348
|
-
}
|
|
349
|
-
async explainNotIndexed(relPath) {
|
|
350
|
-
const owner = this.findProjectRoot(r => r !== relPath && isWithin(relPath, r));
|
|
351
|
-
if (!owner)
|
|
352
|
-
return { kind: 'no_project' };
|
|
353
|
-
const inProject = owner.rel === '.' ? relPath : relPath.slice(owner.rel.length + 1);
|
|
354
|
-
const exclusion = (0, file_utils_1.indexingExclusion)(inProject, (0, file_utils_1.loadIgnore)(this.toAbs(owner.rel)));
|
|
355
|
-
if (exclusion)
|
|
356
|
-
return exclusion;
|
|
357
|
-
const [{ exists }] = await this.db.query(`SELECT EXISTS (
|
|
358
|
-
SELECT 1 FROM code_chunk WHERE "serviceName" = $1
|
|
359
|
-
UNION ALL SELECT 1 FROM code_symbol WHERE project = $1
|
|
360
|
-
UNION ALL SELECT 1 FROM code_dependency WHERE project = $1
|
|
361
|
-
UNION ALL SELECT 1 FROM code_call WHERE project = $1
|
|
362
|
-
) AS exists`, [owner.name]);
|
|
363
|
-
return exists ? { kind: 'unknown' } : { kind: 'project_not_indexed', project: owner.name };
|
|
364
|
-
}
|
|
365
|
-
async getProjectEdges() {
|
|
366
|
-
const rows = await this.db.query(`SELECT DISTINCT project, "toFile" FROM code_dependency`);
|
|
367
|
-
const byRelPath = [...this.projectRootMap().entries()]
|
|
368
|
-
.map(([name, abs]) => ({ name, rel: path.relative(this.config.workspaceRoot, abs) }))
|
|
369
|
-
.sort((a, b) => b.rel.length - a.rel.length);
|
|
370
|
-
const ownerOf = (toFile) => {
|
|
371
|
-
for (const { name, rel } of byRelPath) {
|
|
372
|
-
if (toFile === rel || toFile.startsWith(rel + path.sep) || toFile.startsWith(rel + '/'))
|
|
373
|
-
return name;
|
|
374
|
-
}
|
|
375
|
-
return null;
|
|
376
|
-
};
|
|
377
|
-
const seen = new Set();
|
|
378
|
-
const edges = [];
|
|
379
|
-
for (const { project, toFile } of rows) {
|
|
380
|
-
const to = ownerOf(toFile);
|
|
381
|
-
if (!to || to === project)
|
|
382
|
-
continue;
|
|
383
|
-
const key = project + ' ' + to;
|
|
384
|
-
if (seen.has(key))
|
|
385
|
-
continue;
|
|
386
|
-
seen.add(key);
|
|
387
|
-
edges.push({ from: project, to });
|
|
388
|
-
}
|
|
389
|
-
return edges;
|
|
390
|
-
}
|
|
391
|
-
async prepareQuery(raw) {
|
|
392
|
-
return { query: raw, translated: false };
|
|
393
|
-
}
|
|
394
|
-
async assertIndexMatches() {
|
|
395
|
-
const meta = await this.db.readIndexMeta();
|
|
396
|
-
if (meta) {
|
|
397
|
-
this.compareIndexed(meta.provider, meta.model, meta.dimensions);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (await (0, legacy_ollama_index_1.hasIndexedChunks)(this.db)) {
|
|
401
|
-
this.compareIndexed(legacy_ollama_index_1.LEGACY_OLLAMA_IDENTITY.provider, legacy_ollama_index_1.LEGACY_OLLAMA_IDENTITY.model, null);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
compareIndexed(indexedProvider, indexedModel, indexedDimensions) {
|
|
405
|
-
const { provider, model } = this.config.embedding;
|
|
406
|
-
if (indexedProvider !== provider || indexedModel !== model) {
|
|
407
|
-
throw new Error(`Index was built with ${indexedProvider}/${indexedModel} but the config selects ${provider}/${model}. ` +
|
|
408
|
-
`Run \`rag-index --reindex\` to rebuild the index for the new model.`);
|
|
409
|
-
}
|
|
410
|
-
const dim = this.embedder.dimensions;
|
|
411
|
-
if (indexedDimensions != null && dim > 0 && dim !== indexedDimensions) {
|
|
412
|
-
throw new Error(`Index was built at ${indexedDimensions} dimensions but ${provider}/${model} now resolves to ${dim}. ` +
|
|
413
|
-
`Run \`rag-index --reindex\` to rebuild the index at the new dimension.`);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
async listProjects() {
|
|
417
|
-
const rows = await this.db.query(`SELECT "serviceName", COUNT(*)::int AS chunks
|
|
418
|
-
FROM code_chunk
|
|
419
|
-
GROUP BY "serviceName"
|
|
420
|
-
ORDER BY "serviceName"`);
|
|
421
|
-
return Promise.all(rows.map(async (r) => {
|
|
422
|
-
const projectRoot = this.resolveProjectRoot(r.serviceName);
|
|
423
|
-
const meta = (0, file_utils_1.readRagMeta)(projectRoot);
|
|
424
|
-
let rootError = null;
|
|
425
|
-
try {
|
|
426
|
-
await this.fs.stat(projectRoot);
|
|
427
|
-
}
|
|
428
|
-
catch (err) {
|
|
429
|
-
rootError = errorCode(err);
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
...r, path: projectRoot, relPath: this.toRel(projectRoot), rootError,
|
|
433
|
-
...(typeof meta === 'object' ? meta : EMPTY_META),
|
|
434
|
-
};
|
|
435
|
-
}));
|
|
436
|
-
}
|
|
437
|
-
async searchChunks(query, limit, project, threshold, rerankOverride, perFile = result_shaping_1.DEFAULT_PER_FILE) {
|
|
438
|
-
const { stages } = await this.searchChunksWithStages(query, limit, project, threshold, rerankOverride, perFile);
|
|
439
|
-
return stages.reranked ?? stages.vector;
|
|
440
|
-
}
|
|
441
|
-
async searchChunksWithStages(query, limit, project, threshold, rerankOverride, perFile = result_shaping_1.DEFAULT_PER_FILE) {
|
|
442
|
-
const t0 = Date.now();
|
|
443
|
-
await this.assertIndexMatches();
|
|
444
|
-
const effectiveThreshold = threshold ?? this.config.search.similarityThreshold;
|
|
445
|
-
const reranking = rerankOverride === false ? false : !!this.embedder.rerank;
|
|
446
|
-
const perFileActive = perFile < limit;
|
|
447
|
-
const fetchLimit = reranking
|
|
448
|
-
? Math.min(Math.max(limit * this.config.reranker.overfetch, this.config.reranker.candidateFloor), 200)
|
|
449
|
-
: perFileActive ? Math.min(limit * 3, 200) : limit;
|
|
450
|
-
const dbLimit = fetchLimit + 1;
|
|
451
|
-
this.logger.debug('SearcherService: searchChunks', {
|
|
452
|
-
query: query.slice(0, 120), limit, fetchLimit, project, threshold: effectiveThreshold,
|
|
453
|
-
});
|
|
454
|
-
const tEmbed0 = Date.now();
|
|
455
|
-
const embedding = await this.embedder.embed(query, 'query');
|
|
456
|
-
const embedMs = Date.now() - tEmbed0;
|
|
457
|
-
const params = [`[${embedding}]`, effectiveThreshold, dbLimit];
|
|
458
|
-
let projectFilter = '';
|
|
459
|
-
if (project) {
|
|
460
|
-
params.push(project);
|
|
461
|
-
projectFilter = `AND "serviceName" = $${params.length}`;
|
|
462
|
-
}
|
|
463
|
-
const iterative = await this.iterativeScanEnabled();
|
|
464
|
-
const settings = (0, hnsw_settings_1.hnswSettings)(dbLimit, iterative);
|
|
465
|
-
const indexParams = {
|
|
466
|
-
efSearch: settings['hnsw.ef_search'],
|
|
467
|
-
iterativeScan: iterative ? hnsw_settings_1.ITERATIVE_SCAN_MODE : 'off',
|
|
468
|
-
};
|
|
469
|
-
const tDb0 = Date.now();
|
|
470
|
-
const fetched = await this.db.queryWithLocalSettings(settings, `SELECT "serviceName", "filePath", "startLine", content, "ownerPath",
|
|
471
|
-
ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
|
|
472
|
-
FROM code_chunk
|
|
473
|
-
WHERE embedding <=> $1 < $2
|
|
474
|
-
${projectFilter}
|
|
475
|
-
ORDER BY embedding <=> $1
|
|
476
|
-
LIMIT $3`, params);
|
|
477
|
-
const dbMs = Date.now() - tDb0;
|
|
478
|
-
fetched.sort((a, b) => a.score - b.score);
|
|
479
|
-
const poolSaturated = fetched.length > fetchLimit;
|
|
480
|
-
const candidates = fetched.slice(0, fetchLimit);
|
|
481
|
-
this.logger.debug('SearcherService: searchChunks candidates', {
|
|
482
|
-
count: candidates.length,
|
|
483
|
-
scores: candidates.map(r => r.score),
|
|
484
|
-
files: candidates.map(r => r.filePath),
|
|
485
|
-
ms: Date.now() - t0,
|
|
486
|
-
});
|
|
487
|
-
const vector = (0, result_shaping_1.limitPerFile)(candidates, limit, perFile);
|
|
488
|
-
let reranked;
|
|
489
|
-
let rerankFailed = false;
|
|
490
|
-
let rerankMs = 0;
|
|
491
|
-
if (reranking && candidates.length === 0) {
|
|
492
|
-
reranked = [];
|
|
493
|
-
}
|
|
494
|
-
else if (reranking) {
|
|
495
|
-
const tRerank0 = Date.now();
|
|
496
|
-
try {
|
|
497
|
-
const topN = perFileActive ? candidates.length : limit;
|
|
498
|
-
const indices = await this.embedder.rerank(query, candidates.map(rerankDocument), topN);
|
|
499
|
-
rerankMs = Date.now() - tRerank0;
|
|
500
|
-
this.logger.debug('SearcherService: searchChunks rerank done', { indices, ms: Date.now() - t0 });
|
|
501
|
-
const order = indices.filter(i => i >= 0 && i < candidates.length).map(i => candidates[i]);
|
|
502
|
-
reranked = (0, result_shaping_1.limitPerFile)(order, limit, perFile);
|
|
503
|
-
}
|
|
504
|
-
catch (err) {
|
|
505
|
-
rerankMs = Date.now() - tRerank0;
|
|
506
|
-
this.logger.warn('SearcherService: rerank failed, falling back to vector order', {
|
|
507
|
-
err,
|
|
508
|
-
ms: Date.now() - t0,
|
|
509
|
-
});
|
|
510
|
-
reranked = vector;
|
|
511
|
-
rerankFailed = true;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
this.emitSearchMetrics(query, {
|
|
515
|
-
fetchLimit, candidates, results: reranked ?? vector, poolSaturated, indexParams,
|
|
516
|
-
embedMs, dbMs, rerankMs, totalMs: Date.now() - t0,
|
|
517
|
-
});
|
|
518
|
-
return {
|
|
519
|
-
pool: candidates,
|
|
520
|
-
stages: reranked ? { vector, reranked } : { vector },
|
|
521
|
-
rerankFailed,
|
|
522
|
-
fetchLimit,
|
|
523
|
-
indexParams,
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
emitSearchMetrics(query, m) {
|
|
527
|
-
const { candidates, results } = m;
|
|
528
|
-
const contentIndex = new Map(candidates.map((c, i) => [c, i]));
|
|
529
|
-
const maxCosineRankSurvived = results.reduce((max, r) => Math.max(max, contentIndex.get(r) ?? -1), -1);
|
|
530
|
-
const fileCounts = new Map();
|
|
531
|
-
for (const r of results)
|
|
532
|
-
fileCounts.set(r.filePath, (fileCounts.get(r.filePath) ?? 0) + 1);
|
|
533
|
-
const topFileCount = results.length ? Math.max(...fileCounts.values()) : 0;
|
|
534
|
-
this.logger.info('search metrics', {
|
|
535
|
-
query: query.slice(0, 120),
|
|
536
|
-
fetchLimit: m.fetchLimit,
|
|
537
|
-
candidatesReturned: candidates.length,
|
|
538
|
-
poolSaturated: m.poolSaturated,
|
|
539
|
-
maxCosineRankSurvived,
|
|
540
|
-
resultCount: results.length,
|
|
541
|
-
distinctFilesInResult: fileCounts.size,
|
|
542
|
-
topFileShare: results.length ? +(topFileCount / results.length).toFixed(3) : 0,
|
|
543
|
-
bestResultScore: results.length ? Math.min(...results.map(r => r.score)) : null,
|
|
544
|
-
worstResultScore: results.length ? Math.max(...results.map(r => r.score)) : null,
|
|
545
|
-
efSearch: m.indexParams.efSearch,
|
|
546
|
-
iterativeScan: m.indexParams.iterativeScan,
|
|
547
|
-
embedMs: m.embedMs,
|
|
548
|
-
rerankMs: m.rerankMs,
|
|
549
|
-
dbMs: m.dbMs,
|
|
550
|
-
totalMs: m.totalMs,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
async fetchNeighbors(results) {
|
|
554
|
-
const t0 = Date.now();
|
|
555
|
-
if (results.length === 0)
|
|
556
|
-
return [];
|
|
557
|
-
this.logger.debug('SearcherService: fetchNeighbors', { count: results.length });
|
|
558
|
-
const values = results.map((_, i) => `(${i}, $${i * 2 + 1}, $${i * 2 + 2}::int)`).join(',');
|
|
559
|
-
const params = results.flatMap(r => [r.filePath, r.startLine]);
|
|
560
|
-
const rows = await this.db.query(`SELECT r.idx,
|
|
561
|
-
prev."serviceName" AS prev_sn, prev."filePath" AS prev_fp, prev."startLine" AS prev_sl, prev.content AS prev_c,
|
|
562
|
-
next."serviceName" AS next_sn, next."filePath" AS next_fp, next."startLine" AS next_sl, next.content AS next_c
|
|
563
|
-
FROM (VALUES ${values}) r(idx, fp, sl)
|
|
564
|
-
LEFT JOIN LATERAL (
|
|
565
|
-
SELECT "serviceName", "filePath", "startLine", content
|
|
566
|
-
FROM code_chunk WHERE "filePath" = r.fp AND "startLine" < r.sl
|
|
567
|
-
ORDER BY "startLine" DESC LIMIT 1
|
|
568
|
-
) prev ON true
|
|
569
|
-
LEFT JOIN LATERAL (
|
|
570
|
-
SELECT "serviceName", "filePath", "startLine", content
|
|
571
|
-
FROM code_chunk WHERE "filePath" = r.fp AND "startLine" > r.sl
|
|
572
|
-
ORDER BY "startLine" ASC LIMIT 1
|
|
573
|
-
) next ON true
|
|
574
|
-
ORDER BY r.idx`, params);
|
|
575
|
-
const neighbors = rows.map((row, i) => ({
|
|
576
|
-
result: results[i],
|
|
577
|
-
prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
|
|
578
|
-
nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
|
|
579
|
-
}));
|
|
580
|
-
this.logger.debug('SearcherService: fetchNeighbors result', {
|
|
581
|
-
withPrev: neighbors.filter(n => n.prevChunk).length,
|
|
582
|
-
withNext: neighbors.filter(n => n.nextChunk).length,
|
|
583
|
-
ms: Date.now() - t0,
|
|
584
|
-
});
|
|
585
|
-
return neighbors;
|
|
586
|
-
}
|
|
587
|
-
async fetchGraphContext(results) {
|
|
588
|
-
const t0 = Date.now();
|
|
589
|
-
const files = [...new Set(results.map(r => r.filePath))];
|
|
590
|
-
this.logger.debug('SearcherService: fetchGraphContext', { files: files.length });
|
|
591
|
-
if (files.length === 0)
|
|
592
|
-
return new Map();
|
|
593
|
-
const proportionalLimit = Math.max(2000, files.length * 100);
|
|
594
|
-
const values = files.map((_, i) => `($${i + 1}::text)`).join(', ');
|
|
595
|
-
const rows = await this.db.query(`SELECT r.fp AS "filePath", 'import' AS direction, d."toFile" AS related
|
|
596
|
-
FROM (VALUES ${values}) AS r(fp)
|
|
597
|
-
JOIN code_dependency d ON d."fromFile" = r.fp
|
|
598
|
-
WHERE NOT ${OUTSIDE_WORKSPACE_VALUE('d."toFile"')}
|
|
599
|
-
|
|
600
|
-
UNION ALL
|
|
601
|
-
|
|
602
|
-
SELECT r.fp AS "filePath", 'usedby' AS direction, d."fromFile" AS related
|
|
603
|
-
FROM (VALUES ${values}) AS r(fp)
|
|
604
|
-
JOIN code_dependency d ON d."toFile" = r.fp
|
|
605
|
-
WHERE NOT ${OUTSIDE_WORKSPACE_VALUE('d."fromFile"')}
|
|
606
|
-
|
|
607
|
-
LIMIT ${proportionalLimit}`, files);
|
|
608
|
-
const ctx = new Map();
|
|
609
|
-
for (const filePath of files)
|
|
610
|
-
ctx.set(filePath, { imports: new Set(), usedBy: new Set() });
|
|
611
|
-
for (const row of rows) {
|
|
612
|
-
const g = ctx.get(row.filePath);
|
|
613
|
-
if (row.direction === 'import')
|
|
614
|
-
g.imports.add(row.related);
|
|
615
|
-
else
|
|
616
|
-
g.usedBy.add(row.related);
|
|
617
|
-
}
|
|
618
|
-
this.logger.debug('SearcherService: fetchGraphContext result', { files: files.length, rows: rows.length, ms: Date.now() - t0 });
|
|
619
|
-
const result = new Map();
|
|
620
|
-
for (const [fp, g] of ctx)
|
|
621
|
-
result.set(fp, {
|
|
622
|
-
imports: [...g.imports].slice(0, 50),
|
|
623
|
-
usedBy: [...g.usedBy].slice(0, 50),
|
|
624
|
-
});
|
|
625
|
-
return result;
|
|
626
|
-
}
|
|
627
|
-
async getDependencies(filePath, symbol = null, transitive = false) {
|
|
628
|
-
const t0 = Date.now();
|
|
629
|
-
this.logger.debug('SearcherService: getDependencies', { filePath, symbol, transitive });
|
|
630
|
-
const params = [filePath];
|
|
631
|
-
let symbolFilter = '';
|
|
632
|
-
if (symbol) {
|
|
633
|
-
params.push(symbol);
|
|
634
|
-
symbolFilter = `AND "toSymbol" = $${params.length}`;
|
|
635
|
-
}
|
|
636
|
-
const direct = await this.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType", min("id"::text) AS "id"
|
|
637
|
-
FROM code_dependency
|
|
638
|
-
WHERE "fromFile" = $1 AND NOT ${OUTSIDE_WORKSPACE_VALUE('"toFile"')}
|
|
639
|
-
${symbolFilter}
|
|
640
|
-
GROUP BY "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
|
|
641
|
-
ORDER BY "toFile", min("id"::text)`, params);
|
|
642
|
-
if (!transitive) {
|
|
643
|
-
this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, ms: Date.now() - t0 });
|
|
644
|
-
return direct;
|
|
645
|
-
}
|
|
646
|
-
const neighbours = async (frontier) => {
|
|
647
|
-
const rows = await this.db.query(`SELECT DISTINCT "fromFile", "toFile" FROM code_dependency
|
|
648
|
-
WHERE "fromFile" = ANY($1::text[]) AND NOT ${OUTSIDE_WORKSPACE_VALUE('"toFile"')}`, [frontier]);
|
|
649
|
-
return rows.map(r => ({ from: r.fromFile, to: r.toFile }));
|
|
650
|
-
};
|
|
651
|
-
const indirect = (await (0, dependency_traversal_1.traverse)(filePath, neighbours)).map(r => ({
|
|
652
|
-
toFile: r.file, depth: r.depth, via: r.via,
|
|
653
|
-
fromFile: null, fromSymbol: null, toSymbol: null, depType: null,
|
|
654
|
-
}));
|
|
655
|
-
this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, indirect: indirect.length, ms: Date.now() - t0 });
|
|
656
|
-
return [...direct, ...indirect];
|
|
657
|
-
}
|
|
658
|
-
async getDependents(filePath, symbol = null, transitive = false) {
|
|
659
|
-
const t0 = Date.now();
|
|
660
|
-
this.logger.debug('SearcherService: getDependents', { filePath, symbol, transitive });
|
|
661
|
-
const params = [filePath];
|
|
662
|
-
let symbolFilter = '';
|
|
663
|
-
if (symbol) {
|
|
664
|
-
params.push(symbol);
|
|
665
|
-
symbolFilter = `AND "toSymbol" = $${params.length}`;
|
|
666
|
-
}
|
|
667
|
-
const direct = await this.db.query(`SELECT "fromFile", "fromSymbol", "toFile", "toSymbol", "depType", min("id"::text) AS "id"
|
|
668
|
-
FROM code_dependency
|
|
669
|
-
WHERE "toFile" = $1 AND NOT ${OUTSIDE_WORKSPACE_VALUE('"fromFile"')}
|
|
670
|
-
${symbolFilter}
|
|
671
|
-
GROUP BY "fromFile", "fromSymbol", "toFile", "toSymbol", "depType"
|
|
672
|
-
ORDER BY "fromFile", min("id"::text)`, params);
|
|
673
|
-
if (!transitive) {
|
|
674
|
-
this.logger.debug('SearcherService: getDependents result', { direct: direct.length, ms: Date.now() - t0 });
|
|
675
|
-
return direct;
|
|
676
|
-
}
|
|
677
|
-
const neighbours = async (frontier) => {
|
|
678
|
-
const rows = await this.db.query(`SELECT DISTINCT "fromFile", "toFile" FROM code_dependency
|
|
679
|
-
WHERE "toFile" = ANY($1::text[]) AND NOT ${OUTSIDE_WORKSPACE_VALUE('"fromFile"')}`, [frontier]);
|
|
680
|
-
return rows.map(r => ({ from: r.toFile, to: r.fromFile }));
|
|
681
|
-
};
|
|
682
|
-
const indirect = (await (0, dependency_traversal_1.traverse)(filePath, neighbours)).map(r => ({
|
|
683
|
-
fromFile: r.file, depth: r.depth, via: r.via,
|
|
684
|
-
toFile: null, fromSymbol: null, toSymbol: null, depType: null,
|
|
685
|
-
}));
|
|
686
|
-
this.logger.debug('SearcherService: getDependents result', { direct: direct.length, indirect: indirect.length, ms: Date.now() - t0 });
|
|
687
|
-
return [...direct, ...indirect];
|
|
688
|
-
}
|
|
689
|
-
async getCallers(symbol, project, calleeFile = null) {
|
|
690
|
-
const t0 = Date.now();
|
|
691
|
-
this.logger.debug('SearcherService: getCallers', { symbol, project, calleeFile });
|
|
692
|
-
const parsed = (0, symbol_name_1.parseCallerSymbol)(symbol);
|
|
693
|
-
if (!parsed)
|
|
694
|
-
throw new Error('Invalid symbol name');
|
|
695
|
-
const params = [project];
|
|
696
|
-
let nameFilter;
|
|
697
|
-
if (parsed.form === 'bare') {
|
|
698
|
-
params.push(parsed.name);
|
|
699
|
-
nameFilter = `"calleeName" = $${params.length}`;
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
params.push(`${parsed.container}.${parsed.name}`);
|
|
703
|
-
const exactParam = params.length;
|
|
704
|
-
params.push(`%.${(0, symbol_name_1.escapeLike)(parsed.container)}.${(0, symbol_name_1.escapeLike)(parsed.name)}`);
|
|
705
|
-
const suffixParam = params.length;
|
|
706
|
-
nameFilter = `("calleeSymbol" = $${exactParam} OR "calleeSymbol" LIKE $${suffixParam} ESCAPE '\\')`;
|
|
707
|
-
}
|
|
708
|
-
let fileFilter = '';
|
|
709
|
-
if (calleeFile) {
|
|
710
|
-
params.push(calleeFile);
|
|
711
|
-
fileFilter = `AND "calleeFile" = $${params.length}`;
|
|
712
|
-
}
|
|
713
|
-
const rows = await this.db.query(`SELECT "fromFile", "fromSymbol", "calleeName", "calleeFile", line
|
|
714
|
-
FROM code_call
|
|
715
|
-
WHERE project = $1 AND ${nameFilter}
|
|
716
|
-
${fileFilter}
|
|
717
|
-
ORDER BY "fromFile", line
|
|
718
|
-
LIMIT 200`, params);
|
|
719
|
-
this.logger.debug('SearcherService: getCallers result', { count: rows.length, ms: Date.now() - t0 });
|
|
720
|
-
return rows;
|
|
721
|
-
}
|
|
722
|
-
async grepSymbol(symbol, rootDir, excludeFile) {
|
|
723
|
-
const t0 = Date.now();
|
|
724
|
-
this.logger.debug('SearcherService: grepSymbol', { symbol, rootDir, excludeFile });
|
|
725
|
-
const result = await this.spawnGrep([
|
|
726
|
-
'-rn', '-F', '-w',
|
|
727
|
-
'-m', String(GREP_RAW_LIMIT),
|
|
728
|
-
'--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
|
|
729
|
-
...GREP_EXCLUDE_DIRS,
|
|
730
|
-
symbol,
|
|
731
|
-
this.toAbs(rootDir),
|
|
732
|
-
]);
|
|
733
|
-
if (!result.ok && result.output === null)
|
|
734
|
-
return { ok: false };
|
|
735
|
-
const excludeAbsPath = path.join(this.config.workspaceRoot, excludeFile);
|
|
736
|
-
const all = (result.output ?? '').split('\n').flatMap(line => {
|
|
737
|
-
const match = line.match(/^(.+?):(\d+):/);
|
|
738
|
-
if (!match)
|
|
739
|
-
return [];
|
|
740
|
-
const [, absFile, lineNum] = match;
|
|
741
|
-
if (absFile === excludeAbsPath)
|
|
742
|
-
return [];
|
|
743
|
-
return [{ filePath: path.relative(this.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
|
|
744
|
-
});
|
|
745
|
-
const truncated = all.length > GREP_MAX_RESULTS;
|
|
746
|
-
this.logger.debug('SearcherService: grepSymbol result', { matches: Math.min(all.length, GREP_MAX_RESULTS), truncated, partial: !result.ok, ms: Date.now() - t0 });
|
|
747
|
-
return { ok: true, matches: all.slice(0, GREP_MAX_RESULTS), truncated, partial: !result.ok };
|
|
748
|
-
}
|
|
749
|
-
findSymbol(name, project, kind) {
|
|
750
|
-
const parsed = (0, symbol_name_1.parseFindSymbolName)(name);
|
|
751
|
-
if (!parsed)
|
|
752
|
-
throw new Error('Invalid symbol name');
|
|
753
|
-
return this.findSymbolFromIndex(parsed, project, kind);
|
|
754
|
-
}
|
|
755
|
-
async hasTargetColumn() {
|
|
756
|
-
if (this.targetColumnPresent)
|
|
757
|
-
return true;
|
|
758
|
-
const rows = await this.db.query(`SELECT EXISTS (
|
|
759
|
-
SELECT 1 FROM information_schema.columns
|
|
760
|
-
WHERE table_schema = 'public' AND table_name = 'code_symbol' AND column_name = 'target'
|
|
761
|
-
) AS present`);
|
|
762
|
-
const present = !!rows[0]?.present;
|
|
763
|
-
if (present)
|
|
764
|
-
this.targetColumnPresent = true;
|
|
765
|
-
return present;
|
|
766
|
-
}
|
|
767
|
-
async findSymbolFromIndex(parsed, project, kind) {
|
|
768
|
-
const t0 = Date.now();
|
|
769
|
-
this.logger.debug('SearcherService: findSymbol', { parsed, project, kind, source: 'index' });
|
|
770
|
-
const container = 'container' in parsed ? parsed.container : null;
|
|
771
|
-
const isGraphqlKind = GRAPHQL_KINDS.has(kind);
|
|
772
|
-
try {
|
|
773
|
-
const hasTarget = await this.hasTargetColumn();
|
|
774
|
-
const params = [parsed.name];
|
|
775
|
-
let nameFilter = 'name = $1';
|
|
776
|
-
let orderPrefix = '';
|
|
777
|
-
if (container !== null) {
|
|
778
|
-
params.push(container);
|
|
779
|
-
const exactParam = params.length;
|
|
780
|
-
params.push(`%.${(0, symbol_name_1.escapeLike)(container)}`);
|
|
781
|
-
const suffixParam = params.length;
|
|
782
|
-
nameFilter += ` AND (container = $${exactParam} OR container LIKE $${suffixParam} ESCAPE '\\')`;
|
|
783
|
-
orderPrefix = `(CASE WHEN container = $${exactParam} THEN 0 ELSE 1 END), `;
|
|
784
|
-
}
|
|
785
|
-
let projectFilter = '';
|
|
786
|
-
if (project) {
|
|
787
|
-
params.push(project);
|
|
788
|
-
projectFilter = `AND project = $${params.length}`;
|
|
789
|
-
}
|
|
790
|
-
let kindFilter = '';
|
|
791
|
-
if (kind !== 'all') {
|
|
792
|
-
params.push(kind);
|
|
793
|
-
kindFilter = `AND kind = $${params.length}`;
|
|
794
|
-
}
|
|
795
|
-
params.push(FIND_SYMBOL_LIMIT + 1);
|
|
796
|
-
const targetSelect = hasTarget ? '"target"' : 'NULL AS target';
|
|
797
|
-
const rows = await this.db.query(`SELECT "filePath", name, kind, signature, line, col, container, "isStatic", ${targetSelect}
|
|
798
|
-
FROM code_symbol
|
|
799
|
-
WHERE ${nameFilter} ${projectFilter} ${kindFilter}
|
|
800
|
-
ORDER BY ${orderPrefix}project, "filePath", line, col, kind COLLATE "C", container COLLATE "C" NULLS FIRST
|
|
801
|
-
LIMIT $${params.length}`, params);
|
|
802
|
-
const truncated = rows.length > FIND_SYMBOL_LIMIT;
|
|
803
|
-
const hits = rows.slice(0, FIND_SYMBOL_LIMIT).map(row => ({
|
|
804
|
-
filePath: row.filePath,
|
|
805
|
-
line: row.line ?? 1,
|
|
806
|
-
column: row.col ?? 1,
|
|
807
|
-
kind: row.kind,
|
|
808
|
-
signature: row.signature ?? row.name,
|
|
809
|
-
qualified: row.container ? `${row.container}.${row.name}` : row.name,
|
|
810
|
-
isStatic: !!row.isStatic,
|
|
811
|
-
source: 'index',
|
|
812
|
-
target: row.target ?? null,
|
|
813
|
-
}));
|
|
814
|
-
this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated, ms: Date.now() - t0 });
|
|
815
|
-
if (hits.length > 0)
|
|
816
|
-
return { hits, truncated, grepFailed: false };
|
|
817
|
-
if (isGraphqlKind)
|
|
818
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
819
|
-
if (parsed.form === 'quoted' || parsed.form === 'computed')
|
|
820
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
821
|
-
return this.grepFindSymbol(parsed, project, kind);
|
|
822
|
-
}
|
|
823
|
-
catch (err) {
|
|
824
|
-
this.logger.warn('SearcherService: symbol index unavailable', { err, ms: Date.now() - t0 });
|
|
825
|
-
if (isGraphqlKind)
|
|
826
|
-
throw err;
|
|
827
|
-
if (parsed.form === 'quoted' || parsed.form === 'computed')
|
|
828
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
829
|
-
return this.grepFindSymbol(parsed, project, kind);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
async grepFindSymbol(parsed, project, kind) {
|
|
833
|
-
const t0 = Date.now();
|
|
834
|
-
const grepName = parsed.name;
|
|
835
|
-
this.logger.debug('SearcherService: findSymbol', { grepName, project, kind, source: 'grep' });
|
|
836
|
-
const pattern = this.buildFindSymbolPattern(grepName, kind);
|
|
837
|
-
if (pattern === null)
|
|
838
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
839
|
-
const searchDir = project
|
|
840
|
-
? this.resolveProjectRoot(project)
|
|
841
|
-
: this.config.workspaceRoot;
|
|
842
|
-
const result = await this.spawnGrep([
|
|
843
|
-
'-rn', '-E',
|
|
844
|
-
'-m', String(FIND_SYMBOL_RAW_LIMIT),
|
|
845
|
-
'--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
|
|
846
|
-
...GREP_EXCLUDE_DIRS,
|
|
847
|
-
pattern,
|
|
848
|
-
searchDir,
|
|
849
|
-
]);
|
|
850
|
-
if (!result.ok) {
|
|
851
|
-
this.logger.warn('SearcherService: findSymbol grep failed', { grepName, project });
|
|
852
|
-
return { hits: [], truncated: false, grepFailed: true };
|
|
853
|
-
}
|
|
854
|
-
if (result.output === null)
|
|
855
|
-
return { hits: [], truncated: false, grepFailed: false };
|
|
856
|
-
const parsedHits = [];
|
|
857
|
-
for (const raw of result.output.split('\n')) {
|
|
858
|
-
const m = raw.match(/^(.+?):(\d+):(.*)$/);
|
|
859
|
-
if (!m)
|
|
860
|
-
continue;
|
|
861
|
-
const [, absFile, lineNumStr, matchedLine] = m;
|
|
862
|
-
const lineNum = parseInt(lineNumStr, 10);
|
|
863
|
-
if (isNaN(lineNum))
|
|
864
|
-
continue;
|
|
865
|
-
const col = SearcherService.wholeWordIndex(matchedLine, grepName);
|
|
866
|
-
if (col === -1)
|
|
867
|
-
continue;
|
|
868
|
-
parsedHits.push({
|
|
869
|
-
filePath: path.relative(this.config.workspaceRoot, absFile),
|
|
870
|
-
line: lineNum,
|
|
871
|
-
column: col + 1,
|
|
872
|
-
kind: this.inferKind(matchedLine, grepName),
|
|
873
|
-
signature: matchedLine.trim(),
|
|
874
|
-
qualified: grepName,
|
|
875
|
-
isStatic: false,
|
|
876
|
-
source: 'grep',
|
|
877
|
-
target: null,
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
const truncated = parsedHits.length > FIND_SYMBOL_LIMIT;
|
|
881
|
-
this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: Math.min(parsedHits.length, FIND_SYMBOL_LIMIT), truncated, ms: Date.now() - t0 });
|
|
882
|
-
return { hits: parsedHits.slice(0, FIND_SYMBOL_LIMIT), truncated, grepFailed: false };
|
|
883
|
-
}
|
|
884
|
-
async listFiles(relPath, depth, pattern) {
|
|
885
|
-
const t0 = Date.now();
|
|
886
|
-
this.logger.debug('SearcherService: listFiles', { relPath, depth, pattern });
|
|
887
|
-
const absPath = this.toAbs(relPath);
|
|
888
|
-
const loc = await this.checkRealLocation(relPath, absPath, 'dir');
|
|
889
|
-
if (!loc.ok)
|
|
890
|
-
return loc;
|
|
891
|
-
let stat;
|
|
892
|
-
try {
|
|
893
|
-
stat = await this.fs.stat(absPath);
|
|
894
|
-
}
|
|
895
|
-
catch (err) {
|
|
896
|
-
if (errorCode(err) !== 'ENOENT')
|
|
897
|
-
return { ok: false, reason: 'unreadable', code: errorCode(err) };
|
|
898
|
-
return { ok: false, reason: 'not_found', candidates: await this.candidatesOrNone(relPath, 'dir') };
|
|
899
|
-
}
|
|
900
|
-
if (!stat.isDirectory())
|
|
901
|
-
return { ok: false, reason: 'not_a_directory' };
|
|
902
|
-
const clampedDepth = Math.min(5, Math.max(1, depth));
|
|
903
|
-
const lines = [];
|
|
904
|
-
try {
|
|
905
|
-
await this.walkDir(absPath, 0, clampedDepth - 1, pattern, lines);
|
|
906
|
-
}
|
|
907
|
-
catch (err) {
|
|
908
|
-
return { ok: false, reason: 'unreadable', code: errorCode(err) };
|
|
909
|
-
}
|
|
910
|
-
this.logger.debug('SearcherService: listFiles result', { entries: lines.length, ms: Date.now() - t0 });
|
|
911
|
-
if (lines.length === 0) {
|
|
912
|
-
return { ok: true, tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
|
|
913
|
-
}
|
|
914
|
-
return { ok: true, tree: lines.join('\n') };
|
|
915
|
-
}
|
|
916
|
-
async openableFile(relPath, absPath) {
|
|
917
|
-
const loc = await this.checkRealLocation(relPath, absPath, 'file');
|
|
918
|
-
if (!loc.ok)
|
|
919
|
-
return loc;
|
|
920
|
-
let stat;
|
|
921
|
-
try {
|
|
922
|
-
stat = await this.fs.stat(absPath);
|
|
923
|
-
}
|
|
924
|
-
catch (err) {
|
|
925
|
-
if (errorCode(err) !== 'ENOENT')
|
|
926
|
-
return { ok: false, reason: 'unreadable', code: errorCode(err) };
|
|
927
|
-
if (await this.isIndexedPath(relPath))
|
|
928
|
-
return { ok: false, reason: 'deleted' };
|
|
929
|
-
return { ok: false, reason: 'not_found', candidates: await this.candidatesOrNone(relPath, 'file') };
|
|
930
|
-
}
|
|
931
|
-
return stat.isFile() ? null : { ok: false, reason: 'not_a_file' };
|
|
932
|
-
}
|
|
933
|
-
async readLines(absPath, onLine) {
|
|
934
|
-
const stream = this.fs.createReadStream(absPath);
|
|
935
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
936
|
-
let lineNo = 0;
|
|
937
|
-
try {
|
|
938
|
-
for await (const line of rl) {
|
|
939
|
-
lineNo++;
|
|
940
|
-
if (!onLine(line, lineNo))
|
|
941
|
-
break;
|
|
942
|
-
}
|
|
943
|
-
return { ok: true, lineCount: lineNo };
|
|
944
|
-
}
|
|
945
|
-
catch (err) {
|
|
946
|
-
return { ok: false, code: errorCode(err) };
|
|
947
|
-
}
|
|
948
|
-
finally {
|
|
949
|
-
rl.close();
|
|
950
|
-
stream.destroy?.();
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
async readFile(relPath, maxLines) {
|
|
954
|
-
const t0 = Date.now();
|
|
955
|
-
this.logger.debug('SearcherService: readFile', { relPath, maxLines });
|
|
956
|
-
const absPath = this.toAbs(relPath);
|
|
957
|
-
const failure = await this.openableFile(relPath, absPath);
|
|
958
|
-
if (failure)
|
|
959
|
-
return failure;
|
|
960
|
-
const lines = [];
|
|
961
|
-
let hasMore = false;
|
|
962
|
-
const read = await this.readLines(absPath, line => {
|
|
963
|
-
if (lines.length < maxLines) {
|
|
964
|
-
lines.push(line);
|
|
965
|
-
return true;
|
|
966
|
-
}
|
|
967
|
-
hasMore = true;
|
|
968
|
-
return false;
|
|
969
|
-
});
|
|
970
|
-
if (!read.ok)
|
|
971
|
-
return { ok: false, reason: 'unreadable', code: read.code };
|
|
972
|
-
this.logger.debug('SearcherService: readFile result', { linesRead: lines.length, hasMore, ms: Date.now() - t0 });
|
|
973
|
-
return { ok: true, filePath: relPath, startLine: 1, endLine: lines.length, lines, hasMore };
|
|
974
|
-
}
|
|
975
|
-
async readFileRange(relPath, startLine, endLine) {
|
|
976
|
-
const t0 = Date.now();
|
|
977
|
-
this.logger.debug('SearcherService: readFileRange', { relPath, startLine, endLine });
|
|
978
|
-
if (endLine < startLine)
|
|
979
|
-
return { ok: false, reason: 'bad_range', startLine, endLine };
|
|
980
|
-
const absPath = this.toAbs(relPath);
|
|
981
|
-
const failure = await this.openableFile(relPath, absPath);
|
|
982
|
-
if (failure)
|
|
983
|
-
return failure;
|
|
984
|
-
const lines = [];
|
|
985
|
-
let lastRead = 0;
|
|
986
|
-
let hasMore = false;
|
|
987
|
-
const read = await this.readLines(absPath, (line, lineNo) => {
|
|
988
|
-
if (lineNo > endLine) {
|
|
989
|
-
hasMore = true;
|
|
990
|
-
return false;
|
|
991
|
-
}
|
|
992
|
-
if (lineNo >= startLine)
|
|
993
|
-
lines.push(line);
|
|
994
|
-
lastRead = lineNo;
|
|
995
|
-
return true;
|
|
996
|
-
});
|
|
997
|
-
if (!read.ok)
|
|
998
|
-
return { ok: false, reason: 'unreadable', code: read.code };
|
|
999
|
-
if (!hasMore && startLine > Math.max(read.lineCount, 1)) {
|
|
1000
|
-
return { ok: false, reason: 'past_end', lineCount: read.lineCount, startLine };
|
|
1001
|
-
}
|
|
1002
|
-
this.logger.debug('SearcherService: readFileRange result', { linesRead: lines.length, hasMore, ms: Date.now() - t0 });
|
|
1003
|
-
return { ok: true, filePath: relPath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
|
|
1004
|
-
}
|
|
1005
|
-
async walkDir(absDir, currentDepth, maxDepth, pattern, lines) {
|
|
1006
|
-
const entries = await this.fs.readdir(absDir);
|
|
1007
|
-
entries.sort((a, b) => {
|
|
1008
|
-
const ad = a.isDirectory() ? 0 : 1;
|
|
1009
|
-
const bd = b.isDirectory() ? 0 : 1;
|
|
1010
|
-
if (ad !== bd)
|
|
1011
|
-
return ad - bd;
|
|
1012
|
-
return a.name.localeCompare(b.name);
|
|
1013
|
-
});
|
|
1014
|
-
for (const entry of entries) {
|
|
1015
|
-
if (file_utils_1.IGNORE_DIRS.has(entry.name))
|
|
1016
|
-
continue;
|
|
1017
|
-
const indent = ' '.repeat(currentDepth);
|
|
1018
|
-
if (entry.isDirectory()) {
|
|
1019
|
-
const at = lines.push(`${indent}${entry.name}/`) - 1;
|
|
1020
|
-
if (currentDepth < maxDepth) {
|
|
1021
|
-
try {
|
|
1022
|
-
await this.walkDir(path.join(absDir, entry.name), currentDepth + 1, maxDepth, pattern, lines);
|
|
1023
|
-
}
|
|
1024
|
-
catch {
|
|
1025
|
-
lines[at] += ' (unreadable)';
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
else {
|
|
1030
|
-
if (pattern && !entry.name.includes(pattern))
|
|
1031
|
-
continue;
|
|
1032
|
-
lines.push(`${indent}${entry.name}`);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
buildFindSymbolPattern(name, kind) {
|
|
1037
|
-
if (kind === 'function_property' || kind === 'method_signature')
|
|
1038
|
-
return null;
|
|
1039
|
-
if (GRAPHQL_KINDS.has(kind))
|
|
1040
|
-
return null;
|
|
1041
|
-
if (kind === 'constructor')
|
|
1042
|
-
return name === 'constructor' ? `(^|[[:space:]])constructor[[:space:]]*\\(` : null;
|
|
1043
|
-
const esc = name.replace(/\$/g, '\\$');
|
|
1044
|
-
const MODIFIER = '(public|private|protected|static|abstract|async|override)';
|
|
1045
|
-
const BOUNDARY = '(^|[^[:alnum:]_$])';
|
|
1046
|
-
const patterns = {
|
|
1047
|
-
function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${esc}[[:space:](<]`,
|
|
1048
|
-
class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${esc}[[:space:]{<]`,
|
|
1049
|
-
type: `(export[[:space:]]+)?type[[:space:]]+${esc}[[:space:]=<]`,
|
|
1050
|
-
interface: `(export[[:space:]]+)?interface[[:space:]]+${esc}[[:space:]{<]`,
|
|
1051
|
-
variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${esc}[[:space:]=:]`,
|
|
1052
|
-
method: `${BOUNDARY}(${MODIFIER}[[:space:]]+)+\\*?[[:space:]]*${esc}[[:space:]]*[(<]`,
|
|
1053
|
-
get_accessor: `(^|[[:space:]])get[[:space:]]+${esc}[[:space:]]*\\(`,
|
|
1054
|
-
set_accessor: `(^|[[:space:]])set[[:space:]]+${esc}[[:space:]]*\\(`,
|
|
1055
|
-
};
|
|
1056
|
-
if (kind === 'all')
|
|
1057
|
-
return Object.values(patterns).join('|');
|
|
1058
|
-
return patterns[kind];
|
|
1059
|
-
}
|
|
1060
|
-
static wholeWordIndex(line, name) {
|
|
1061
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1062
|
-
const re = new RegExp(`(?<![\\w$#])${escaped}(?![\\w$#])`);
|
|
1063
|
-
const m = re.exec(line);
|
|
1064
|
-
return m ? m.index : -1;
|
|
1065
|
-
}
|
|
1066
|
-
inferKind(line, name) {
|
|
1067
|
-
if (name === 'constructor')
|
|
1068
|
-
return 'constructor';
|
|
1069
|
-
const idx = SearcherService.wholeWordIndex(line, name);
|
|
1070
|
-
const before = idx === -1 ? line : line.slice(0, idx);
|
|
1071
|
-
const tokens = before.trim().split(/\s+/).filter(Boolean);
|
|
1072
|
-
const last = (tokens[tokens.length - 1] ?? '').replace(/\*$/, '');
|
|
1073
|
-
if (last === 'get')
|
|
1074
|
-
return 'get_accessor';
|
|
1075
|
-
if (last === 'set')
|
|
1076
|
-
return 'set_accessor';
|
|
1077
|
-
if (last === 'function')
|
|
1078
|
-
return 'function';
|
|
1079
|
-
if (last === 'class')
|
|
1080
|
-
return 'class';
|
|
1081
|
-
if (last === 'interface')
|
|
1082
|
-
return 'interface';
|
|
1083
|
-
if (last === 'type')
|
|
1084
|
-
return 'type';
|
|
1085
|
-
if (['public', 'private', 'protected', 'static', 'abstract', 'async', 'override'].includes(last))
|
|
1086
|
-
return 'method';
|
|
1087
|
-
return 'variable';
|
|
1088
|
-
}
|
|
1089
|
-
spawnGrep(args) {
|
|
1090
|
-
return new Promise((resolve) => {
|
|
1091
|
-
const proc = (0, child_process_1.spawn)('grep', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1092
|
-
const chunks = [];
|
|
1093
|
-
let totalBytes = 0;
|
|
1094
|
-
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
1095
|
-
proc.stdout.on('data', (chunk) => {
|
|
1096
|
-
totalBytes += chunk.length;
|
|
1097
|
-
if (totalBytes <= MAX_BUFFER) {
|
|
1098
|
-
chunks.push(chunk);
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
proc.stderr.on('data', () => { });
|
|
1102
|
-
proc.on('error', (err) => {
|
|
1103
|
-
this.logger.error('SearcherService: grep spawn failed', { err });
|
|
1104
|
-
resolve({ ok: false, output: null });
|
|
1105
|
-
});
|
|
1106
|
-
proc.on('close', (code) => {
|
|
1107
|
-
const output = chunks.length === 0 ? null : Buffer.concat(chunks).toString('utf-8');
|
|
1108
|
-
if (code !== null && code >= 2) {
|
|
1109
|
-
this.logger.warn('SearcherService: grep exited with code', { code });
|
|
1110
|
-
resolve({ ok: false, output });
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
resolve({ ok: true, output });
|
|
1114
|
-
});
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
exports.SearcherService = SearcherService;
|