From ba817351dd5e1e1eed171d2b40d05684d3b41676 Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Sun, 22 Feb 2026 15:05:58 +0100 Subject: [PATCH 1/2] fix: close v1.8 post-merge integration gaps Addresses 5 live code gaps left after PRs #39 and #40 merged: 1. Delete orphaned get-component-usage source file (already removed from dispatcher in PR #39, but file remained on disk causing confusion) 2. Remove 'get_component_usage' from INDEX_CONSUMING_TOOL_NAMES allowlist in src/index.ts. The dispatcher no longer routes to this tool, so the allowlist entry created a phantom entry that would be validated during test runs despite not being available. 3. Remove get_component_usage branch from args dispatch ternary in index-versioning-migration test. Now that the tool is removed, this branch is dead code and should not be validated. 4. Replace guidance strings in search-quality.ts that still directed agents to the removed get_component_usage tool. Both instances now point to get_symbol_references, which covers the same use case. 5. Add fallback decision card when intelligence.json is absent in search-codebase.ts. Previously, preflight would silently skip when intelligence was missing, leaving users without guidance. Now returns ready=false with actionable nextAction message. All changes verified: - Zero references to get_component_usage in src/ and tests/ - Index-versioning-migration test passes (5/5) - Full test suite: 231/234 passing (3 pre-existing timeouts) --- CHANGELOG.md | 1 + internal-docs | 2 +- src/core/search-quality.ts | 4 +- src/index.ts | 1 - src/tools/get-component-usage.ts | 108 ----------------------- src/tools/search-codebase.ts | 23 +++-- tests/index-versioning-migration.test.ts | 10 +-- 7 files changed, 23 insertions(+), 126 deletions(-) delete mode 100644 src/tools/get-component-usage.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 822b660..7d34085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ - Null-pointer crash in GenericAnalyzer when chunk content is undefined. - Tree-sitter symbol extraction now treats node offsets as UTF-8 byte ranges and evicts cached parsers on failures/timeouts. +- **Post-merge integration gaps** (v1.8 audit): Removed orphaned `get_component_usage` source file, deleted phantom allowlist entry, removed dead guidance strings referencing the deleted tool. Added fallback decision card when `intelligence.json` is absent during edit-intent searches, now returns `ready: false` with actionable guidance instead of silently skipping. ## [1.6.2] - 2026-02-17 diff --git a/internal-docs b/internal-docs index b68f95b..bcc8ebb 160000 --- a/internal-docs +++ b/internal-docs @@ -1 +1 @@ -Subproject commit b68f95b8938d90ef24e9c8b947621e33210ba893 +Subproject commit bcc8ebb0640cabb60e4d75973863910b2771df73 diff --git a/src/core/search-quality.ts b/src/core/search-quality.ts index 4b89ef7..49c1938 100644 --- a/src/core/search-quality.ts +++ b/src/core/search-quality.ts @@ -30,7 +30,7 @@ export function assessSearchQuality( nextSteps: [ 'Try a narrower query with one concrete symbol, route, or file hint.', 'Apply search filters (framework/language/componentType/layer).', - 'Use get_component_usage for dependency or wiring lookups.' + 'Use get_symbol_references to find where a specific symbol is used across the codebase.' ] }; } @@ -76,7 +76,7 @@ export function assessSearchQuality( nextSteps: [ 'Add one or two concrete symbols, routes, or file hints to the query.', 'Apply filters (framework/language/componentType/layer) to narrow candidates.', - 'Use get_component_usage when the question is about wiring or usages.' + 'Use get_symbol_references when the question is about where a symbol or function is used.' ] }) }; diff --git a/src/index.ts b/src/index.ts index aee4844..b537c72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,7 +84,6 @@ const LEGACY_PATHS = { export const INDEX_CONSUMING_TOOL_NAMES = [ 'search_codebase', 'get_symbol_references', - 'get_component_usage', 'detect_circular_dependencies', 'get_team_patterns', 'get_codebase_metadata' diff --git a/src/tools/get-component-usage.ts b/src/tools/get-component-usage.ts deleted file mode 100644 index 44f9e81..0000000 --- a/src/tools/get-component-usage.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { promises as fs } from 'fs'; -import type { ToolContext, ToolResponse } from './types.js'; - -export const definition: Tool = { - name: 'get_component_usage', - description: - 'Find WHERE a library or component is used in the codebase. ' + - "This is 'Find Usages' - returns all files that import a given package/module. " + - "Example: get_component_usage('@mycompany/utils') -> shows all files using it.", - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: - "Import source to find usages for (e.g., 'primeng/table', '@mycompany/ui/button', 'lodash')" - } - }, - required: ['name'] - } -}; - -export async function handle( - args: Record, - ctx: ToolContext -): Promise { - const { name: componentName } = args as { name: string }; - - try { - const intelligencePath = ctx.paths.intelligence; - const content = await fs.readFile(intelligencePath, 'utf-8'); - const intelligence = JSON.parse(content); - - const importGraph = intelligence.importGraph || {}; - const usages = importGraph.usages || {}; - - // Find matching usages (exact match or partial match) - let matchedUsage = usages[componentName]; - - // Try partial match if exact match not found - if (!matchedUsage) { - const matchingKeys = Object.keys(usages).filter( - (key) => key.includes(componentName) || componentName.includes(key) - ); - if (matchingKeys.length > 0) { - matchedUsage = usages[matchingKeys[0]]; - } - } - - if (matchedUsage) { - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - status: 'success', - component: componentName, - usageCount: matchedUsage.usageCount, - usedIn: matchedUsage.usedIn - }, - null, - 2 - ) - } - ] - }; - } else { - // Show top used as alternatives - const topUsed = importGraph.topUsed || []; - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - status: 'not_found', - component: componentName, - message: `No usages found for '${componentName}'.`, - suggestions: topUsed.slice(0, 10) - }, - null, - 2 - ) - } - ] - }; - } - } catch (error) { - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - status: 'error', - message: 'Failed to get component usage. Run indexing first.', - error: error instanceof Error ? error.message : String(error) - }, - null, - 2 - ) - } - ] - }; - } -} diff --git a/src/tools/search-codebase.ts b/src/tools/search-codebase.ts index 1fb5550..07d93c3 100644 --- a/src/tools/search-codebase.ts +++ b/src/tools/search-codebase.ts @@ -439,12 +439,18 @@ export async function handle( // Compose preflight card for edit/refactor/migrate intents let preflight: any = undefined; const preflightIntents = ['edit', 'refactor', 'migrate']; - if (intent && preflightIntents.includes(intent) && intelligence) { - try { - // --- Avoid / Prefer patterns --- - const avoidPatternsList: any[] = []; - const preferredPatternsList: any[] = []; - const patterns = intelligence.patterns || {}; + if (intent && preflightIntents.includes(intent)) { + if (!intelligence) { + preflight = { + ready: false, + nextAction: 'Run a full index rebuild to generate pattern intelligence before editing.' + }; + } else { + try { + // --- Avoid / Prefer patterns --- + const avoidPatternsList: any[] = []; + const preferredPatternsList: any[] = []; + const patterns = intelligence.patterns || {}; for (const [category, data] of Object.entries(patterns)) { // Primary pattern = preferred if Rising or Stable if (data.primary) { @@ -658,8 +664,9 @@ export async function handle( } preflight = decisionCard; - } catch { - // Preflight construction failed — skip preflight, don't fail the search + } catch { + // Preflight construction failed — skip preflight, don't fail the search + } } } diff --git a/tests/index-versioning-migration.test.ts b/tests/index-versioning-migration.test.ts index d570a13..89b1867 100644 --- a/tests/index-versioning-migration.test.ts +++ b/tests/index-versioning-migration.test.ts @@ -461,13 +461,11 @@ describe('index-consuming allowlist enforcement', () => { ? { query: 'test' } : toolName === 'get_symbol_references' ? { symbol: 'alpha' } - : toolName === 'get_component_usage' - ? { name: 'x' } - : toolName === 'detect_circular_dependencies' + : toolName === 'detect_circular_dependencies' + ? {} + : toolName === 'get_team_patterns' ? {} - : toolName === 'get_team_patterns' - ? {} - : {} + : {} } }); From 27440bb58b35e57dfa4e4ca496322f127ae4cf69 Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Sun, 22 Feb 2026 18:54:32 +0100 Subject: [PATCH 2/2] Fix formatting --- src/tools/search-codebase.ts | 380 +++++++++++++++++------------------ 1 file changed, 190 insertions(+), 190 deletions(-) diff --git a/src/tools/search-codebase.ts b/src/tools/search-codebase.ts index 07d93c3..6b1f66d 100644 --- a/src/tools/search-codebase.ts +++ b/src/tools/search-codebase.ts @@ -451,219 +451,219 @@ export async function handle( const avoidPatternsList: any[] = []; const preferredPatternsList: any[] = []; const patterns = intelligence.patterns || {}; - for (const [category, data] of Object.entries(patterns)) { - // Primary pattern = preferred if Rising or Stable - if (data.primary) { - const p = data.primary; - if (p.trend === 'Rising' || p.trend === 'Stable') { - preferredPatternsList.push({ - pattern: p.name, - category, - adoption: p.frequency, - trend: p.trend, - guidance: p.guidance, - ...(p.canonicalExample && { example: p.canonicalExample.file }) - }); - } - } - // Also-detected patterns that are Declining = avoid - if (data.alsoDetected) { - for (const alt of data.alsoDetected) { - if (alt.trend === 'Declining') { - avoidPatternsList.push({ - pattern: alt.name, + for (const [category, data] of Object.entries(patterns)) { + // Primary pattern = preferred if Rising or Stable + if (data.primary) { + const p = data.primary; + if (p.trend === 'Rising' || p.trend === 'Stable') { + preferredPatternsList.push({ + pattern: p.name, category, - adoption: alt.frequency, - trend: 'Declining', - guidance: alt.guidance + adoption: p.frequency, + trend: p.trend, + guidance: p.guidance, + ...(p.canonicalExample && { example: p.canonicalExample.file }) }); } } + // Also-detected patterns that are Declining = avoid + if (data.alsoDetected) { + for (const alt of data.alsoDetected) { + if (alt.trend === 'Declining') { + avoidPatternsList.push({ + pattern: alt.name, + category, + adoption: alt.frequency, + trend: 'Declining', + guidance: alt.guidance + }); + } + } + } } - } - // --- Impact candidates (files importing the result files) --- - const resultPaths = results.map((r) => r.filePath); - const impactCandidates = computeImpactCandidates(resultPaths); + // --- Impact candidates (files importing the result files) --- + const resultPaths = results.map((r) => r.filePath); + const impactCandidates = computeImpactCandidates(resultPaths); - // PREF-02: Compute impact coverage (callers of result files that appear in results) - const callerFiles = resultPaths.flatMap((p) => { - const importers: string[] = []; - for (const [dep, importerList] of reverseImports) { - if (dep.endsWith(p) || p.endsWith(dep)) { - importers.push(...importerList); + // PREF-02: Compute impact coverage (callers of result files that appear in results) + const callerFiles = resultPaths.flatMap((p) => { + const importers: string[] = []; + for (const [dep, importerList] of reverseImports) { + if (dep.endsWith(p) || p.endsWith(dep)) { + importers.push(...importerList); + } } - } - return importers; - }); - const uniqueCallers = new Set(callerFiles); - const callersCovered = Array.from(uniqueCallers).filter((f) => - resultPaths.some((rp) => f.endsWith(rp) || rp.endsWith(f)) - ).length; - const callersTotal = uniqueCallers.size; - const impactCoverage = - callersTotal > 0 ? { covered: callersCovered, total: callersTotal } : undefined; - - // --- Risk level (based on circular deps + impact breadth) --- - //TODO: Review this risk level calculation - let _riskLevel: 'low' | 'medium' | 'high' = 'low'; - let cycleCount = 0; - const graphDataSource = relationships?.graph || intelligence?.internalFileGraph; - if (graphDataSource) { - try { - const graph = InternalFileGraph.fromJSON(graphDataSource, ctx.rootPath); - // Use directory prefixes as scope (not full file paths) - // findCycles(scope) filters files by startsWith, so a full path would only match itself - const scopes = new Set( - resultPaths.map((rp) => { - const lastSlash = rp.lastIndexOf('/'); - return lastSlash > 0 ? rp.substring(0, lastSlash + 1) : rp; - }) - ); - for (const scope of scopes) { - const cycles = graph.findCycles(scope); - cycleCount += cycles.length; + return importers; + }); + const uniqueCallers = new Set(callerFiles); + const callersCovered = Array.from(uniqueCallers).filter((f) => + resultPaths.some((rp) => f.endsWith(rp) || rp.endsWith(f)) + ).length; + const callersTotal = uniqueCallers.size; + const impactCoverage = + callersTotal > 0 ? { covered: callersCovered, total: callersTotal } : undefined; + + // --- Risk level (based on circular deps + impact breadth) --- + //TODO: Review this risk level calculation + let _riskLevel: 'low' | 'medium' | 'high' = 'low'; + let cycleCount = 0; + const graphDataSource = relationships?.graph || intelligence?.internalFileGraph; + if (graphDataSource) { + try { + const graph = InternalFileGraph.fromJSON(graphDataSource, ctx.rootPath); + // Use directory prefixes as scope (not full file paths) + // findCycles(scope) filters files by startsWith, so a full path would only match itself + const scopes = new Set( + resultPaths.map((rp) => { + const lastSlash = rp.lastIndexOf('/'); + return lastSlash > 0 ? rp.substring(0, lastSlash + 1) : rp; + }) + ); + for (const scope of scopes) { + const cycles = graph.findCycles(scope); + cycleCount += cycles.length; + } + } catch { + // Graph reconstruction failed — skip cycle check } - } catch { - // Graph reconstruction failed — skip cycle check } - } - if (cycleCount > 0 || impactCandidates.length > 10) { - _riskLevel = 'high'; - } else if (impactCandidates.length > 3) { - _riskLevel = 'medium'; - } + if (cycleCount > 0 || impactCandidates.length > 10) { + _riskLevel = 'high'; + } else if (impactCandidates.length > 3) { + _riskLevel = 'medium'; + } - // --- Golden files (exemplar code) --- - const goldenFiles = (intelligence.goldenFiles || []).slice(0, 3).map((g: any) => ({ - file: g.file, - score: g.score - })); - - // --- Confidence (index freshness) --- - // TODO: Review this confidence calculation - //const confidence = computeIndexConfidence(); - - // --- Failure memories (1.5x relevance boost) --- - const failureWarnings = relatedMemories - .filter((m) => m.type === 'failure' && !m.stale) - .map((m) => ({ - memory: m.memory, - reason: m.reason, - confidence: m.effectiveConfidence - })) - .slice(0, 3); - - const preferredPatternsForOutput = preferredPatternsList.slice(0, 5); - const avoidPatternsForOutput = avoidPatternsList.slice(0, 5); - - // --- Pattern conflicts (split decisions within categories) --- - const patternConflicts: Array<{ - category: string; - 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; - if (!data.primary || !data.alsoDetected?.length) continue; - const primaryFreq = parseFloat(data.primary.frequency) || 100; - if (primaryFreq >= 80) continue; - for (const alt of data.alsoDetected) { - const altFreq = parseFloat(alt.frequency) || 0; - if (altFreq >= 20) { - if (isComplementaryPatternConflict(cat, data.primary.name, alt.name)) continue; - if (hasUnitTestFramework && cat === 'testingFramework') continue; - patternConflicts.push({ - category: cat, - primary: { name: data.primary.name, adoption: data.primary.frequency }, - alternative: { name: alt.name, adoption: alt.frequency } - }); + // --- Golden files (exemplar code) --- + const goldenFiles = (intelligence.goldenFiles || []).slice(0, 3).map((g: any) => ({ + file: g.file, + score: g.score + })); + + // --- Confidence (index freshness) --- + // TODO: Review this confidence calculation + //const confidence = computeIndexConfidence(); + + // --- Failure memories (1.5x relevance boost) --- + const failureWarnings = relatedMemories + .filter((m) => m.type === 'failure' && !m.stale) + .map((m) => ({ + memory: m.memory, + reason: m.reason, + confidence: m.effectiveConfidence + })) + .slice(0, 3); + + const preferredPatternsForOutput = preferredPatternsList.slice(0, 5); + const avoidPatternsForOutput = avoidPatternsList.slice(0, 5); + + // --- Pattern conflicts (split decisions within categories) --- + const patternConflicts: Array<{ + category: string; + 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; + if (!data.primary || !data.alsoDetected?.length) continue; + const primaryFreq = parseFloat(data.primary.frequency) || 100; + if (primaryFreq >= 80) continue; + for (const alt of data.alsoDetected) { + const altFreq = parseFloat(alt.frequency) || 0; + if (altFreq >= 20) { + if (isComplementaryPatternConflict(cat, data.primary.name, alt.name)) continue; + if (hasUnitTestFramework && cat === 'testingFramework') continue; + patternConflicts.push({ + category: cat, + primary: { name: data.primary.name, adoption: data.primary.frequency }, + alternative: { name: alt.name, adoption: alt.frequency } + }); + } } } - } - const evidenceLock = buildEvidenceLock({ - results, - preferredPatterns: preferredPatternsForOutput, - relatedMemories, - failureWarnings, - patternConflicts, - searchQualityStatus: searchQuality.status, - impactCoverage - }); - - // 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 - }; - - // Add nextAction if not ready - if (!decisionCard.ready && evidenceLock.nextAction) { - decisionCard.nextAction = evidenceLock.nextAction; - } - - // Add warnings from failure memories (capped at 3) - if (failureWarnings.length > 0) { - decisionCard.warnings = failureWarnings.slice(0, 3).map((w) => w.memory); - } + const evidenceLock = buildEvidenceLock({ + results, + preferredPatterns: preferredPatternsForOutput, + relatedMemories, + failureWarnings, + patternConflicts, + searchQualityStatus: searchQuality.status, + impactCoverage + }); + + // 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[]; + } - // Add patterns (do/avoid, capped at 3 each, with adoption %) - const doPatterns = preferredPatternsForOutput - .slice(0, 3) - .map((p) => `${p.pattern} — ${p.adoption ? ` ${p.adoption}% adoption` : ''}`); - const avoidPatterns = avoidPatternsForOutput - .slice(0, 3) - .map((p) => `${p.pattern} — ${p.adoption ? ` ${p.adoption}% adoption` : ''} (declining)`); - if (doPatterns.length > 0 || avoidPatterns.length > 0) { - decisionCard.patterns = { - ...(doPatterns.length > 0 && { do: doPatterns }), - ...(avoidPatterns.length > 0 && { avoid: avoidPatterns }) + const decisionCard: DecisionCard = { + ready: evidenceLock.readyToEdit }; - } - // Add bestExample (top 1 golden file) - if (goldenFiles.length > 0) { - decisionCard.bestExample = `${goldenFiles[0].file}`; - } + // Add nextAction if not ready + if (!decisionCard.ready && evidenceLock.nextAction) { + decisionCard.nextAction = evidenceLock.nextAction; + } - // Add impact (coverage + top 3 files) - if (impactCoverage || impactCandidates.length > 0) { - const impactObj: { coverage?: string; files?: string[] } = {}; - if (impactCoverage) { - impactObj.coverage = `${impactCoverage.covered}/${impactCoverage.total} callers in results`; + // Add warnings from failure memories (capped at 3) + if (failureWarnings.length > 0) { + decisionCard.warnings = failureWarnings.slice(0, 3).map((w) => w.memory); } - if (impactCandidates.length > 0) { - impactObj.files = impactCandidates.slice(0, 3); + + // Add patterns (do/avoid, capped at 3 each, with adoption %) + const doPatterns = preferredPatternsForOutput + .slice(0, 3) + .map((p) => `${p.pattern} — ${p.adoption ? ` ${p.adoption}% adoption` : ''}`); + const avoidPatterns = avoidPatternsForOutput + .slice(0, 3) + .map((p) => `${p.pattern} — ${p.adoption ? ` ${p.adoption}% adoption` : ''} (declining)`); + if (doPatterns.length > 0 || avoidPatterns.length > 0) { + decisionCard.patterns = { + ...(doPatterns.length > 0 && { do: doPatterns }), + ...(avoidPatterns.length > 0 && { avoid: avoidPatterns }) + }; } - if (Object.keys(impactObj).length > 0) { - decisionCard.impact = impactObj; + + // Add bestExample (top 1 golden file) + if (goldenFiles.length > 0) { + decisionCard.bestExample = `${goldenFiles[0].file}`; } - } - // Add whatWouldHelp from evidenceLock - if (evidenceLock.whatWouldHelp && evidenceLock.whatWouldHelp.length > 0) { - decisionCard.whatWouldHelp = evidenceLock.whatWouldHelp; - } + // Add impact (coverage + top 3 files) + if (impactCoverage || impactCandidates.length > 0) { + const impactObj: { coverage?: string; files?: string[] } = {}; + if (impactCoverage) { + impactObj.coverage = `${impactCoverage.covered}/${impactCoverage.total} callers in results`; + } + if (impactCandidates.length > 0) { + impactObj.files = impactCandidates.slice(0, 3); + } + if (Object.keys(impactObj).length > 0) { + decisionCard.impact = impactObj; + } + } + + // Add whatWouldHelp from evidenceLock + if (evidenceLock.whatWouldHelp && evidenceLock.whatWouldHelp.length > 0) { + decisionCard.whatWouldHelp = evidenceLock.whatWouldHelp; + } - preflight = decisionCard; + preflight = decisionCard; } catch { // Preflight construction failed — skip preflight, don't fail the search }