Skip to content
Merged
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
5 changes: 0 additions & 5 deletions .claude/scratchpad.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@
## Future Work

- Antfly SDK: server-side path filter for `getDocsByFilePath` (eliminates 5k cap)
- Wire `shortestPath` into `dev_refs` as a "trace path" feature (graph.ts is ready, adapter wiring is separate scope)
- Wire `connectedComponents` into `dev_map` verbose output (graph.ts is ready)
- Betweenness centrality — identifies bridge files between subsystems. Worth adding if agents need refactoring guidance. graphology (MIT, 1.6k stars) is the upgrade path if we need more than 3 hand-rolled algorithms.
- **Connected components hub filtering** — widely-shared utility files (e.g., logger.ts imported by 50+ files) merge separate subsystems into one component. Filter out hub nodes (high in-degree) before computing components for better subsystem identification.
- **PageRank at 10k+ nodes** — convergence tolerance 1e-6 may require all 100 iterations for large sparse graphs. Monitor performance. Consider reducing maxIterations or loosening tolerance for dev_map where approximate ranks are fine.
- **getAll(limit: 10000) truncation** — medium-large monorepos may exceed 10k docs. Warning is logged but results are silently incomplete. Long-term: paginate or make limit configurable.
- E2E tests in CI — blocked on Antfly memory requirements vs GitHub runner limits (7GB)
- **Python language support** — plan written at `.claude/da-plans/core/phase-4-python-support/`. 4 parts: bundle WASM + queries, PythonScanner, pattern rules, test fixtures + docs.
- Vue/Svelte SFC support — `.vue`/`.svelte` files have embedded `<script lang="ts">` blocks. Would need script block extraction before tree-sitter parsing. Lower priority — co-located `.ts` files in those projects already get full analysis.
- Swap `WasmPatternMatcher` to `@ast-grep/napi` if bulk scanning perf becomes an issue (~4x faster native Rust). Interface is ready; implementation is mechanical.

Expand All @@ -29,12 +26,10 @@

## Test Gaps

- **RefsAdapter integration test with `dependsOn`.** The `traceTo` path tracing feature is tested at the algorithm level (shortestPath in graph.test.ts) but not at the adapter level. Needs a test that constructs RefsAdapter with a mock indexer, calls `execute()` with `traceTo`, and verifies the path output format. Also needs a test for the error case when indexer is missing.
- **InspectAdapter integration test with PatternMatcher.** The InspectAdapter test constructs without a `patternMatcher` — the AST path is never exercised through the MCP layer. Needs a test that constructs `InspectAdapter` with `createPatternMatcher()`, mocks the search service, calls `execute()`, and verifies AST-enhanced results flow through. Requires mock search service setup — larger integration test scope.

## Tech Debt

- **`isTestFile()` is hardcoded for JS/TS and Go.** Only checks for `.test.`, `.spec.` (JS/TS) and `_test.go` (Go). Python needs `test_*.py`, `*_test.py`, `conftest.py`. Future languages will need their own patterns. Should be refactored to a language-aware registry or pattern map instead of growing if/else chains. Tracked in Phase 4 Part 4.2.

## Notes

Expand Down
116 changes: 116 additions & 0 deletions packages/mcp-server/src/adapters/__tests__/map-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,122 @@ describe('MapAdapter', () => {
});
});

describe('Connected Components', () => {
it('should include subsystems when docs have call graph edges', async () => {
// Docs with callees forming two separate clusters
const clusterDocs: SearchResult[] = [
{
id: 'packages/core/src/a.ts:fnA:1',
score: 0.9,
metadata: {
path: 'packages/core/src/a.ts',
type: 'function',
name: 'fnA',
exported: true,
callees: [{ name: 'fnB', file: 'packages/core/src/b.ts', line: 1 }],
},
},
{
id: 'packages/core/src/b.ts:fnB:1',
score: 0.9,
metadata: {
path: 'packages/core/src/b.ts',
type: 'function',
name: 'fnB',
exported: true,
callees: [],
},
},
{
id: 'packages/mcp/src/x.ts:fnX:1',
score: 0.9,
metadata: {
path: 'packages/mcp/src/x.ts',
type: 'function',
name: 'fnX',
exported: true,
callees: [{ name: 'fnY', file: 'packages/mcp/src/y.ts', line: 1 }],
},
},
{
id: 'packages/mcp/src/y.ts:fnY:1',
score: 0.9,
metadata: {
path: 'packages/mcp/src/y.ts',
type: 'function',
name: 'fnY',
exported: true,
callees: [],
},
},
];

const clusterIndexer = {
search: vi.fn().mockResolvedValue(clusterDocs),
getAll: vi.fn().mockResolvedValue(clusterDocs),
} as unknown as RepositoryIndexer;

const clusterAdapter = new MapAdapter({
repositoryIndexer: clusterIndexer,
defaultDepth: 3,
defaultTokenBudget: 5000,
});
await clusterAdapter.initialize(context);

const result = await clusterAdapter.execute({ depth: 3 }, execContext);

expect(result.success).toBe(true);
expect(result.data).toContain('Subsystems');
expect(result.data).toContain('connected');
});

it('should not show subsystems section when all docs are in one cluster', async () => {
// All docs in same cluster (single connected component)
const singleClusterDocs: SearchResult[] = [
{
id: 'src/a.ts:fnA:1',
score: 0.9,
metadata: {
path: 'src/a.ts',
type: 'function',
name: 'fnA',
exported: true,
callees: [{ name: 'fnB', file: 'src/b.ts', line: 1 }],
},
},
{
id: 'src/b.ts:fnB:1',
score: 0.9,
metadata: {
path: 'src/b.ts',
type: 'function',
name: 'fnB',
exported: true,
callees: [{ name: 'fnA', file: 'src/a.ts', line: 1 }],
},
},
];

const singleIndexer = {
search: vi.fn().mockResolvedValue(singleClusterDocs),
getAll: vi.fn().mockResolvedValue(singleClusterDocs),
} as unknown as RepositoryIndexer;

const singleAdapter = new MapAdapter({
repositoryIndexer: singleIndexer,
defaultDepth: 2,
defaultTokenBudget: 5000,
});
await singleAdapter.initialize(context);

const result = await singleAdapter.execute({}, execContext);

expect(result.success).toBe(true);
// Only 1 component — formatCodebaseMap skips the section when <= 1
expect(result.data).not.toContain('Subsystems');
});
});

describe('Token Estimation', () => {
it('should estimate tokens based on depth', () => {
const shallow = adapter.estimateTokens({ depth: 1 });
Expand Down
92 changes: 92 additions & 0 deletions packages/mcp-server/src/adapters/__tests__/refs-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,98 @@ describe('RefsAdapter', () => {
});
});

describe('Dependency Path Tracing (dependsOn)', () => {
let adapterWithIndexer: RefsAdapter;

// Mock indexer that returns docs with callees forming a chain:
// src/planner.ts → src/github.ts → src/api.ts
const chainDocs: SearchResult[] = [
{
id: '1',
score: 0.9,
metadata: {
path: 'src/planner.ts',
callees: [{ name: 'fetchIssue', file: 'src/github.ts', line: 15 }],
},
},
{
id: '2',
score: 0.9,
metadata: {
path: 'src/github.ts',
callees: [{ name: 'httpGet', file: 'src/api.ts', line: 5 }],
},
},
{
id: '3',
score: 0.9,
metadata: {
path: 'src/api.ts',
callees: [],
},
},
];

beforeEach(async () => {
const mockIndexer = {
getAll: vi.fn().mockResolvedValue(chainDocs),
};

adapterWithIndexer = new RefsAdapter({
searchService: mockSearchService,
indexer: mockIndexer as unknown as import('@prosdevlab/dev-agent-core').RepositoryIndexer,
defaultLimit: 20,
});

await adapterWithIndexer.initialize(context);
});

it('should trace dependency path between files', async () => {
const result = await adapterWithIndexer.execute(
{ name: 'createPlan', dependsOn: 'src/api.ts' },
execContext
);

expect(result.success).toBe(true);
expect(result.data).toContain('Dependency Path');
expect(result.data).toContain('src/planner.ts');
expect(result.data).toContain('src/api.ts');
expect(result.data).toContain('→');
});

it('should report hop count', async () => {
const result = await adapterWithIndexer.execute(
{ name: 'createPlan', dependsOn: 'src/api.ts' },
execContext
);

expect(result.success).toBe(true);
expect(result.data).toContain('2 hops');
});

it('should report when no path exists', async () => {
const result = await adapterWithIndexer.execute(
{ name: 'createPlan', dependsOn: 'src/nonexistent.ts' },
execContext
);

expect(result.success).toBe(true);
expect(result.data).toContain('No Path Found');
expect(result.data).toContain('separate subsystems');
});

it('should return error when indexer is not available', async () => {
// The base adapter (no indexer) should fail for dependsOn
const result = await adapter.execute(
{ name: 'createPlan', dependsOn: 'src/api.ts' },
execContext
);

expect(result.success).toBe(false);
expect(result.error?.code).toBe('INDEX_REQUIRED');
});
});

describe('Not Found', () => {
it('should return error when function not found', async () => {
// Mock empty results
Expand Down
Loading