codesearch-mcp 0.2.1-alpha → 0.2.3-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.
Files changed (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +15 -8
  3. package/dist/app.js +45 -7
  4. package/dist/src/commands/EvalCommand.d.ts +10 -0
  5. package/dist/src/commands/EvalCommand.js +231 -0
  6. package/dist/src/commands/IndexCommand.js +79 -53
  7. package/dist/src/db/index.d.ts +1 -1
  8. package/dist/src/db/index.js +1 -2
  9. package/dist/src/db/schema.d.ts +5 -29
  10. package/dist/src/db/schema.js +2 -8
  11. package/dist/src/dependency-traversal.d.ts +16 -0
  12. package/dist/src/dependency-traversal.js +45 -0
  13. package/dist/src/enriched-prompt.d.ts +3 -2
  14. package/dist/src/enriched-prompt.js +3 -2
  15. package/dist/src/eval-results.d.ts +12 -0
  16. package/dist/src/eval-results.js +114 -0
  17. package/dist/src/file-utils.d.ts +15 -0
  18. package/dist/src/file-utils.js +24 -1
  19. package/dist/src/help.js +4 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +13 -1
  21. package/dist/src/interfaces/IDatabase.d.ts +0 -14
  22. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
  23. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  24. package/dist/src/interfaces/ISearcher.d.ts +11 -16
  25. package/dist/src/interfaces/index.d.ts +1 -0
  26. package/dist/src/project-scope.d.ts +19 -0
  27. package/dist/src/project-scope.js +49 -0
  28. package/dist/src/project-selector.d.ts +6 -0
  29. package/dist/src/project-selector.js +18 -0
  30. package/dist/src/retrieval-eval.d.ts +16 -0
  31. package/dist/src/retrieval-eval.js +214 -0
  32. package/dist/src/services/AstParserService.d.ts +66 -1
  33. package/dist/src/services/AstParserService.js +432 -28
  34. package/dist/src/services/DatabaseService.d.ts +0 -12
  35. package/dist/src/services/DatabaseService.js +2 -52
  36. package/dist/src/services/IndexerService.d.ts +8 -0
  37. package/dist/src/services/IndexerService.js +24 -11
  38. package/dist/src/services/McpServer.d.ts +7 -0
  39. package/dist/src/services/McpServer.js +42 -38
  40. package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
  41. package/dist/src/services/SearcherService.d.ts +189 -25
  42. package/dist/src/services/SearcherService.js +505 -203
  43. package/dist/src/symbol-rows.d.ts +14 -9
  44. package/dist/src/symbol-rows.js +17 -11
  45. package/dist/src/tools/FindSymbolTool.js +14 -5
  46. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  47. package/dist/src/tools/GetCallersTool.js +16 -6
  48. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  49. package/dist/src/tools/GetDependenciesTool.js +15 -8
  50. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  51. package/dist/src/tools/GetDependentsTool.js +31 -16
  52. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  53. package/dist/src/tools/ListFilesTool.js +14 -12
  54. package/dist/src/tools/ListProjectsTool.js +5 -1
  55. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  56. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  57. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  58. package/dist/src/tools/ReadFileTool.js +12 -10
  59. package/dist/src/tools/SearchCodebaseTool.js +3 -1
  60. package/dist/src/tools/format-helpers.d.ts +16 -1
  61. package/dist/src/tools/format-helpers.js +64 -0
  62. package/package.json +9 -8
@@ -2,6 +2,12 @@ export interface Project {
2
2
  name: string;
3
3
  path: string;
4
4
  }
5
+ /**
6
+ * One-off single-choice prompt for `--project` with no name — distinct from
7
+ * the multi-select `checkbox` flow: it reads/writes nothing persisted.
8
+ * Resolves to `undefined` if the user cancels (e.g. Ctrl+C).
9
+ */
10
+ export declare function selectSingleProject(units: Project[]): Promise<string | undefined>;
5
11
  export declare class ProjectSelector {
6
12
  private readonly workspaceDir;
7
13
  private included;
@@ -34,9 +34,27 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ProjectSelector = void 0;
37
+ exports.selectSingleProject = selectSingleProject;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  const prompts_1 = require("@inquirer/prompts");
41
+ /**
42
+ * One-off single-choice prompt for `--project` with no name — distinct from
43
+ * the multi-select `checkbox` flow: it reads/writes nothing persisted.
44
+ * Resolves to `undefined` if the user cancels (e.g. Ctrl+C).
45
+ */
46
+ async function selectSingleProject(units) {
47
+ try {
48
+ return await (0, prompts_1.select)({
49
+ message: 'Select a project to index',
50
+ choices: units.map(p => ({ name: p.name, value: p.name })),
51
+ pageSize: 15,
52
+ });
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
40
58
  async function loadLines(filePath) {
41
59
  try {
42
60
  const content = await fs.promises.readFile(filePath, 'utf-8');
@@ -0,0 +1,16 @@
1
+ import type { EvalSuite, SearchFn, IndexInspector, IRetrievalEvaluator, EvalRunResult, EvalResultFile, CompareResult } from './interfaces/IRetrievalEvaluator';
2
+ /** Thrown by `parseSuite` before any search runs — names the offending case and field. */
3
+ export declare class EvalSuiteError extends Error {
4
+ readonly caseId: string | null;
5
+ readonly field: string | null;
6
+ constructor(message: string, caseId: string | null, field: string | null);
7
+ }
8
+ /** Validates a loaded suite object against the schema; rejects before any search runs. */
9
+ export declare function parseSuite(raw: unknown): EvalSuite;
10
+ /** Merges several suites (already parsed) into one; case ids must be unique across all of them. */
11
+ export declare function mergeSuites(suites: EvalSuite[]): EvalSuite;
12
+ export declare class RetrievalEvaluator implements IRetrievalEvaluator {
13
+ evaluate(suite: EvalSuite, search: SearchFn, inspect: IndexInspector): Promise<EvalRunResult>;
14
+ }
15
+ /** Pure comparison of two result files; prints a warning (never refuses) on mismatched conditions. */
16
+ export declare function compare(baseline: EvalResultFile, current: EvalResultFile): CompareResult;
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RetrievalEvaluator = exports.EvalSuiteError = void 0;
4
+ exports.parseSuite = parseSuite;
5
+ exports.mergeSuites = mergeSuites;
6
+ exports.compare = compare;
7
+ const zod_1 = require("zod");
8
+ /** Thrown by `parseSuite` before any search runs — names the offending case and field. */
9
+ class EvalSuiteError extends Error {
10
+ caseId;
11
+ field;
12
+ constructor(message, caseId, field) {
13
+ super(message);
14
+ this.caseId = caseId;
15
+ this.field = field;
16
+ this.name = 'EvalSuiteError';
17
+ }
18
+ }
19
+ exports.EvalSuiteError = EvalSuiteError;
20
+ const ExpectedAnswerSchema = zod_1.z.object({
21
+ file: zod_1.z.string().min(1),
22
+ owner: zod_1.z.string().min(1).optional(),
23
+ });
24
+ const CaseSchema = zod_1.z.object({
25
+ id: zod_1.z.string().min(1),
26
+ project: zod_1.z.string().min(1),
27
+ category: zod_1.z.enum(['identifier', 'behaviour', 'concept']),
28
+ query: zod_1.z.string().min(1),
29
+ expect: zod_1.z.array(ExpectedAnswerSchema).min(1),
30
+ threshold: zod_1.z.number().min(0).max(1).optional(),
31
+ });
32
+ const SuiteSchema = zod_1.z.object({
33
+ version: zod_1.z.literal(1),
34
+ cases: zod_1.z.array(CaseSchema),
35
+ });
36
+ /** Best-effort id of the case at `cases[index]` in a raw (possibly invalid) object, for error messages. */
37
+ function rawCaseId(raw, index) {
38
+ if (typeof raw !== 'object' || raw === null)
39
+ return null;
40
+ const cases = raw.cases;
41
+ if (!Array.isArray(cases))
42
+ return null;
43
+ const entry = cases[index];
44
+ if (typeof entry !== 'object' || entry === null)
45
+ return null;
46
+ const id = entry.id;
47
+ return typeof id === 'string' ? id : null;
48
+ }
49
+ /** Validates a loaded suite object against the schema; rejects before any search runs. */
50
+ function parseSuite(raw) {
51
+ const result = SuiteSchema.safeParse(raw);
52
+ if (!result.success) {
53
+ const issue = result.error.issues[0];
54
+ const pathParts = issue.path;
55
+ const caseIndex = typeof pathParts[1] === 'number' ? pathParts[1] : null;
56
+ const caseId = caseIndex !== null ? rawCaseId(raw, caseIndex) ?? `#${caseIndex}` : null;
57
+ const field = pathParts.length > 2 ? String(pathParts[pathParts.length - 1]) : pathParts.length === 2 ? 'id' : String(pathParts[0] ?? '');
58
+ throw new EvalSuiteError(`${issue.message} (case ${caseId ?? '?'}, field ${field})`, caseId, field);
59
+ }
60
+ const seen = new Set();
61
+ for (const c of result.data.cases) {
62
+ if (seen.has(c.id)) {
63
+ throw new EvalSuiteError(`Duplicate case id (case ${c.id}, field id)`, c.id, 'id');
64
+ }
65
+ seen.add(c.id);
66
+ }
67
+ return result.data;
68
+ }
69
+ /** Merges several suites (already parsed) into one; case ids must be unique across all of them. */
70
+ function mergeSuites(suites) {
71
+ const cases = [];
72
+ const seen = new Set();
73
+ for (const suite of suites) {
74
+ for (const c of suite.cases) {
75
+ if (seen.has(c.id))
76
+ throw new EvalSuiteError(`Duplicate case id across suites (case ${c.id}, field id)`, c.id, 'id');
77
+ seen.add(c.id);
78
+ cases.push(c);
79
+ }
80
+ }
81
+ return { version: 1, cases };
82
+ }
83
+ /** `owner` matches exactly, or as a prefix at a `.` boundary (`RepairsService` accepts any member of the class). */
84
+ function ownerMatches(ownerPath, expectedOwner) {
85
+ return ownerPath === expectedOwner || ownerPath.startsWith(expectedOwner + '.');
86
+ }
87
+ async function validateCase(c, inspect) {
88
+ const valid = [];
89
+ const invalid = [];
90
+ for (const expected of c.expect) {
91
+ const indexed = await inspect.isFileIndexed(c.project, expected.file);
92
+ if (!indexed) {
93
+ invalid.push(expected);
94
+ continue;
95
+ }
96
+ if (expected.owner) {
97
+ const owners = await inspect.ownerPathsOfFile(c.project, expected.file);
98
+ const found = owners.some(o => ownerMatches(o, expected.owner));
99
+ if (!found) {
100
+ invalid.push(expected);
101
+ continue;
102
+ }
103
+ }
104
+ valid.push(expected);
105
+ }
106
+ return { valid, invalid };
107
+ }
108
+ function rankOf(hits, valid) {
109
+ for (let i = 0; i < hits.length; i++) {
110
+ const hit = hits[i];
111
+ const matches = valid.some(e => hit.filePath === e.file && (!e.owner || (hit.ownerPath !== null && ownerMatches(hit.ownerPath, e.owner))));
112
+ if (matches)
113
+ return { rank: i + 1, matched: hit };
114
+ }
115
+ return { rank: null, matched: null };
116
+ }
117
+ function emptyMetrics() {
118
+ return { count: 0, hit1: 0, hit5: 0, mrr10: 0 };
119
+ }
120
+ function accumulate(cases) {
121
+ const n = cases.length;
122
+ if (n === 0)
123
+ return emptyMetrics();
124
+ let hit1 = 0, hit5 = 0, mrrSum = 0;
125
+ for (const c of cases) {
126
+ if (c.rank === 1)
127
+ hit1++;
128
+ if (c.rank !== null && c.rank <= 5)
129
+ hit5++;
130
+ mrrSum += c.rank !== null ? 1 / c.rank : 0;
131
+ }
132
+ return { count: n, hit1: hit1 / n, hit5: hit5 / n, mrr10: mrrSum / n };
133
+ }
134
+ class RetrievalEvaluator {
135
+ async evaluate(suite, search, inspect) {
136
+ const cases = [];
137
+ const invalid = [];
138
+ const warnings = [];
139
+ for (const c of suite.cases) {
140
+ const { valid, invalid: invalidExpects } = await validateCase(c, inspect);
141
+ if (valid.length === 0) {
142
+ invalid.push({ id: c.id, reason: 'no valid expected answer', invalidExpects });
143
+ continue;
144
+ }
145
+ if (invalidExpects.length > 0) {
146
+ warnings.push({ id: c.id, droppedExpects: invalidExpects });
147
+ }
148
+ const hits = await search(c.query, 10, c.project, c.threshold);
149
+ const { rank, matched } = rankOf(hits, valid);
150
+ cases.push({ id: c.id, project: c.project, category: c.category, query: c.query, rank, matched });
151
+ }
152
+ const byCategory = {};
153
+ for (const category of ['identifier', 'behaviour', 'concept']) {
154
+ const subset = cases.filter(c => c.category === category);
155
+ if (subset.length > 0)
156
+ byCategory[category] = accumulate(subset);
157
+ }
158
+ return { cases, invalid, warnings, metrics: { overall: accumulate(cases), byCategory } };
159
+ }
160
+ }
161
+ exports.RetrievalEvaluator = RetrievalEvaluator;
162
+ function metricDelta(baseline, current) {
163
+ return { baseline, current, delta: current - baseline };
164
+ }
165
+ function metricsDelta(baseline, current) {
166
+ return {
167
+ count: metricDelta(baseline.count, current.count),
168
+ hit1: metricDelta(baseline.hit1, current.hit1),
169
+ hit5: metricDelta(baseline.hit5, current.hit5),
170
+ mrr10: metricDelta(baseline.mrr10, current.mrr10),
171
+ };
172
+ }
173
+ /** Pure comparison of two result files; prints a warning (never refuses) on mismatched conditions. */
174
+ function compare(baseline, current) {
175
+ const conditionWarnings = [];
176
+ if (baseline.metadata.suiteHash !== current.metadata.suiteHash) {
177
+ conditionWarnings.push(`suite differs: baseline ${baseline.metadata.suiteHash} vs current ${current.metadata.suiteHash}`);
178
+ }
179
+ if (baseline.metadata.embeddingModel !== current.metadata.embeddingModel) {
180
+ conditionWarnings.push(`embedding model differs: baseline ${baseline.metadata.embeddingModel} vs current ${current.metadata.embeddingModel}`);
181
+ }
182
+ if (baseline.metadata.rerankerModel !== current.metadata.rerankerModel) {
183
+ conditionWarnings.push(`reranker differs: baseline ${baseline.metadata.rerankerModel ?? 'none'} vs current ${current.metadata.rerankerModel ?? 'none'}`);
184
+ }
185
+ const byCategory = {};
186
+ for (const category of ['identifier', 'behaviour', 'concept']) {
187
+ const b = baseline.run.metrics.byCategory[category];
188
+ const cur = current.run.metrics.byCategory[category];
189
+ if (b && cur)
190
+ byCategory[category] = metricsDelta(b, cur);
191
+ }
192
+ const baselineRanked = new Map(baseline.run.cases.map(c => [c.id, c.rank]));
193
+ const currentRanked = new Map(current.run.cases.map(c => [c.id, c.rank]));
194
+ const hit5Gained = [];
195
+ const hit5Lost = [];
196
+ const allIds = new Set([...baselineRanked.keys(), ...currentRanked.keys()]);
197
+ for (const id of allIds) {
198
+ const b = baselineRanked.get(id) ?? null;
199
+ const cur = currentRanked.get(id) ?? null;
200
+ const bHit5 = b !== null && b <= 5;
201
+ const curHit5 = cur !== null && cur <= 5;
202
+ if (!bHit5 && curHit5)
203
+ hit5Gained.push(id);
204
+ if (bHit5 && !curHit5)
205
+ hit5Lost.push(id);
206
+ }
207
+ return {
208
+ overall: metricsDelta(baseline.run.metrics.overall, current.run.metrics.overall),
209
+ byCategory,
210
+ hit5Gained,
211
+ hit5Lost,
212
+ conditionWarnings,
213
+ };
214
+ }
@@ -1,9 +1,16 @@
1
+ import { Node } from 'ts-morph';
1
2
  import type { IAstParser, DepEdge, FileAnalysis, AnalyzeProjectOpts } from '../interfaces/IAstParser';
2
3
  import type { ILogger } from '../interfaces';
4
+ /** Test seam for `graphql-symbols`' resolve-field parent-type resolution —
5
+ * defaults to the alias-following type-checker lookup `extractCalls` already uses. */
6
+ export interface AstParserDeps {
7
+ resolveDeclaration?(identifier: Node): Node | undefined;
8
+ }
3
9
  export declare class AstParserService implements IAstParser {
4
10
  private logger;
11
+ private deps;
5
12
  private morphProjects;
6
- constructor(logger?: ILogger);
13
+ constructor(logger?: ILogger, deps?: AstParserDeps);
7
14
  analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: AnalyzeProjectOpts): Promise<Map<string, FileAnalysis>>;
8
15
  clearCache(projectRoot?: string): void;
9
16
  analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
@@ -12,6 +19,10 @@ export declare class AstParserService implements IAstParser {
12
19
  private createMorphProject;
13
20
  private getMorphProject;
14
21
  private extractEdges;
22
+ /** Manual fallback for a bindingless import specifier (design.md decision 2) —
23
+ * ts-morph's `getModuleSpecifierSourceFile()` is `null` for these even when
24
+ * the target is a real project file, since nothing is bound to resolve through. */
25
+ private resolveSideEffectModuleSpecifier;
15
26
  private resolveFromSymbol;
16
27
  private buildUsageMap;
17
28
  private getPos;
@@ -26,6 +37,60 @@ export declare class AstParserService implements IAstParser {
26
37
  * own first-to-last line — decorator-inclusive, comment-exclusive, and (for a
27
38
  * variable) the declarator alone, never the whole statement. */
28
39
  private buildOwnerRanges;
40
+ /** Local name → the name exported by `@nestjs/graphql`, for every named import
41
+ * of that module specifier (aliases followed). Recognition is import-based,
42
+ * not type-checker-based, so `skipFileDependencyResolution` batches agree. */
43
+ private buildGraphqlImportMap;
44
+ /** The identifier naming a decorator's expression (`@Foo` or the callee of `@Foo(...)`). */
45
+ private static decoratorIdentifier;
46
+ /** `dec` recognised against `importMap` — its exported `@nestjs/graphql` name and its call arguments (`[]` for a bare `@Foo`). */
47
+ private recognizedDecorator;
48
+ private static literalStringValue;
49
+ private static checkGraphqlGrammar;
50
+ /** `true` when any decorator argument is an (unwrapped) options object
51
+ * literal with `isAbstract: true` — an abstract base type (`PaginatedType`
52
+ * pattern) that NestJS never registers in the actual GraphQL schema. */
53
+ private static isAbstractGraphqlType;
54
+ /** `obj`'s own `name` property (`PropertyAssignment` or `ShorthandPropertyAssignment`), if any. */
55
+ private static findNameProperty;
56
+ /** Strips parens/`as`/`satisfies`/non-null wrappers (any nesting) to find the
57
+ * underlying node — used only to classify an argument's *shape* (type
58
+ * function vs. options object vs. literal vs. other), never to read a name
59
+ * value through the wrapper (decision 4 of `graphql-symbols` forbids that). */
60
+ private static peekThroughWrapper;
61
+ /**
62
+ * GraphQL name from a decorator's arguments (design decision 4 of
63
+ * `graphql-symbols`) — syntactic, no wrapper looked through at any step.
64
+ * `allowOptionsName` gates step 2 (the `name` option — `Query`/`Mutation`/
65
+ * `Subscription`/`ResolveField` only); `fallbackName` is the TS name used at
66
+ * step 3.
67
+ */
68
+ private deriveGraphqlName;
69
+ private logGraphqlSkip;
70
+ /** `declNode`'s qualified TypeScript name for diagnostics, independent of
71
+ * whether its own `code_symbol` row was actually written. */
72
+ private declarationNameFor;
73
+ /** The identifier named by a `@Resolver` argument: directly, or as an arrow
74
+ * function's expression body; or the sole identifier of a single-element
75
+ * array literal in either position (list-returning resolver). */
76
+ private static extractResolverIdentifier;
77
+ private defaultResolveDeclaration;
78
+ /** The GraphQL type name of a `@ResolveField`'s resolved parent class
79
+ * (design decision 5, second half): its recognised `@ObjectType` (else
80
+ * `@InterfaceType`) decorator, or its class name when it carries no
81
+ * decorator spelled `ObjectType`/`InterfaceType` at all, recognised or not. */
82
+ private classGraphqlTypeName;
83
+ /** The parent type of a `@ResolveField` — the enclosing class's recognised
84
+ * `@Resolver` argument (design decision 5, first half). */
85
+ private resolveResolverParentType;
86
+ private extractClassTypeRows;
87
+ private extractResolveFieldRow;
88
+ private extractMethodOperationRows;
89
+ /** GraphQL-named rows for every recognised `@nestjs/graphql` decorator in
90
+ * the file (`graphql-symbols`). `ownRows` is `symbols ∪ members` — the
91
+ * declarations' own `code_symbol` candidates, checked so a GraphQL row is
92
+ * never written without its decorated declaration's own row. */
93
+ private extractGraphqlNames;
29
94
  private static isValidSegment;
30
95
  private static memberName;
31
96
  private static unwrapExpr;