Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 2 additions & 8 deletions src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ import {
handleGraphCallees,
handleGraphChain,
} from './handlers/graphHandlers';
import { SemanticSearchSchema } from './schemas/semanticSchemas';
import { IndexRepoSchema } from './schemas/indexSchemas';
import { SearchSymbolsSchema } from './schemas/querySchemas';
import { SearchFilesSchema } from './schemas/queryFilesSchemas';
import { handleSemanticSearch } from './handlers/semanticHandlers';
import { handleIndexRepo } from './handlers/indexHandlers';
import { handleSearchSymbols } from './handlers/queryHandlers';
import { handleSearchFiles } from './handlers/queryFilesHandlers';
Expand All @@ -42,17 +40,13 @@ import { handleRepoMap } from './handlers/repoMapHandler';
* Maps command keys to their schema + handler implementations.
*
* Command keys follow the pattern:
* - Top-level commands: 'index', 'semantic', 'status'
* - Subcommands: 'graph:find', 'graph:query', 'dsr:generate'
* - Top-level commands: 'index', 'status'
* - Subcommands: 'graph:find', 'graph:query'
*
* This will be populated as commands are migrated from src/commands/*.ts
*/
export const cliHandlers: Record<string, HandlerRegistration<any>> = {
// Top-level commands
'semantic': {
schema: SemanticSearchSchema,
handler: handleSemanticSearch,
},
'index': {
schema: IndexRepoSchema,
handler: handleIndexRepo,
Expand Down
2 changes: 0 additions & 2 deletions src/commands/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Command } from 'commander';
import { indexCommand } from '../cli/commands/indexCommand.js';
import { queryCommand } from '../cli/commands/queryCommand.js';
import { queryFilesCommand } from '../cli/commands/queryFilesCommand.js';
import { semanticCommand } from '../cli/commands/semanticCommand.js';
import { serveCommand, agentCommand } from '../cli/commands/serveCommands.js';
import { packCommand, unpackCommand } from '../cli/commands/archiveCommands.js';
import { hooksCommand } from '../cli/commands/hooksCommands.js';
Expand All @@ -18,7 +17,6 @@ export const aiCommand = new Command('ai')
.addCommand(repoMapCommand)
.addCommand(queryCommand)
.addCommand(queryFilesCommand)
.addCommand(semanticCommand)
.addCommand(graphCommand)
.addCommand(packCommand)
.addCommand(unpackCommand)
Expand Down
47 changes: 0 additions & 47 deletions src/core/search.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,12 @@
import { dequantizeSQ8, cosineSimilarity, quantizeSQ8, SQ8Vector } from './sq8';
import { hashEmbedding } from './embedding';
import { classifyQuery } from './retrieval/classifier';
import { expandQuery } from './retrieval/expander';
import { fuseResults } from './retrieval/fuser';
import { rerank } from './retrieval/reranker';
import { computeWeights, type WeightFeedback } from './retrieval/weights';
import type { QueryType, RankedResult, RetrievalResult, RetrievalWeights } from './retrieval/types';

export interface SemanticHit {
content_hash: string;
score: number;
text?: string;
}

export interface AdaptiveQueryPlan {
query: string;
expanded: string[];
queryType: QueryType;
weights: RetrievalWeights;
}

export interface AdaptiveFusionOptions {
feedback?: WeightFeedback;
limit?: number;
}

export interface AdaptiveFusionOutput extends AdaptiveQueryPlan {
results: RankedResult[];
}

export function buildQueryVector(text: string, dim: number): SQ8Vector {
const vec = hashEmbedding(text, { dim });
return quantizeSQ8(vec);
Expand All @@ -39,28 +17,3 @@ export function scoreAgainst(q: SQ8Vector, item: { scale: number; qvec: Int8Arra
const vf = dequantizeSQ8({ dim: item.dim, scale: item.scale, q: item.qvec });
return cosineSimilarity(qf, vf);
}

export function buildAdaptiveQueryPlan(query: string, feedback?: WeightFeedback): AdaptiveQueryPlan {
const q = String(query ?? '').trim();
const queryType = classifyQuery(q);
const expanded = expandQuery(q, queryType);
const weights = computeWeights(queryType, feedback);
return { query: q, expanded, queryType, weights };
}

/**
* Runs the adaptive retrieval pipeline: classification -> expansion -> weighting -> fusion -> heuristic reranking.
*
* Note: This uses synchronous heuristic reranking. For higher quality but slower reranking using
* the ONNX Cross-Encoder, use the `CrossEncoderReranker` class directly (which is async).
*/
export function runAdaptiveRetrieval(
query: string,
candidates: RetrievalResult[],
options: AdaptiveFusionOptions = {}
): AdaptiveFusionOutput {
const plan = buildAdaptiveQueryPlan(query, options.feedback);
const fused = fuseResults(candidates, plan.weights, options.limit);
const results = rerank(plan.query, fused, { limit: options.limit });
return { ...plan, results };
}
69 changes: 1 addition & 68 deletions src/mcp/handlers/searchHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import type { ToolHandler } from '../types';
import { successResponse, errorResponse } from '../types';
import type {
SearchSymbolsArgs,
SemanticSearchArgs,
RepoMapArgs
} from '../schemas';
import { resolveGitRoot, inferScanRoot, inferWorkspaceRoot } from '../../core/git';
import { defaultDbDir, openTablesByLang } from '../../core/lancedb';
import { buildQueryVector, scoreAgainst } from '../../core/search';
import { checkIndex, resolveLangs } from '../../core/indexCheck';
import { generateRepoMap } from '../../core/repoMap';
import { buildCoarseWhere, filterAndRankSymbolRows, inferSymbolSearchMode, pickCoarseToken } from '../../core/symbolSearch';
Expand Down Expand Up @@ -253,69 +251,4 @@ export const handleSearchSymbols: ToolHandler<SearchSymbolsArgs> = async (args)
});
};

export const handleSemanticSearch: ToolHandler<SemanticSearchArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const query = args.query;
const topk = args.topk ?? 10;
const langSel = args.lang ?? 'auto';
const withRepoMap = args.with_repo_map ?? false;
const wikiDir = resolveWikiDirInsideRepo(repoRoot, args.wiki_dir ?? '');
const repoMapMaxFiles = args.repo_map_max_files ?? 20;
const repoMapMaxSymbols = args.repo_map_max_symbols ?? 5;

const status = await checkIndex(repoRoot);
if (!status.ok) {
return errorResponse(
new Error('Index incompatible or missing'),
'index_incompatible'
);
}

const langs = resolveLangs(status.found.meta ?? null, langSel as any);
const dim = typeof status.found.meta?.dim === 'number' ? status.found.meta.dim : 256;
const dbDir = defaultDbDir(repoRoot);
const { byLang } = await openTablesByLang({
dbDir,
dim,
mode: 'open_only',
languages: langs
});
const q = buildQueryVector(query, dim);

const allScored: any[] = [];
for (const lang of langs) {
const t = byLang[lang];
if (!t) continue;
const chunkRows = await t.chunks
.query()
.select(['content_hash', 'text', 'dim', 'scale', 'qvec_b64'])
.limit(1_000_000)
.toArray();
for (const r of chunkRows as any[]) {
allScored.push({
lang,
content_hash: String(r.content_hash),
score: scoreAgainst(q, {
dim: Number(r.dim),
scale: Number(r.scale),
qvec: new Int8Array(Buffer.from(String(r.qvec_b64), 'base64'))
}),
text: String(r.text)
});
}
}

const rows = allScored
.sort((a, b) => b.score - a.score)
.slice(0, topk);
const repoMap = withRepoMap
? await buildRepoMapAttachment(repoRoot, wikiDir, repoMapMaxFiles, repoMapMaxSymbols)
: undefined;

return successResponse({
repoRoot,
lang: langSel,
rows,
...(repoMap ? { repo_map: repoMap } : {})
});
};

16 changes: 0 additions & 16 deletions src/mcp/schemas/searchSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,6 @@ export const SearchSymbolsArgsSchema = z.object({

export type SearchSymbolsArgs = z.infer<typeof SearchSymbolsArgsSchema>;

/**
* Schema for semantic_search tool
*/
export const SemanticSearchArgsSchema = z.object({
path: z.string().min(1, 'path is required'),
query: z.string().min(1, 'query is required'),
topk: z.number().int().positive().default(10),
lang: LangEnum.default('auto'),
with_repo_map: z.boolean().default(false),
repo_map_max_files: z.number().int().positive().default(20),
repo_map_max_symbols: z.number().int().positive().default(5),
wiki_dir: z.string().optional(),
});

export type SemanticSearchArgs = z.infer<typeof SemanticSearchArgsSchema>;

/**
* Schema for repo_map tool
*/
Expand Down
1 change: 0 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ export class GitAIV2MCPServer {
list_files: schemas.ListFilesArgsSchema,
read_file: schemas.ReadFileArgsSchema,
search_symbols: schemas.SearchSymbolsArgsSchema,
semantic_search: schemas.SemanticSearchArgsSchema,
repo_map: schemas.RepoMapArgsSchema,
ast_graph_query: schemas.AstGraphQueryArgsSchema,
ast_graph_find: schemas.AstGraphFindArgsSchema,
Expand Down
4 changes: 1 addition & 3 deletions src/mcp/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from './fileTools';
import {
searchSymbolsDefinition,
semanticSearchDefinition,
repoMapDefinition
} from './searchTools';
import {
Expand All @@ -37,9 +36,8 @@ export const allTools: ToolDefinition[] = [
listFilesDefinition,
readFileDefinition,

// Search tools (3)
// Search tools (2)
searchSymbolsDefinition,
semanticSearchDefinition,
repoMapDefinition,

// AST graph tools (7)
Expand Down
21 changes: 0 additions & 21 deletions src/mcp/tools/searchTools.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ToolDefinition } from '../types';
import {
handleSearchSymbols,
handleSemanticSearch,
handleRepoMap
} from '../handlers';

Expand All @@ -28,26 +27,6 @@ export const searchSymbolsDefinition: ToolDefinition = {
handler: handleSearchSymbols
};

export const semanticSearchDefinition: ToolDefinition = {
name: 'semantic_search',
description: 'Semantic search using SQ8 vectors stored in LanceDB (brute-force). Risk: low (read-only).',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string' },
path: { type: 'string', description: 'Repository root path' },
topk: { type: 'number', default: 10 },
lang: { type: 'string', enum: ['auto', 'all', 'java', 'ts'], default: 'auto' },
with_repo_map: { type: 'boolean', default: false },
repo_map_max_files: { type: 'number', default: 20 },
repo_map_max_symbols: { type: 'number', default: 5 },
wiki_dir: { type: 'string', description: 'Wiki dir relative to repo root (optional)' }
},
required: ['path', 'query']
},
handler: handleSemanticSearch
};

export const repoMapDefinition: ToolDefinition = {
name: 'repo_map',
description: 'Generate a lightweight repository map (ranked files + top symbols + wiki links). Risk: low (read-only).',
Expand Down
16 changes: 0 additions & 16 deletions test/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,6 @@ test('git-ai works in Spring Boot and Vue repos', async () => {
assert.ok(obj.rows.some(r => String(r.file || '').includes('app/src/main/java/')));
}

{
const res = runOk('node', [CLI, 'ai', 'semantic', 'hello controller', '--topk', '5'], springRepo);
const obj = JSON.parse(res.stdout);
assert.ok(Array.isArray(obj.hits));
assert.ok(obj.hits.length > 0);
}

{
const res = runOk('node', [CLI, 'ai', 'semantic', 'hello controller', '--topk', '5', '--with-repo-map', '--repo-map-files', '5', '--repo-map-symbols', '2'], springRepo);
const obj = JSON.parse(res.stdout);
assert.ok(Array.isArray(obj.hits));
assert.ok(obj.repo_map && obj.repo_map.enabled === true);
assert.ok(Array.isArray(obj.repo_map.files));
assert.ok(obj.repo_map.files.length > 0);
}

{
const res = runOk('node', [CLI, 'ai', 'graph', 'find', 'HelloController'], springRepo);
const obj = JSON.parse(res.stdout);
Expand Down
10 changes: 1 addition & 9 deletions test/mcp.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ test('mcp server supports atomic tool calls via path arg', async () => {
const toolNames = new Set((res.tools ?? []).map(t => t.name));

assert.ok(toolNames.has('search_symbols'));
assert.ok(toolNames.has('semantic_search'));
assert.ok(!toolNames.has('semantic_search'), 'semantic_search should not be registered');
assert.ok(toolNames.has('repo_map'));
assert.ok(toolNames.has('get_repo'));
assert.ok(toolNames.has('check_index'));
Expand Down Expand Up @@ -150,14 +150,6 @@ test('mcp server supports atomic tool calls via path arg', async () => {
assert.ok(parsed.repo_map.files.length > 0);
}

{
const call = await client.callTool({ name: 'semantic_search', arguments: { path: repoDir, query: 'hello world', topk: 3 } });
const text = String(call?.content?.[0]?.text ?? '');
const parsed = text ? JSON.parse(text) : null;
assert.ok(parsed && Array.isArray(parsed.rows));
assert.ok(parsed.rows.length > 0);
}

{
const call = await client.callTool({ name: 'repo_map', arguments: { path: repoDir, max_files: 5, max_symbols: 2 } });
const text = String(call?.content?.[0]?.text ?? '');
Expand Down
Loading