From 1b507bf3be8816564db7fd7238329982f90d48ee Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Sun, 22 Feb 2026 20:04:45 +0100 Subject: [PATCH 1/4] refactor: eliminate all any types and consolidate type definitions - Remove 68 `any` occurrences across 15 files with proper TypeScript types - Replace `Record` with `Record` for JSON - Add type guards and narrowing for unsafe external data - Promote @typescript-eslint/no-explicit-any from warn to error - Consolidate duplicate types: PatternTrend, PatternCandidateBase, UsageLocation - Make GoldenFile extend IntelligenceGoldenFile to eliminate field duplication - Define PatternCandidateBase for shared pattern candidate fields - Create UsageLocation base for ImportUsage and SymbolUsage - All 234 tests passing, type-check clean, 0 lint errors --- CHANGELOG.md | 10 ++ eslint.config.js | 2 +- src/analyzers/angular/index.ts | 165 ++++++++++++++-------- src/analyzers/generic/index.ts | 21 +-- src/core/indexer.ts | 43 ++++-- src/core/reranker.ts | 18 ++- src/core/search.ts | 16 ++- src/core/symbol-references.ts | 9 +- src/embeddings/openai.ts | 9 +- src/embeddings/transformers.ts | 15 +- src/embeddings/types.ts | 3 +- src/index.ts | 18 ++- src/patterns/semantics.ts | 3 +- src/storage/lancedb.ts | 32 ++++- src/tools/detect-circular-dependencies.ts | 5 +- src/tools/get-team-patterns.ts | 46 +++--- src/tools/search-codebase.ts | 92 ++++++------ src/tools/types.ts | 16 +++ src/types/index.ts | 89 ++++++++++-- src/utils/usage-tracker.ts | 45 +++--- 20 files changed, 423 insertions(+), 234 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d34085..0eba259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,16 @@ ### Added - **Definition-first ranking**: Exact-name searches now show the file that *defines* a symbol before files that use it. For example, searching `parseConfig` shows the function definition first, then callers. + +### Refactored + +- **Eliminated all `any` types**: 68 occurrences across 15 files now use proper TypeScript types. Replaced unsafe `Record` with `Record` and narrowed types using proper type guards. Promoted `@typescript-eslint/no-explicit-any` from `warn` to `error` to enforce strict typing. +- **Consolidated duplicate type definitions**: Single source of truth for shared types: + - `PatternTrend` canonical location in `types/index.ts` (imported by `usage-tracker.ts`) + - New `PatternCandidateBase` for shared pattern fields; `PatternCandidate extends PatternCandidateBase`; runtime adds optional internal fields + - New `UsageLocation` base for both `ImportUsage` and `SymbolUsage` (extends with `preview` field) + - `GoldenFile extends IntelligenceGoldenFile` to eliminate field duplication (`file`, `score`) + - Introduced `RuntimePatternPrimary` and `DecisionCard` types for tool-specific outputs - **Scope headers in code snippets**: When requesting snippets (`includeSnippets: true`), each code block now starts with a comment like `// UserService.login()` so agents know where the code lives without extra file reads. - **Edit decision card**: When searching with `intent="edit"`, `intent="refactor"`, or `intent="migrate"`, results now include a decision card telling you whether there's enough evidence to proceed safely. The card shows: whether you're ready (`ready: true/false`), what to do next if not (`nextAction`), relevant team patterns to follow, a top example file, how many callers appear in results (`impact.coverage`), and what searches would help close gaps (`whatWouldHelp`). - **Caller coverage tracking**: The decision card shows how many of a symbol's callers are in your search results. Low coverage (less than 40% when there are lots of callers) triggers an alert so you know to search more before editing. diff --git a/eslint.config.js b/eslint.config.js index f381ac8..828c0cc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -16,7 +16,7 @@ export default tseslint.config( // import plugin is handled via recommended usually, but kept simple for now }, rules: { - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_' }], 'no-console': ['warn', { 'allow': ['warn', 'error'] }], }, diff --git a/src/analyzers/angular/index.ts b/src/analyzers/angular/index.ts index 7e7016c..70fa364 100644 --- a/src/analyzers/angular/index.ts +++ b/src/analyzers/angular/index.ts @@ -4,9 +4,10 @@ * Detects state management patterns, architectural layers, and Angular-specific patterns */ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { promises as fs } from 'fs'; import path from 'path'; +import { parse } from '@typescript-eslint/typescript-estree'; +import type { TSESTree } from '@typescript-eslint/typescript-estree'; import { FrameworkAnalyzer, AnalysisResult, @@ -15,9 +16,9 @@ import { CodeComponent, ImportStatement, ExportStatement, - ArchitecturalLayer + ArchitecturalLayer, + DependencyCategory } from '../../types/index.js'; -import { parse } from '@typescript-eslint/typescript-estree'; import { createChunksFromCode } from '../../utils/chunking.js'; import { CODEBASE_CONTEXT_DIRNAME, @@ -25,6 +26,19 @@ import { } from '../../constants/codebase-context.js'; import { registerComplementaryPatterns } from '../../patterns/semantics.js'; +interface AngularInput { + name: string; + type: string; + style: 'decorator' | 'signal'; + required?: boolean; +} + +interface AngularOutput { + name: string; + type: string; + style: 'decorator' | 'signal'; +} + export class AngularAnalyzer implements FrameworkAnalyzer { readonly name = 'angular'; readonly version = '1.0.0'; @@ -144,12 +158,13 @@ export class AngularAnalyzer implements FrameworkAnalyzer { const source = node.source.value as string; imports.push({ source, - imports: node.specifiers.map((s: any) => { + imports: node.specifiers.map((s: TSESTree.ImportClause) => { if (s.type === 'ImportDefaultSpecifier') return 'default'; if (s.type === 'ImportNamespaceSpecifier') return '*'; - return s.imported?.name || s.local.name; + const specifier = s as TSESTree.ImportSpecifier; + return specifier.imported.name || specifier.local.name; }), - isDefault: node.specifiers.some((s: any) => s.type === 'ImportDefaultSpecifier'), + isDefault: node.specifiers.some((s: TSESTree.ImportClause) => s.type === 'ImportDefaultSpecifier'), isDynamic: false, line: node.loc?.start.line }); @@ -352,15 +367,21 @@ export class AngularAnalyzer implements FrameworkAnalyzer { } private async extractAngularComponent( - classNode: any, + classNode: TSESTree.ClassDeclaration, content: string ): Promise { - if (!classNode.decorators || classNode.decorators.length === 0) { + if (!classNode.id || !classNode.decorators || classNode.decorators.length === 0) { return null; } const decorator = classNode.decorators[0]; - const decoratorName = decorator.expression.callee?.name || decorator.expression.name; + const expr = decorator.expression; + const decoratorName: string = + expr.type === 'CallExpression' && expr.callee.type === 'Identifier' + ? expr.callee.name + : expr.type === 'Identifier' + ? expr.name + : ''; let componentType: string | undefined; let angularType: string | undefined; @@ -466,27 +487,28 @@ export class AngularAnalyzer implements FrameworkAnalyzer { }; } - private extractDecoratorMetadata(decorator: any): Record { - const metadata: Record = {}; + private extractDecoratorMetadata(decorator: TSESTree.Decorator): Record { + const metadata: Record = {}; try { - if (decorator.expression.arguments && decorator.expression.arguments[0]) { + if (decorator.expression.type === 'CallExpression' && decorator.expression.arguments[0]) { const arg = decorator.expression.arguments[0]; if (arg.type === 'ObjectExpression') { for (const prop of arg.properties) { - if (prop.key && prop.value) { - const key = prop.key.name || prop.key.value; - - if (prop.value.type === 'Literal') { - metadata[key] = prop.value.value; - } else if (prop.value.type === 'ArrayExpression') { - metadata[key] = prop.value.elements - .map((el: any) => (el.type === 'Literal' ? el.value : null)) - .filter(Boolean); - } else if (prop.value.type === 'Identifier') { - metadata[key] = prop.value.name; - } + if (prop.type !== 'Property') continue; + const keyNode = prop.key as { name?: string; value?: unknown }; + const key = keyNode.name ?? String(keyNode.value ?? ''); + if (!key) continue; + + if (prop.value.type === 'Literal') { + metadata[key] = prop.value.value; + } else if (prop.value.type === 'ArrayExpression') { + metadata[key] = prop.value.elements + .map((el) => (el && el.type === 'Literal' ? el.value : null)) + .filter(Boolean); + } else if (prop.value.type === 'Identifier') { + metadata[key] = prop.value.name; } } } @@ -498,7 +520,7 @@ export class AngularAnalyzer implements FrameworkAnalyzer { return metadata; } - private extractLifecycleHooks(classNode: any): string[] { + private extractLifecycleHooks(classNode: TSESTree.ClassDeclaration): string[] { const hooks: string[] = []; const lifecycleHooks = [ 'ngOnChanges', @@ -513,7 +535,7 @@ export class AngularAnalyzer implements FrameworkAnalyzer { if (classNode.body && classNode.body.body) { for (const member of classNode.body.body) { - if (member.type === 'MethodDefinition' && member.key) { + if (member.type === 'MethodDefinition' && member.key && member.key.type === 'Identifier') { const methodName = member.key.name; if (lifecycleHooks.includes(methodName)) { hooks.push(methodName); @@ -525,7 +547,7 @@ export class AngularAnalyzer implements FrameworkAnalyzer { return hooks; } - private extractInjectedServices(classNode: any): string[] { + private extractInjectedServices(classNode: TSESTree.ClassDeclaration): string[] { const services: string[] = []; // Look for constructor parameters @@ -534,8 +556,12 @@ export class AngularAnalyzer implements FrameworkAnalyzer { if (member.type === 'MethodDefinition' && member.kind === 'constructor') { if (member.value.params) { for (const param of member.value.params) { - if (param.typeAnnotation?.typeAnnotation?.typeName) { - services.push(param.typeAnnotation.typeAnnotation.typeName.name); + const typedParam = param as TSESTree.Identifier; + if (typedParam.typeAnnotation?.typeAnnotation?.type === 'TSTypeReference') { + const typeRef = typedParam.typeAnnotation.typeAnnotation as TSESTree.TSTypeReference; + if (typeRef.typeName.type === 'Identifier') { + services.push(typeRef.typeName.name); + } } } } @@ -546,40 +572,46 @@ export class AngularAnalyzer implements FrameworkAnalyzer { return services; } - private extractInputs(classNode: any): any[] { - const inputs: any[] = []; + private extractInputs(classNode: TSESTree.ClassDeclaration): AngularInput[] { + const inputs: AngularInput[] = []; if (classNode.body && classNode.body.body) { for (const member of classNode.body.body) { if (member.type === 'PropertyDefinition') { // Check for decorator-based @Input() if (member.decorators) { - const hasInput = member.decorators.some( - (d: any) => d.expression?.callee?.name === 'Input' || d.expression?.name === 'Input' - ); - - if (hasInput && member.key) { + const hasInput = member.decorators.some((d: TSESTree.Decorator) => { + const expr = d.expression; + return ( + (expr.type === 'CallExpression' && + expr.callee.type === 'Identifier' && + expr.callee.name === 'Input') || + (expr.type === 'Identifier' && expr.name === 'Input') + ); + }); + + if (hasInput && member.key && 'name' in member.key) { inputs.push({ name: member.key.name, - type: member.typeAnnotation?.typeAnnotation?.type || 'any', + type: (member.typeAnnotation?.typeAnnotation?.type as string | undefined) || 'unknown', style: 'decorator' }); } } // Check for signal-based input() (Angular v17.1+) - if (member.value && member.key) { - const valueStr = - member.value.type === 'CallExpression' - ? member.value.callee?.name || member.value.callee?.object?.name - : null; + if (member.value && member.key && 'name' in member.key) { + const callee = member.value.type === 'CallExpression' + ? (member.value.callee as { type: string; name?: string; object?: { name?: string }; property?: { name?: string } }) + : null; + const valueStr = callee?.name ?? callee?.object?.name ?? null; if (valueStr === 'input') { inputs.push({ name: member.key.name, type: 'InputSignal', style: 'signal', - required: member.value.callee?.property?.name === 'required' + required: callee?.property?.name === 'required' }); } } @@ -590,19 +622,25 @@ export class AngularAnalyzer implements FrameworkAnalyzer { return inputs; } - private extractOutputs(classNode: any): any[] { - const outputs: any[] = []; + private extractOutputs(classNode: TSESTree.ClassDeclaration): AngularOutput[] { + const outputs: AngularOutput[] = []; if (classNode.body && classNode.body.body) { for (const member of classNode.body.body) { if (member.type === 'PropertyDefinition') { // Check for decorator-based @Output() if (member.decorators) { - const hasOutput = member.decorators.some( - (d: any) => d.expression?.callee?.name === 'Output' || d.expression?.name === 'Output' - ); - - if (hasOutput && member.key) { + const hasOutput = member.decorators.some((d: TSESTree.Decorator) => { + const expr = d.expression; + return ( + (expr.type === 'CallExpression' && + expr.callee.type === 'Identifier' && + expr.callee.name === 'Output') || + (expr.type === 'Identifier' && expr.name === 'Output') + ); + }); + + if (hasOutput && member.key && 'name' in member.key) { outputs.push({ name: member.key.name, type: 'EventEmitter', @@ -612,9 +650,11 @@ export class AngularAnalyzer implements FrameworkAnalyzer { } // Check for signal-based output() (Angular v17.1+) - if (member.value && member.key) { - const valueStr = - member.value.type === 'CallExpression' ? member.value.callee?.name : null; + if (member.value && member.key && 'name' in member.key) { + const callee = member.value.type === 'CallExpression' + ? (member.value.callee as { type: string; name?: string }) + : null; + const valueStr = callee?.name ?? null; if (valueStr === 'output') { outputs.push({ @@ -755,7 +795,7 @@ export class AngularAnalyzer implements FrameworkAnalyzer { return 'unknown'; } - private categorizeDependency(name: string): any { + private categorizeDependency(name: string): DependencyCategory { if (name.startsWith('@angular/')) { return 'framework'; } @@ -885,20 +925,21 @@ export class AngularAnalyzer implements FrameworkAnalyzer { try { const indexPath = path.join(rootPath, CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME); const indexContent = await fs.readFile(indexPath, 'utf-8'); - const parsed = JSON.parse(indexContent) as any; + const parsed = JSON.parse(indexContent) as unknown; // Legacy index.json is an array — do not consume it (missing version/meta headers). if (Array.isArray(parsed)) { return metadata; } - const chunks = parsed && Array.isArray(parsed.chunks) ? parsed.chunks : null; + const parsedObj = parsed as { chunks?: unknown }; + const chunks = parsedObj && Array.isArray(parsedObj.chunks) ? (parsedObj.chunks as Array<{ filePath?: string; startLine?: number; endLine?: number; componentType?: string; layer?: string }>) : null; if (Array.isArray(chunks) && chunks.length > 0) { console.error(`Loading statistics from ${indexPath}: ${chunks.length} chunks`); - metadata.statistics.totalFiles = new Set(chunks.map((c: any) => c.filePath)).size; + metadata.statistics.totalFiles = new Set(chunks.map((c) => c.filePath)).size; metadata.statistics.totalLines = chunks.reduce( - (sum: number, c: any) => sum + (c.endLine - c.startLine + 1), + (sum, c) => sum + ((c.endLine ?? 0) - (c.startLine ?? 0) + 1), 0 ); @@ -954,8 +995,8 @@ export class AngularAnalyzer implements FrameworkAnalyzer { switch (componentType) { case 'component': { const selector = metadata?.selector || 'unknown'; - const inputs = metadata?.inputs?.length || 0; - const outputs = metadata?.outputs?.length || 0; + const inputs = Array.isArray(metadata?.inputs) ? metadata.inputs.length : 0; + const outputs = Array.isArray(metadata?.outputs) ? metadata.outputs.length : 0; const lifecycle = this.extractLifecycleMethods(content); return `Angular component '${className}' (selector: ${selector})${ lifecycle ? ` with ${lifecycle}` : '' @@ -986,8 +1027,8 @@ export class AngularAnalyzer implements FrameworkAnalyzer { } case 'module': { - const imports = metadata?.imports?.length || 0; - const declarations = metadata?.declarations?.length || 0; + const imports = Array.isArray(metadata?.imports) ? metadata.imports.length : 0; + const declarations = Array.isArray(metadata?.declarations) ? metadata.declarations.length : 0; return `Angular module '${className}' with ${declarations} declarations and ${imports} imports.`; } diff --git a/src/analyzers/generic/index.ts b/src/analyzers/generic/index.ts index 0b51b91..a7578a8 100644 --- a/src/analyzers/generic/index.ts +++ b/src/analyzers/generic/index.ts @@ -3,9 +3,9 @@ * Provides basic AST parsing and chunking for languages without specialized analyzers */ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { promises as fs } from 'fs'; import path from 'path'; +import type { TSESTree } from '@typescript-eslint/typescript-estree'; import { FrameworkAnalyzer, AnalysisResult, @@ -30,6 +30,7 @@ import { aggregateWorkspaceDependencies, categorizeDependency } from '../../utils/dependency-detection.js'; +import type { WorkspacePackageJson } from '../../utils/workspace-detection.js'; export class GenericAnalyzer implements FrameworkAnalyzer { readonly name = 'generic'; @@ -141,7 +142,7 @@ export class GenericAnalyzer implements FrameworkAnalyzer { console.warn(`Failed to parse ${filePath}:`, error); } - const metadata: Record = { + const metadata: Record = { analyzer: this.name, fileSize: content.length, lineCount: content.split('\n').length, @@ -239,7 +240,7 @@ export class GenericAnalyzer implements FrameworkAnalyzer { let dependencies: Dependency[] = []; let workspaceType: string = 'single'; - let workspacePackages: any[] = []; + let workspacePackages: WorkspacePackageJson[] = []; try { workspaceType = await detectWorkspaceType(rootPath); @@ -247,7 +248,7 @@ export class GenericAnalyzer implements FrameworkAnalyzer { workspaceType !== 'single' ? await scanWorkspacePackageJsons(rootPath) : []; const pkgPath = path.join(rootPath, 'package.json'); - let packageJson: any = {}; + let packageJson: { name?: string; dependencies?: Record; devDependencies?: Record } = {}; try { packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8')); projectName = packageJson.name || projectName; @@ -273,7 +274,7 @@ export class GenericAnalyzer implements FrameworkAnalyzer { name: projectName, rootPath, languages: [], - dependencies: dependencies as any, + dependencies, architecture: { type: 'mixed', layers: { @@ -353,12 +354,13 @@ export class GenericAnalyzer implements FrameworkAnalyzer { if (node.type === 'ImportDeclaration' && node.source.value) { imports.push({ source: node.source.value as string, - imports: node.specifiers.map((s: any) => { + imports: node.specifiers.map((s: TSESTree.ImportClause) => { if (s.type === 'ImportDefaultSpecifier') return 'default'; if (s.type === 'ImportNamespaceSpecifier') return '*'; - return s.imported?.name || s.local.name; + const specifier = s as TSESTree.ImportSpecifier; + return specifier.imported.name || specifier.local.name; }), - isDefault: node.specifiers.some((s: any) => s.type === 'ImportDefaultSpecifier'), + isDefault: node.specifiers.some((s: TSESTree.ImportClause) => s.type === 'ImportDefaultSpecifier'), isDynamic: false, line: node.loc?.start.line }); @@ -419,8 +421,9 @@ export class GenericAnalyzer implements FrameworkAnalyzer { } } } else if ('id' in node.declaration && node.declaration.id) { + const declId = node.declaration.id as { name: string }; exports.push({ - name: (node.declaration.id as any).name, + name: declId.name, isDefault: false, type: 'named' }); diff --git a/src/core/indexer.ts b/src/core/indexer.ts index 7f607d5..8629ad0 100644 --- a/src/core/indexer.ts +++ b/src/core/indexer.ts @@ -3,7 +3,6 @@ * Scans files, delegates to analyzers, creates embeddings, stores in vector DB */ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { randomUUID } from 'crypto'; import { promises as fs } from 'fs'; import path from 'path'; @@ -15,7 +14,10 @@ import { IndexingProgress, IndexingStats, IndexingPhase, - CodebaseConfig + CodebaseConfig, + Dependency, + ArchitecturalLayer, + IntelligenceData } from '../types/index.js'; import { analyzerRegistry } from './analyzer-registry.js'; import { isCodeFile, isBinaryFile } from '../utils/language-detection.js'; @@ -26,7 +28,8 @@ import { PatternDetector, ImportGraph, InternalFileGraph, - FileExport + FileExport, + GoldenFile } from '../utils/usage-tracker.js'; import { mergeSmallChunks } from '../utils/chunking.js'; import { getFileCommitDates } from '../utils/git-dates.js'; @@ -408,11 +411,12 @@ export class CodebaseIndexer { try { const existingIndexPath = path.join(contextDir, KEYWORD_INDEX_FILENAME); - const existing = JSON.parse(await fs.readFile(existingIndexPath, 'utf-8')) as any; + const existing = JSON.parse(await fs.readFile(existingIndexPath, 'utf-8')) as unknown; + const existingObj = existing as { chunks?: unknown }; const existingChunks: unknown = Array.isArray(existing) ? existing - : existing && Array.isArray(existing.chunks) - ? existing.chunks + : existingObj && Array.isArray(existingObj.chunks) + ? existingObj.chunks : null; if (Array.isArray(existingChunks)) { stats.totalChunks = existingChunks.length; @@ -546,8 +550,8 @@ export class CodebaseIndexer { // GENERIC PATTERN FORWARDING // Framework analyzers return detectedPatterns in metadata - we just forward them // This keeps the indexer framework-agnostic - if (result.metadata?.detectedPatterns) { - for (const pattern of result.metadata.detectedPatterns) { + if (result.metadata?.detectedPatterns && Array.isArray(result.metadata.detectedPatterns)) { + for (const pattern of result.metadata.detectedPatterns as Array<{ category: string; name: string }>) { // Try to extract a relevant snippet for the pattern // Ask analyzer registry for snippet pattern (framework-agnostic delegation) const analyzer = analyzerRegistry.findAnalyzer(file); @@ -565,9 +569,12 @@ export class CodebaseIndexer { // Track file for Golden File scoring (framework-agnostic) // A golden file = file with patterns in ≥3 distinct categories - const detectedPatterns = result.metadata?.detectedPatterns || []; + const rawPatterns = result.metadata?.detectedPatterns; + const detectedPatterns: Array<{ category: string; name: string }> = Array.isArray(rawPatterns) + ? (rawPatterns as Array<{ category: string; name: string }>) + : []; const uniqueCategories = new Set( - detectedPatterns.map((p: { category: string }) => p.category) + detectedPatterns.map((p) => p.category) ); const patternScore = uniqueCategories.size; if (patternScore >= 3) { @@ -575,7 +582,7 @@ export class CodebaseIndexer { for (const p of detectedPatterns) { patternFlags[`${p.category}:${p.name}`] = true; } - patternDetector.trackGoldenFile(relPath, patternScore, patternFlags as any); // TODO: fix type; + patternDetector.trackGoldenFile(relPath, patternScore, patternFlags as GoldenFile['patterns']); } // Update component statistics @@ -1076,7 +1083,7 @@ export class CodebaseIndexer { INTELLIGENCE_FILENAME ); const intelligenceContent = await fs.readFile(intelligencePath, 'utf-8'); - const intelligence = JSON.parse(intelligenceContent) as any; + const intelligence = JSON.parse(intelligenceContent) as IntelligenceData; // Phase 06: ignore legacy intelligence files that lack a versioned header. if (!intelligence || typeof intelligence !== 'object' || !intelligence.header) { @@ -1128,7 +1135,7 @@ export class CodebaseIndexer { }; } - private mergeDependencies(base: any[], incoming: any[]): any[] { + private mergeDependencies(base: Dependency[], incoming: Dependency[]): Dependency[] { const seen = new Set(base.map((d) => d.name)); const result = [...base]; for (const dep of incoming) { @@ -1140,7 +1147,10 @@ export class CodebaseIndexer { return result; } - private mergeLayers(base: any, incoming?: any): any { + private mergeLayers( + base: Record, + incoming?: Partial> + ): Record { if (!incoming) return base; return { presentation: Math.max(base.presentation || 0, incoming.presentation || 0), @@ -1155,7 +1165,10 @@ export class CodebaseIndexer { }; } - private mergeStatistics(base: any, incoming: any): any { + private mergeStatistics( + base: CodebaseMetadata['statistics'], + incoming: CodebaseMetadata['statistics'] + ): CodebaseMetadata['statistics'] { return { totalFiles: Math.max(base.totalFiles || 0, incoming.totalFiles || 0), totalLines: Math.max(base.totalLines || 0, incoming.totalLines || 0), diff --git a/src/core/reranker.ts b/src/core/reranker.ts index 6d037ce..1c60324 100644 --- a/src/core/reranker.ts +++ b/src/core/reranker.ts @@ -8,8 +8,6 @@ * Default model: Xenova/ms-marco-MiniLM-L-6-v2 (~22M params, ~80MB, CPU-safe). */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - import type { SearchResult } from '../types/index.js'; const DEFAULT_RERANKER_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2'; @@ -20,8 +18,16 @@ const RERANK_TOP_K = 10; /** Trigger reranking when the score gap between #1 and #3 is below this threshold */ const AMBIGUITY_THRESHOLD = 0.08; -let cachedTokenizer: any = null; -let cachedModel: any = null; +interface CrossEncoderTokenizer { + (query: string, passage: string, options: { padding: boolean; truncation: boolean; max_length: number }): unknown; +} + +interface CrossEncoderModel { + (inputs: unknown): Promise<{ logits: { data: ArrayLike } }>; +} + +let cachedTokenizer: CrossEncoderTokenizer | null = null; +let cachedModel: CrossEncoderModel | null = null; let initPromise: Promise | null = null; async function ensureModelLoaded(): Promise { @@ -83,6 +89,10 @@ function buildPassage(result: SearchResult): string { * Returns a relevance score (higher = more relevant). */ async function scorePair(query: string, passage: string): Promise { + if (!cachedTokenizer || !cachedModel) { + throw new Error('[reranker] Model not loaded — call ensureModelLoaded() first'); + } + const inputs = cachedTokenizer(query, passage, { padding: true, truncation: true, diff --git a/src/core/search.ts b/src/core/search.ts index 2f03e6a..f7b4f99 100644 --- a/src/core/search.ts +++ b/src/core/search.ts @@ -2,11 +2,10 @@ * Hybrid search combining semantic vector search with keyword matching */ -/* eslint-disable @typescript-eslint/no-explicit-any */ import Fuse from 'fuse.js'; import path from 'path'; import { promises as fs } from 'fs'; -import { CodeChunk, SearchResult, SearchFilters } from '../types/index.js'; +import { CodeChunk, SearchResult, SearchFilters, IntelligenceData } from '../types/index.js'; import { EmbeddingProvider, getEmbeddingProvider } from '../embeddings/index.js'; import { VectorStorageProvider, getStorageProvider } from '../storage/index.js'; import { analyzerRegistry } from './analyzer-registry.js'; @@ -167,7 +166,7 @@ export class CodebaseSearcher { try { const indexPath = path.join(this.rootPath, CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME); const content = await fs.readFile(indexPath, 'utf-8'); - const parsed = JSON.parse(content) as any; + const parsed = JSON.parse(content) as unknown; if (Array.isArray(parsed)) { throw new IndexCorruptedError( @@ -175,7 +174,11 @@ export class CodebaseSearcher { ); } - const chunks = parsed && Array.isArray(parsed.chunks) ? parsed.chunks : null; + const parsedObj = parsed as { chunks?: unknown }; + const chunks = + parsedObj && typeof parsedObj === 'object' && Array.isArray(parsedObj.chunks) + ? (parsedObj.chunks as CodeChunk[]) + : null; if (!chunks) { throw new IndexCorruptedError('Keyword index corrupted: expected { header, chunks }'); } @@ -218,7 +221,7 @@ export class CodebaseSearcher { INTELLIGENCE_FILENAME ); const content = await fs.readFile(intelligencePath, 'utf-8'); - const intelligence = JSON.parse(content); + const intelligence = JSON.parse(content) as IntelligenceData; const decliningPatterns = new Set(); const risingPatterns = new Set(); @@ -226,8 +229,7 @@ export class CodebaseSearcher { // Extract pattern indicators from intelligence data if (intelligence.patterns) { - for (const [_category, data] of Object.entries(intelligence.patterns)) { - const patternData = data as any; + for (const [_category, patternData] of Object.entries(intelligence.patterns)) { // Track primary pattern if (patternData.primary?.trend === 'Rising') { diff --git a/src/core/symbol-references.ts b/src/core/symbol-references.ts index d0c0f0f..f34b5d0 100644 --- a/src/core/symbol-references.ts +++ b/src/core/symbol-references.ts @@ -2,6 +2,7 @@ import { promises as fs } from 'fs'; import path from 'path'; import { CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME } from '../constants/codebase-context.js'; import { IndexCorruptedError } from '../errors/index.js'; +import type { UsageLocation } from '../types/index.js'; interface IndexedChunk { content?: unknown; @@ -10,9 +11,7 @@ interface IndexedChunk { filePath?: unknown; } -export interface SymbolUsage { - file: string; - line: number; +export interface SymbolUsage extends UsageLocation { preview: string; } @@ -96,8 +95,8 @@ export async function findSymbolReferences( } const chunks = - chunksRaw && typeof chunksRaw === 'object' && Array.isArray((chunksRaw as any).chunks) - ? ((chunksRaw as any).chunks as unknown[]) + chunksRaw !== null && typeof chunksRaw === 'object' && 'chunks' in chunksRaw && Array.isArray(chunksRaw.chunks) + ? (chunksRaw.chunks as unknown[]) : null; if (!chunks) { diff --git a/src/embeddings/openai.ts b/src/embeddings/openai.ts index f04c9a9..089e110 100644 --- a/src/embeddings/openai.ts +++ b/src/embeddings/openai.ts @@ -1,6 +1,9 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { EmbeddingProvider } from './types.js'; +interface OpenAIEmbeddingResponse { + data: Array<{ embedding: number[] }>; +} + /** * OpenAI Embedding Provider * Uses native fetch to avoid adding the heavy openai npm package dependency. @@ -55,10 +58,10 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider { throw new Error(`OpenAI API Error ${response.status}: ${error}`); } - const data = (await response.json()) as any; + const data = (await response.json()) as OpenAIEmbeddingResponse; // OpenAI guarantees order matches input - return data.data.map((item: any) => item.embedding); + return data.data.map((item) => item.embedding); } catch (error) { console.error('OpenAI Embedding Failed:', error); throw error; diff --git a/src/embeddings/transformers.ts b/src/embeddings/transformers.ts index 20f2b41..0d9c0ce 100644 --- a/src/embeddings/transformers.ts +++ b/src/embeddings/transformers.ts @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { EmbeddingProvider, DEFAULT_MODEL } from './types.js'; +import type { FeatureExtractionPipelineType } from '@huggingface/transformers'; interface ModelConfig { dimensions: number; @@ -28,7 +28,7 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider { readonly modelName: string; readonly dimensions: number; - private pipeline: any = null; + private pipeline: FeatureExtractionPipelineType | null = null; private ready = false; private initPromise: Promise | null = null; @@ -52,9 +52,10 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider { const { pipeline } = await import('@huggingface/transformers'); - this.pipeline = await pipeline('feature-extraction', this.modelName, { - dtype: 'q8' - }); + // TS2590: pipeline() resolves AllTasks[T] — a union too complex for TSC to represent. + // Cast to a simpler signature; the actual return type IS FeatureExtractionPipelineType. + type PipelineFn = (task: 'feature-extraction', model: string, opts: Record) => Promise; + this.pipeline = await (pipeline as PipelineFn)('feature-extraction', this.modelName, { dtype: 'q8' }); this.ready = true; console.error(`Model loaded successfully: ${this.modelName}`); @@ -69,6 +70,8 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider { await this.initialize(); } + if (!this.pipeline) throw new Error('Pipeline not initialized'); + try { const output = await this.pipeline(text, { pooling: 'mean', @@ -87,6 +90,8 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider { await this.initialize(); } + if (!this.pipeline) throw new Error('Pipeline not initialized'); + const embeddings: number[][] = []; const batchSize = computeSafeBatchSize(this.modelName); diff --git a/src/embeddings/types.ts b/src/embeddings/types.ts index 8a46961..0406209 100644 --- a/src/embeddings/types.ts +++ b/src/embeddings/types.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ export interface EmbeddingProvider { readonly name: string; readonly modelName: string; @@ -25,7 +24,7 @@ export interface EmbeddingConfig { export const DEFAULT_MODEL = process.env.EMBEDDING_MODEL || 'Xenova/bge-small-en-v1.5'; export const DEFAULT_EMBEDDING_CONFIG: EmbeddingConfig = { - provider: (process.env.EMBEDDING_PROVIDER as any) || 'transformers', + provider: (process.env.EMBEDDING_PROVIDER as EmbeddingConfig['provider']) || 'transformers', model: DEFAULT_MODEL, batchSize: 32, maxRetries: 3, diff --git a/src/index.ts b/src/index.ts index b537c72..931484a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,5 @@ #!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-explicit-any */ - /** * MCP Server for Codebase Context * Provides codebase indexing and semantic search capabilities @@ -20,7 +18,7 @@ import { Resource } from '@modelcontextprotocol/sdk/types.js'; import { CodebaseIndexer } from './core/indexer.js'; -import type { IndexingStats } from './types/index.js'; +import type { IndexingStats, IntelligenceData, PatternsData, PatternEntry, PatternCandidate } from './types/index.js'; import { analyzerRegistry } from './core/analyzer-registry.js'; import { AngularAnalyzer } from './analyzers/angular/index.js'; import { GenericAnalyzer } from './analyzers/generic/index.js'; @@ -303,7 +301,7 @@ async function generateCodebaseContext(): Promise { try { const content = await fs.readFile(intelligencePath, 'utf-8'); - const intelligence = JSON.parse(content); + const intelligence = JSON.parse(content) as IntelligenceData; const lines: string[] = []; lines.push('# Codebase Intelligence'); @@ -320,7 +318,7 @@ async function generateCodebaseContext(): Promise { // Library usage - sorted by count const libraryEntries = Object.entries(intelligence.libraryUsage || {}) - .map(([lib, data]: [string, any]) => ({ + .map(([lib, data]) => ({ lib, count: data.count })) @@ -349,7 +347,7 @@ async function generateCodebaseContext(): Promise { // Pattern consensus if (intelligence.patterns && Object.keys(intelligence.patterns).length > 0) { - const patterns = intelligence.patterns as Record; + const patterns: PatternsData = intelligence.patterns; lines.push("## YOUR Codebase's Actual Patterns (Not Generic Best Practices)"); lines.push(''); lines.push('These patterns were detected by analyzing your actual code.'); @@ -361,16 +359,16 @@ async function generateCodebaseContext(): Promise { continue; } - const patternData: any = data; - const primary = patternData.primary; - const alternatives = patternData.alsoDetected ?? []; + const patternData: PatternEntry = data; + const primary: PatternCandidate | undefined = patternData.primary; + const alternatives: PatternCandidate[] = patternData.alsoDetected ?? []; if (!primary) continue; if ( isComplementaryPatternCategory( category, - [primary.name, ...alternatives.map((alt: any) => alt.name)].filter(Boolean) + [primary.name, ...alternatives.map((alt) => alt.name)].filter(Boolean) ) ) { const secondary = alternatives[0]; diff --git a/src/patterns/semantics.ts b/src/patterns/semantics.ts index 7383e96..ae0c644 100644 --- a/src/patterns/semantics.ts +++ b/src/patterns/semantics.ts @@ -1,5 +1,6 @@ // Complementary pattern pairs are registered by analyzers at startup. // This keeps the core logic framework-agnostic. +import type { PatternsData } from '../types/index.js'; const complementaryPairs: Map> = new Map(); /** @@ -44,7 +45,7 @@ export function isComplementaryPatternCategory(category: string, patternNames: s export function shouldSkipLegacyTestingFrameworkCategory( category: string, - patterns: Record + patterns: PatternsData ): boolean { return category === 'testingFramework' && Boolean(patterns.unitTestFramework?.primary); } diff --git a/src/storage/lancedb.ts b/src/storage/lancedb.ts index f9288ea..27be1b4 100644 --- a/src/storage/lancedb.ts +++ b/src/storage/lancedb.ts @@ -1,19 +1,38 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /** * LanceDB Storage Provider * Embedded vector database for storing and searching code embeddings */ import { promises as fs } from 'fs'; +import type { Connection, Table } from '@lancedb/lancedb'; import { VectorStorageProvider, CodeChunkWithEmbedding, VectorSearchResult } from './types.js'; import { CodeChunk, SearchFilters } from '../types/index.js'; import { IndexCorruptedError } from '../errors/index.js'; +interface LanceDBRecord { + id: string; + content: string; + filePath: string; + relativePath: string; + startLine: number; + endLine: number; + language: string; + framework?: string; + componentType?: string; + layer?: string; + dependencies?: string; + imports?: string; + exports?: string; + tags?: string; + metadata?: string; + _distance?: number; +} + export class LanceDBStorageProvider implements VectorStorageProvider { readonly name = 'lancedb'; - private db: any = null; - private table: any = null; + private db: Connection | null = null; + private table: Table | null = null; private storagePath: string = ''; private initialized = false; @@ -39,7 +58,7 @@ export class LanceDBStorageProvider implements VectorStorageProvider { this.table = await this.db.openTable('code_chunks'); const schema = await this.table.schema(); - const hasVectorColumn = schema.fields.some((f: any) => f.name === 'vector'); + const hasVectorColumn = schema.fields.some((f: { name: string }) => f.name === 'vector'); if (!hasVectorColumn) { throw new IndexCorruptedError('LanceDB index corrupted: missing vector column'); @@ -68,7 +87,7 @@ export class LanceDBStorageProvider implements VectorStorageProvider { } async store(chunks: CodeChunkWithEmbedding[]): Promise { - if (!this.initialized) { + if (!this.initialized || !this.db) { throw new Error('Storage not initialized'); } @@ -159,7 +178,7 @@ export class LanceDBStorageProvider implements VectorStorageProvider { const results = await query.toArray(); // Convert to VectorSearchResult format - return results.map((result: any) => ({ + return (results as LanceDBRecord[]).map((result) => ({ chunk: { id: result.id, content: result.content, @@ -231,6 +250,7 @@ export class LanceDBStorageProvider implements VectorStorageProvider { try { // Drop table if exists + if (!this.db) return; const tableNames = await this.db.tableNames(); if (tableNames.includes('code_chunks')) { await this.db.dropTable('code_chunks'); diff --git a/src/tools/detect-circular-dependencies.ts b/src/tools/detect-circular-dependencies.ts index e49a7ca..8157911 100644 --- a/src/tools/detect-circular-dependencies.ts +++ b/src/tools/detect-circular-dependencies.ts @@ -3,6 +3,7 @@ import { promises as fs } from 'fs'; import path from 'path'; import type { ToolContext, ToolResponse } from './types.js'; import { InternalFileGraph } from '../utils/usage-tracker.js'; +import type { FileExport } from '../utils/usage-tracker.js'; import { RELATIONSHIPS_FILENAME } from '../constants/codebase-context.js'; export const definition: Tool = { @@ -30,8 +31,8 @@ export async function handle( try { // Try relationships sidecar first (preferred), then intelligence - let graphDataSource: any = null; - let graphStats: any = null; + let graphDataSource: { imports?: Record; exports?: Record } | null = null; + let graphStats: unknown = null; const relationshipsPath = path.join( path.dirname(ctx.paths.intelligence), diff --git a/src/tools/get-team-patterns.ts b/src/tools/get-team-patterns.ts index 86b3d5f..79af7a4 100644 --- a/src/tools/get-team-patterns.ts +++ b/src/tools/get-team-patterns.ts @@ -6,6 +6,7 @@ import { isComplementaryPatternConflict, shouldSkipLegacyTestingFrameworkCategory } from '../patterns/semantics.js'; +import type { IntelligenceData, PatternsData } from '../types/index.js'; export const definition: Tool = { name: 'get_team_patterns', @@ -33,29 +34,33 @@ export async function handle( try { const intelligencePath = ctx.paths.intelligence; const content = await fs.readFile(intelligencePath, 'utf-8'); - const intelligence = JSON.parse(content); + const intelligence = JSON.parse(content) as unknown; + if (typeof intelligence !== 'object' || intelligence === null) { + throw new Error('Invalid intelligence.json: expected an object'); + } + const intel = intelligence as IntelligenceData; - const result: any = { status: 'success' }; + const result: Record = { status: 'success' }; if (category === 'all' || !category) { - result.patterns = intelligence.patterns || {}; - result.goldenFiles = intelligence.goldenFiles || []; - if (intelligence.tsconfigPaths) { - result.tsconfigPaths = intelligence.tsconfigPaths; + result.patterns = intel.patterns || {}; + result.goldenFiles = intel.goldenFiles || []; + if (intel.tsconfigPaths) { + result.tsconfigPaths = intel.tsconfigPaths; } } else if (category === 'di') { - result.dependencyInjection = intelligence.patterns?.dependencyInjection; + result.dependencyInjection = intel.patterns?.dependencyInjection; } else if (category === 'state') { - result.stateManagement = intelligence.patterns?.stateManagement; + result.stateManagement = intel.patterns?.stateManagement; } else if (category === 'testing') { - result.unitTestFramework = intelligence.patterns?.unitTestFramework; - result.e2eFramework = intelligence.patterns?.e2eFramework; - result.testingFramework = intelligence.patterns?.testingFramework; - result.testMocking = intelligence.patterns?.testMocking; + result.unitTestFramework = intel.patterns?.unitTestFramework; + result.e2eFramework = intel.patterns?.e2eFramework; + result.testingFramework = intel.patterns?.testingFramework; + result.testMocking = intel.patterns?.testMocking; } else if (category === 'libraries') { - result.topUsed = intelligence.importGraph?.topUsed || []; - if (intelligence.tsconfigPaths) { - result.tsconfigPaths = intelligence.tsconfigPaths; + result.topUsed = intel.importGraph?.topUsed || []; + if (intel.tsconfigPaths) { + result.tsconfigPaths = intel.tsconfigPaths; } } @@ -83,10 +88,15 @@ export async function handle( } // Detect pattern conflicts: primary < 80% and any alternative > 20% - const conflicts: any[] = []; - const patternsData = intelligence.patterns || {}; + const conflicts: Array<{ + category: string; + primary: { name: string; adoption: string; trend: string | undefined }; + alternative: { name: string; adoption: string; trend: string | undefined }; + note: string; + }> = []; + const patternsData: PatternsData = intel.patterns || {}; const hasUnitTestFramework = Boolean(patternsData.unitTestFramework?.primary); - for (const [cat, data] of Object.entries(patternsData)) { + for (const [cat, data] of Object.entries(patternsData)) { if (shouldSkipLegacyTestingFrameworkCategory(cat, patternsData)) continue; if (category && category !== 'all' && cat !== category) continue; if (!data.primary || !data.alsoDetected?.length) continue; diff --git a/src/tools/search-codebase.ts b/src/tools/search-codebase.ts index 6b1f66d..3762085 100644 --- a/src/tools/search-codebase.ts +++ b/src/tools/search-codebase.ts @@ -1,12 +1,12 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - import type { Tool } from '@modelcontextprotocol/sdk/types.js'; import { promises as fs } from 'fs'; import path from 'path'; -import type { ToolContext, ToolResponse } from './types.js'; +import type { ToolContext, ToolResponse, DecisionCard } from './types.js'; import { CodebaseSearcher } from '../core/search.js'; -import type { SearchResult } from '../types/index.js'; +import type { SearchIntentProfile } from '../core/search.js'; +import type { SearchResult, IntelligenceData, PatternsData, IntelligenceGoldenFile, ChunkMetadata } from '../types/index.js'; import { buildEvidenceLock } from '../preflight/evidence-lock.js'; +import type { EvidenceLock } from '../preflight/evidence-lock.js'; import { shouldIncludePatternConflictCategory } from '../preflight/query-scope.js'; import { isComplementaryPatternConflict, @@ -18,6 +18,13 @@ import { readMemoriesFile, withConfidence } from '../memory/store.js'; import { InternalFileGraph } from '../utils/usage-tracker.js'; import { RELATIONSHIPS_FILENAME } from '../constants/codebase-context.js'; +interface RelationshipsData { + graph?: { + imports?: Record; + }; + stats?: unknown; +} + export const definition: Tool = { name: 'search_codebase', description: @@ -84,7 +91,13 @@ export async function handle( args: Record, ctx: ToolContext ): Promise { - const { query, limit, filters, intent, includeSnippets } = args as any; + const { query, limit, filters, intent, includeSnippets } = args as { + query?: unknown; + limit?: number; + filters?: Record; + intent?: string; + includeSnippets?: boolean; + }; const queryStr = typeof query === 'string' ? query.trim() : ''; if (!queryStr) { @@ -146,9 +159,10 @@ export async function handle( } const searcher = new CodebaseSearcher(ctx.rootPath); - let results: any[]; - const searchProfile = - intent && ['explore', 'edit', 'refactor', 'migrate'].includes(intent) ? intent : 'explore'; + let results: SearchResult[]; + const searchProfile = ( + intent && ['explore', 'edit', 'refactor', 'migrate'].includes(intent) ? intent : 'explore' + ) as SearchIntentProfile; try { results = await searcher.search(queryStr, limit || 5, filters, { @@ -222,23 +236,29 @@ export async function handle( .sort((a, b) => b.effectiveConfidence - a.effectiveConfidence); // Load intelligence data for enrichment (all intents, not just preflight) - let intelligence: any = null; + let intelligence: IntelligenceData | null = null; try { const intelligenceContent = await fs.readFile(ctx.paths.intelligence, 'utf-8'); - intelligence = JSON.parse(intelligenceContent); + const parsed = JSON.parse(intelligenceContent) as unknown; + if (typeof parsed === 'object' && parsed !== null) { + intelligence = parsed as IntelligenceData; + } } catch { /* graceful degradation — intelligence file may not exist yet */ } // Load relationships sidecar (preferred over intelligence.internalFileGraph) - let relationships: any = null; + let relationships: RelationshipsData | null = null; try { const relationshipsPath = path.join( path.dirname(ctx.paths.intelligence), RELATIONSHIPS_FILENAME ); const relationshipsContent = await fs.readFile(relationshipsPath, 'utf-8'); - relationships = JSON.parse(relationshipsContent); + const parsed = JSON.parse(relationshipsContent) as unknown; + if (typeof parsed === 'object' && parsed !== null) { + relationships = parsed as RelationshipsData; + } } catch { /* graceful degradation — relationships sidecar may not exist yet */ } @@ -387,10 +407,10 @@ export async function handle( return output; } - const searchQuality = assessSearchQuality(query, results); + const searchQuality = assessSearchQuality(queryStr, results); // Always-on edit preflight (lite): do not require intent and keep payload small. - let editPreflight: any = undefined; + let editPreflight: { mode: string; riskLevel: string; confidence: string; evidenceLock: EvidenceLock } | undefined = undefined; if (intelligence && (!intent || intent === 'explore')) { try { const resultPaths = results.map((r) => r.filePath); @@ -398,8 +418,8 @@ export async function handle( // Use existing pattern intelligence for evidenceLock scoring, but keep the output payload lite. const preferredPatternsForEvidence: Array<{ pattern: string; example?: string }> = []; - const patterns = intelligence.patterns || {}; - for (const [_, data] of Object.entries(patterns)) { + const patterns: PatternsData = intelligence.patterns || {}; + for (const [_, data] of Object.entries(patterns)) { if (data.primary) { const p = data.primary; if (p.trend === 'Rising' || p.trend === 'Stable') { @@ -437,7 +457,7 @@ export async function handle( } // Compose preflight card for edit/refactor/migrate intents - let preflight: any = undefined; + let preflight: DecisionCard | undefined = undefined; const preflightIntents = ['edit', 'refactor', 'migrate']; if (intent && preflightIntents.includes(intent)) { if (!intelligence) { @@ -448,10 +468,10 @@ export async function handle( } else { try { // --- Avoid / Prefer patterns --- - const avoidPatternsList: any[] = []; - const preferredPatternsList: any[] = []; - const patterns = intelligence.patterns || {}; - for (const [category, data] of Object.entries(patterns)) { + const avoidPatternsList: Array<{ pattern: string; category: string; adoption: string; trend: string; guidance?: string }> = []; + const preferredPatternsList: Array<{ pattern: string; category: string; adoption: string; trend: string; guidance?: string; example?: string }> = []; + const patterns: PatternsData = intelligence.patterns || {}; + for (const [category, data] of Object.entries(patterns)) { // Primary pattern = preferred if Rising or Stable if (data.primary) { const p = data.primary; @@ -535,7 +555,7 @@ export async function handle( } // --- Golden files (exemplar code) --- - const goldenFiles = (intelligence.goldenFiles || []).slice(0, 3).map((g: any) => ({ + const goldenFiles = (intelligence.goldenFiles ?? []).slice(0, 3).map((g: IntelligenceGoldenFile) => ({ file: g.file, score: g.score })); @@ -563,10 +583,10 @@ export async function handle( primary: { name: string; adoption: string }; alternative: { name: string; adoption: string }; }> = []; - const hasUnitTestFramework = Boolean((patterns as any).unitTestFramework?.primary); - for (const [cat, data] of Object.entries(patterns)) { - if (shouldSkipLegacyTestingFrameworkCategory(cat, patterns as any)) continue; - if (!shouldIncludePatternConflictCategory(cat, query)) continue; + const hasUnitTestFramework = Boolean(patterns.unitTestFramework?.primary); + for (const [cat, data] of Object.entries(patterns)) { + if (shouldSkipLegacyTestingFrameworkCategory(cat, patterns)) continue; + if (!shouldIncludePatternConflictCategory(cat, queryStr)) continue; if (!data.primary || !data.alsoDetected?.length) continue; const primaryFreq = parseFloat(data.primary.frequency) || 100; if (primaryFreq >= 80) continue; @@ -595,22 +615,6 @@ export async function handle( }); // Build clean decision card (PREF-01 to PREF-04) - interface DecisionCard { - ready: boolean; - nextAction?: string; - warnings?: string[]; - patterns?: { - do?: string[]; - avoid?: string[]; - }; - bestExample?: string; - impact?: { - coverage?: string; - files?: string[]; - }; - whatWouldHelp?: string[]; - } - const decisionCard: DecisionCard = { ready: evidenceLock.readyToEdit }; @@ -686,7 +690,7 @@ export async function handle( } // Helper: Build scope header for symbol-aware chunks (SEARCH-02) - function buildScopeHeader(metadata: any): string | null { + function buildScopeHeader(metadata: ChunkMetadata): string | null { // Try symbolPath first (most reliable for AST-based symbols) if (metadata?.symbolPath && Array.isArray(metadata.symbolPath)) { return metadata.symbolPath.join('.'); @@ -710,7 +714,7 @@ export async function handle( return null; } - function enrichSnippetWithScope(snippet: string | undefined, metadata: any): string | undefined { + function enrichSnippetWithScope(snippet: string | undefined, metadata: ChunkMetadata): string | undefined { if (!snippet) return undefined; const scopeHeader = buildScopeHeader(metadata); if (scopeHeader) { diff --git a/src/tools/types.ts b/src/tools/types.ts index 9b6b2b1..837643f 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -1,6 +1,22 @@ import type { CodebaseIndexer } from '../core/indexer.js'; import type { IndexingStats } from '../types/index.js'; +export interface DecisionCard { + ready: boolean; + nextAction?: string; + warnings?: string[]; + patterns?: { + do?: string[]; + avoid?: string[]; + }; + bestExample?: string; + impact?: { + coverage?: string; + files?: string[]; + }; + whatWouldHelp?: string[]; +} + export interface ToolPaths { baseDir: string; memory: string; diff --git a/src/types/index.ts b/src/types/index.ts index fcf6ae6..2c3c33d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /** * Core types for the modular codebase context system * These types define the contract that all framework analyzers must implement @@ -52,7 +51,7 @@ export interface AnalysisResult { imports: ImportStatement[]; exports: ExportStatement[]; dependencies: Dependency[]; - metadata: Record; + metadata: Record; chunks: CodeChunk[]; } @@ -68,7 +67,7 @@ export interface CodeComponent { methods?: Method[]; lifecycle?: string[]; dependencies?: string[]; - metadata: Record; + metadata: Record; } export interface ImportStatement { @@ -183,7 +182,7 @@ export interface ChunkMetadata { category?: string; // Any framework-specific metadata - [key: string]: any; + [key: string]: unknown; } // ============================================================================ @@ -201,7 +200,7 @@ export interface CodebaseMetadata { documentation: DocumentationFile[]; projectStructure: ProjectStructure; statistics: CodebaseStatistics; - customMetadata: Record; + customMetadata: Record; } export interface FrameworkInfo { @@ -268,7 +267,7 @@ export interface StyleGuide { name: string; filePath: string; content: string; - metadata: Record; + metadata: Record; category: string; tags: string[]; rules: StyleRule[]; @@ -298,7 +297,7 @@ export interface DocumentationFile { title: string; content: string; type: 'readme' | 'guide' | 'api' | 'changelog' | 'contributing' | 'other'; - metadata: Record; + metadata: Record; sections?: DocumentationSection[]; } @@ -449,7 +448,7 @@ export interface IndexingStats { export interface AnalyzerConfig { enabled: boolean; priority?: number; - options?: Record; + options?: Record; } export interface CodebaseConfig { @@ -505,11 +504,11 @@ export interface CodebaseConfig { storage?: { provider?: 'lancedb' | 'milvus' | 'chromadb' | 'custom'; path?: string; - connection?: Record; + connection?: Record; }; // Custom metadata - customMetadata?: Record; + customMetadata?: Record; } // ============================================================================ @@ -518,8 +517,8 @@ export interface CodebaseConfig { export interface Decorator { name: string; - arguments?: any[]; - properties?: Record; + arguments?: unknown[]; + properties?: Record; } export interface Property { @@ -592,3 +591,69 @@ export interface Memory { /** Source of the memory: 'user' (default) or 'git' (auto-extracted from commits) */ source?: 'user' | 'git'; } + +// ============================================================================ +// SHARED PRIMITIVES +// ============================================================================ + +/** File + line reference — canonical base for all usage location types */ +export interface UsageLocation { + file: string; + line: number; +} + +// ============================================================================ +// INTELLIGENCE / PATTERN DATA +// ============================================================================ + +/** Pattern trend direction — shared between runtime tracker and serialized form */ +export type PatternTrend = 'Rising' | 'Declining' | 'Stable'; + +/** Common fields for any pattern candidate, at runtime or in intelligence.json */ +export interface PatternCandidateBase { + name: string; + /** e.g. "72%" */ + frequency: string; + trend?: PatternTrend; + guidance?: string; + newestFileDate?: string; +} + +/** A single detected pattern variant within a category (serialized form) */ +export interface PatternCandidate extends PatternCandidateBase { + canonicalExample?: { file: string; snippet?: string }; + count?: number; +} + +/** Primary + alternatives for one pattern category */ +export interface PatternEntry { + primary?: PatternCandidate; + alsoDetected?: PatternCandidate[]; +} + +/** intelligence.json patterns map */ +export type PatternsData = Record; + +/** Minimal golden file shape as stored in intelligence.json */ +export interface IntelligenceGoldenFile { + file: string; + score: number; +} + +/** Typed shape for intelligence.json content */ +export interface IntelligenceData { + patterns?: PatternsData; + goldenFiles?: IntelligenceGoldenFile[]; + /** Opaque — consumed via InternalFileGraph.fromJSON */ + internalFileGraph?: { + imports?: Record; + exports?: Record; + stats?: unknown; + }; + generatedAt?: string; + libraryUsage?: Record; + tsconfigPaths?: Record; + importGraph?: { topUsed?: unknown[] }; + header?: unknown; + [key: string]: unknown; +} diff --git a/src/utils/usage-tracker.ts b/src/utils/usage-tracker.ts index 9fa403c..34bfcf6 100644 --- a/src/utils/usage-tracker.ts +++ b/src/utils/usage-tracker.ts @@ -3,6 +3,10 @@ * Tracks what libraries are used and detects common coding patterns */ +import type { PatternTrend, PatternCandidateBase, IntelligenceGoldenFile, UsageLocation } from '../types/index.js'; + +export type { PatternTrend }; + export interface LibraryUsageStats { [libraryPath: string]: { count: number; @@ -10,37 +14,23 @@ export interface LibraryUsageStats { }; } -export type PatternTrend = 'Rising' | 'Declining' | 'Stable'; +/** Runtime pattern candidate — extends the base with fields only needed during indexing */ +type RuntimePatternPrimary = PatternCandidateBase & { + count: number; + examples: string[]; + canonicalExample?: { file: string; snippet: string }; +}; export interface PatternUsageStats { [patternName: string]: { - primary: { - name: string; - count: number; - frequency: string; - examples: string[]; - canonicalExample?: { file: string; snippet: string }; - newestFileDate?: string; - trend?: PatternTrend; - /** Actionable guidance: "USE: X" or "CAUTION: Y" */ - guidance?: string; - }; - alsoDetected?: Array<{ - name: string; - count: number; - frequency: string; - newestFileDate?: string; - trend?: PatternTrend; - /** Actionable guidance for alternative patterns */ - guidance?: string; - }>; + primary: RuntimePatternPrimary; + /** Actionable guidance for alternative patterns */ + alsoDetected?: Array; }; } -export interface ImportUsage { - file: string; - line: number; -} +/** File + line reference for tracked imports */ +export type ImportUsage = UsageLocation; export interface ComponentUsageInfo { definedIn?: string; @@ -189,9 +179,8 @@ interface TestFrameworkConfig { priority: number; } -export interface GoldenFile { - file: string; - score: number; +/** Runtime golden file — extends the serialized shape with pattern detection flags */ +export interface GoldenFile extends IntelligenceGoldenFile { patterns: { inject: boolean; signals: boolean; From 57e31301f487edae8ee57315e803220644ef9942 Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Sun, 22 Feb 2026 20:28:26 +0100 Subject: [PATCH 2/4] fix pr comments --- src/core/indexer.ts | 26 ++++++++++++-------------- src/utils/usage-tracker.ts | 15 +++++---------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/core/indexer.ts b/src/core/indexer.ts index 8629ad0..e78910b 100644 --- a/src/core/indexer.ts +++ b/src/core/indexer.ts @@ -28,8 +28,7 @@ import { PatternDetector, ImportGraph, InternalFileGraph, - FileExport, - GoldenFile + FileExport } from '../utils/usage-tracker.js'; import { mergeSmallChunks } from '../utils/chunking.js'; import { getFileCommitDates } from '../utils/git-dates.js'; @@ -366,7 +365,7 @@ export class CodebaseIndexer { console.error( `Incremental diff: ${diff.added.length} added, ${diff.changed.length} changed, ` + - `${diff.deleted.length} deleted, ${diff.unchanged.length} unchanged` + `${diff.deleted.length} deleted, ${diff.unchanged.length} unchanged` ); stats.incremental = { @@ -441,9 +440,9 @@ export class CodebaseIndexer { // Build the set of files that need analysis + embedding (incremental: only added/changed) const filesToProcess = diff ? files.filter((f) => { - const rel = path.relative(this.rootPath, f).replace(/\\/g, '/'); - return diff!.added.includes(rel) || diff!.changed.includes(rel); - }) + const rel = path.relative(this.rootPath, f).replace(/\\/g, '/'); + return diff!.added.includes(rel) || diff!.changed.includes(rel); + }) : files; // Phase 2: Analyzing & Parsing @@ -582,7 +581,7 @@ export class CodebaseIndexer { for (const p of detectedPatterns) { patternFlags[`${p.category}:${p.name}`] = true; } - patternDetector.trackGoldenFile(relPath, patternScore, patternFlags as GoldenFile['patterns']); + patternDetector.trackGoldenFile(relPath, patternScore, patternFlags); } // Update component statistics @@ -639,8 +638,8 @@ export class CodebaseIndexer { this.updateProgress('embedding', 50); console.error( `Creating embeddings for ${chunksToEmbed.length} chunks` + - (diff ? ` (${allChunks.length} total, ${chunksToEmbed.length} changed)` : '') + - '...' + (diff ? ` (${allChunks.length} total, ${chunksToEmbed.length} changed)` : '') + + '...' ); // Initialize embedding provider @@ -686,8 +685,7 @@ export class CodebaseIndexer { if ((i + batchSize) % 100 === 0 || i + batchSize >= chunksToEmbed.length) { console.error( - `Embedded ${Math.min(i + batchSize, chunksToEmbed.length)}/${ - chunksToEmbed.length + `Embedded ${Math.min(i + batchSize, chunksToEmbed.length)}/${chunksToEmbed.length } chunks` ); } @@ -740,7 +738,7 @@ export class CodebaseIndexer { } console.error( `Incremental store: deleted chunks for ${diff.changed.length + diff.deleted.length} files, ` + - `added ${chunksWithEmbeddings.length} new chunks` + `added ${chunksWithEmbeddings.length} new chunks` ); } else { // Full rebuild: store to staging (no clear - fresh directory) @@ -913,8 +911,8 @@ export class CodebaseIndexer { if (diff) { console.error( `Incremental indexing complete in ${stats.duration}ms ` + - `(${diff.added.length} added, ${diff.changed.length} changed, ` + - `${diff.deleted.length} deleted, ${diff.unchanged.length} unchanged)` + `(${diff.added.length} added, ${diff.changed.length} changed, ` + + `${diff.deleted.length} deleted, ${diff.unchanged.length} unchanged)` ); } else { console.error(`Indexing complete in ${stats.duration}ms`); diff --git a/src/utils/usage-tracker.ts b/src/utils/usage-tracker.ts index 34bfcf6..583e4ea 100644 --- a/src/utils/usage-tracker.ts +++ b/src/utils/usage-tracker.ts @@ -179,16 +179,11 @@ interface TestFrameworkConfig { priority: number; } -/** Runtime golden file — extends the serialized shape with pattern detection flags */ +/** Runtime golden file — extends the serialized shape with generic pattern detection flags. + * Keys are "category:name" strings (e.g. "dependencyInjection:inject() function"). + * Framework-specific field names never appear here — they stay in their analyzer. */ export interface GoldenFile extends IntelligenceGoldenFile { - patterns: { - inject: boolean; - signals: boolean; - computed: boolean; - effect: boolean; - standalone: boolean; - signalInputs: boolean; - }; + patterns: Record; } const DEFAULT_TEST_FRAMEWORK_CONFIGS: TestFrameworkConfig[] = [ @@ -310,7 +305,7 @@ export class PatternDetector { /** * Track a file as a potential "Golden File" - a file that demonstrates multiple modern patterns */ - trackGoldenFile(file: string, score: number, patterns: GoldenFile['patterns']): void { + trackGoldenFile(file: string, score: number, patterns: Record): void { // Check if already tracked const existing = this.goldenFiles.find((gf) => gf.file === file); if (existing) { From 024db0b2220714f68fb55371e004fca3472ff8d8 Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Sun, 22 Feb 2026 20:33:58 +0100 Subject: [PATCH 3/4] feat: enhance AngularAnalyzer with new IndexChunk interface and refactor signal input/output handling - Introduced IndexChunk interface for better type definition of chunk properties. - Refactored signal input() and output() handling to improve clarity and type safety. - Simplified logic for determining required properties in signal-based inputs and outputs. - Updated type assertions to leverage the new IndexChunk interface for chunk processing. --- AGENTS.md | 5 ++++ src/analyzers/angular/index.ts | 42 ++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index db3c552..8c84981 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,6 +128,11 @@ If an agent has to call the tool twice to understand the response, the tool fail - `src/core/` is framework-agnostic. No hardcoded framework strings (Angular, React, Vue, etc.). - CLI code belongs in `src/cli.ts`. Never in `src/index.ts`. - Framework analyzers self-register their own patterns (e.g., Angular computed+effect pairing belongs in the Angular analyzer, not protocol layer). +- **Types and interfaces must be framework-agnostic in core and shared utilities.** + Framework-specific field names (Angular-only, React-only, etc.) belong exclusively in their + respective analyzer files under `src/analyzers/`. Never add named framework-specific fields + to interfaces in `src/types/`, `src/utils/`, or `src/core/`. + Use `Record` for open-ended data that frameworks extend at runtime. ### Release Checklist diff --git a/src/analyzers/angular/index.ts b/src/analyzers/angular/index.ts index 70fa364..622b6aa 100644 --- a/src/analyzers/angular/index.ts +++ b/src/analyzers/angular/index.ts @@ -39,6 +39,14 @@ interface AngularOutput { style: 'decorator' | 'signal'; } +interface IndexChunk { + filePath?: string; + startLine?: number; + endLine?: number; + componentType?: string; + layer?: string; +} + export class AngularAnalyzer implements FrameworkAnalyzer { readonly name = 'angular'; readonly version = '1.0.0'; @@ -600,18 +608,28 @@ export class AngularAnalyzer implements FrameworkAnalyzer { } // Check for signal-based input() (Angular v17.1+) - if (member.value && member.key && 'name' in member.key) { - const callee = member.value.type === 'CallExpression' - ? (member.value.callee as { type: string; name?: string; object?: { name?: string }; property?: { name?: string } }) - : null; - const valueStr = callee?.name ?? callee?.object?.name ?? null; + if (member.value && member.key && 'name' in member.key && member.value.type === 'CallExpression') { + const callee = member.value.callee; + let valueStr: string | null = null; + let isRequired = false; + + if (callee.type === 'Identifier') { + valueStr = callee.name; + } else if (callee.type === 'MemberExpression') { + if (callee.object.type === 'Identifier') { + valueStr = callee.object.name; + } + if (callee.property.type === 'Identifier') { + isRequired = callee.property.name === 'required'; + } + } if (valueStr === 'input') { inputs.push({ name: member.key.name, type: 'InputSignal', style: 'signal', - required: callee?.property?.name === 'required' + required: isRequired }); } } @@ -650,11 +668,9 @@ export class AngularAnalyzer implements FrameworkAnalyzer { } // Check for signal-based output() (Angular v17.1+) - if (member.value && member.key && 'name' in member.key) { - const callee = member.value.type === 'CallExpression' - ? (member.value.callee as { type: string; name?: string }) - : null; - const valueStr = callee?.name ?? null; + if (member.value && member.key && 'name' in member.key && member.value.type === 'CallExpression') { + const callee = member.value.callee; + const valueStr = callee.type === 'Identifier' ? callee.name : null; if (valueStr === 'output') { outputs.push({ @@ -933,7 +949,9 @@ export class AngularAnalyzer implements FrameworkAnalyzer { } const parsedObj = parsed as { chunks?: unknown }; - const chunks = parsedObj && Array.isArray(parsedObj.chunks) ? (parsedObj.chunks as Array<{ filePath?: string; startLine?: number; endLine?: number; componentType?: string; layer?: string }>) : null; + const chunks = parsedObj && Array.isArray(parsedObj.chunks) + ? (parsedObj.chunks as IndexChunk[]) + : null; if (Array.isArray(chunks) && chunks.length > 0) { console.error(`Loading statistics from ${indexPath}: ${chunks.length} chunks`); From 6a58e39bf17c80cf6c04a0b3d7ba92f5ea75c9be Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Sun, 22 Feb 2026 20:42:04 +0100 Subject: [PATCH 4/4] fix linting --- AGENTS.md | 6 ++-- src/analyzers/angular/index.ts | 33 ++++++++++++----- src/analyzers/generic/index.ts | 10 ++++-- src/core/indexer.ts | 39 +++++++++++--------- src/core/reranker.ts | 6 +++- src/core/search.ts | 1 - src/core/symbol-references.ts | 5 ++- src/embeddings/transformers.ts | 10 ++++-- src/index.ts | 8 ++++- src/tools/detect-circular-dependencies.ts | 5 ++- src/tools/search-codebase.ts | 44 ++++++++++++++++++----- src/utils/usage-tracker.ts | 7 +++- 12 files changed, 126 insertions(+), 48 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8c84981..f54aa26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,8 @@ These are non-negotiable. Every PR, feature, and design decision must respect th - **Never stage/commit `.planning/**`\*\* (or any other local workflow artifacts) unless the user explicitly asks in that message. - **Never use `gsd-tools ... commit` wrappers** in this repo. Use plain `git add ` and `git commit -m "..."`. - **Before every commit:** run `git status --short` and confirm staged files match intent; abort if any `.planning/**` is staged. -- **Avoid using `any` Type AT ALL COSTS. +- \*\*Avoid using `any` Type AT ALL COSTS. + ## Evaluation Integrity (NON-NEGOTIABLE) These rules prevent metric gaming, overfitting, and false quality claims. Violation of these rules means the feature CANNOT ship. @@ -158,6 +159,3 @@ These came from behavioral observation across multiple sessions. They're here so See `internal-docs/AGENTS.md` for internal-only guidelines and context. --- - -**Current focus:** See `internal-docs/ISSUES.md` for active release blockers. -For full project history and context handover, see `internal-docs/ARCHIVE/WALKTHROUGH-v1.6.1.md`. diff --git a/src/analyzers/angular/index.ts b/src/analyzers/angular/index.ts index 622b6aa..6208b88 100644 --- a/src/analyzers/angular/index.ts +++ b/src/analyzers/angular/index.ts @@ -172,7 +172,9 @@ export class AngularAnalyzer implements FrameworkAnalyzer { const specifier = s as TSESTree.ImportSpecifier; return specifier.imported.name || specifier.local.name; }), - isDefault: node.specifiers.some((s: TSESTree.ImportClause) => s.type === 'ImportDefaultSpecifier'), + isDefault: node.specifiers.some( + (s: TSESTree.ImportClause) => s.type === 'ImportDefaultSpecifier' + ), isDynamic: false, line: node.loc?.start.line }); @@ -566,7 +568,8 @@ export class AngularAnalyzer implements FrameworkAnalyzer { for (const param of member.value.params) { const typedParam = param as TSESTree.Identifier; if (typedParam.typeAnnotation?.typeAnnotation?.type === 'TSTypeReference') { - const typeRef = typedParam.typeAnnotation.typeAnnotation as TSESTree.TSTypeReference; + const typeRef = typedParam.typeAnnotation + .typeAnnotation as TSESTree.TSTypeReference; if (typeRef.typeName.type === 'Identifier') { services.push(typeRef.typeName.name); } @@ -601,14 +604,20 @@ export class AngularAnalyzer implements FrameworkAnalyzer { if (hasInput && member.key && 'name' in member.key) { inputs.push({ name: member.key.name, - type: (member.typeAnnotation?.typeAnnotation?.type as string | undefined) || 'unknown', + type: + (member.typeAnnotation?.typeAnnotation?.type as string | undefined) || 'unknown', style: 'decorator' }); } } // Check for signal-based input() (Angular v17.1+) - if (member.value && member.key && 'name' in member.key && member.value.type === 'CallExpression') { + if ( + member.value && + member.key && + 'name' in member.key && + member.value.type === 'CallExpression' + ) { const callee = member.value.callee; let valueStr: string | null = null; let isRequired = false; @@ -668,7 +677,12 @@ export class AngularAnalyzer implements FrameworkAnalyzer { } // Check for signal-based output() (Angular v17.1+) - if (member.value && member.key && 'name' in member.key && member.value.type === 'CallExpression') { + if ( + member.value && + member.key && + 'name' in member.key && + member.value.type === 'CallExpression' + ) { const callee = member.value.callee; const valueStr = callee.type === 'Identifier' ? callee.name : null; @@ -949,9 +963,8 @@ export class AngularAnalyzer implements FrameworkAnalyzer { } const parsedObj = parsed as { chunks?: unknown }; - const chunks = parsedObj && Array.isArray(parsedObj.chunks) - ? (parsedObj.chunks as IndexChunk[]) - : null; + const chunks = + parsedObj && Array.isArray(parsedObj.chunks) ? (parsedObj.chunks as IndexChunk[]) : null; if (Array.isArray(chunks) && chunks.length > 0) { console.error(`Loading statistics from ${indexPath}: ${chunks.length} chunks`); @@ -1046,7 +1059,9 @@ export class AngularAnalyzer implements FrameworkAnalyzer { case 'module': { const imports = Array.isArray(metadata?.imports) ? metadata.imports.length : 0; - const declarations = Array.isArray(metadata?.declarations) ? metadata.declarations.length : 0; + const declarations = Array.isArray(metadata?.declarations) + ? metadata.declarations.length + : 0; return `Angular module '${className}' with ${declarations} declarations and ${imports} imports.`; } diff --git a/src/analyzers/generic/index.ts b/src/analyzers/generic/index.ts index a7578a8..6dcfcaa 100644 --- a/src/analyzers/generic/index.ts +++ b/src/analyzers/generic/index.ts @@ -248,7 +248,11 @@ export class GenericAnalyzer implements FrameworkAnalyzer { workspaceType !== 'single' ? await scanWorkspacePackageJsons(rootPath) : []; const pkgPath = path.join(rootPath, 'package.json'); - let packageJson: { name?: string; dependencies?: Record; devDependencies?: Record } = {}; + let packageJson: { + name?: string; + dependencies?: Record; + devDependencies?: Record; + } = {}; try { packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8')); projectName = packageJson.name || projectName; @@ -360,7 +364,9 @@ export class GenericAnalyzer implements FrameworkAnalyzer { const specifier = s as TSESTree.ImportSpecifier; return specifier.imported.name || specifier.local.name; }), - isDefault: node.specifiers.some((s: TSESTree.ImportClause) => s.type === 'ImportDefaultSpecifier'), + isDefault: node.specifiers.some( + (s: TSESTree.ImportClause) => s.type === 'ImportDefaultSpecifier' + ), isDynamic: false, line: node.loc?.start.line }); diff --git a/src/core/indexer.ts b/src/core/indexer.ts index e78910b..3ee9c2e 100644 --- a/src/core/indexer.ts +++ b/src/core/indexer.ts @@ -365,7 +365,7 @@ export class CodebaseIndexer { console.error( `Incremental diff: ${diff.added.length} added, ${diff.changed.length} changed, ` + - `${diff.deleted.length} deleted, ${diff.unchanged.length} unchanged` + `${diff.deleted.length} deleted, ${diff.unchanged.length} unchanged` ); stats.incremental = { @@ -440,9 +440,9 @@ export class CodebaseIndexer { // Build the set of files that need analysis + embedding (incremental: only added/changed) const filesToProcess = diff ? files.filter((f) => { - const rel = path.relative(this.rootPath, f).replace(/\\/g, '/'); - return diff!.added.includes(rel) || diff!.changed.includes(rel); - }) + const rel = path.relative(this.rootPath, f).replace(/\\/g, '/'); + return diff!.added.includes(rel) || diff!.changed.includes(rel); + }) : files; // Phase 2: Analyzing & Parsing @@ -549,8 +549,14 @@ export class CodebaseIndexer { // GENERIC PATTERN FORWARDING // Framework analyzers return detectedPatterns in metadata - we just forward them // This keeps the indexer framework-agnostic - if (result.metadata?.detectedPatterns && Array.isArray(result.metadata.detectedPatterns)) { - for (const pattern of result.metadata.detectedPatterns as Array<{ category: string; name: string }>) { + if ( + result.metadata?.detectedPatterns && + Array.isArray(result.metadata.detectedPatterns) + ) { + for (const pattern of result.metadata.detectedPatterns as Array<{ + category: string; + name: string; + }>) { // Try to extract a relevant snippet for the pattern // Ask analyzer registry for snippet pattern (framework-agnostic delegation) const analyzer = analyzerRegistry.findAnalyzer(file); @@ -569,12 +575,12 @@ export class CodebaseIndexer { // Track file for Golden File scoring (framework-agnostic) // A golden file = file with patterns in ≥3 distinct categories const rawPatterns = result.metadata?.detectedPatterns; - const detectedPatterns: Array<{ category: string; name: string }> = Array.isArray(rawPatterns) + const detectedPatterns: Array<{ category: string; name: string }> = Array.isArray( + rawPatterns + ) ? (rawPatterns as Array<{ category: string; name: string }>) : []; - const uniqueCategories = new Set( - detectedPatterns.map((p) => p.category) - ); + const uniqueCategories = new Set(detectedPatterns.map((p) => p.category)); const patternScore = uniqueCategories.size; if (patternScore >= 3) { const patternFlags: Record = {}; @@ -638,8 +644,8 @@ export class CodebaseIndexer { this.updateProgress('embedding', 50); console.error( `Creating embeddings for ${chunksToEmbed.length} chunks` + - (diff ? ` (${allChunks.length} total, ${chunksToEmbed.length} changed)` : '') + - '...' + (diff ? ` (${allChunks.length} total, ${chunksToEmbed.length} changed)` : '') + + '...' ); // Initialize embedding provider @@ -685,7 +691,8 @@ export class CodebaseIndexer { if ((i + batchSize) % 100 === 0 || i + batchSize >= chunksToEmbed.length) { console.error( - `Embedded ${Math.min(i + batchSize, chunksToEmbed.length)}/${chunksToEmbed.length + `Embedded ${Math.min(i + batchSize, chunksToEmbed.length)}/${ + chunksToEmbed.length } chunks` ); } @@ -738,7 +745,7 @@ export class CodebaseIndexer { } console.error( `Incremental store: deleted chunks for ${diff.changed.length + diff.deleted.length} files, ` + - `added ${chunksWithEmbeddings.length} new chunks` + `added ${chunksWithEmbeddings.length} new chunks` ); } else { // Full rebuild: store to staging (no clear - fresh directory) @@ -911,8 +918,8 @@ export class CodebaseIndexer { if (diff) { console.error( `Incremental indexing complete in ${stats.duration}ms ` + - `(${diff.added.length} added, ${diff.changed.length} changed, ` + - `${diff.deleted.length} deleted, ${diff.unchanged.length} unchanged)` + `(${diff.added.length} added, ${diff.changed.length} changed, ` + + `${diff.deleted.length} deleted, ${diff.unchanged.length} unchanged)` ); } else { console.error(`Indexing complete in ${stats.duration}ms`); diff --git a/src/core/reranker.ts b/src/core/reranker.ts index 1c60324..bfca1b2 100644 --- a/src/core/reranker.ts +++ b/src/core/reranker.ts @@ -19,7 +19,11 @@ const RERANK_TOP_K = 10; const AMBIGUITY_THRESHOLD = 0.08; interface CrossEncoderTokenizer { - (query: string, passage: string, options: { padding: boolean; truncation: boolean; max_length: number }): unknown; + ( + query: string, + passage: string, + options: { padding: boolean; truncation: boolean; max_length: number } + ): unknown; } interface CrossEncoderModel { diff --git a/src/core/search.ts b/src/core/search.ts index f7b4f99..13bd1d8 100644 --- a/src/core/search.ts +++ b/src/core/search.ts @@ -230,7 +230,6 @@ export class CodebaseSearcher { // Extract pattern indicators from intelligence data if (intelligence.patterns) { for (const [_category, patternData] of Object.entries(intelligence.patterns)) { - // Track primary pattern if (patternData.primary?.trend === 'Rising') { risingPatterns.add(patternData.primary.name.toLowerCase()); diff --git a/src/core/symbol-references.ts b/src/core/symbol-references.ts index f34b5d0..73c7a0f 100644 --- a/src/core/symbol-references.ts +++ b/src/core/symbol-references.ts @@ -95,7 +95,10 @@ export async function findSymbolReferences( } const chunks = - chunksRaw !== null && typeof chunksRaw === 'object' && 'chunks' in chunksRaw && Array.isArray(chunksRaw.chunks) + chunksRaw !== null && + typeof chunksRaw === 'object' && + 'chunks' in chunksRaw && + Array.isArray(chunksRaw.chunks) ? (chunksRaw.chunks as unknown[]) : null; diff --git a/src/embeddings/transformers.ts b/src/embeddings/transformers.ts index 0d9c0ce..43388c5 100644 --- a/src/embeddings/transformers.ts +++ b/src/embeddings/transformers.ts @@ -54,8 +54,14 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider { // TS2590: pipeline() resolves AllTasks[T] — a union too complex for TSC to represent. // Cast to a simpler signature; the actual return type IS FeatureExtractionPipelineType. - type PipelineFn = (task: 'feature-extraction', model: string, opts: Record) => Promise; - this.pipeline = await (pipeline as PipelineFn)('feature-extraction', this.modelName, { dtype: 'q8' }); + type PipelineFn = ( + task: 'feature-extraction', + model: string, + opts: Record + ) => Promise; + this.pipeline = await (pipeline as PipelineFn)('feature-extraction', this.modelName, { + dtype: 'q8' + }); this.ready = true; console.error(`Model loaded successfully: ${this.modelName}`); diff --git a/src/index.ts b/src/index.ts index 931484a..15fda08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,13 @@ import { Resource } from '@modelcontextprotocol/sdk/types.js'; import { CodebaseIndexer } from './core/indexer.js'; -import type { IndexingStats, IntelligenceData, PatternsData, PatternEntry, PatternCandidate } from './types/index.js'; +import type { + IndexingStats, + IntelligenceData, + PatternsData, + PatternEntry, + PatternCandidate +} from './types/index.js'; import { analyzerRegistry } from './core/analyzer-registry.js'; import { AngularAnalyzer } from './analyzers/angular/index.js'; import { GenericAnalyzer } from './analyzers/generic/index.js'; diff --git a/src/tools/detect-circular-dependencies.ts b/src/tools/detect-circular-dependencies.ts index 8157911..8a9a679 100644 --- a/src/tools/detect-circular-dependencies.ts +++ b/src/tools/detect-circular-dependencies.ts @@ -31,7 +31,10 @@ export async function handle( try { // Try relationships sidecar first (preferred), then intelligence - let graphDataSource: { imports?: Record; exports?: Record } | null = null; + let graphDataSource: { + imports?: Record; + exports?: Record; + } | null = null; let graphStats: unknown = null; const relationshipsPath = path.join( diff --git a/src/tools/search-codebase.ts b/src/tools/search-codebase.ts index 3762085..d9258c5 100644 --- a/src/tools/search-codebase.ts +++ b/src/tools/search-codebase.ts @@ -4,7 +4,13 @@ import path from 'path'; import type { ToolContext, ToolResponse, DecisionCard } from './types.js'; import { CodebaseSearcher } from '../core/search.js'; import type { SearchIntentProfile } from '../core/search.js'; -import type { SearchResult, IntelligenceData, PatternsData, IntelligenceGoldenFile, ChunkMetadata } from '../types/index.js'; +import type { + SearchResult, + IntelligenceData, + PatternsData, + IntelligenceGoldenFile, + ChunkMetadata +} from '../types/index.js'; import { buildEvidenceLock } from '../preflight/evidence-lock.js'; import type { EvidenceLock } from '../preflight/evidence-lock.js'; import { shouldIncludePatternConflictCategory } from '../preflight/query-scope.js'; @@ -410,7 +416,9 @@ export async function handle( const searchQuality = assessSearchQuality(queryStr, results); // Always-on edit preflight (lite): do not require intent and keep payload small. - let editPreflight: { mode: string; riskLevel: string; confidence: string; evidenceLock: EvidenceLock } | undefined = undefined; + let editPreflight: + | { mode: string; riskLevel: string; confidence: string; evidenceLock: EvidenceLock } + | undefined = undefined; if (intelligence && (!intent || intent === 'explore')) { try { const resultPaths = results.map((r) => r.filePath); @@ -468,8 +476,21 @@ export async function handle( } else { try { // --- Avoid / Prefer patterns --- - const avoidPatternsList: Array<{ pattern: string; category: string; adoption: string; trend: string; guidance?: string }> = []; - const preferredPatternsList: Array<{ pattern: string; category: string; adoption: string; trend: string; guidance?: string; example?: string }> = []; + const avoidPatternsList: Array<{ + pattern: string; + category: string; + adoption: string; + trend: string; + guidance?: string; + }> = []; + const preferredPatternsList: Array<{ + pattern: string; + category: string; + adoption: string; + trend: string; + guidance?: string; + example?: string; + }> = []; const patterns: PatternsData = intelligence.patterns || {}; for (const [category, data] of Object.entries(patterns)) { // Primary pattern = preferred if Rising or Stable @@ -555,10 +576,12 @@ export async function handle( } // --- Golden files (exemplar code) --- - const goldenFiles = (intelligence.goldenFiles ?? []).slice(0, 3).map((g: IntelligenceGoldenFile) => ({ - file: g.file, - score: g.score - })); + const goldenFiles = (intelligence.goldenFiles ?? []) + .slice(0, 3) + .map((g: IntelligenceGoldenFile) => ({ + file: g.file, + score: g.score + })); // --- Confidence (index freshness) --- // TODO: Review this confidence calculation @@ -714,7 +737,10 @@ export async function handle( return null; } - function enrichSnippetWithScope(snippet: string | undefined, metadata: ChunkMetadata): string | undefined { + function enrichSnippetWithScope( + snippet: string | undefined, + metadata: ChunkMetadata + ): string | undefined { if (!snippet) return undefined; const scopeHeader = buildScopeHeader(metadata); if (scopeHeader) { diff --git a/src/utils/usage-tracker.ts b/src/utils/usage-tracker.ts index 583e4ea..6150ac2 100644 --- a/src/utils/usage-tracker.ts +++ b/src/utils/usage-tracker.ts @@ -3,7 +3,12 @@ * Tracks what libraries are used and detects common coding patterns */ -import type { PatternTrend, PatternCandidateBase, IntelligenceGoldenFile, UsageLocation } from '../types/index.js'; +import type { + PatternTrend, + PatternCandidateBase, + IntelligenceGoldenFile, + UsageLocation +} from '../types/index.js'; export type { PatternTrend };