From daef72270547e6314335c2fac852b7d7d9fc170a Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 17 Nov 2025 16:21:28 +0200 Subject: [PATCH 1/5] feat(angular-mcp-server): migrate violations reponse to json --- .../src/lib/angular-mcp-server.ts | 22 +-- .../lib/tools/ds/report-violations/index.ts | 15 +- .../ds/report-violations/models/types.ts | 51 +++--- .../report-all-violations.tool.ts | 135 ++++++++++++---- .../report-violations.tool.ts | 70 ++++++-- .../tools/ds/shared/utils/handler-helpers.ts | 5 + .../violation-analysis/coverage-analyzer.ts | 122 +++----------- .../shared/violation-analysis/formatters.ts | 151 +----------------- .../ds/shared/violation-analysis/index.ts | 12 +- 9 files changed, 234 insertions(+), 349 deletions(-) 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..16846b6 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,10 @@ 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, + 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..1cad5a2 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,33 @@ -import { - BaseViolationOptions, - BaseViolationResult, - BaseViolationIssue, - BaseViolationAudit, -} from '../../shared/violation-analysis/types.js'; - -export interface ReportViolationsOptions extends BaseViolationOptions { - groupBy?: 'file' | 'folder'; +// JSON output types +export interface ViolationEntry { + file: string; + lines: number[]; + violation: string; + replacement: string; } -export type ViolationResult = BaseViolationResult; -export type ViolationIssue = BaseViolationIssue; -export type ViolationAudit = BaseViolationAudit; - -// File-specific types (when groupBy: 'file') -export interface FileViolation { - fileName: string; - message: string; - lines: number[]; +export interface ComponentViolationReport { + component: string; + violations: ViolationEntry[]; } -export interface FileViolationGroup { - message: string; - lines: number[]; +export interface AllViolationsReport { + components: ComponentViolationReport[]; } -export interface FileViolationGroups { - [fileName: string]: FileViolationGroup; +// File-grouped output types +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..27efe8a 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,16 +11,22 @@ import { extractComponentName, } from '../shared/violation-analysis/coverage-analyzer.js'; import { - formatViolations, filterFailedAudits, groupIssuesByFile, } from '../shared/violation-analysis/formatters.js'; import { loadAndValidateDsComponentsFile } from '../../../validation/ds-components-file-loader.validation.js'; -import { RESULT_FORMATTERS } from '../shared/utils/handler-helpers.js'; +import { + AllViolationsReport, + ComponentViolationReport, + ViolationEntry, + AllViolationsReportByFile, + FileViolationReport, + ComponentViolationInFile +} from './models/types.js'; interface ReportAllViolationsOptions extends BaseHandlerOptions { directory: string; - groupBy?: 'file' | 'folder'; + groupBy?: 'component' | 'file'; } export const reportAllViolationsSchema = { @@ -30,9 +36,9 @@ export const reportAllViolationsSchema = { 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" groups by design system component, "file" groups by file path', + default: 'component', }, }), annotations: { @@ -41,9 +47,27 @@ export const reportAllViolationsSchema = { }, }; +/** + * Extracts deprecated class and replacement from violation message + */ +function parseViolationMessage(message: string): { violation: string; replacement: 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 `([^`]+)`/); + const violation = classMatch ? classMatch[1] : 'unknown'; + + // Extract replacement component - look for "Use `ComponentName`" + const replacementMatch = cleanMessage.match(/Use `([^`]+)`/); + const replacement = replacementMatch ? replacementMatch[1] : 'unknown'; + + return { violation, replacement }; +} + export const reportAllViolationsHandler = createHandler< ReportAllViolationsOptions, - string[] + AllViolationsReport | AllViolationsReportByFile >( reportAllViolationsSchema.name, async (params, { cwd, deprecatedCssClassesPath }) => { @@ -52,7 +76,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,19 +90,28 @@ export const reportAllViolationsHandler = createHandler< }); const raw = coverageResult.rawData?.rawPluginResult; - if (!raw) return []; + if (!raw) { + return params.groupBy === 'file' ? { files: [] } : { components: [] }; + } const failedAudits = filterFailedAudits(raw); - if (failedAudits.length === 0) return ['No violations found.']; + if (failedAudits.length === 0) { + return params.groupBy === 'file' ? { files: [] } : { components: [] }; + } + + // Group by component (default behavior) + if (params.groupBy !== 'file') { + const components: ComponentViolationReport[] = []; - if (groupBy === 'file') { - const lines: string[] = []; for (const audit of failedAudits) { - extractComponentName(audit.title); + const componentName = extractComponentName(audit.title); + const violations: ViolationEntry[] = []; + const fileGroups = groupIssuesByFile( audit.details?.issues ?? [], params.directory, ); + for (const [fileName, { lines: fileLines, message }] of Object.entries( fileGroups, )) { @@ -86,25 +119,73 @@ export const reportAllViolationsHandler = createHandler< 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 { violation, replacement } = parseViolationMessage(message); + + violations.push({ + file: fileName, + lines: sorted, + violation, + replacement, + }); } + + components.push({ + component: componentName, + violations, + }); } - return lines; + + 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 audit of failedAudits) { + const componentName = 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 { violation, replacement } = parseViolationMessage(message); + + if (!fileMap.has(fileName)) { + fileMap.set(fileName, []); + } + + const fileComponents = fileMap.get(fileName); + if (fileComponents) { + fileComponents.push({ + component: componentName, + lines: sorted, + violation, + replacement, + }); + } + } + } + + const files: FileViolationReport[] = Array.from(fileMap.entries()) + .map(([file, components]) => ({ file, components })) + .sort((a, b) => a.file.localeCompare(b.file)); + + return { files }; + }, + (result) => { + // Return structured JSON for token efficiency + return [JSON.stringify(result, null, 2)]; }, - (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..78be901 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 @@ -8,8 +8,9 @@ import { 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; @@ -27,32 +28,67 @@ export const reportViolationsSchema = { }, }; +/** + * Extracts deprecated class and replacement from violation message + */ +function parseViolationMessage(message: string): { violation: string; replacement: 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 `([^`]+)`/); + const violation = classMatch ? classMatch[1] : 'unknown'; + + // Extract replacement component - look for "Use `ComponentName`" + const replacementMatch = cleanMessage.match(/Use `([^`]+)`/); + const replacement = replacementMatch ? replacementMatch[1] : 'unknown'; + + return { violation, replacement }; +} + 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, replacement } = parseViolationMessage(message); + + violations.push({ + file: fileName, + lines: lines.sort((a, b) => a - b), + violation, + replacement, + }); } - return String(item); - }); + } - return violationLines; + return { + component: params.componentName, + violations, + }; }, - (result) => RESULT_FORMATTERS.list(result, 'Design System Violations:'), + (result) => RESULT_FORMATTERS.json(result), ); export const reportViolationsTools = [ 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..858931c 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,25 +67,6 @@ 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 @@ -133,8 +85,14 @@ 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); @@ -146,98 +104,3 @@ export function groupIssuesByFile( 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`, - ), - ); - } - } - - return content; -} 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 From 18a65d78c5f100adb05e338868aa59da0d59c8f6 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 17 Nov 2025 16:39:19 +0200 Subject: [PATCH 2/5] feat(angular-mcp-server): add guidance message --- .../report-all-violations.tool.ts | 189 +++++++++++------- .../shared/violation-analysis/formatters.ts | 8 + 2 files changed, 129 insertions(+), 68 deletions(-) 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 27efe8a..f4f27e4 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 @@ -14,6 +14,7 @@ import { 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 { AllViolationsReport, @@ -49,8 +50,17 @@ export const reportAllViolationsSchema = { /** * Extracts deprecated class and replacement from violation message + * Performance optimized with caching to avoid repeated regex operations */ +const messageParsingCache = new Map(); + 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, '`'); @@ -62,7 +72,56 @@ function parseViolationMessage(message: string): { violation: string; replacemen const replacementMatch = cleanMessage.match(/Use `([^`]+)`/); const replacement = replacementMatch ? replacementMatch[1] : 'unknown'; - return { violation, replacement }; + 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< @@ -90,101 +149,95 @@ export const reportAllViolationsHandler = createHandler< }); const raw = coverageResult.rawData?.rawPluginResult; - if (!raw) { - return params.groupBy === 'file' ? { files: [] } : { components: [] }; - } + const failedAudits = raw ? filterFailedAudits(raw) : []; - const failedAudits = 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 components: ComponentViolationReport[] = []; - - for (const audit of failedAudits) { - const componentName = extractComponentName(audit.title); - const violations: ViolationEntry[] = []; - - 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 componentMap = new Map(); - const { violation, replacement } = parseViolationMessage(message); + 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: fileName, - lines: sorted, - violation, - replacement, + file: item.fileName, + lines: item.lines, + violation: item.violation, + replacement: item.replacement, }); } - - components.push({ - component: componentName, - violations, - }); } + const components: ComponentViolationReport[] = Array.from( + componentMap.entries(), + ([component, violations]) => ({ component, violations }), + ); + return { components }; } // Group by file const fileMap = new Map(); - for (const audit of failedAudits) { - const componentName = 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 { violation, replacement } = parseViolationMessage(message); - - if (!fileMap.has(fileName)) { - fileMap.set(fileName, []); - } + for (const item of processed) { + if (!fileMap.has(item.fileName)) { + fileMap.set(item.fileName, []); + } - const fileComponents = fileMap.get(fileName); - if (fileComponents) { - fileComponents.push({ - component: componentName, - lines: sorted, - violation, - replacement, - }); - } + const components = fileMap.get(item.fileName); + if (components) { + components.push({ + component: item.component, + lines: item.lines, + violation: item.violation, + replacement: item.replacement, + }); } } - const files: FileViolationReport[] = Array.from(fileMap.entries()) - .map(([file, components]) => ({ file, components })) - .sort((a, b) => a.file.localeCompare(b.file)); + // 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) => { - // Return structured JSON for token efficiency - return [JSON.stringify(result, null, 2)]; + 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; }, ); 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 858931c..7908585 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 @@ -70,6 +70,7 @@ export function normalizeFilePath(filePath: string, directory: string): string { /** * 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[], @@ -101,6 +102,13 @@ export function groupIssuesByFile( fileGroups[fileName].lines.push(lineNumber); } + // 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 fileGroups; } From 8b22cfb9e86804759ef6dc91f95c1ae57633bd00 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 17 Nov 2025 16:47:53 +0200 Subject: [PATCH 3/5] feat(angular-mcp-server): separate reporter for single violation --- .../lib/tools/ds/report-violations/index.ts | 2 ++ .../ds/report-violations/models/types.ts | 20 ++++++++--- .../report-all-violations.tool.ts | 10 +++--- .../report-violations.tool.ts | 36 ++++++++++++------- 4 files changed, 46 insertions(+), 22 deletions(-) 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 16846b6..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 @@ -4,6 +4,8 @@ export { reportAllViolationsTools } from './report-all-violations.tool.js'; export type { ViolationEntry, ComponentViolationReport, + AllViolationsEntry, + AllViolationsComponentReport, AllViolationsReport, ComponentViolationInFile, FileViolationReport, 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 1cad5a2..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,9 +1,8 @@ -// JSON output types +// Types for report-violations (single component, no replacement field needed) export interface ViolationEntry { file: string; lines: number[]; violation: string; - replacement: string; } export interface ComponentViolationReport { @@ -11,11 +10,24 @@ export interface ComponentViolationReport { violations: ViolationEntry[]; } +// Types for report-all-violations (multiple components, replacement field needed) +export interface AllViolationsEntry { + file: string; + lines: number[]; + violation: string; + replacement: string; +} + +export interface AllViolationsComponentReport { + component: string; + violations: AllViolationsEntry[]; +} + export interface AllViolationsReport { - components: ComponentViolationReport[]; + components: AllViolationsComponentReport[]; } -// File-grouped output types +// File-grouped output types for report-all-violations export interface ComponentViolationInFile { component: string; lines: number[]; 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 f4f27e4..afae64c 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 @@ -17,9 +17,9 @@ import { import type { BaseViolationAudit } from '../shared/violation-analysis/types.js'; import { loadAndValidateDsComponentsFile } from '../../../validation/ds-components-file-loader.validation.js'; import { - AllViolationsReport, - ComponentViolationReport, - ViolationEntry, + AllViolationsReport, + AllViolationsComponentReport, + AllViolationsEntry, AllViolationsReportByFile, FileViolationReport, ComponentViolationInFile @@ -161,7 +161,7 @@ export const reportAllViolationsHandler = createHandler< // Group by component (default behavior) if (params.groupBy !== 'file') { - const componentMap = new Map(); + const componentMap = new Map(); for (const item of processed) { if (!componentMap.has(item.component)) { @@ -179,7 +179,7 @@ export const reportAllViolationsHandler = createHandler< } } - const components: ComponentViolationReport[] = Array.from( + const components: AllViolationsComponentReport[] = Array.from( componentMap.entries(), ([component, violations]) => ({ component, violations }), ); 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 78be901..7f6b200 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,7 +1,6 @@ import { createHandler, BaseHandlerOptions, - RESULT_FORMATTERS, } from '../shared/utils/handler-helpers.js'; import { createViolationReportingSchema, @@ -29,21 +28,15 @@ export const reportViolationsSchema = { }; /** - * Extracts deprecated class and replacement from violation message + * Extracts deprecated class from violation message */ -function parseViolationMessage(message: string): { violation: string; replacement: string } { +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 `([^`]+)`/); - const violation = classMatch ? classMatch[1] : 'unknown'; - - // Extract replacement component - look for "Use `ComponentName`" - const replacementMatch = cleanMessage.match(/Use `([^`]+)`/); - const replacement = replacementMatch ? replacementMatch[1] : 'unknown'; - - return { violation, replacement }; + return classMatch ? classMatch[1] : 'unknown'; } export const reportViolationsHandler = createHandler< @@ -72,13 +65,12 @@ export const reportViolationsHandler = createHandler< ); for (const [fileName, { lines, message }] of Object.entries(fileGroups)) { - const { violation, replacement } = parseViolationMessage(message); + const violation = parseViolationMessage(message); violations.push({ file: fileName, lines: lines.sort((a, b) => a - b), violation, - replacement, }); } } @@ -88,7 +80,25 @@ export const reportViolationsHandler = createHandler< violations, }; }, - (result) => RESULT_FORMATTERS.json(result), + (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 [message.join('\n')]; + }, ); export const reportViolationsTools = [ From 76b458e14d2c30b5d85a349256f9d31c1d19edfc Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 17 Nov 2025 16:53:43 +0200 Subject: [PATCH 4/5] feat(angular-toolkit-mcp): improve schemas --- .../ds/report-violations/report-all-violations.tool.ts | 4 ++-- .../tools/ds/report-violations/report-violations.tool.ts | 2 +- .../src/lib/tools/ds/shared/models/schema-helpers.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) 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 afae64c..315b670 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 @@ -33,12 +33,12 @@ interface ReportAllViolationsOptions extends BaseHandlerOptions { 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: ['component', 'file'], - description: 'How to group the results: "component" groups by design system component, "file" groups by file path', + 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', }, }), 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 7f6b200..a99d3a6 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 @@ -19,7 +19,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', 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..bd0d897 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,18 @@ 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; From e041ed15652252a8ef868765594d0e3d841a001c Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 17 Nov 2025 17:02:48 +0200 Subject: [PATCH 5/5] chore(): format fix --- .../report-all-violations.tool.ts | 31 ++++++++++++------- .../report-violations.tool.ts | 13 +++++--- .../tools/ds/shared/models/schema-helpers.ts | 6 ++-- .../shared/violation-analysis/formatters.ts | 5 +-- 4 files changed, 36 insertions(+), 19 deletions(-) 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 315b670..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 @@ -16,13 +16,13 @@ import { } 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 { +import { AllViolationsReport, AllViolationsComponentReport, AllViolationsEntry, AllViolationsReportByFile, FileViolationReport, - ComponentViolationInFile + ComponentViolationInFile, } from './models/types.js'; interface ReportAllViolationsOptions extends BaseHandlerOptions { @@ -38,7 +38,8 @@ export const reportAllViolationsSchema = { groupBy: { type: 'string', 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', + 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', }, }), @@ -52,9 +53,15 @@ export const reportAllViolationsSchema = { * Extracts deprecated class and replacement from violation message * Performance optimized with caching to avoid repeated regex operations */ -const messageParsingCache = new Map(); +const messageParsingCache = new Map< + string, + { violation: string; replacement: string } +>(); -function parseViolationMessage(message: string): { violation: string; replacement: string } { +function parseViolationMessage(message: string): { + violation: string; + replacement: string; +} { // Check cache first const cached = messageParsingCache.get(message); if (cached) { @@ -62,16 +69,18 @@ function parseViolationMessage(message: string): { violation: string; replacemen } // Clean up HTML tags - const cleanMessage = message.replace(//g, '`').replace(/<\/code>/g, '`'); - + 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; @@ -216,8 +225,8 @@ export const reportAllViolationsHandler = createHandler< }, (result) => { const isFileGrouping = 'files' in result; - const isEmpty = isFileGrouping - ? result.files.length === 0 + const isEmpty = isFileGrouping + ? result.files.length === 0 : result.components.length === 0; if (isEmpty) { 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 a99d3a6..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 @@ -7,7 +7,10 @@ import { COMMON_ANNOTATIONS, } from '../shared/models/schema-helpers.js'; import { analyzeViolationsBase } from '../shared/violation-analysis/base-analyzer.js'; -import { groupIssuesByFile, filterFailedAudits } from '../shared/violation-analysis/formatters.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'; @@ -32,8 +35,10 @@ export const reportViolationsSchema = { */ function parseViolationMessage(message: string): string { // Clean up HTML tags - const cleanMessage = message.replace(//g, '`').replace(/<\/code>/g, '`'); - + 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'; @@ -66,7 +71,7 @@ export const reportViolationsHandler = createHandler< for (const [fileName, { lines, message }] of Object.entries(fileGroups)) { const violation = parseViolationMessage(message); - + violations.push({ file: fileName, lines: lines.sort((a, b) => a - b), 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 bd0d897..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 @@ -13,13 +13,15 @@ export const COMMON_SCHEMA_PROPERTIES = { componentName: { type: 'string' as const, - 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.', + 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 violation results in the output. "file" groups violations by individual file paths, "folder" groups by directory structure.', + 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/violation-analysis/formatters.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/formatters.ts index 7908585..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 @@ -87,7 +87,9 @@ export function groupIssuesByFile( if (!fileGroups[fileName]) { // Normalize message inline (remove directory prefix) - const directoryPrefix = directory.endsWith('/') ? directory : directory + '/'; + const directoryPrefix = directory.endsWith('/') + ? directory + : directory + '/'; const normalizedMessage = message.includes(directoryPrefix) ? message.replace(directoryPrefix, '') : message; @@ -111,4 +113,3 @@ export function groupIssuesByFile( return fileGroups; } -