Skip to content

Commit d082551

Browse files
prosdevclaude
andcommitted
feat(core,mcp): use AST pattern matching for all 3 analysis categories
Wire PatternMatcher through InspectAdapter to PatternAnalysisService. Three AST-enhanced extractors with regex fallback: - extractErrorHandlingWithAst: detects try/catch, promise.catch, error classes - extractImportStyleWithAst: detects dynamic imports, precise require - extractTypeCoverageWithAst: detects arrow function return types All extractors fall back to regex when PatternMatcher is not configured or file extension is unsupported. Existing 49 tests pass unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6f69c87 commit d082551

5 files changed

Lines changed: 147 additions & 6 deletions

File tree

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './events';
66
export * from './indexer';
77
export * from './map';
88
export * from './observability';
9+
export { createPatternMatcher, type PatternMatcher } from './pattern-matcher';
910
export * from './scanner';
1011
export * from './services';
1112
export * from './storage';

packages/core/src/services/pattern-analysis-service.ts

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77

88
import * as fs from 'node:fs/promises';
99
import * as path from 'node:path';
10+
import {
11+
ERROR_HANDLING_QUERIES,
12+
IMPORT_STYLE_QUERIES,
13+
TYPE_COVERAGE_QUERIES,
14+
} from '../pattern-matcher/rules';
15+
import type { PatternMatcher, PatternMatchRule } from '../pattern-matcher/wasm-matcher';
16+
import { resolveLanguage } from '../pattern-matcher/wasm-matcher';
1017
import { scanRepository } from '../scanner';
1118
import type { Document } from '../scanner/types';
1219
import { findTestFile, isTestFile } from '../utils/test-utils';
@@ -102,6 +109,121 @@ export function extractTypeCoverageFromSignatures(signatures: string[]): TypeAnn
102109
return { coverage, annotatedCount: annotated.length, totalCount: signatures.length };
103110
}
104111

112+
// ========================================================================
113+
// AST-Enhanced Extractors — use PatternMatcher when available, regex fallback
114+
// ========================================================================
115+
116+
/**
117+
* Run AST queries if matcher and language are available, else return empty map.
118+
*/
119+
async function runAstQueries(
120+
content: string,
121+
filePath: string | undefined,
122+
matcher: PatternMatcher | undefined,
123+
queries: PatternMatchRule[]
124+
): Promise<Map<string, number>> {
125+
if (!matcher || !filePath) return new Map();
126+
const language = resolveLanguage(filePath);
127+
if (!language) return new Map();
128+
return matcher.match(content, language, queries);
129+
}
130+
131+
/**
132+
* Extract error handling using AST (preferred) + regex (fallback/supplement).
133+
*/
134+
export async function extractErrorHandlingWithAst(
135+
content: string,
136+
filePath?: string,
137+
matcher?: PatternMatcher
138+
): Promise<ErrorHandlingPattern> {
139+
const regex = extractErrorHandlingFromContent(content);
140+
const ast = await runAstQueries(content, filePath, matcher, ERROR_HANDLING_QUERIES);
141+
142+
if (ast.size === 0) return regex;
143+
144+
const hasThrow = (ast.get('throw') ?? 0) > 0;
145+
const hasTryCatch = (ast.get('try-catch') ?? 0) > 0;
146+
const hasPromiseCatch = (ast.get('promise-catch') ?? 0) > 0;
147+
const hasResultRegex = regex.style === 'result';
148+
149+
// Classification: throw is the style, try-catch is the mechanism
150+
if (hasThrow && (hasTryCatch || hasPromiseCatch || hasResultRegex)) {
151+
return { style: 'mixed', examples: [] };
152+
}
153+
if (hasThrow) return { style: 'throw', examples: [] };
154+
if (hasResultRegex) return regex; // AST can't detect Result<T>, keep regex
155+
if (hasTryCatch || hasPromiseCatch) return { style: 'throw', examples: [] };
156+
157+
return regex;
158+
}
159+
160+
/**
161+
* Extract import style using AST (preferred) + regex (fallback/supplement).
162+
*/
163+
export async function extractImportStyleWithAst(
164+
content: string,
165+
filePath?: string,
166+
matcher?: PatternMatcher
167+
): Promise<ImportStylePattern> {
168+
const regex = extractImportStyleFromContent(content);
169+
const ast = await runAstQueries(content, filePath, matcher, IMPORT_STYLE_QUERIES);
170+
171+
if (ast.size === 0) return regex;
172+
173+
const dynamicImports = ast.get('dynamic-import') ?? 0;
174+
const reExports = ast.get('re-export') ?? 0;
175+
const requires = ast.get('require') ?? 0;
176+
177+
// Dynamic imports count as ESM
178+
const esmCount =
179+
(regex.style === 'esm' || regex.style === 'mixed' ? regex.importCount : 0) +
180+
dynamicImports +
181+
reExports;
182+
const cjsCount = requires;
183+
184+
if (esmCount === 0 && cjsCount === 0) return regex;
185+
186+
const hasESM = esmCount > 0 || regex.style === 'esm' || regex.style === 'mixed';
187+
const hasCJS = cjsCount > 0 || regex.style === 'cjs' || regex.style === 'mixed';
188+
const style: ImportStylePattern['style'] = hasESM && hasCJS ? 'mixed' : hasESM ? 'esm' : 'cjs';
189+
return { style, importCount: regex.importCount + dynamicImports };
190+
}
191+
192+
/**
193+
* Extract type coverage using AST (preferred) + regex signatures (supplement).
194+
*/
195+
export async function extractTypeCoverageWithAst(
196+
content: string,
197+
filePath?: string,
198+
matcher?: PatternMatcher,
199+
signatures?: string[]
200+
): Promise<TypeAnnotationPattern> {
201+
// Start with signature-based detection (from index or ts-morph)
202+
const regex = extractTypeCoverageFromSignatures(signatures ?? []);
203+
const ast = await runAstQueries(content, filePath, matcher, TYPE_COVERAGE_QUERIES);
204+
205+
if (ast.size === 0) return regex;
206+
207+
const arrowTyped = ast.get('arrow-return-type') ?? 0;
208+
const functionTyped = ast.get('function-return-type') ?? 0;
209+
const astAnnotated = arrowTyped + functionTyped;
210+
211+
// Merge: use the higher count (AST catches arrows that signatures miss)
212+
const annotatedCount = Math.max(regex.annotatedCount, astAnnotated);
213+
const totalCount = Math.max(regex.totalCount, astAnnotated); // at least as many as annotated
214+
215+
if (totalCount === 0) return regex;
216+
217+
const ratio = annotatedCount / totalCount;
218+
let coverage: TypeAnnotationPattern['coverage'];
219+
if (ratio >= 0.9) coverage = 'full';
220+
else if (ratio >= 0.5) coverage = 'partial';
221+
else if (ratio > 0) coverage = 'minimal';
222+
else coverage = 'none';
223+
224+
return { coverage, annotatedCount, totalCount };
225+
}
226+
105227
/**
106228
* Pattern Analysis Service
107229
*
@@ -160,12 +282,18 @@ export class PatternAnalysisService {
160282
.map((d) => (d.metadata.signature as string) || '')
161283
.filter(Boolean);
162284

285+
const [importStyle, errorHandling, typeAnnotations] = await Promise.all([
286+
extractImportStyleWithAst(content, filePath, this.config.patternMatcher),
287+
extractErrorHandlingWithAst(content, filePath, this.config.patternMatcher),
288+
extractTypeCoverageWithAst(content, filePath, this.config.patternMatcher, signatures),
289+
]);
290+
163291
return {
164292
fileSize: { lines, bytes },
165293
testing,
166-
importStyle: extractImportStyleFromContent(content),
167-
errorHandling: extractErrorHandlingFromContent(content),
168-
typeAnnotations: extractTypeCoverageFromSignatures(signatures),
294+
importStyle,
295+
errorHandling,
296+
typeAnnotations,
169297
};
170298
}
171299

@@ -256,12 +384,18 @@ export class PatternAnalysisService {
256384
.map((d) => d.metadata.signature || '')
257385
.filter(Boolean);
258386

387+
const [importStyle, errorHandling, typeAnnotations] = await Promise.all([
388+
extractImportStyleWithAst(content, filePath, this.config.patternMatcher),
389+
extractErrorHandlingWithAst(content, filePath, this.config.patternMatcher),
390+
extractTypeCoverageWithAst(content, filePath, this.config.patternMatcher, signatures),
391+
]);
392+
259393
return {
260394
fileSize: { lines: content.split('\n').length, bytes: stat.size },
261395
testing,
262-
importStyle: extractImportStyleFromContent(content),
263-
errorHandling: extractErrorHandlingFromContent(content),
264-
typeAnnotations: extractTypeCoverageFromSignatures(signatures),
396+
importStyle,
397+
errorHandling,
398+
typeAnnotations,
265399
};
266400
}
267401

packages/core/src/services/pattern-analysis-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,5 @@ export interface PatternComparison {
123123
export interface PatternAnalysisConfig {
124124
repositoryPath: string;
125125
vectorStorage?: import('../vector/index.js').VectorStorage;
126+
patternMatcher?: import('../pattern-matcher/wasm-matcher.js').PatternMatcher;
126127
}

packages/mcp-server/bin/dev-agent-mcp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import {
8+
createPatternMatcher,
89
ensureStorageDirectory,
910
getStorageFilePaths,
1011
getStoragePath,
@@ -268,6 +269,7 @@ async function main() {
268269
repositoryPath,
269270
searchService,
270271
vectorStorage: indexer.getVectorStorage(),
272+
patternMatcher: createPatternMatcher(),
271273
defaultLimit: 10,
272274
defaultThreshold: 0.7,
273275
defaultFormat: 'compact',

packages/mcp-server/src/adapters/built-in/inspect-adapter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import {
99
PatternAnalysisService,
1010
type PatternComparison,
11+
type PatternMatcher,
1112
type SearchService,
1213
type VectorStorage,
1314
} from '@prosdevlab/dev-agent-core';
@@ -20,6 +21,7 @@ export interface InspectAdapterConfig {
2021
repositoryPath: string;
2122
searchService: SearchService;
2223
vectorStorage?: VectorStorage;
24+
patternMatcher?: PatternMatcher;
2325
defaultLimit?: number;
2426
defaultThreshold?: number;
2527
defaultFormat?: 'compact' | 'verbose';
@@ -52,6 +54,7 @@ export class InspectAdapter extends ToolAdapter {
5254
this.patternService = new PatternAnalysisService({
5355
repositoryPath: config.repositoryPath,
5456
vectorStorage: config.vectorStorage,
57+
patternMatcher: config.patternMatcher,
5558
});
5659
this.defaultLimit = config.defaultLimit ?? 10;
5760
this.defaultThreshold = config.defaultThreshold ?? 0.7;

0 commit comments

Comments
 (0)