diff --git a/packages/angular-mcp-server/src/lib/angular-mcp-server.ts b/packages/angular-mcp-server/src/lib/angular-mcp-server.ts index d03b7b2..c75c19f 100644 --- a/packages/angular-mcp-server/src/lib/angular-mcp-server.ts +++ b/packages/angular-mcp-server/src/lib/angular-mcp-server.ts @@ -15,6 +15,7 @@ import { TOOLS } from './tools/tools.js'; import { toolNotFound } from './tools/utils.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { AngularMcpServerOptionsSchema, AngularMcpServerOptions, @@ -94,11 +95,11 @@ export class AngularMcpServerWrapper { // Try to read the llms.txt file from the package root (optional) try { - const filePath = path.resolve(__dirname, '../../llms.txt'); + const currentDir = path.dirname(fileURLToPath(import.meta.url)); + const filePath = path.resolve(currentDir, '../../llms.txt'); // Only attempt to read if file exists if (fs.existsSync(filePath)) { - console.log('Reading llms.txt from:', filePath); const content = fs.readFileSync(filePath, 'utf-8'); const lines = content.split('\n'); @@ -143,13 +144,9 @@ export class AngularMcpServerWrapper { }); } } - } else { - console.log('llms.txt not found at:', filePath, '(skipping)'); - } - } catch (ctx: unknown) { - if (ctx instanceof Error) { - console.error('Error reading llms.txt (non-fatal):', ctx.message); } + } catch { + // Silently ignore errors reading llms.txt (non-fatal) } // Scan available design system components to add them as discoverable resources @@ -183,13 +180,8 @@ export class AngularMcpServerWrapper { } } } - } catch (ctx: unknown) { - if (ctx instanceof Error) { - console.error( - 'Error scanning DS components (non-fatal):', - ctx.message, - ); - } + } catch { + // Silently ignore errors scanning DS components (non-fatal) } return { diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/index.ts index f68803d..72b3734 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/index.ts @@ -2,13 +2,12 @@ export { reportViolationsTools } from './report-violations.tool.js'; export { reportAllViolationsTools } from './report-all-violations.tool.js'; export type { - ReportViolationsOptions, - ViolationResult, - ViolationIssue, - ViolationAudit, - FileViolation, - FileViolationGroup, - FileViolationGroups, - FolderViolationSummary, - FolderViolationGroups, + ViolationEntry, + ComponentViolationReport, + AllViolationsEntry, + AllViolationsComponentReport, + AllViolationsReport, + ComponentViolationInFile, + FileViolationReport, + AllViolationsReportByFile, } from './models/types.js'; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/types.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/types.ts index f9673d4..cd32494 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/types.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/types.ts @@ -1,40 +1,45 @@ -import { - BaseViolationOptions, - BaseViolationResult, - BaseViolationIssue, - BaseViolationAudit, -} from '../../shared/violation-analysis/types.js'; - -export interface ReportViolationsOptions extends BaseViolationOptions { - groupBy?: 'file' | 'folder'; +// Types for report-violations (single component, no replacement field needed) +export interface ViolationEntry { + file: string; + lines: number[]; + violation: string; } -export type ViolationResult = BaseViolationResult; -export type ViolationIssue = BaseViolationIssue; -export type ViolationAudit = BaseViolationAudit; +export interface ComponentViolationReport { + component: string; + violations: ViolationEntry[]; +} -// File-specific types (when groupBy: 'file') -export interface FileViolation { - fileName: string; - message: string; +// Types for report-all-violations (multiple components, replacement field needed) +export interface AllViolationsEntry { + file: string; lines: number[]; + violation: string; + replacement: string; } -export interface FileViolationGroup { - message: string; - lines: number[]; +export interface AllViolationsComponentReport { + component: string; + violations: AllViolationsEntry[]; } -export interface FileViolationGroups { - [fileName: string]: FileViolationGroup; +export interface AllViolationsReport { + components: AllViolationsComponentReport[]; +} + +// File-grouped output types for report-all-violations +export interface ComponentViolationInFile { + component: string; + lines: number[]; + violation: string; + replacement: string; } -// Folder-specific types (when groupBy: 'folder') -export interface FolderViolationSummary { - violations: number; - files: string[]; +export interface FileViolationReport { + file: string; + components: ComponentViolationInFile[]; } -export interface FolderViolationGroups { - [folderPath: string]: FolderViolationSummary; +export interface AllViolationsReportByFile { + files: FileViolationReport[]; } diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts index b57a622..dfb6a4a 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts @@ -11,28 +11,36 @@ import { extractComponentName, } from '../shared/violation-analysis/coverage-analyzer.js'; import { - formatViolations, filterFailedAudits, groupIssuesByFile, } from '../shared/violation-analysis/formatters.js'; +import type { BaseViolationAudit } from '../shared/violation-analysis/types.js'; import { loadAndValidateDsComponentsFile } from '../../../validation/ds-components-file-loader.validation.js'; -import { RESULT_FORMATTERS } from '../shared/utils/handler-helpers.js'; +import { + AllViolationsReport, + AllViolationsComponentReport, + AllViolationsEntry, + AllViolationsReportByFile, + FileViolationReport, + ComponentViolationInFile, +} from './models/types.js'; interface ReportAllViolationsOptions extends BaseHandlerOptions { directory: string; - groupBy?: 'file' | 'folder'; + groupBy?: 'component' | 'file'; } export const reportAllViolationsSchema = { name: 'report-all-violations', description: - 'Scan a directory for deprecated design system CSS classes defined in the config at `deprecatedCssClassesPath`, and output a usage report', + 'Scan a directory for all deprecated design system CSS classes and output a comprehensive violation report. Use this to discover all violations across multiple components. Output can be grouped by component (default) or by file, and includes: file paths, line numbers, violation details, and replacement suggestions (which component should be used instead). This is ideal for getting an overview of all violations in a directory.', inputSchema: createProjectAnalysisSchema({ groupBy: { type: 'string', - enum: ['file', 'folder'], - description: 'How to group the results', - default: 'file', + enum: ['component', 'file'], + description: + 'How to group the results: "component" (default) groups by design system component showing all files affected by each component, "file" groups by file path showing all components violated in each file', + default: 'component', }, }), annotations: { @@ -41,9 +49,93 @@ export const reportAllViolationsSchema = { }, }; +/** + * Extracts deprecated class and replacement from violation message + * Performance optimized with caching to avoid repeated regex operations + */ +const messageParsingCache = new Map< + string, + { violation: string; replacement: string } +>(); + +function parseViolationMessage(message: string): { + violation: string; + replacement: string; +} { + // Check cache first + const cached = messageParsingCache.get(message); + if (cached) { + return cached; + } + + // Clean up HTML tags + const cleanMessage = message + .replace(//g, '`') + .replace(/<\/code>/g, '`'); + + // Extract deprecated class - look for patterns like "class `offer-badge`" or "class `btn, btn-primary`" + const classMatch = cleanMessage.match(/class `([^`]+)`/); + const violation = classMatch ? classMatch[1] : 'unknown'; + + // Extract replacement component - look for "Use `ComponentName`" + const replacementMatch = cleanMessage.match(/Use `([^`]+)`/); + const replacement = replacementMatch ? replacementMatch[1] : 'unknown'; + + const result = { violation, replacement }; + messageParsingCache.set(message, result); + return result; +} + +/** + * Processed violation data structure used internally for both grouping modes + */ +interface ProcessedViolation { + component: string; + fileName: string; + lines: number[]; + violation: string; + replacement: string; +} + +/** + * Processes all failed audits into a unified structure + * This eliminates code duplication between component and file grouping modes + */ +function processAudits( + failedAudits: BaseViolationAudit[], + directory: string, +): ProcessedViolation[] { + const processed: ProcessedViolation[] = []; + + for (const audit of failedAudits) { + const componentName = extractComponentName(audit.title); + const fileGroups = groupIssuesByFile( + audit.details?.issues ?? [], + directory, + ); + + for (const [fileName, { lines: fileLines, message }] of Object.entries( + fileGroups, + )) { + // Lines are already sorted by groupIssuesByFile, so we can use them directly + const { violation, replacement } = parseViolationMessage(message); + + processed.push({ + component: componentName, + fileName, + lines: fileLines, // Already sorted + violation, + replacement, + }); + } + } + + return processed; +} + export const reportAllViolationsHandler = createHandler< ReportAllViolationsOptions, - string[] + AllViolationsReport | AllViolationsReportByFile >( reportAllViolationsSchema.name, async (params, { cwd, deprecatedCssClassesPath }) => { @@ -52,7 +144,7 @@ export const reportAllViolationsHandler = createHandler< 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', ); } - const groupBy = params.groupBy || 'file'; + const dsComponents = await loadAndValidateDsComponentsFile( cwd, deprecatedCssClassesPath || '', @@ -66,45 +158,96 @@ export const reportAllViolationsHandler = createHandler< }); const raw = coverageResult.rawData?.rawPluginResult; - if (!raw) return []; - - const failedAudits = filterFailedAudits(raw); - if (failedAudits.length === 0) return ['No violations found.']; - - if (groupBy === 'file') { - const lines: string[] = []; - for (const audit of failedAudits) { - extractComponentName(audit.title); - const fileGroups = groupIssuesByFile( - audit.details?.issues ?? [], - params.directory, - ); - for (const [fileName, { lines: fileLines, message }] of Object.entries( - fileGroups, - )) { - const sorted = - fileLines.length > 1 - ? [...fileLines].sort((a, b) => a - b) - : fileLines; - const lineInfo = - sorted.length > 1 - ? `lines ${sorted.join(', ')}` - : `line ${sorted[0]}`; - lines.push(`${fileName} (${lineInfo}): ${message}`); + const failedAudits = raw ? filterFailedAudits(raw) : []; + + // Early exit for empty results + if (failedAudits.length === 0) { + return params.groupBy === 'file' ? { files: [] } : { components: [] }; + } + + // Process all audits into unified structure (eliminates code duplication) + const processed = processAudits(failedAudits, params.directory); + + // Group by component (default behavior) + if (params.groupBy !== 'file') { + const componentMap = new Map(); + + for (const item of processed) { + if (!componentMap.has(item.component)) { + componentMap.set(item.component, []); + } + + const violations = componentMap.get(item.component); + if (violations) { + violations.push({ + file: item.fileName, + lines: item.lines, + violation: item.violation, + replacement: item.replacement, + }); } } - return lines; + + const components: AllViolationsComponentReport[] = Array.from( + componentMap.entries(), + ([component, violations]) => ({ component, violations }), + ); + + return { components }; } - const formattedContent = formatViolations(raw, params.directory, { - groupBy: 'folder', - }); - return formattedContent.map( - (item: { type?: string; text?: string } | string) => - typeof item === 'string' ? item : (item?.text ?? String(item)), - ); + // Group by file + const fileMap = new Map(); + + for (const item of processed) { + if (!fileMap.has(item.fileName)) { + fileMap.set(item.fileName, []); + } + + const components = fileMap.get(item.fileName); + if (components) { + components.push({ + component: item.component, + lines: item.lines, + violation: item.violation, + replacement: item.replacement, + }); + } + } + + // Optimized conversion: build array directly and sort once + const files: FileViolationReport[] = Array.from( + fileMap.entries(), + ([file, components]) => ({ file, components }), + ).sort((a, b) => a.file.localeCompare(b.file)); + + return { files }; + }, + (result) => { + const isFileGrouping = 'files' in result; + const isEmpty = isFileGrouping + ? result.files.length === 0 + : result.components.length === 0; + + if (isEmpty) { + return ['No violations found in the specified directory.']; + } + + const groupingType = isFileGrouping ? 'file' : 'component'; + const message = [ + `Found violations grouped by ${groupingType}.`, + 'Use this output to identify:', + ' - Which files contain violations', + ' - The specific line numbers where violations occur', + ' - What is being used that violates the rules (violation field)', + ' - What should be used instead (replacement field)', + '', + 'Violation Report:', + JSON.stringify(result, null, 2), + ]; + + return message; }, - (result) => RESULT_FORMATTERS.list(result, 'Design System Violations:'), ); export const reportAllViolationsTools = [ diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts index 0388ded..1a6fdf6 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts @@ -1,15 +1,18 @@ import { createHandler, BaseHandlerOptions, - RESULT_FORMATTERS, } from '../shared/utils/handler-helpers.js'; import { createViolationReportingSchema, COMMON_ANNOTATIONS, } from '../shared/models/schema-helpers.js'; import { analyzeViolationsBase } from '../shared/violation-analysis/base-analyzer.js'; -import { formatViolations } from '../shared/violation-analysis/formatters.js'; -import { ViolationResult } from './models/types.js'; +import { + groupIssuesByFile, + filterFailedAudits, +} from '../shared/violation-analysis/formatters.js'; +import { BaseViolationResult } from '../shared/violation-analysis/types.js'; +import { ComponentViolationReport, ViolationEntry } from './models/types.js'; interface ReportViolationsOptions extends BaseHandlerOptions { directory: string; @@ -19,7 +22,7 @@ interface ReportViolationsOptions extends BaseHandlerOptions { export const reportViolationsSchema = { name: 'report-violations', - description: `Report deprecated DS CSS usage in a directory with configurable grouping format.`, + description: `Report deprecated CSS usage for a specific design system component in a directory. Returns violations grouped by file, showing which deprecated classes are used and where. Use this when you know which component you're checking for. Output includes: file paths, line numbers, and violation details (but not replacement suggestions since the component is already known).`, inputSchema: createViolationReportingSchema(), annotations: { title: 'Report Violations', @@ -27,32 +30,80 @@ export const reportViolationsSchema = { }, }; +/** + * Extracts deprecated class from violation message + */ +function parseViolationMessage(message: string): string { + // Clean up HTML tags + const cleanMessage = message + .replace(//g, '`') + .replace(/<\/code>/g, '`'); + + // Extract deprecated class - look for patterns like "class `offer-badge`" or "class `btn, btn-primary`" + const classMatch = cleanMessage.match(/class `([^`]+)`/); + return classMatch ? classMatch[1] : 'unknown'; +} + export const reportViolationsHandler = createHandler< ReportViolationsOptions, - string[] + ComponentViolationReport >( reportViolationsSchema.name, async (params) => { - // Default to 'file' grouping if not specified - const groupBy = params.groupBy || 'file'; + const result = await analyzeViolationsBase(params); + const failedAudits = filterFailedAudits(result); + + if (failedAudits.length === 0) { + return { + component: params.componentName, + violations: [], + }; + } - const result = await analyzeViolationsBase(params); + const violations: ViolationEntry[] = []; - const formattedContent = formatViolations(result, params.directory, { - groupBy, - }); + // Process all issues from all audits + for (const audit of failedAudits) { + const fileGroups = groupIssuesByFile( + audit.details?.issues ?? [], + params.directory, + ); - // Extract text content from the formatted violations - const violationLines = formattedContent.map((item) => { - if (item.type === 'text') { - return item.text; + for (const [fileName, { lines, message }] of Object.entries(fileGroups)) { + const violation = parseViolationMessage(message); + + violations.push({ + file: fileName, + lines: lines.sort((a, b) => a - b), + violation, + }); } - return String(item); - }); + } + + return { + component: params.componentName, + violations, + }; + }, + (result) => { + if (result.violations.length === 0) { + return [`No violations found for component: ${result.component}`]; + } + + const message = [ + `Found violations for component: ${result.component}`, + 'Use this output to identify:', + ' - Which files contain violations', + ' - The specific line numbers where violations occur', + ' - What is being used that violates the rules (violation field)', + '', + 'Violation Report:', + '', + JSON.stringify(result, null, 2), + ]; - return violationLines; + return [message.join('\n')]; }, - (result) => RESULT_FORMATTERS.list(result, 'Design System Violations:'), ); export const reportViolationsTools = [ diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts index 7792dfb..774b4e6 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts @@ -8,18 +8,20 @@ export const COMMON_SCHEMA_PROPERTIES = { directory: { type: 'string' as const, description: - 'The relative path to the directory (starting with "./path/to/dir") to scan. Respect the OS specifics.', + 'The relative path to the directory (starting with "./path/to/dir") to scan. Respect the OS specifics. Should point to the directory containing the files you want to analyze for violations.', }, componentName: { type: 'string' as const, - description: 'The class name of the component (e.g., DsButton)', + description: + 'The class name of the design system component to check for violations (e.g., DsButton, DsBadge, DsCard). This should be the TypeScript class name, not the selector.', }, groupBy: { type: 'string' as const, enum: ['file', 'folder'] as const, - description: 'How to group the results', + description: + 'How to group the violation results in the output. "file" groups violations by individual file paths, "folder" groups by directory structure.', default: 'file' as const, }, } as const; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/handler-helpers.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/handler-helpers.ts index a446a17..930a617 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/handler-helpers.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/handler-helpers.ts @@ -125,4 +125,9 @@ export const RESULT_FORMATTERS = { * Formats empty results */ empty: (entityType: string): string[] => [`No ${entityType} found`], + + /** + * Formats result as JSON + */ + json: (result: any): string[] => [JSON.stringify(result, null, 2)], } as const; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts index fe87133..0aa9bd2 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts @@ -5,49 +5,9 @@ import { ReportCoverageParams, BaseViolationResult, FormattedCoverageResult, - BaseViolationAudit, } from './types.js'; -import { groupIssuesByFile } from './formatters.js'; import { resolveCrossPlatformPath } from '../utils/cross-platform-path.js'; -/** - * Validates the input parameters for the report coverage tool - */ -export function validateReportInput(params: ReportCoverageParams): void { - if (!params.directory || typeof params.directory !== 'string') { - throw new Error('Directory parameter is required and must be a string'); - } - - try { - validateDsComponentsArray(params.dsComponents); - } catch (ctx) { - throw new Error( - `Invalid dsComponents parameter: ${(ctx as Error).message}`, - ); - } -} - -/** - * Executes the coverage plugin and returns the result - */ -export async function executeCoveragePlugin( - params: ReportCoverageParams, -): Promise { - const pluginConfig = await dsComponentCoveragePlugin({ - dsComponents: params.dsComponents, - directory: resolveCrossPlatformPath( - params.cwd || process.cwd(), - params.directory, - ), - }); - - const { executePlugin } = await import('@code-pushup/core'); - return (await executePlugin(pluginConfig as any, { - cache: { read: false, write: false }, - persist: { outputDir: '' }, - })) as BaseViolationResult; -} - /** * Extracts component name from audit title - performance optimized with caching */ @@ -66,77 +26,45 @@ export function extractComponentName(title: string): string { } /** - * Formats the coverage result as text output - performance optimized + * Main implementation function for reporting project coverage */ -export function formatCoverageResult( - result: BaseViolationResult, +export async function analyzeProjectCoverage( params: ReportCoverageParams, -): string { - // Pre-filter failed audits to avoid repeated filtering - const failedAudits = result.audits.filter( - ({ score }: BaseViolationAudit) => score < 1, - ); - - if (failedAudits.length === 0) { - return ''; +): Promise { + // Validate input parameters + if (!params.directory || typeof params.directory !== 'string') { + throw new Error('Directory parameter is required and must be a string'); } - const output: string[] = []; - output.push(''); // Pre-allocate with expected size for better performance - - for (const { details, title } of failedAudits) { - const componentName = extractComponentName(title); - - output.push(`- design system component: ${componentName}`); - output.push(`- base directory: ${params.directory}`); - output.push(''); - - // Use shared, optimized groupIssuesByFile function - const fileGroups = groupIssuesByFile( - details?.issues ?? [], - params.directory, + try { + validateDsComponentsArray(params.dsComponents); + } catch (ctx) { + throw new Error( + `Invalid dsComponents parameter: ${(ctx as Error).message}`, ); - - // Add first message - const firstFile = Object.keys(fileGroups)[0]; - if (firstFile && fileGroups[firstFile]) { - output.push(fileGroups[firstFile].message); - output.push(''); - } - - // Add files and lines - optimize sorting - for (const [fileName, { lines }] of Object.entries(fileGroups)) { - output.push(`- ${fileName}`); - // Sort once and cache the result - const sortedLines = - lines.length > 1 ? lines.sort((a, b) => a - b) : lines; - output.push(` - lines: ${sortedLines.join(',')}`); - } - - output.push(''); } - return output.join('\n'); -} - -/** - * Main implementation function for reporting project coverage - */ -export async function analyzeProjectCoverage( - params: ReportCoverageParams, -): Promise { - validateReportInput(params); - if (params.cwd) { process.chdir(params.cwd); } - const result = await executeCoveragePlugin(params); + // Execute the coverage plugin + const pluginConfig = await dsComponentCoveragePlugin({ + dsComponents: params.dsComponents, + directory: resolveCrossPlatformPath( + params.cwd || process.cwd(), + params.directory, + ), + }); - const textOutput = formatCoverageResult(result, params); + const { executePlugin } = await import('@code-pushup/core'); + const result = (await executePlugin(pluginConfig as any, { + cache: { read: false, write: false }, + persist: { outputDir: '' }, + })) as BaseViolationResult; const formattedResult: FormattedCoverageResult = { - textOutput, + textOutput: '', // No longer used, kept for backwards compatibility }; if (params.returnRawData) { diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/formatters.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/formatters.ts index 9c4a2f6..53394c5 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/formatters.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/formatters.ts @@ -1,5 +1,3 @@ -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { buildText } from '../utils/output.utils.js'; import { BaseViolationResult, BaseViolationAudit, @@ -20,33 +18,6 @@ export function filterFailedAudits( return result.audits.filter(({ score }) => score < 1); } -/** - * Creates standard "no violations found" content - */ -export function createNoViolationsContent(): CallToolResult['content'] { - return [ - buildText( - '✅ No violations found! All files are compliant with the design system.', - ), - ]; -} - -/** - * Extracts all issues from failed audits - */ -export function extractIssuesFromAudits( - audits: BaseViolationAudit[], -): BaseViolationIssue[] { - return audits.flatMap(({ details }) => details?.issues ?? []); -} - -/** - * Checks if a violation result has any failures - */ -export function hasViolations(result: BaseViolationResult): boolean { - return filterFailedAudits(result).length > 0; -} - /** * Performance-optimized file path normalization with caching */ @@ -96,28 +67,10 @@ export function normalizeFilePath(filePath: string, directory: string): string { return normalized; } -/** - * Performance-optimized message normalization with caching - */ -export function normalizeMessage(message: string, directory: string): string { - const cacheKey = `msg::${message}::${directory}`; - - if (pathCache[cacheKey]) { - return pathCache[cacheKey]; - } - - const directoryPrefix = directory.endsWith('/') ? directory : directory + '/'; - const normalized = message.includes(directoryPrefix) - ? message.replace(directoryPrefix, '') - : message; - - pathCache[cacheKey] = normalized; - return normalized; -} - /** * Groups violation issues by file name - consolidated from multiple modules * Performance optimized with Set for duplicate checking and cached normalizations + * Lines are sorted inline for efficiency */ export function groupIssuesByFile( issues: BaseViolationIssue[], @@ -133,8 +86,16 @@ export function groupIssuesByFile( const lineNumber = source.position?.startLine || 0; if (!fileGroups[fileName]) { + // Normalize message inline (remove directory prefix) + const directoryPrefix = directory.endsWith('/') + ? directory + : directory + '/'; + const normalizedMessage = message.includes(directoryPrefix) + ? message.replace(directoryPrefix, '') + : message; + fileGroups[fileName] = { - message: normalizeMessage(message, directory), + message: normalizedMessage, lines: [], }; processedFiles.add(fileName); @@ -143,101 +104,12 @@ export function groupIssuesByFile( fileGroups[fileName].lines.push(lineNumber); } - return fileGroups; -} - -/** - * Extracts unique file paths from violation issues - performance optimized - */ -export function extractUniqueFilePaths( - issues: BaseViolationIssue[], - directory: string, -): string[] { - const filePathSet = new Set(); // Eliminate O(n) includes() calls - - for (const { source } of issues) { - if (source?.file) { - filePathSet.add(normalizeFilePath(source.file, directory)); - } - } - - return Array.from(filePathSet); -} - -/** - * Clears the path cache - useful for testing or memory management - */ -export function clearPathCache(): void { - Object.keys(pathCache).forEach((key) => delete pathCache[key]); -} - -/** - * Unified formatter for violations - supports both file and folder grouping with minimal output - */ -export function formatViolations( - result: BaseViolationResult, - directory: string, - options: { - groupBy: 'file' | 'folder'; - } = { groupBy: 'file' }, -): CallToolResult['content'] { - const failedAudits = filterFailedAudits(result); - - if (failedAudits.length === 0) { - return [buildText('No violations found.')]; - } - - const allIssues = extractIssuesFromAudits(failedAudits); - const content: CallToolResult['content'] = []; - - if (options.groupBy === 'file') { - // Group by individual files - minimal format - const fileGroups = groupIssuesByFile(allIssues, directory); - - for (const [fileName, { message, lines }] of Object.entries(fileGroups)) { - const sortedLines = lines.sort((a, b) => a - b); - const lineInfo = - sortedLines.length > 1 - ? `lines ${sortedLines.join(', ')}` - : `line ${sortedLines[0]}`; - - content.push(buildText(`${fileName} (${lineInfo}): ${message}`)); - } - } else { - // Group by folders - minimal format - const folderGroups: Record< - string, - { violations: number; files: Set } - > = {}; - - for (const { source } of allIssues) { - if (!source?.file) continue; - - const normalizedPath = normalizeFilePath(source.file, directory); - const folderPath = normalizedPath.includes('/') - ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) - : '.'; - - if (!folderGroups[folderPath]) { - folderGroups[folderPath] = { violations: 0, files: new Set() }; - } - - folderGroups[folderPath].violations++; - folderGroups[folderPath].files.add(normalizedPath); - } - - // Sort folders for consistent output - for (const [folder, { violations, files }] of Object.entries( - folderGroups, - ).sort()) { - const displayPath = folder === '.' ? directory : `${directory}/${folder}`; - content.push( - buildText( - `${displayPath}: ${violations} violations in ${files.size} files`, - ), - ); + // Sort lines inline for each file (single sort operation per file) + for (const fileGroup of Object.values(fileGroups)) { + if (fileGroup.lines.length > 1) { + fileGroup.lines.sort((a, b) => a - b); } } - return content; + return fileGroups; } diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/index.ts index 15d5661..9f9793d 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/index.ts @@ -2,7 +2,7 @@ * Shared violation analysis utilities * * This module provides shared functionality for analyzing design system violations - * across different reporting formats (file and folder-based). + * across different reporting formats. */ // Core analysis @@ -11,24 +11,14 @@ export { analyzeViolationsBase } from './base-analyzer.js'; // Coverage analysis export { analyzeProjectCoverage, - validateReportInput, - executeCoveragePlugin, extractComponentName, - formatCoverageResult, } from './coverage-analyzer.js'; // Formatting utilities export { filterFailedAudits, - createNoViolationsContent, - extractIssuesFromAudits, - hasViolations, normalizeFilePath, - normalizeMessage, groupIssuesByFile, - extractUniqueFilePaths, - clearPathCache, - formatViolations, } from './formatters.js'; // Types