From 9734ad999bca34fa3dc0302e7eda7267d4b908f2 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 24 Nov 2025 23:16:07 +0200 Subject: [PATCH 01/13] feat(angular-mcp-server): add violation grouping and enhance reporting tools --- README.md | 6 +- docs/tools.md | 40 ++- .../src/lib/tools/ds/ds.tools.ts | 2 + .../group-violations.tool.ts | 240 ++++++++++++++++++ .../lib/tools/ds/report-violations/index.ts | 1 + .../ds/report-violations/models/schema.ts | 88 +++++-- .../ds/report-violations/models/types.ts | 103 +++++++- .../report-all-violations.tool.ts | 172 +++++++------ .../report-violations.tool.ts | 132 ++++++---- .../utils/directory-grouping.utils.ts | 56 ++++ .../utils/file-enrichment.utils.ts | 26 ++ .../report-violations/utils/filename.utils.ts | 11 + .../utils/format-converter.utils.ts | 51 ++++ .../tools/ds/report-violations/utils/index.ts | 33 +++ .../utils/markdown-generator.utils.ts | 64 +++++ .../utils/message-parser.utils.ts | 50 ++++ .../ds/report-violations/utils/stats.utils.ts | 67 +++++ .../utils/work-group.utils.ts | 83 ++++++ .../src/lib/tools/ds/shared/index.ts | 1 + .../lib/tools/ds/shared/utils/path-helpers.ts | 19 ++ .../src/lib/tools/ds/tools.ts | 2 + 21 files changed, 1080 insertions(+), 167 deletions(-) create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/directory-grouping.utils.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/file-enrichment.utils.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/filename.utils.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/markdown-generator.utils.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/message-parser.utils.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/shared/utils/path-helpers.ts diff --git a/README.md b/README.md index e518b55..ccdb827 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,11 @@ my-angular-workspace/ ### Component Analysis -- **`report-violations`**: Report deprecated CSS usage in a directory with configurable grouping format +- **`report-violations`**: Report deprecated CSS usage for a specific DS component in a directory. Supports optional file output with statistics (components, files, lines). + +- **`report-all-violations`**: Report all deprecated CSS usage across all DS components in a directory. Supports optional file output with statistics (components, files, lines). + +- **`group-violations`**: Creates balanced work distribution groups from violations reports using bin-packing algorithm. Maintains path exclusivity and directory boundaries for parallel development. - **`report-deprecated-css`**: Report deprecated CSS classes found in styling files diff --git a/docs/tools.md b/docs/tools.md index fa5dcfb..721d62e 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -7,23 +7,29 @@ This document provides comprehensive guidance for AI agents working with Angular ### 🔍 Project Analysis Tools #### `report-violations` -**Purpose**: Identifies deprecated DS CSS usage patterns in Angular projects -**AI Usage**: Use as the first step in migration workflows to identify all violations before planning refactoring +**Purpose**: Identifies deprecated DS CSS usage patterns for a specific DS component in Angular projects +**AI Usage**: Use when you need to analyze violations for a specific component before planning refactoring **Key Parameters**: - `directory`: Target analysis directory (use relative paths like `./src/app`) - `componentName`: DS component class name (e.g., `DsButton`) - `groupBy`: `"file"` or `"folder"` for result organization -**Output**: Structured violation reports grouped by file or folder -**Best Practice**: Always run this before other migration tools to establish baseline +- `saveAsFile`: Optional boolean - if `true`, saves report to `tmp/.angular-toolkit-mcp/violations-report//-violations.json` +**Output**: +- Default: Structured violation reports grouped by file or folder +- With `saveAsFile: true`: File path and statistics (components, files, lines) +**Best Practice**: Use `saveAsFile: true` when you need to persist results for later processing or grouping workflows #### `report-all-violations` **Purpose**: Reports all deprecated DS CSS usage for every DS component within a directory **AI Usage**: Use for a fast, global inventory of violations across the codebase before narrowing to specific components **Key Parameters**: - `directory`: Target analysis directory (use relative paths like `./src/app`) -- `groupBy`: `"file"` or `"folder"` for result organization (default: `"file"`) -**Output**: Structured violation reports grouped by file or folder covering all DS components -**Best Practice**: Use to discover all violations and establish the baseline for subsequent refactoring. +- `groupBy`: `"component"` or `"file"` for result organization (default: `"component"`) +- `saveAsFile`: Optional boolean - if `true`, saves report to `tmp/.angular-toolkit-mcp/violations-report/-violations.json` +**Output**: +- Default: Structured violation reports grouped by component or file covering all DS components +- With `saveAsFile: true`: File path and statistics (components, files, lines) +**Best Practice**: Use `saveAsFile: true` to persist results for grouping workflows or large-scale migration planning. The saved file can be used as input for work distribution grouping tools. #### `get-project-dependencies` **Purpose**: Analyzes project structure, dependencies, and buildability @@ -34,6 +40,21 @@ This document provides comprehensive guidance for AI agents working with Angular **Output**: Dependency analysis, buildable/publishable status, peer dependencies **Best Practice**: Use to understand project constraints before recommending changes +#### `group-violations` +**Purpose**: Creates balanced work distribution groups from violations reports for parallel development +**AI Usage**: Use after `report-all-violations` to organize violations into balanced work groups for team distribution +**Key Parameters**: +- `fileName`: Name of violations JSON file in `tmp/.angular-toolkit-mcp/violations-report/` (e.g., `"packages-poker-core-lib-violations.json"`) +- `minGroups`: Minimum number of groups (default: 3) +- `maxGroups`: Maximum number of groups (default: 5) +- `variance`: Acceptable variance percentage for balance (default: 20) +**Output**: +- Work groups with balanced violation counts +- Individual JSON and Markdown files per group +- Metadata with validation results +- Saved to `tmp/.angular-toolkit-mcp/violation-groups//` +**Best Practice**: Use after saving violations with `saveAsFile: true`. The tool accepts both component-grouped and file-grouped reports. Groups maintain path exclusivity (each file in exactly one group) and preserve directory boundaries to enable parallel development without merge conflicts. + #### `report-deprecated-css` **Purpose**: Scans styling files for deprecated CSS classes **AI Usage**: Complement violation reports with style-specific analysis @@ -147,8 +168,9 @@ This document provides comprehensive guidance for AI agents working with Angular ### 1. Discovery & Analysis Workflow ``` 1. list-ds-components → Discover available DS components -2. report-violations → Identify all violations -3. get-project-dependencies → Analyze project structure +2. report-all-violations (saveAsFile: true) → Identify all violations and save to file +3. group-violations → Create balanced work distribution groups +4. get-project-dependencies → Analyze project structure ``` ### 2. Planning & Preparation Workflow diff --git a/packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts b/packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts index 9304f6b..28d6b07 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts @@ -9,6 +9,7 @@ import { join } from 'node:path'; import { reportViolationsTools, reportAllViolationsTools, + groupViolationsTools, } from './report-violations/index.js'; export const componentCoverageToolsSchema: ToolSchemaOptions = { @@ -137,4 +138,5 @@ export const dsTools = [ ...componentCoverageTools, ...reportViolationsTools, ...reportAllViolationsTools, + ...groupViolationsTools, ]; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts new file mode 100644 index 0000000..96c29b9 --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts @@ -0,0 +1,240 @@ +import { createHandler } from '../shared/utils/handler-helpers.js'; +import { toWorkspaceRelativePath } from '../shared/utils/path-helpers.js'; +import { readFile, writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import type { + AllViolationsReportByFile, + AllViolationsReport, + GroupViolationsOptions, + GroupViolationsReport, + GroupViolationsResult, +} from './models/types.js'; +import { groupViolationsSchema } from './models/schema.js'; +import { + detectReportFormat, + convertComponentToFileFormat, + enrichFiles, + groupByDirectory, + determineOptimalGroups, + createWorkGroups, + generateGroupMarkdown, +} from './utils/index.js'; + +export { groupViolationsSchema }; + +export const groupViolationsHandler = createHandler< + GroupViolationsOptions, + GroupViolationsResult +>( + groupViolationsSchema.name, + async (params, { cwd, workspaceRoot }) => { + const minGroups = params.minGroups ?? 3; + const maxGroups = params.maxGroups ?? 5; + const variance = params.variance ?? 20; + + // Read violations file + const inputPath = join( + cwd, + 'tmp', + '.angular-toolkit-mcp', + 'violations-report', + params.fileName, + ); + + let rawData: any; + try { + const fileContent = await readFile(inputPath, 'utf-8'); + rawData = JSON.parse(fileContent); + } catch (error) { + throw new Error( + `Failed to read violations file at ${inputPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // Detect format and convert if necessary + const format = detectReportFormat(rawData); + + if (format === 'unknown') { + throw new Error( + 'Invalid violations report format. Expected either { files: [...] } or { components: [...] }', + ); + } + + let violationsData: AllViolationsReportByFile; + + if (format === 'component') { + // Convert component-grouped to file-grouped format + violationsData = convertComponentToFileFormat(rawData as AllViolationsReport); + } else { + violationsData = rawData as AllViolationsReportByFile; + } + + if (!violationsData.files || violationsData.files.length === 0) { + throw new Error('No violations found in the input file'); + } + + // Enrich files with metadata + const enrichedFiles = enrichFiles(violationsData.files); + const totalViolations = enrichedFiles.reduce( + (sum, f) => sum + f.violations, + 0, + ); + const totalFiles = enrichedFiles.length; + + // Group by directory + const directorySummary = groupByDirectory(enrichedFiles); + + // Determine optimal number of groups + const optimalGroups = determineOptimalGroups( + totalViolations, + directorySummary, + minGroups, + maxGroups, + variance, + ); + + const targetPerGroup = totalViolations / optimalGroups; + const minAcceptable = Math.floor( + targetPerGroup * (1 - variance / 100), + ); + const maxAcceptable = Math.ceil( + targetPerGroup * (1 + variance / 100), + ); + + // Create work groups + const groups = createWorkGroups( + directorySummary, + optimalGroups, + maxAcceptable, + ); + + // Validation + const totalGroupViolations = groups.reduce( + (sum, g) => sum + g.violations, + 0, + ); + const allFilesInGroups = groups.flatMap((g) => g.files.map((f) => f.file)); + const uniqueFiles = new Set(allFilesInGroups); + const pathExclusivity = uniqueFiles.size === allFilesInGroups.length; + const balanced = groups.every( + (g) => g.violations >= minAcceptable && g.violations <= maxAcceptable, + ); + + // Create output structure + const report: GroupViolationsReport = { + metadata: { + generatedAt: new Date().toISOString(), + inputFile: params.fileName, + totalFiles, + totalViolations, + groupCount: optimalGroups, + targetPerGroup, + acceptableRange: { min: minAcceptable, max: maxAcceptable }, + variance, + }, + groups: groups.map((g) => ({ + id: g.id, + name: g.name, + directories: g.directories, + files: g.files.map((f) => ({ + file: f.file, + violations: f.violations, + components: f.components, + })), + statistics: { + fileCount: g.files.length, + violationCount: g.violations, + }, + componentDistribution: g.componentDistribution, + })), + validation: { + totalViolations: totalGroupViolations, + totalFiles: allFilesInGroups.length, + uniqueFiles: uniqueFiles.size, + pathExclusivity, + balanced, + }, + }; + + // Save each group as individual file + const reportName = params.fileName.replace('.json', ''); + const outputDir = join( + cwd, + 'tmp', + '.angular-toolkit-mcp', + 'violation-groups', + reportName, + ); + + await mkdir(outputDir, { recursive: true }); + + // Save metadata file + const metadataPath = join(outputDir, 'metadata.json'); + await writeFile( + metadataPath, + JSON.stringify( + { + metadata: report.metadata, + validation: report.validation, + }, + null, + 2, + ), + 'utf-8', + ); + + // Save each group as separate file (JSON + Markdown) + for (const group of report.groups) { + // Save JSON + const groupPath = join(outputDir, `group-${group.id}.json`); + await writeFile(groupPath, JSON.stringify(group, null, 2), 'utf-8'); + + // Save Markdown report + const markdownPath = join(outputDir, `group-${group.id}.md`); + const markdown = generateGroupMarkdown(group); + await writeFile(markdownPath, markdown, 'utf-8'); + } + + return { + ...report, + outputDir: toWorkspaceRelativePath(outputDir, workspaceRoot), + }; + }, + (result) => { + const { metadata, groups, validation, outputDir } = result; + + const message = [ + `✅ Created ${metadata.groupCount} work distribution groups`, + '', + `📊 Summary:`, + ` - Total files: ${metadata.totalFiles}`, + ` - Total violations: ${metadata.totalViolations}`, + ` - Target per group: ${metadata.targetPerGroup.toFixed(1)} violations`, + ` - Acceptable range: ${metadata.acceptableRange.min}-${metadata.acceptableRange.max}`, + '', + `📦 Groups:`, + ...groups.map( + (g) => + ` ${g.name} - ${g.statistics.violationCount} violations (${g.statistics.fileCount} files)`, + ), + '', + `✅ Validation:`, + ` - Total violations: ${validation.totalViolations} ${validation.totalViolations === metadata.totalViolations ? '✅' : '❌'}`, + ` - Path exclusivity: ${validation.pathExclusivity ? '✅' : '❌'}`, + ` - Balance: ${validation.balanced ? '✅' : '⚠️'}`, + '', + `📁 Output directory: ${outputDir}`, + ` - metadata.json (summary and validation)`, + ...groups.map((g) => ` - group-${g.id}.json + group-${g.id}.md`), + ]; + + return message; + }, +); + +export const groupViolationsTools = [ + { + schema: groupViolationsSchema, + handler: groupViolationsHandler, + }, +]; 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 72b3734..1f9e9fb 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 @@ -1,5 +1,6 @@ export { reportViolationsTools } from './report-violations.tool.js'; export { reportAllViolationsTools } from './report-all-violations.tool.js'; +export { groupViolationsTools } from './group-violations.tool.js'; export type { ViolationEntry, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts index 17ebb1b..94c122d 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts @@ -1,34 +1,82 @@ -import { ToolSchemaOptions } from '@push-based/models'; +import { + createViolationReportingSchema, + createProjectAnalysisSchema, + COMMON_ANNOTATIONS, +} from '../../shared/models/schema-helpers.js'; -export const reportViolationsSchema: ToolSchemaOptions = { +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({ + saveAsFile: { + type: 'boolean', + description: + 'If true, saves the violations report to /tmp/.angular-toolkit-mcp/violations-report// with filename pattern -violations.json (e.g., packages-poker-core-lib-violations.json). Overwrites if file exists.', + }, + }), + annotations: { + title: 'Report Violations', + ...COMMON_ANNOTATIONS.readOnly, + }, +}; + +export const reportAllViolationsSchema = { + name: 'report-all-violations', + description: + '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" (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', + }, + saveAsFile: { + type: 'boolean', + description: + 'If true, saves the violations report to /tmp/.angular-toolkit-mcp/violations-report/ with filename pattern -violations.json (e.g., packages-poker-core-lib-violations.json). Overwrites if file exists.', + }, + }), + annotations: { + title: 'Report All Violations', + ...COMMON_ANNOTATIONS.readOnly, + }, +}; + +export const groupViolationsSchema = { + name: 'group-violations', + description: + 'Creates work distribution groups from violations report. Reads a violations JSON file (e.g., packages-poker-violations.json) from tmp/.angular-toolkit-mcp/violations-report/ and creates balanced work groups using bin-packing algorithm. Accepts both file-grouped and component-grouped violation reports. Groups are balanced by violation count, maintain path exclusivity (each file in one group), and preserve directory boundaries for parallel development.', inputSchema: { - type: 'object', + type: 'object' as const, properties: { - directory: { + fileName: { type: 'string', description: - 'The relative path the directory (starting with "./path/to/dir" avoid big folders.) to run the task in starting from CWD. Respect the OS specifics.', + 'Name of the violations JSON file (e.g., "packages-poker-violations.json"). File must exist in tmp/.angular-toolkit-mcp/violations-report/', }, - componentName: { - type: 'string', - description: - 'The class name of the component to search for (e.g., DsButton)', + minGroups: { + type: 'number', + description: 'Minimum number of groups to create (default: 3)', + default: 3, }, - groupBy: { - type: 'string', - enum: ['file', 'folder'], - description: 'How to group the violation results', - default: 'file', + maxGroups: { + type: 'number', + description: 'Maximum number of groups to create (default: 5)', + default: 5, + }, + variance: { + type: 'number', + description: + 'Acceptable variance percentage for group balance (default: 20). Groups will have violations within target ± variance%', + default: 20, }, }, - required: ['directory', 'componentName'], + required: ['fileName'], }, annotations: { - title: 'Report Violations', - readOnlyHint: true, - openWorldHint: false, - idempotentHint: false, + title: 'Group Violations', + ...COMMON_ANNOTATIONS.readOnly, }, }; 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 cd32494..1b67f7d 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,4 +1,16 @@ -// Types for report-violations (single component, no replacement field needed) +import type { BaseHandlerOptions } from '../../shared/utils/handler-helpers.js'; + +// ============================================================================ +// report-violations types +// ============================================================================ + +export interface ReportViolationsOptions extends BaseHandlerOptions { + directory: string; + componentName: string; + groupBy?: 'file' | 'folder'; + saveAsFile?: boolean; +} + export interface ViolationEntry { file: string; lines: number[]; @@ -10,7 +22,30 @@ export interface ComponentViolationReport { violations: ViolationEntry[]; } -// Types for report-all-violations (multiple components, replacement field needed) +export interface ViolationFileOutput { + message: string; + filePath: string; + stats?: { + components: number; + files: number; + lines: number; + }; +} + +export type ReportViolationsResult = + | ComponentViolationReport + | ViolationFileOutput; + +// ============================================================================ +// report-all-violations types +// ============================================================================ + +export interface ReportAllViolationsOptions extends BaseHandlerOptions { + directory: string; + groupBy?: 'component' | 'file'; + saveAsFile?: boolean; +} + export interface AllViolationsEntry { file: string; lines: number[]; @@ -27,7 +62,6 @@ export interface AllViolationsReport { components: AllViolationsComponentReport[]; } -// File-grouped output types for report-all-violations export interface ComponentViolationInFile { component: string; lines: number[]; @@ -43,3 +77,66 @@ export interface FileViolationReport { export interface AllViolationsReportByFile { files: FileViolationReport[]; } + +export interface ProcessedViolation { + component: string; + fileName: string; + lines: number[]; + violation: string; + replacement: string; +} + +export type ReportAllViolationsResult = + | AllViolationsReport + | AllViolationsReportByFile + | ViolationFileOutput; + +// ============================================================================ +// group-violations types +// ============================================================================ + +export interface GroupViolationsOptions extends BaseHandlerOptions { + fileName: string; + minGroups?: number; + maxGroups?: number; + variance?: number; +} + +export interface GroupViolationsReport { + metadata: { + generatedAt: string; + inputFile: string; + totalFiles: number; + totalViolations: number; + groupCount: number; + targetPerGroup: number; + acceptableRange: { min: number; max: number }; + variance: number; + }; + groups: Array<{ + id: number; + name: string; + directories: string[]; + files: Array<{ + file: string; + violations: number; + components: FileViolationReport['components']; + }>; + statistics: { + fileCount: number; + violationCount: number; + }; + componentDistribution: Record; + }>; + validation: { + totalViolations: number; + totalFiles: number; + uniqueFiles: number; + pathExclusivity: boolean; + balanced: boolean; + }; +} + +export type GroupViolationsResult = GroupViolationsReport & { + outputDir: string; +}; 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 dfb6a4a..c37c31c 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 @@ -1,11 +1,5 @@ -import { - BaseHandlerOptions, - createHandler, -} from '../shared/utils/handler-helpers.js'; -import { - COMMON_ANNOTATIONS, - createProjectAnalysisSchema, -} from '../shared/models/schema-helpers.js'; +import { createHandler } from '../shared/utils/handler-helpers.js'; +import { toWorkspaceRelativePath } from '../shared/utils/path-helpers.js'; import { analyzeProjectCoverage, extractComponentName, @@ -17,85 +11,25 @@ import { import type { BaseViolationAudit } from '../shared/violation-analysis/types.js'; import { loadAndValidateDsComponentsFile } from '../../../validation/ds-components-file-loader.validation.js'; import { - AllViolationsReport, AllViolationsComponentReport, AllViolationsEntry, - AllViolationsReportByFile, FileViolationReport, ComponentViolationInFile, + ReportAllViolationsOptions, + ReportAllViolationsResult, + ProcessedViolation, } from './models/types.js'; +import { reportAllViolationsSchema } from './models/schema.js'; +import { writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { + generateFilename, + parseViolationMessageWithReplacement, + calculateComponentGroupedStats, + calculateFileGroupedStats, +} from './utils/index.js'; -interface ReportAllViolationsOptions extends BaseHandlerOptions { - directory: string; - groupBy?: 'component' | 'file'; -} - -export const reportAllViolationsSchema = { - name: 'report-all-violations', - description: - '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" (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: { - title: 'Report All Violations', - ...COMMON_ANNOTATIONS.readOnly, - }, -}; - -/** - * 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; -} +export { reportAllViolationsSchema }; /** * Processes all failed audits into a unified structure @@ -118,7 +52,7 @@ function processAudits( fileGroups, )) { // Lines are already sorted by groupIssuesByFile, so we can use them directly - const { violation, replacement } = parseViolationMessage(message); + const { violation, replacement } = parseViolationMessageWithReplacement(message); processed.push({ component: componentName, @@ -135,10 +69,10 @@ function processAudits( export const reportAllViolationsHandler = createHandler< ReportAllViolationsOptions, - AllViolationsReport | AllViolationsReportByFile + ReportAllViolationsResult >( reportAllViolationsSchema.name, - async (params, { cwd, deprecatedCssClassesPath }) => { + async (params, { cwd, workspaceRoot, deprecatedCssClassesPath }) => { if (!deprecatedCssClassesPath) { throw new Error( 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', @@ -162,7 +96,26 @@ export const reportAllViolationsHandler = createHandler< // Early exit for empty results if (failedAudits.length === 0) { - return params.groupBy === 'file' ? { files: [] } : { components: [] }; + const report = params.groupBy === 'file' ? { files: [] } : { components: [] }; + + if (params.saveAsFile) { + const outputDir = join(cwd, 'tmp', '.angular-toolkit-mcp', 'violations-report'); + const filename = generateFilename(params.directory); + const filePath = join(outputDir, filename); + await mkdir(outputDir, { recursive: true }); + await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); + return { + message: 'Violations report saved', + filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + stats: { + components: 0, + files: 0, + lines: 0, + }, + }; + } + + return report; } // Process all audits into unified structure (eliminates code duplication) @@ -193,7 +146,25 @@ export const reportAllViolationsHandler = createHandler< ([component, violations]) => ({ component, violations }), ); - return { components }; + const report = { components }; + + if (params.saveAsFile) { + const outputDir = join(cwd, 'tmp', '.angular-toolkit-mcp', 'violations-report'); + const filename = generateFilename(params.directory); + const filePath = join(outputDir, filename); + await mkdir(outputDir, { recursive: true }); + await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); + + const stats = calculateComponentGroupedStats(components); + + return { + message: 'Violations report saved', + filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + stats, + }; + } + + return report; } // Group by file @@ -221,9 +192,36 @@ export const reportAllViolationsHandler = createHandler< ([file, components]) => ({ file, components }), ).sort((a, b) => a.file.localeCompare(b.file)); - return { files }; + const report = { files }; + + if (params.saveAsFile) { + const outputDir = join(cwd, 'tmp', '.angular-toolkit-mcp', 'violations-report'); + const filename = generateFilename(params.directory); + const filePath = join(outputDir, filename); + await mkdir(outputDir, { recursive: true }); + await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); + + const stats = calculateFileGroupedStats(files); + + return { + message: 'Violations report saved', + filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + stats, + }; + } + + return report; }, (result) => { + // Check if this is a file output response + if ('message' in result && 'filePath' in result) { + const stats = 'stats' in result && result.stats ? result.stats : null; + const statsMessage = stats + ? ` (${stats.components} components, ${stats.files} files, ${stats.lines} lines)` + : ''; + return [`Violations report saved to ${result.filePath}${statsMessage}`]; + } + const isFileGrouping = 'files' in result; const isEmpty = isFileGrouping ? result.files.length === 0 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 1a6fdf6..bcc3d08 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,63 +1,67 @@ -import { - createHandler, - BaseHandlerOptions, -} from '../shared/utils/handler-helpers.js'; -import { - createViolationReportingSchema, - COMMON_ANNOTATIONS, -} from '../shared/models/schema-helpers.js'; +import { createHandler } from '../shared/utils/handler-helpers.js'; +import { toWorkspaceRelativePath } from '../shared/utils/path-helpers.js'; import { analyzeViolationsBase } from '../shared/violation-analysis/base-analyzer.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; - componentName: string; - groupBy?: 'file' | 'folder'; -} - -export const reportViolationsSchema = { - name: 'report-violations', - 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', - ...COMMON_ANNOTATIONS.readOnly, - }, -}; - -/** - * 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'; -} +import { + ComponentViolationReport, + ViolationEntry, + ReportViolationsOptions, + ReportViolationsResult, +} from './models/types.js'; +import { reportViolationsSchema } from './models/schema.js'; +import { writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { + generateFilename, + parseViolationMessage, + calculateSingleComponentStats, +} from './utils/index.js'; + +export { reportViolationsSchema }; export const reportViolationsHandler = createHandler< ReportViolationsOptions, - ComponentViolationReport + ReportViolationsResult >( reportViolationsSchema.name, - async (params) => { + async (params, { cwd, workspaceRoot }) => { const result = await analyzeViolationsBase(params); const failedAudits = filterFailedAudits(result); if (failedAudits.length === 0) { - return { + const report = { component: params.componentName, violations: [], }; + + if (params.saveAsFile) { + const outputDir = join( + cwd, + 'tmp', + '.angular-toolkit-mcp', + 'violations-report', + params.componentName, + ); + const filename = generateFilename(params.directory); + const filePath = join(outputDir, filename); + await mkdir(outputDir, { recursive: true }); + await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); + return { + message: 'Violations report saved', + filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + stats: { + components: 1, + files: 0, + lines: 0, + }, + }; + } + + return report; } const violations: ViolationEntry[] = []; @@ -80,18 +84,52 @@ export const reportViolationsHandler = createHandler< } } - return { + const report = { component: params.componentName, violations, }; + + if (params.saveAsFile) { + const outputDir = join( + cwd, + 'tmp', + '.angular-toolkit-mcp', + 'violations-report', + params.componentName, + ); + const filename = generateFilename(params.directory); + const filePath = join(outputDir, filename); + await mkdir(outputDir, { recursive: true }); + await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); + + const stats = calculateSingleComponentStats(violations); + + return { + message: 'Violations report saved', + filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + stats, + }; + } + + return report; }, (result) => { - if (result.violations.length === 0) { - return [`No violations found for component: ${result.component}`]; + // Check if this is a file output response + if ('message' in result && 'filePath' in result) { + const stats = 'stats' in result ? result.stats : null; + const statsMessage = stats + ? ` (${stats.components} component, ${stats.files} files, ${stats.lines} lines)` + : ''; + return [`Violations report saved to ${result.filePath}${statsMessage}`]; + } + + const report = result as ComponentViolationReport; + if (report.violations.length === 0) { + return [`No violations found for component: ${report.component}`]; } const message = [ - `Found violations for component: ${result.component}`, + `Found violations for component: ${report.component}`, 'Use this output to identify:', ' - Which files contain violations', ' - The specific line numbers where violations occur', @@ -99,7 +137,7 @@ export const reportViolationsHandler = createHandler< '', 'Violation Report:', '', - JSON.stringify(result, null, 2), + JSON.stringify(report, null, 2), ]; return [message.join('\n')]; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/directory-grouping.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/directory-grouping.utils.ts new file mode 100644 index 0000000..dd31107 --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/directory-grouping.utils.ts @@ -0,0 +1,56 @@ +import type { EnrichedFile } from './file-enrichment.utils.js'; + +export interface DirectorySummary { + directory: string; + files: EnrichedFile[]; + fileCount: number; + violations: number; +} + +/** + * Group files by directory hierarchy + */ +export function groupByDirectory(files: EnrichedFile[]): DirectorySummary[] { + const directoryMap = new Map(); + + files.forEach((file) => { + const dir = file.subdirectory || file.directory; + if (!directoryMap.has(dir)) { + directoryMap.set(dir, []); + } + directoryMap.get(dir)!.push(file); + }); + + return Array.from(directoryMap.entries()) + .map(([directory, dirFiles]) => ({ + directory, + files: dirFiles, + fileCount: dirFiles.length, + violations: dirFiles.reduce((sum, f) => sum + f.violations, 0), + })) + .sort((a, b) => b.violations - a.violations); +} + +/** + * Determine optimal number of groups + */ +export function determineOptimalGroups( + totalViolations: number, + directorySummary: DirectorySummary[], + minGroups: number, + maxGroups: number, + variance: number, +): number { + for (let g = minGroups; g <= maxGroups; g++) { + const target = totalViolations / g; + const maxAcceptable = target * (1 + variance / 100); + + const canBalance = directorySummary.every( + (dir) => dir.violations <= maxAcceptable, + ); + if (canBalance) { + return g; + } + } + return minGroups; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/file-enrichment.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/file-enrichment.utils.ts new file mode 100644 index 0000000..c39cc0b --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/file-enrichment.utils.ts @@ -0,0 +1,26 @@ +import type { FileViolationReport } from '../models/types.js'; + +export interface EnrichedFile extends FileViolationReport { + violations: number; + directory: string; + subdirectory: string; +} + +/** + * Calculate total violations for a file + */ +export function calculateViolations(file: FileViolationReport): number { + return file.components.reduce((sum, comp) => sum + comp.lines.length, 0); +} + +/** + * Enrich files with metadata + */ +export function enrichFiles(files: FileViolationReport[]): EnrichedFile[] { + return files.map((file) => ({ + ...file, + violations: calculateViolations(file), + directory: file.file.split('/')[0], + subdirectory: file.file.split('/').slice(0, 2).join('/'), + })); +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/filename.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/filename.utils.ts new file mode 100644 index 0000000..fa59090 --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/filename.utils.ts @@ -0,0 +1,11 @@ +/** + * Generates filename from directory path + * Example: "./packages/poker/core-lib" -> "packages-poker-core-lib-violations.json" + */ +export function generateFilename(directory: string): string { + const normalized = directory + .replace(/^\.\//, '') // Remove leading ./ + .replace(/\/+$/, '') // Remove trailing slashes + .replace(/\//g, '-'); // Replace slashes with dashes + return `${normalized}-violations.json`; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts new file mode 100644 index 0000000..9fbbbd6 --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts @@ -0,0 +1,51 @@ +import type { + AllViolationsReport, + AllViolationsReportByFile, + FileViolationReport, +} from '../models/types.js'; + +/** + * Detect the format of the violations report + */ +export function detectReportFormat( + data: any, +): 'file' | 'component' | 'unknown' { + if (data.files && Array.isArray(data.files)) { + return 'file'; + } + if (data.components && Array.isArray(data.components)) { + return 'component'; + } + return 'unknown'; +} + +/** + * Convert component-grouped report to file-grouped format + */ +export function convertComponentToFileFormat( + report: AllViolationsReport, +): AllViolationsReportByFile { + const fileMap = new Map(); + + report.components.forEach((componentReport) => { + componentReport.violations.forEach((violation) => { + if (!fileMap.has(violation.file)) { + fileMap.set(violation.file, []); + } + + fileMap.get(violation.file)!.push({ + component: componentReport.component, + lines: violation.lines, + violation: violation.violation, + replacement: violation.replacement, + }); + }); + }); + + const files: FileViolationReport[] = Array.from( + fileMap.entries(), + ([file, components]) => ({ file, components }), + ).sort((a, b) => a.file.localeCompare(b.file)); + + return { files }; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts new file mode 100644 index 0000000..427003d --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts @@ -0,0 +1,33 @@ +export { generateFilename } from './filename.utils.js'; +export { + parseViolationMessage, + parseViolationMessageWithReplacement, +} from './message-parser.utils.js'; +export { + calculateSingleComponentStats, + calculateComponentGroupedStats, + calculateFileGroupedStats, +} from './stats.utils.js'; +export type { ViolationStats } from './stats.utils.js'; +export { + detectReportFormat, + convertComponentToFileFormat, +} from './format-converter.utils.js'; +export { + calculateViolations, + enrichFiles, +} from './file-enrichment.utils.js'; +export type { EnrichedFile } from './file-enrichment.utils.js'; +export { + groupByDirectory, + determineOptimalGroups, +} from './directory-grouping.utils.js'; +export type { DirectorySummary } from './directory-grouping.utils.js'; +export { + assignGroupName, + calculateComponentDistribution, + createWorkGroups, +} from './work-group.utils.js'; +export type { WorkGroup } from './work-group.utils.js'; +export { generateGroupMarkdown } from './markdown-generator.utils.js'; +export type { GroupForMarkdown } from './markdown-generator.utils.js'; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/markdown-generator.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/markdown-generator.utils.ts new file mode 100644 index 0000000..005cb48 --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/markdown-generator.utils.ts @@ -0,0 +1,64 @@ +import type { FileViolationReport } from '../models/types.js'; + +export interface GroupForMarkdown { + name: string; + statistics: { + fileCount: number; + violationCount: number; + }; + componentDistribution: Record; + files: Array<{ + file: string; + violations: number; + components: FileViolationReport['components']; + }>; +} + +/** + * Generate markdown report for a group + */ +export function generateGroupMarkdown(group: GroupForMarkdown): string { + // Component distribution summary + const componentSummary = Object.entries(group.componentDistribution) + .sort((a, b) => b[1] - a[1]) + .map(([comp, count]) => `${comp} (${count})`) + .join(', '); + + // Directory list with file counts + const directoryCounts = new Map(); + group.files.forEach((file) => { + const dir = file.file.split('/').slice(0, 2).join('/'); + directoryCounts.set(dir, (directoryCounts.get(dir) || 0) + 1); + }); + + const directoryList = Array.from(directoryCounts.entries()) + .map(([dir, count]) => `- ${dir} (${count} file${count > 1 ? 's' : ''})`) + .join('\n'); + + // File list with violations + const fileList = group.files + .map((file) => { + const componentLines = file.components + .map((comp) => { + const lines = comp.lines.map((l) => `L${l}`).join(','); + return `- ${comp.component}: ${lines}`; + }) + .join('\n'); + + return `\`${file.file}\` — ${file.violations} violation${file.violations > 1 ? 's' : ''}\n${componentLines}`; + }) + .join('\n\n'); + + return `# ${group.name} + +## Summary +${group.statistics.fileCount} files | ${group.statistics.violationCount} violations | ${componentSummary} + +## Directories +${directoryList} + +## Files to Update + +${fileList} +`; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/message-parser.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/message-parser.utils.ts new file mode 100644 index 0000000..c9581ff --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/message-parser.utils.ts @@ -0,0 +1,50 @@ +/** + * Extracts deprecated class from violation message + */ +export 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'; +} + +/** + * 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 } +>(); + +export function parseViolationMessageWithReplacement(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; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts new file mode 100644 index 0000000..aa311c3 --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts @@ -0,0 +1,67 @@ +import type { ViolationEntry, AllViolationsComponentReport, FileViolationReport } from '../models/types.js'; + +export interface ViolationStats { + components: number; + files: number; + lines: number; +} + +/** + * Calculate statistics for single component violations + */ +export function calculateSingleComponentStats( + violations: ViolationEntry[], +): ViolationStats { + const uniqueFiles = new Set(violations.map(v => v.file)).size; + const totalLines = violations.reduce((sum, v) => sum + v.lines.length, 0); + + return { + components: 1, + files: uniqueFiles, + lines: totalLines, + }; +} + +/** + * Calculate statistics for component-grouped violations + */ +export function calculateComponentGroupedStats( + components: AllViolationsComponentReport[], +): ViolationStats { + const uniqueComponents = components.length; + const uniqueFiles = new Set( + components.flatMap(c => c.violations.map(v => v.file)) + ).size; + const totalLines = components.reduce( + (sum, c) => sum + c.violations.reduce((s, v) => s + v.lines.length, 0), + 0 + ); + + return { + components: uniqueComponents, + files: uniqueFiles, + lines: totalLines, + }; +} + +/** + * Calculate statistics for file-grouped violations + */ +export function calculateFileGroupedStats( + files: FileViolationReport[], +): ViolationStats { + const uniqueComponents = new Set( + files.flatMap(f => f.components.map(c => c.component)) + ).size; + const uniqueFiles = files.length; + const totalLines = files.reduce( + (sum, f) => sum + f.components.reduce((s, c) => s + c.lines.length, 0), + 0 + ); + + return { + components: uniqueComponents, + files: uniqueFiles, + lines: totalLines, + }; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts new file mode 100644 index 0000000..7dac3ab --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts @@ -0,0 +1,83 @@ +import type { EnrichedFile } from './file-enrichment.utils.js'; +import type { DirectorySummary } from './directory-grouping.utils.js'; + +export interface WorkGroup { + id: number; + name: string; + directories: string[]; + files: EnrichedFile[]; + violations: number; + componentDistribution: Record; +} + +/** + * Assign group name based on primary directory + */ +export function assignGroupName(directories: string[], groupId: number): string { + const primaryDir = directories[0] || 'misc'; + return `Group ${groupId} - ${primaryDir}`; +} + +/** + * Calculate component distribution for a group + */ +export function calculateComponentDistribution( + files: EnrichedFile[], +): Record { + const distribution: Record = {}; + + files.forEach((file) => { + file.components.forEach((comp) => { + if (!distribution[comp.component]) { + distribution[comp.component] = 0; + } + distribution[comp.component] += comp.lines.length; + }); + }); + + return distribution; +} + +/** + * Create work groups using bin-packing algorithm + */ +export function createWorkGroups( + directorySummary: DirectorySummary[], + optimalGroups: number, + maxAcceptable: number, +): WorkGroup[] { + const groups: WorkGroup[] = Array.from({ length: optimalGroups }, (_, i) => ({ + id: i + 1, + name: '', + directories: [], + files: [], + violations: 0, + componentDistribution: {}, + })); + + // Bin packing: first-fit decreasing + const sortedDirs = [...directorySummary].sort( + (a, b) => b.violations - a.violations, + ); + + sortedDirs.forEach((dir) => { + // Find group with least violations that can fit this directory + const targetGroup = groups + .filter((g) => g.violations + dir.violations <= maxAcceptable) + .sort((a, b) => a.violations - b.violations)[0]; + + const selectedGroup = targetGroup || groups.sort((a, b) => a.violations - b.violations)[0]; + + selectedGroup.directories.push(dir.directory); + selectedGroup.files.push(...dir.files); + selectedGroup.violations += dir.violations; + }); + + // Assign names and component distribution + groups.forEach((group) => { + group.name = assignGroupName(group.directories, group.id); + group.componentDistribution = calculateComponentDistribution(group.files); + }); + + return groups; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts index 0d8b0e1..58c6f3d 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts @@ -4,4 +4,5 @@ export * from './utils/handler-helpers.js'; export * from './utils/component-validation.js'; export * from './utils/output.utils.js'; export * from './utils/cross-platform-path.js'; +export * from './utils/path-helpers.js'; export * from './models/schema-helpers.js'; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/path-helpers.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/path-helpers.ts new file mode 100644 index 0000000..6a2136a --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/path-helpers.ts @@ -0,0 +1,19 @@ +import { relative } from 'path'; + +/** + * Convert an absolute path to a workspace-relative path + * @param absolutePath - The absolute path to convert + * @param workspaceRoot - The workspace root directory + * @returns Path relative to workspace root + */ +export function toWorkspaceRelativePath( + absolutePath: string, + workspaceRoot: string, +): string { + const relativePath = relative(workspaceRoot, absolutePath); + // If the path is outside workspace, return the absolute path + if (relativePath.startsWith('..')) { + return absolutePath; + } + return relativePath; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/tools.ts b/packages/angular-mcp-server/src/lib/tools/ds/tools.ts index 5690766..eb61c4f 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/tools.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/tools.ts @@ -2,6 +2,7 @@ import { ToolsConfig } from '@push-based/models'; import { reportViolationsTools, reportAllViolationsTools, + groupViolationsTools, } from './report-violations/index.js'; import { getProjectDependenciesTools } from './project/get-project-dependencies.tool.js'; import { reportDeprecatedCssTools } from './project/report-deprecated-css.tool.js'; @@ -19,6 +20,7 @@ export const dsTools: ToolsConfig[] = [ // Project tools ...reportViolationsTools, ...reportAllViolationsTools, + ...groupViolationsTools, ...getProjectDependenciesTools, ...reportDeprecatedCssTools, ...buildComponentUsageGraphTools, From b29ed61fc3516d848c1494dae9f6bc62a0197171 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 24 Nov 2025 23:20:24 +0200 Subject: [PATCH 02/13] chore: fix formatting --- .../group-violations.tool.ts | 16 ++++---- .../report-all-violations.tool.ts | 41 +++++++++++++------ .../report-violations.tool.ts | 6 +-- .../tools/ds/report-violations/utils/index.ts | 5 +-- .../ds/report-violations/utils/stats.utils.ts | 22 ++++++---- .../utils/work-group.utils.ts | 8 +++- 6 files changed, 59 insertions(+), 39 deletions(-) diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts index 96c29b9..29aa123 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts @@ -53,7 +53,7 @@ export const groupViolationsHandler = createHandler< // Detect format and convert if necessary const format = detectReportFormat(rawData); - + if (format === 'unknown') { throw new Error( 'Invalid violations report format. Expected either { files: [...] } or { components: [...] }', @@ -61,10 +61,12 @@ export const groupViolationsHandler = createHandler< } let violationsData: AllViolationsReportByFile; - + if (format === 'component') { // Convert component-grouped to file-grouped format - violationsData = convertComponentToFileFormat(rawData as AllViolationsReport); + violationsData = convertComponentToFileFormat( + rawData as AllViolationsReport, + ); } else { violationsData = rawData as AllViolationsReportByFile; } @@ -94,12 +96,8 @@ export const groupViolationsHandler = createHandler< ); const targetPerGroup = totalViolations / optimalGroups; - const minAcceptable = Math.floor( - targetPerGroup * (1 - variance / 100), - ); - const maxAcceptable = Math.ceil( - targetPerGroup * (1 + variance / 100), - ); + const minAcceptable = Math.floor(targetPerGroup * (1 - variance / 100)); + const maxAcceptable = Math.ceil(targetPerGroup * (1 + variance / 100)); // Create work groups const groups = createWorkGroups( 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 c37c31c..a185bd2 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 @@ -52,7 +52,8 @@ function processAudits( fileGroups, )) { // Lines are already sorted by groupIssuesByFile, so we can use them directly - const { violation, replacement } = parseViolationMessageWithReplacement(message); + const { violation, replacement } = + parseViolationMessageWithReplacement(message); processed.push({ component: componentName, @@ -96,10 +97,16 @@ export const reportAllViolationsHandler = createHandler< // Early exit for empty results if (failedAudits.length === 0) { - const report = params.groupBy === 'file' ? { files: [] } : { components: [] }; - + const report = + params.groupBy === 'file' ? { files: [] } : { components: [] }; + if (params.saveAsFile) { - const outputDir = join(cwd, 'tmp', '.angular-toolkit-mcp', 'violations-report'); + const outputDir = join( + cwd, + 'tmp', + '.angular-toolkit-mcp', + 'violations-report', + ); const filename = generateFilename(params.directory); const filePath = join(outputDir, filename); await mkdir(outputDir, { recursive: true }); @@ -114,7 +121,7 @@ export const reportAllViolationsHandler = createHandler< }, }; } - + return report; } @@ -149,14 +156,19 @@ export const reportAllViolationsHandler = createHandler< const report = { components }; if (params.saveAsFile) { - const outputDir = join(cwd, 'tmp', '.angular-toolkit-mcp', 'violations-report'); + const outputDir = join( + cwd, + 'tmp', + '.angular-toolkit-mcp', + 'violations-report', + ); const filename = generateFilename(params.directory); const filePath = join(outputDir, filename); await mkdir(outputDir, { recursive: true }); await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); - + const stats = calculateComponentGroupedStats(components); - + return { message: 'Violations report saved', filePath: toWorkspaceRelativePath(filePath, workspaceRoot), @@ -195,14 +207,19 @@ export const reportAllViolationsHandler = createHandler< const report = { files }; if (params.saveAsFile) { - const outputDir = join(cwd, 'tmp', '.angular-toolkit-mcp', 'violations-report'); + const outputDir = join( + cwd, + 'tmp', + '.angular-toolkit-mcp', + 'violations-report', + ); const filename = generateFilename(params.directory); const filePath = join(outputDir, filename); await mkdir(outputDir, { recursive: true }); await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); - + const stats = calculateFileGroupedStats(files); - + return { message: 'Violations report saved', filePath: toWorkspaceRelativePath(filePath, workspaceRoot), @@ -216,7 +233,7 @@ export const reportAllViolationsHandler = createHandler< // Check if this is a file output response if ('message' in result && 'filePath' in result) { const stats = 'stats' in result && result.stats ? result.stats : null; - const statsMessage = stats + const statsMessage = stats ? ` (${stats.components} components, ${stats.files} files, ${stats.lines} lines)` : ''; return [`Violations report saved to ${result.filePath}${statsMessage}`]; 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 bcc3d08..aa3a271 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 @@ -101,9 +101,9 @@ export const reportViolationsHandler = createHandler< const filePath = join(outputDir, filename); await mkdir(outputDir, { recursive: true }); await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); - + const stats = calculateSingleComponentStats(violations); - + return { message: 'Violations report saved', filePath: toWorkspaceRelativePath(filePath, workspaceRoot), @@ -117,7 +117,7 @@ export const reportViolationsHandler = createHandler< // Check if this is a file output response if ('message' in result && 'filePath' in result) { const stats = 'stats' in result ? result.stats : null; - const statsMessage = stats + const statsMessage = stats ? ` (${stats.components} component, ${stats.files} files, ${stats.lines} lines)` : ''; return [`Violations report saved to ${result.filePath}${statsMessage}`]; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts index 427003d..e8595b5 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts @@ -13,10 +13,7 @@ export { detectReportFormat, convertComponentToFileFormat, } from './format-converter.utils.js'; -export { - calculateViolations, - enrichFiles, -} from './file-enrichment.utils.js'; +export { calculateViolations, enrichFiles } from './file-enrichment.utils.js'; export type { EnrichedFile } from './file-enrichment.utils.js'; export { groupByDirectory, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts index aa311c3..95a98c9 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts @@ -1,4 +1,8 @@ -import type { ViolationEntry, AllViolationsComponentReport, FileViolationReport } from '../models/types.js'; +import type { + ViolationEntry, + AllViolationsComponentReport, + FileViolationReport, +} from '../models/types.js'; export interface ViolationStats { components: number; @@ -12,9 +16,9 @@ export interface ViolationStats { export function calculateSingleComponentStats( violations: ViolationEntry[], ): ViolationStats { - const uniqueFiles = new Set(violations.map(v => v.file)).size; + const uniqueFiles = new Set(violations.map((v) => v.file)).size; const totalLines = violations.reduce((sum, v) => sum + v.lines.length, 0); - + return { components: 1, files: uniqueFiles, @@ -30,13 +34,13 @@ export function calculateComponentGroupedStats( ): ViolationStats { const uniqueComponents = components.length; const uniqueFiles = new Set( - components.flatMap(c => c.violations.map(v => v.file)) + components.flatMap((c) => c.violations.map((v) => v.file)), ).size; const totalLines = components.reduce( (sum, c) => sum + c.violations.reduce((s, v) => s + v.lines.length, 0), - 0 + 0, ); - + return { components: uniqueComponents, files: uniqueFiles, @@ -51,14 +55,14 @@ export function calculateFileGroupedStats( files: FileViolationReport[], ): ViolationStats { const uniqueComponents = new Set( - files.flatMap(f => f.components.map(c => c.component)) + files.flatMap((f) => f.components.map((c) => c.component)), ).size; const uniqueFiles = files.length; const totalLines = files.reduce( (sum, f) => sum + f.components.reduce((s, c) => s + c.lines.length, 0), - 0 + 0, ); - + return { components: uniqueComponents, files: uniqueFiles, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts index 7dac3ab..0e4234e 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts @@ -13,7 +13,10 @@ export interface WorkGroup { /** * Assign group name based on primary directory */ -export function assignGroupName(directories: string[], groupId: number): string { +export function assignGroupName( + directories: string[], + groupId: number, +): string { const primaryDir = directories[0] || 'misc'; return `Group ${groupId} - ${primaryDir}`; } @@ -66,7 +69,8 @@ export function createWorkGroups( .filter((g) => g.violations + dir.violations <= maxAcceptable) .sort((a, b) => a.violations - b.violations)[0]; - const selectedGroup = targetGroup || groups.sort((a, b) => a.violations - b.violations)[0]; + const selectedGroup = + targetGroup || groups.sort((a, b) => a.violations - b.violations)[0]; selectedGroup.directories.push(dir.directory); selectedGroup.files.push(...dir.files); From 244317ea3776a0c8d5056c8d490584f555f6f50b Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 24 Nov 2025 23:41:57 +0200 Subject: [PATCH 03/13] fix: paths issue --- .../group-violations.tool.ts | 4 ++-- .../report-all-violations.tool.ts | 15 ++++++++++----- .../report-violations.tool.ts | 13 +++++++++---- .../src/lib/tools/ds/shared/index.ts | 1 - .../lib/tools/ds/shared/utils/path-helpers.ts | 19 ------------------- 5 files changed, 21 insertions(+), 31 deletions(-) delete mode 100644 packages/angular-mcp-server/src/lib/tools/ds/shared/utils/path-helpers.ts diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts index 29aa123..60ecb29 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts @@ -1,5 +1,5 @@ import { createHandler } from '../shared/utils/handler-helpers.js'; -import { toWorkspaceRelativePath } from '../shared/utils/path-helpers.js'; +import { normalizeAbsolutePathToRelative } from '../shared/utils/cross-platform-path.js'; import { readFile, writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; import type { @@ -195,7 +195,7 @@ export const groupViolationsHandler = createHandler< return { ...report, - outputDir: toWorkspaceRelativePath(outputDir, workspaceRoot), + outputDir: normalizeAbsolutePathToRelative(outputDir, workspaceRoot), }; }, (result) => { 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 a185bd2..d0c4fc0 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 @@ -1,5 +1,5 @@ import { createHandler } from '../shared/utils/handler-helpers.js'; -import { toWorkspaceRelativePath } from '../shared/utils/path-helpers.js'; +import { normalizeAbsolutePathToRelative } from '../shared/utils/cross-platform-path.js'; import { analyzeProjectCoverage, extractComponentName, @@ -55,9 +55,14 @@ function processAudits( const { violation, replacement } = parseViolationMessageWithReplacement(message); + // Construct full path: directory + normalized file path + const fullPath = directory.endsWith('/') + ? `${directory}${fileName}` + : `${directory}/${fileName}`; + processed.push({ component: componentName, - fileName, + fileName: fullPath, lines: fileLines, // Already sorted violation, replacement, @@ -113,7 +118,7 @@ export const reportAllViolationsHandler = createHandler< await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); return { message: 'Violations report saved', - filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + filePath: normalizeAbsolutePathToRelative(filePath, workspaceRoot), stats: { components: 0, files: 0, @@ -171,7 +176,7 @@ export const reportAllViolationsHandler = createHandler< return { message: 'Violations report saved', - filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + filePath: normalizeAbsolutePathToRelative(filePath, workspaceRoot), stats, }; } @@ -222,7 +227,7 @@ export const reportAllViolationsHandler = createHandler< return { message: 'Violations report saved', - filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + filePath: normalizeAbsolutePathToRelative(filePath, workspaceRoot), stats, }; } 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 aa3a271..e52a591 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,5 +1,5 @@ import { createHandler } from '../shared/utils/handler-helpers.js'; -import { toWorkspaceRelativePath } from '../shared/utils/path-helpers.js'; +import { normalizeAbsolutePathToRelative } from '../shared/utils/cross-platform-path.js'; import { analyzeViolationsBase } from '../shared/violation-analysis/base-analyzer.js'; import { groupIssuesByFile, @@ -52,7 +52,7 @@ export const reportViolationsHandler = createHandler< await writeFile(filePath, JSON.stringify(report, null, 2), 'utf-8'); return { message: 'Violations report saved', - filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + filePath: normalizeAbsolutePathToRelative(filePath, workspaceRoot), stats: { components: 1, files: 0, @@ -76,8 +76,13 @@ export const reportViolationsHandler = createHandler< for (const [fileName, { lines, message }] of Object.entries(fileGroups)) { const violation = parseViolationMessage(message); + // Construct full path: directory + normalized file path + const fullPath = params.directory.endsWith('/') + ? `${params.directory}${fileName}` + : `${params.directory}/${fileName}`; + violations.push({ - file: fileName, + file: fullPath, lines: lines.sort((a, b) => a - b), violation, }); @@ -106,7 +111,7 @@ export const reportViolationsHandler = createHandler< return { message: 'Violations report saved', - filePath: toWorkspaceRelativePath(filePath, workspaceRoot), + filePath: normalizeAbsolutePathToRelative(filePath, workspaceRoot), stats, }; } diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts index 58c6f3d..0d8b0e1 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts @@ -4,5 +4,4 @@ export * from './utils/handler-helpers.js'; export * from './utils/component-validation.js'; export * from './utils/output.utils.js'; export * from './utils/cross-platform-path.js'; -export * from './utils/path-helpers.js'; export * from './models/schema-helpers.js'; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/path-helpers.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/path-helpers.ts deleted file mode 100644 index 6a2136a..0000000 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/path-helpers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { relative } from 'path'; - -/** - * Convert an absolute path to a workspace-relative path - * @param absolutePath - The absolute path to convert - * @param workspaceRoot - The workspace root directory - * @returns Path relative to workspace root - */ -export function toWorkspaceRelativePath( - absolutePath: string, - workspaceRoot: string, -): string { - const relativePath = relative(workspaceRoot, absolutePath); - // If the path is outside workspace, return the absolute path - if (relativePath.startsWith('..')) { - return absolutePath; - } - return relativePath; -} From f8761c239fdc0f1048c92081f410437c96d411c0 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 24 Nov 2025 23:44:53 +0200 Subject: [PATCH 04/13] fix: lint error fix --- .../lib/tools/ds/report-violations/group-violations.tool.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts index 60ecb29..79fe34c 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts @@ -45,9 +45,9 @@ export const groupViolationsHandler = createHandler< try { const fileContent = await readFile(inputPath, 'utf-8'); rawData = JSON.parse(fileContent); - } catch (error) { + } catch (ctx) { throw new Error( - `Failed to read violations file at ${inputPath}: ${error instanceof Error ? error.message : String(error)}`, + `Failed to read violations file at ${inputPath}: ${ctx instanceof Error ? ctx.message : String(ctx)}`, ); } From a9b0f0d009d86da587df5af386201720962042e1 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 25 Nov 2025 00:10:36 +0200 Subject: [PATCH 05/13] fix: proper paths fix --- .../ds/report-violations/group-violations.tool.ts | 5 +++++ .../tools/ds/report-violations/models/types.ts | 5 +++++ .../report-all-violations.tool.ts | 15 ++++++--------- .../report-violations/report-violations.tool.ts | 9 +++------ .../utils/format-converter.utils.ts | 2 +- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts index 79fe34c..a04afd2 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts @@ -75,6 +75,9 @@ export const groupViolationsHandler = createHandler< throw new Error('No violations found in the input file'); } + // Extract rootPath from the violations data + const rootPath = violationsData.rootPath || ''; + // Enrich files with metadata const enrichedFiles = enrichFiles(violationsData.files); const totalViolations = enrichedFiles.reduce( @@ -123,6 +126,7 @@ export const groupViolationsHandler = createHandler< metadata: { generatedAt: new Date().toISOString(), inputFile: params.fileName, + rootPath, totalFiles, totalViolations, groupCount: optimalGroups, @@ -133,6 +137,7 @@ export const groupViolationsHandler = createHandler< groups: groups.map((g) => ({ id: g.id, name: g.name, + rootPath, directories: g.directories, files: g.files.map((f) => ({ file: f.file, 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 1b67f7d..d20bf21 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 @@ -20,6 +20,7 @@ export interface ViolationEntry { export interface ComponentViolationReport { component: string; violations: ViolationEntry[]; + rootPath: string; } export interface ViolationFileOutput { @@ -60,6 +61,7 @@ export interface AllViolationsComponentReport { export interface AllViolationsReport { components: AllViolationsComponentReport[]; + rootPath: string; } export interface ComponentViolationInFile { @@ -76,6 +78,7 @@ export interface FileViolationReport { export interface AllViolationsReportByFile { files: FileViolationReport[]; + rootPath: string; } export interface ProcessedViolation { @@ -106,6 +109,7 @@ export interface GroupViolationsReport { metadata: { generatedAt: string; inputFile: string; + rootPath: string; totalFiles: number; totalViolations: number; groupCount: number; @@ -116,6 +120,7 @@ export interface GroupViolationsReport { groups: Array<{ id: number; name: string; + rootPath: string; directories: string[]; files: Array<{ file: string; 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 d0c4fc0..43225a0 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 @@ -55,14 +55,9 @@ function processAudits( const { violation, replacement } = parseViolationMessageWithReplacement(message); - // Construct full path: directory + normalized file path - const fullPath = directory.endsWith('/') - ? `${directory}${fileName}` - : `${directory}/${fileName}`; - processed.push({ component: componentName, - fileName: fullPath, + fileName: fileName, lines: fileLines, // Already sorted violation, replacement, @@ -103,7 +98,9 @@ export const reportAllViolationsHandler = createHandler< // Early exit for empty results if (failedAudits.length === 0) { const report = - params.groupBy === 'file' ? { files: [] } : { components: [] }; + params.groupBy === 'file' + ? { files: [], rootPath: params.directory } + : { components: [], rootPath: params.directory }; if (params.saveAsFile) { const outputDir = join( @@ -158,7 +155,7 @@ export const reportAllViolationsHandler = createHandler< ([component, violations]) => ({ component, violations }), ); - const report = { components }; + const report = { components, rootPath: params.directory }; if (params.saveAsFile) { const outputDir = join( @@ -209,7 +206,7 @@ export const reportAllViolationsHandler = createHandler< ([file, components]) => ({ file, components }), ).sort((a, b) => a.file.localeCompare(b.file)); - const report = { files }; + const report = { files, rootPath: params.directory }; if (params.saveAsFile) { const outputDir = join( 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 e52a591..2e8517d 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 @@ -36,6 +36,7 @@ export const reportViolationsHandler = createHandler< const report = { component: params.componentName, violations: [], + rootPath: params.directory, }; if (params.saveAsFile) { @@ -76,13 +77,8 @@ export const reportViolationsHandler = createHandler< for (const [fileName, { lines, message }] of Object.entries(fileGroups)) { const violation = parseViolationMessage(message); - // Construct full path: directory + normalized file path - const fullPath = params.directory.endsWith('/') - ? `${params.directory}${fileName}` - : `${params.directory}/${fileName}`; - violations.push({ - file: fullPath, + file: fileName, lines: lines.sort((a, b) => a - b), violation, }); @@ -92,6 +88,7 @@ export const reportViolationsHandler = createHandler< const report = { component: params.componentName, violations, + rootPath: params.directory, }; if (params.saveAsFile) { diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts index 9fbbbd6..f180761 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts @@ -47,5 +47,5 @@ export function convertComponentToFileFormat( ([file, components]) => ({ file, components }), ).sort((a, b) => a.file.localeCompare(b.file)); - return { files }; + return { files, rootPath: report.rootPath }; } From e661c57ad434123a0cee7051436e6a4769f1f91e Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 25 Nov 2025 12:07:04 +0200 Subject: [PATCH 06/13] fix: formatting fix --- .../tools/ds/report-violations/report-all-violations.tool.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 43225a0..3e230c0 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 @@ -98,8 +98,8 @@ export const reportAllViolationsHandler = createHandler< // Early exit for empty results if (failedAudits.length === 0) { const report = - params.groupBy === 'file' - ? { files: [], rootPath: params.directory } + params.groupBy === 'file' + ? { files: [], rootPath: params.directory } : { components: [], rootPath: params.directory }; if (params.saveAsFile) { From 2ad2aab6f272360472f1969387ae3efadf8e628e Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 25 Nov 2025 12:15:11 +0200 Subject: [PATCH 07/13] feat: default save location for contracts --- .../builder/build-component-contract.tool.ts | 19 +++++++++++++++++-- .../builder/models/schema.ts | 4 ++-- .../diff/diff-component-contract.tool.ts | 19 +++++++++++++++++-- .../component-contract/diff/models/schema.ts | 4 ++-- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts index 4f9acb8..57db2b8 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts @@ -10,7 +10,7 @@ import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path import { createHash } from 'node:crypto'; interface BuildComponentContractOptions extends BaseHandlerOptions { - saveLocation: string; + saveLocation?: string; templateFile?: string; styleFile?: string; typescriptFile: string; @@ -36,6 +36,18 @@ export const buildComponentContractHandler = createHandler< typescriptFile, ); + // Generate default save location if not provided + const { join } = await import('node:path'); + const { basename } = await import('node:path'); + const defaultSaveLocation = saveLocation + ? saveLocation + : join( + 'tmp', + '.angular-toolkit-mcp', + 'contracts', + `${basename(typescriptFile, '.ts')}-contract.json`, + ); + // If templateFile or styleFile are not provided, use the TypeScript file path // This indicates inline template/styles const effectiveTemplatePath = templateFile @@ -55,7 +67,10 @@ export const buildComponentContractHandler = createHandler< const contractString = JSON.stringify(contract, null, 2); const hash = createHash('sha256').update(contractString).digest('hex'); - const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation); + const effectiveSaveLocation = resolveCrossPlatformPath( + cwd, + defaultSaveLocation, + ); const { mkdir, writeFile } = await import('node:fs/promises'); const { dirname } = await import('node:path'); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts index 4459d2c..8fa2e43 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts @@ -14,7 +14,7 @@ export const buildComponentContractSchema: ToolSchemaOptions = { saveLocation: { type: 'string', description: - 'Path where to save the contract file. Supports both absolute and relative paths.', + 'Path where to save the contract file. Supports both absolute and relative paths. If not provided, defaults to tmp/.angular-toolkit-mcp/contracts/-contract.json', }, templateFile: { type: 'string', @@ -37,7 +37,7 @@ export const buildComponentContractSchema: ToolSchemaOptions = { default: '', }, }, - required: ['saveLocation', 'typescriptFile'], + required: ['typescriptFile'], }, annotations: { title: 'Build Component Contract', diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts index 92d16db..0080355 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts @@ -18,7 +18,7 @@ import { writeFile, mkdir } from 'node:fs/promises'; import diff from 'microdiff'; interface DiffComponentContractOptions extends BaseHandlerOptions { - saveLocation: string; + saveLocation?: string; contractBeforePath: string; contractAfterPath: string; dsComponentName?: string; @@ -46,6 +46,18 @@ export const diffComponentContractHandler = createHandler< ); const effectiveAfterPath = resolveCrossPlatformPath(cwd, contractAfterPath); + // Generate default save location if not provided + const { join, basename } = await import('node:path'); + const defaultSaveLocation = saveLocation + ? saveLocation + : join( + 'tmp', + '.angular-toolkit-mcp', + 'contracts', + 'diffs', + `${basename(contractBeforePath, '-contract.json')}-diff.json`, + ); + const contractBefore = await loadContract(effectiveBeforePath); const contractAfter = await loadContract(effectiveAfterPath); @@ -68,7 +80,10 @@ export const diffComponentContractHandler = createHandler< const normalizedDiffData = normalizePathsInObject(diffData, workspaceRoot); - const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation); + const effectiveSaveLocation = resolveCrossPlatformPath( + cwd, + defaultSaveLocation, + ); const { dirname } = await import('node:path'); await mkdir(dirname(effectiveSaveLocation), { recursive: true }); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts index 7d05a6e..4afca00 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts @@ -14,7 +14,7 @@ export const diffComponentContractSchema: ToolSchemaOptions = { saveLocation: { type: 'string', description: - 'Path where to save the diff result file. Supports both absolute and relative paths.', + 'Path where to save the diff result file. Supports both absolute and relative paths. If not provided, defaults to tmp/.angular-toolkit-mcp/contracts/diffs/-diff.json', }, contractBeforePath: { type: 'string', @@ -32,7 +32,7 @@ export const diffComponentContractSchema: ToolSchemaOptions = { default: '', }, }, - required: ['saveLocation', 'contractBeforePath', 'contractAfterPath'], + required: ['contractBeforePath', 'contractAfterPath'], }, annotations: { title: 'Diff Component Contract', From 26f09820280c7073497e7b0939cd8601e5ab7698 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 2 Dec 2025 13:10:45 +0200 Subject: [PATCH 08/13] feat: introduce constants for default output paths and refactor handlers to use them --- .../builder/build-component-contract.tool.ts | 13 +++++-------- .../diff/diff-component-contract.tool.ts | 11 ++++------- .../report-violations/group-violations.tool.ts | 11 +++++------ .../report-all-violations.tool.ts | 16 +++++++--------- .../report-violations/report-violations.tool.ts | 11 +++++------ .../src/lib/tools/ds/shared/constants.ts | 15 +++++++++++++++ .../src/lib/tools/ds/shared/index.ts | 1 + 7 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/shared/constants.ts diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts index 57db2b8..43d1cfa 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts @@ -7,7 +7,10 @@ import { buildComponentContract } from './utils/build-contract.js'; import { generateContractSummary } from '../shared/utils/contract-file-ops.js'; import { ContractResult } from './models/types.js'; import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; +import { DEFAULT_OUTPUT_BASE, OUTPUT_SUBDIRS } from '../../shared/constants.js'; import { createHash } from 'node:crypto'; +import { join, basename, dirname } from 'node:path'; +import { mkdir, writeFile } from 'node:fs/promises'; interface BuildComponentContractOptions extends BaseHandlerOptions { saveLocation?: string; @@ -36,15 +39,11 @@ export const buildComponentContractHandler = createHandler< typescriptFile, ); - // Generate default save location if not provided - const { join } = await import('node:path'); - const { basename } = await import('node:path'); const defaultSaveLocation = saveLocation ? saveLocation : join( - 'tmp', - '.angular-toolkit-mcp', - 'contracts', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.CONTRACTS, `${basename(typescriptFile, '.ts')}-contract.json`, ); @@ -72,8 +71,6 @@ export const buildComponentContractHandler = createHandler< defaultSaveLocation, ); - const { mkdir, writeFile } = await import('node:fs/promises'); - const { dirname } = await import('node:path'); await mkdir(dirname(effectiveSaveLocation), { recursive: true }); const contractData = { diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts index 0080355..f86ee08 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts @@ -6,6 +6,7 @@ import { resolveCrossPlatformPath, normalizePathsInObject, } from '../../shared/utils/cross-platform-path.js'; +import { DEFAULT_OUTPUT_BASE, OUTPUT_SUBDIRS } from '../../shared/constants.js'; import { diffComponentContractSchema } from './models/schema.js'; import type { DomPathDictionary } from '../shared/models/types.js'; import { loadContract } from '../shared/utils/contract-file-ops.js'; @@ -15,6 +16,7 @@ import { generateDiffSummary, } from './utils/diff-utils.js'; import { writeFile, mkdir } from 'node:fs/promises'; +import { join, basename, dirname } from 'node:path'; import diff from 'microdiff'; interface DiffComponentContractOptions extends BaseHandlerOptions { @@ -46,15 +48,11 @@ export const diffComponentContractHandler = createHandler< ); const effectiveAfterPath = resolveCrossPlatformPath(cwd, contractAfterPath); - // Generate default save location if not provided - const { join, basename } = await import('node:path'); const defaultSaveLocation = saveLocation ? saveLocation : join( - 'tmp', - '.angular-toolkit-mcp', - 'contracts', - 'diffs', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.CONTRACT_DIFFS, `${basename(contractBeforePath, '-contract.json')}-diff.json`, ); @@ -85,7 +83,6 @@ export const diffComponentContractHandler = createHandler< defaultSaveLocation, ); - const { dirname } = await import('node:path'); await mkdir(dirname(effectiveSaveLocation), { recursive: true }); const diffFilePath = effectiveSaveLocation; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts index a04afd2..6cb1603 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts @@ -1,5 +1,6 @@ import { createHandler } from '../shared/utils/handler-helpers.js'; import { normalizeAbsolutePathToRelative } from '../shared/utils/cross-platform-path.js'; +import { DEFAULT_OUTPUT_BASE, OUTPUT_SUBDIRS } from '../shared/constants.js'; import { readFile, writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; import type { @@ -35,9 +36,8 @@ export const groupViolationsHandler = createHandler< // Read violations file const inputPath = join( cwd, - 'tmp', - '.angular-toolkit-mcp', - 'violations-report', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.VIOLATIONS_REPORT, params.fileName, ); @@ -163,9 +163,8 @@ export const groupViolationsHandler = createHandler< const reportName = params.fileName.replace('.json', ''); const outputDir = join( cwd, - 'tmp', - '.angular-toolkit-mcp', - 'violation-groups', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.VIOLATION_GROUPS, reportName, ); 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 3e230c0..ce168ca 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 @@ -1,5 +1,6 @@ import { createHandler } from '../shared/utils/handler-helpers.js'; import { normalizeAbsolutePathToRelative } from '../shared/utils/cross-platform-path.js'; +import { DEFAULT_OUTPUT_BASE, OUTPUT_SUBDIRS } from '../shared/constants.js'; import { analyzeProjectCoverage, extractComponentName, @@ -105,9 +106,8 @@ export const reportAllViolationsHandler = createHandler< if (params.saveAsFile) { const outputDir = join( cwd, - 'tmp', - '.angular-toolkit-mcp', - 'violations-report', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.VIOLATIONS_REPORT, ); const filename = generateFilename(params.directory); const filePath = join(outputDir, filename); @@ -160,9 +160,8 @@ export const reportAllViolationsHandler = createHandler< if (params.saveAsFile) { const outputDir = join( cwd, - 'tmp', - '.angular-toolkit-mcp', - 'violations-report', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.VIOLATIONS_REPORT, ); const filename = generateFilename(params.directory); const filePath = join(outputDir, filename); @@ -211,9 +210,8 @@ export const reportAllViolationsHandler = createHandler< if (params.saveAsFile) { const outputDir = join( cwd, - 'tmp', - '.angular-toolkit-mcp', - 'violations-report', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.VIOLATIONS_REPORT, ); const filename = generateFilename(params.directory); const filePath = join(outputDir, filename); 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 2e8517d..bc456eb 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,5 +1,6 @@ import { createHandler } from '../shared/utils/handler-helpers.js'; import { normalizeAbsolutePathToRelative } from '../shared/utils/cross-platform-path.js'; +import { DEFAULT_OUTPUT_BASE, OUTPUT_SUBDIRS } from '../shared/constants.js'; import { analyzeViolationsBase } from '../shared/violation-analysis/base-analyzer.js'; import { groupIssuesByFile, @@ -42,9 +43,8 @@ export const reportViolationsHandler = createHandler< if (params.saveAsFile) { const outputDir = join( cwd, - 'tmp', - '.angular-toolkit-mcp', - 'violations-report', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.VIOLATIONS_REPORT, params.componentName, ); const filename = generateFilename(params.directory); @@ -94,9 +94,8 @@ export const reportViolationsHandler = createHandler< if (params.saveAsFile) { const outputDir = join( cwd, - 'tmp', - '.angular-toolkit-mcp', - 'violations-report', + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS.VIOLATIONS_REPORT, params.componentName, ); const filename = generateFilename(params.directory); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/constants.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/constants.ts new file mode 100644 index 0000000..c555c81 --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/constants.ts @@ -0,0 +1,15 @@ +/** + * Default output directory for all generated files. + * Used as the base path for contracts, violations reports, and work groups. + */ +export const DEFAULT_OUTPUT_BASE = 'tmp/.angular-toolkit-mcp'; + +/** + * Subdirectory paths relative to DEFAULT_OUTPUT_BASE + */ +export const OUTPUT_SUBDIRS = { + CONTRACTS: 'contracts', + CONTRACT_DIFFS: 'contracts/diffs', + VIOLATIONS_REPORT: 'violations-report', + VIOLATION_GROUPS: 'violation-groups', +} as const; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts index 0d8b0e1..1c42c56 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/index.ts @@ -5,3 +5,4 @@ export * from './utils/component-validation.js'; export * from './utils/output.utils.js'; export * from './utils/cross-platform-path.js'; export * from './models/schema-helpers.js'; +export * from './constants.js'; From 3e2572987f3ad890bbdfd1ed70ce554fdf1a32d7 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 2 Dec 2025 13:43:30 +0200 Subject: [PATCH 09/13] feat: enhance report generation by mapping work groups to report format and improving file conversion logic --- README.md | 4 +- .../group-violations.tool.ts | 32 +--------- .../utils/format-converter.utils.ts | 19 +++--- .../tools/ds/report-violations/utils/index.ts | 3 +- .../utils/work-group.utils.ts | 61 +++++++++++++++---- 5 files changed, 63 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index ccdb827..8f00c11 100644 --- a/README.md +++ b/README.md @@ -186,9 +186,9 @@ my-angular-workspace/ ### Component Analysis -- **`report-violations`**: Report deprecated CSS usage for a specific DS component in a directory. Supports optional file output with statistics (components, files, lines). +- **`report-violations`**: Report deprecated CSS usage for a specific component in a directory. Supports optional file output with statistics. -- **`report-all-violations`**: Report all deprecated CSS usage across all DS components in a directory. Supports optional file output with statistics (components, files, lines). +- **`report-all-violations`**: Report all deprecated CSS usage across all components in a directory. Supports optional file output with statistics. - **`group-violations`**: Creates balanced work distribution groups from violations reports using bin-packing algorithm. Maintains path exclusivity and directory boundaries for parallel development. diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts index 6cb1603..0aac829 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts @@ -19,6 +19,7 @@ import { determineOptimalGroups, createWorkGroups, generateGroupMarkdown, + mapWorkGroupToReportGroup, } from './utils/index.js'; export { groupViolationsSchema }; @@ -33,7 +34,6 @@ export const groupViolationsHandler = createHandler< const maxGroups = params.maxGroups ?? 5; const variance = params.variance ?? 20; - // Read violations file const inputPath = join( cwd, DEFAULT_OUTPUT_BASE, @@ -51,7 +51,6 @@ export const groupViolationsHandler = createHandler< ); } - // Detect format and convert if necessary const format = detectReportFormat(rawData); if (format === 'unknown') { @@ -75,10 +74,8 @@ export const groupViolationsHandler = createHandler< throw new Error('No violations found in the input file'); } - // Extract rootPath from the violations data const rootPath = violationsData.rootPath || ''; - // Enrich files with metadata const enrichedFiles = enrichFiles(violationsData.files); const totalViolations = enrichedFiles.reduce( (sum, f) => sum + f.violations, @@ -86,10 +83,8 @@ export const groupViolationsHandler = createHandler< ); const totalFiles = enrichedFiles.length; - // Group by directory const directorySummary = groupByDirectory(enrichedFiles); - // Determine optimal number of groups const optimalGroups = determineOptimalGroups( totalViolations, directorySummary, @@ -102,14 +97,12 @@ export const groupViolationsHandler = createHandler< const minAcceptable = Math.floor(targetPerGroup * (1 - variance / 100)); const maxAcceptable = Math.ceil(targetPerGroup * (1 + variance / 100)); - // Create work groups const groups = createWorkGroups( directorySummary, optimalGroups, maxAcceptable, ); - // Validation const totalGroupViolations = groups.reduce( (sum, g) => sum + g.violations, 0, @@ -121,7 +114,6 @@ export const groupViolationsHandler = createHandler< (g) => g.violations >= minAcceptable && g.violations <= maxAcceptable, ); - // Create output structure const report: GroupViolationsReport = { metadata: { generatedAt: new Date().toISOString(), @@ -134,22 +126,7 @@ export const groupViolationsHandler = createHandler< acceptableRange: { min: minAcceptable, max: maxAcceptable }, variance, }, - groups: groups.map((g) => ({ - id: g.id, - name: g.name, - rootPath, - directories: g.directories, - files: g.files.map((f) => ({ - file: f.file, - violations: f.violations, - components: f.components, - })), - statistics: { - fileCount: g.files.length, - violationCount: g.violations, - }, - componentDistribution: g.componentDistribution, - })), + groups: groups.map((g) => mapWorkGroupToReportGroup(g, rootPath)), validation: { totalViolations: totalGroupViolations, totalFiles: allFilesInGroups.length, @@ -159,7 +136,6 @@ export const groupViolationsHandler = createHandler< }, }; - // Save each group as individual file const reportName = params.fileName.replace('.json', ''); const outputDir = join( cwd, @@ -170,7 +146,6 @@ export const groupViolationsHandler = createHandler< await mkdir(outputDir, { recursive: true }); - // Save metadata file const metadataPath = join(outputDir, 'metadata.json'); await writeFile( metadataPath, @@ -185,13 +160,10 @@ export const groupViolationsHandler = createHandler< 'utf-8', ); - // Save each group as separate file (JSON + Markdown) for (const group of report.groups) { - // Save JSON const groupPath = join(outputDir, `group-${group.id}.json`); await writeFile(groupPath, JSON.stringify(group, null, 2), 'utf-8'); - // Save Markdown report const markdownPath = join(outputDir, `group-${group.id}.md`); const markdown = generateGroupMarkdown(group); await writeFile(markdownPath, markdown, 'utf-8'); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts index f180761..95167ce 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/format-converter.utils.ts @@ -25,22 +25,19 @@ export function detectReportFormat( export function convertComponentToFileFormat( report: AllViolationsReport, ): AllViolationsReportByFile { - const fileMap = new Map(); - - report.components.forEach((componentReport) => { - componentReport.violations.forEach((violation) => { - if (!fileMap.has(violation.file)) { - fileMap.set(violation.file, []); - } - - fileMap.get(violation.file)!.push({ + const fileMap = report.components.reduce((map, componentReport) => { + return componentReport.violations.reduce((m, violation) => { + const existing = m.get(violation.file) ?? []; + existing.push({ component: componentReport.component, lines: violation.lines, violation: violation.violation, replacement: violation.replacement, }); - }); - }); + m.set(violation.file, existing); + return m; + }, map); + }, new Map()); const files: FileViolationReport[] = Array.from( fileMap.entries(), diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts index e8595b5..26ef569 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts @@ -24,7 +24,8 @@ export { assignGroupName, calculateComponentDistribution, createWorkGroups, + mapWorkGroupToReportGroup, } from './work-group.utils.js'; -export type { WorkGroup } from './work-group.utils.js'; +export type { WorkGroup, ReportGroup } from './work-group.utils.js'; export { generateGroupMarkdown } from './markdown-generator.utils.js'; export type { GroupForMarkdown } from './markdown-generator.utils.js'; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts index 0e4234e..03d53a8 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts @@ -1,5 +1,6 @@ import type { EnrichedFile } from './file-enrichment.utils.js'; import type { DirectorySummary } from './directory-grouping.utils.js'; +import type { FileViolationReport } from '../models/types.js'; export interface WorkGroup { id: number; @@ -10,6 +11,48 @@ export interface WorkGroup { componentDistribution: Record; } +export interface ReportGroup { + id: number; + name: string; + rootPath: string; + directories: string[]; + files: Array<{ + file: string; + violations: number; + components: FileViolationReport['components']; + }>; + statistics: { + fileCount: number; + violationCount: number; + }; + componentDistribution: Record; +} + +/** + * Map a WorkGroup to the report group format + */ +export function mapWorkGroupToReportGroup( + group: WorkGroup, + rootPath: string, +): ReportGroup { + return { + id: group.id, + name: group.name, + rootPath, + directories: group.directories, + files: group.files.map((f) => ({ + file: f.file, + violations: f.violations, + components: f.components, + })), + statistics: { + fileCount: group.files.length, + violationCount: group.violations, + }, + componentDistribution: group.componentDistribution, + }; +} + /** * Assign group name based on primary directory */ @@ -27,18 +70,12 @@ export function assignGroupName( export function calculateComponentDistribution( files: EnrichedFile[], ): Record { - const distribution: Record = {}; - - files.forEach((file) => { - file.components.forEach((comp) => { - if (!distribution[comp.component]) { - distribution[comp.component] = 0; - } - distribution[comp.component] += comp.lines.length; - }); - }); - - return distribution; + return files.reduce>((distribution, file) => { + return file.components.reduce((dist, comp) => { + dist[comp.component] = (dist[comp.component] || 0) + comp.lines.length; + return dist; + }, distribution); + }, {}); } /** From c0f974397ad3ac5a2332b0e9189675cf977aaed2 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 2 Dec 2025 14:18:23 +0200 Subject: [PATCH 10/13] fix: update saveLocation description to clarify contract naming for comparison --- .../lib/tools/ds/component-contract/builder/models/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts index 8fa2e43..88aeb1e 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts @@ -14,7 +14,7 @@ export const buildComponentContractSchema: ToolSchemaOptions = { saveLocation: { type: 'string', description: - 'Path where to save the contract file. Supports both absolute and relative paths. If not provided, defaults to tmp/.angular-toolkit-mcp/contracts/-contract.json', + 'Path where to save the contract file. Supports both absolute and relative paths. If not provided, defaults to tmp/.angular-toolkit-mcp/contracts/-contract.json. When building contracts for comparison, use descriptive names like -before-contract.json or -after-contract.json to distinguish between refactoring phases.', }, templateFile: { type: 'string', From 3b225d84189cd11a79002eca5c3c7bdd3b6a7d3e Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 2 Dec 2025 14:29:23 +0200 Subject: [PATCH 11/13] feat: update file path descriptions in schemas to use constants for output locations --- .../component-contract/builder/models/schema.ts | 9 ++++++--- .../ds/component-contract/diff/models/schema.ts | 15 ++++++++------- .../tools/ds/report-violations/models/schema.ts | 16 ++++++++-------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts index 88aeb1e..8779d1e 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts @@ -1,5 +1,9 @@ import { ToolSchemaOptions } from '@push-based/models'; -import { COMMON_ANNOTATIONS } from '../../../shared/index.js'; +import { + COMMON_ANNOTATIONS, + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS, +} from '../../../shared/index.js'; /** * Schema for building component contracts @@ -13,8 +17,7 @@ export const buildComponentContractSchema: ToolSchemaOptions = { properties: { saveLocation: { type: 'string', - description: - 'Path where to save the contract file. Supports both absolute and relative paths. If not provided, defaults to tmp/.angular-toolkit-mcp/contracts/-contract.json. When building contracts for comparison, use descriptive names like -before-contract.json or -after-contract.json to distinguish between refactoring phases.', + description: `Path where to save the contract file. Supports both absolute and relative paths. If not provided, defaults to ${DEFAULT_OUTPUT_BASE}/${OUTPUT_SUBDIRS.CONTRACTS}/-contract.json. When building contracts for comparison, use descriptive names like -before-contract.json or -after-contract.json to distinguish between refactoring phases.`, }, templateFile: { type: 'string', diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts index 4afca00..f0f2e6a 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts @@ -1,5 +1,9 @@ import { ToolSchemaOptions } from '@push-based/models'; -import { COMMON_ANNOTATIONS } from '../../../shared/index.js'; +import { + COMMON_ANNOTATIONS, + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS, +} from '../../../shared/index.js'; /** * Schema for diffing component contracts @@ -13,18 +17,15 @@ export const diffComponentContractSchema: ToolSchemaOptions = { properties: { saveLocation: { type: 'string', - description: - 'Path where to save the diff result file. Supports both absolute and relative paths. If not provided, defaults to tmp/.angular-toolkit-mcp/contracts/diffs/-diff.json', + description: `Path where to save the diff result file. Supports both absolute and relative paths. If not provided, defaults to ${DEFAULT_OUTPUT_BASE}/${OUTPUT_SUBDIRS.CONTRACT_DIFFS}/-diff.json`, }, contractBeforePath: { type: 'string', - description: - 'Path to the contract file before refactoring. Supports both absolute and relative paths.', + description: `Path to the contract file before refactoring. Supports both absolute and relative paths. Typically located at ${DEFAULT_OUTPUT_BASE}/${OUTPUT_SUBDIRS.CONTRACTS}/-before-contract.json`, }, contractAfterPath: { type: 'string', - description: - 'Path to the contract file after refactoring. Supports both absolute and relative paths.', + description: `Path to the contract file after refactoring. Supports both absolute and relative paths. Typically located at ${DEFAULT_OUTPUT_BASE}/${OUTPUT_SUBDIRS.CONTRACTS}/-after-contract.json`, }, dsComponentName: { type: 'string', diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts index 94c122d..cf0d238 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts @@ -3,6 +3,10 @@ import { createProjectAnalysisSchema, COMMON_ANNOTATIONS, } from '../../shared/models/schema-helpers.js'; +import { + DEFAULT_OUTPUT_BASE, + OUTPUT_SUBDIRS, +} from '../../shared/constants.js'; export const reportViolationsSchema = { name: 'report-violations', @@ -10,8 +14,7 @@ export const reportViolationsSchema = { inputSchema: createViolationReportingSchema({ saveAsFile: { type: 'boolean', - description: - 'If true, saves the violations report to /tmp/.angular-toolkit-mcp/violations-report// with filename pattern -violations.json (e.g., packages-poker-core-lib-violations.json). Overwrites if file exists.', + description: `If true, saves the violations report to /${DEFAULT_OUTPUT_BASE}/${OUTPUT_SUBDIRS.VIOLATIONS_REPORT}// with filename pattern -violations.json (e.g., packages-poker-core-lib-violations.json). Overwrites if file exists.`, }, }), annotations: { @@ -34,8 +37,7 @@ export const reportAllViolationsSchema = { }, saveAsFile: { type: 'boolean', - description: - 'If true, saves the violations report to /tmp/.angular-toolkit-mcp/violations-report/ with filename pattern -violations.json (e.g., packages-poker-core-lib-violations.json). Overwrites if file exists.', + description: `If true, saves the violations report to /${DEFAULT_OUTPUT_BASE}/${OUTPUT_SUBDIRS.VIOLATIONS_REPORT}/ with filename pattern -violations.json (e.g., packages-poker-core-lib-violations.json). Overwrites if file exists.`, }, }), annotations: { @@ -46,15 +48,13 @@ export const reportAllViolationsSchema = { export const groupViolationsSchema = { name: 'group-violations', - description: - 'Creates work distribution groups from violations report. Reads a violations JSON file (e.g., packages-poker-violations.json) from tmp/.angular-toolkit-mcp/violations-report/ and creates balanced work groups using bin-packing algorithm. Accepts both file-grouped and component-grouped violation reports. Groups are balanced by violation count, maintain path exclusivity (each file in one group), and preserve directory boundaries for parallel development.', + description: `Creates work distribution groups from violations report. Reads a violations JSON file (e.g., packages-poker-violations.json) from ${DEFAULT_OUTPUT_BASE}/${OUTPUT_SUBDIRS.VIOLATIONS_REPORT}/ and creates balanced work groups using bin-packing algorithm. Accepts both file-grouped and component-grouped violation reports. Groups are balanced by violation count, maintain path exclusivity (each file in one group), and preserve directory boundaries for parallel development.`, inputSchema: { type: 'object' as const, properties: { fileName: { type: 'string', - description: - 'Name of the violations JSON file (e.g., "packages-poker-violations.json"). File must exist in tmp/.angular-toolkit-mcp/violations-report/', + description: `Name of the violations JSON file (e.g., "packages-poker-violations.json"). File must exist in ${DEFAULT_OUTPUT_BASE}/${OUTPUT_SUBDIRS.VIOLATIONS_REPORT}/`, }, minGroups: { type: 'number', From 8fb3707b64f0b5bf7141ece90f5f390354f90acd Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 2 Dec 2025 14:32:28 +0200 Subject: [PATCH 12/13] refactor: centralize type definitions by moving interfaces to a dedicated types file --- .../utils/directory-grouping.utils.ts | 9 +-- .../utils/file-enrichment.utils.ts | 7 +-- .../tools/ds/report-violations/utils/index.ts | 15 +++-- .../utils/markdown-generator.utils.ts | 16 +---- .../ds/report-violations/utils/stats.utils.ts | 7 +-- .../tools/ds/report-violations/utils/types.ts | 60 +++++++++++++++++++ .../utils/work-group.utils.ts | 35 ++--------- 7 files changed, 80 insertions(+), 69 deletions(-) create mode 100644 packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/types.ts diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/directory-grouping.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/directory-grouping.utils.ts index dd31107..88f6590 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/directory-grouping.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/directory-grouping.utils.ts @@ -1,11 +1,4 @@ -import type { EnrichedFile } from './file-enrichment.utils.js'; - -export interface DirectorySummary { - directory: string; - files: EnrichedFile[]; - fileCount: number; - violations: number; -} +import type { DirectorySummary, EnrichedFile } from './types.js'; /** * Group files by directory hierarchy diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/file-enrichment.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/file-enrichment.utils.ts index c39cc0b..713f7a9 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/file-enrichment.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/file-enrichment.utils.ts @@ -1,10 +1,5 @@ import type { FileViolationReport } from '../models/types.js'; - -export interface EnrichedFile extends FileViolationReport { - violations: number; - directory: string; - subdirectory: string; -} +import type { EnrichedFile } from './types.js'; /** * Calculate total violations for a file diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts index 26ef569..9bf6c82 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/index.ts @@ -8,24 +8,29 @@ export { calculateComponentGroupedStats, calculateFileGroupedStats, } from './stats.utils.js'; -export type { ViolationStats } from './stats.utils.js'; export { detectReportFormat, convertComponentToFileFormat, } from './format-converter.utils.js'; export { calculateViolations, enrichFiles } from './file-enrichment.utils.js'; -export type { EnrichedFile } from './file-enrichment.utils.js'; export { groupByDirectory, determineOptimalGroups, } from './directory-grouping.utils.js'; -export type { DirectorySummary } from './directory-grouping.utils.js'; export { assignGroupName, calculateComponentDistribution, createWorkGroups, mapWorkGroupToReportGroup, } from './work-group.utils.js'; -export type { WorkGroup, ReportGroup } from './work-group.utils.js'; export { generateGroupMarkdown } from './markdown-generator.utils.js'; -export type { GroupForMarkdown } from './markdown-generator.utils.js'; + +// Re-export all types from the centralized types file +export type { + ViolationStats, + EnrichedFile, + DirectorySummary, + WorkGroup, + ReportGroup, + GroupForMarkdown, +} from './types.js'; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/markdown-generator.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/markdown-generator.utils.ts index 005cb48..d186cbe 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/markdown-generator.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/markdown-generator.utils.ts @@ -1,18 +1,4 @@ -import type { FileViolationReport } from '../models/types.js'; - -export interface GroupForMarkdown { - name: string; - statistics: { - fileCount: number; - violationCount: number; - }; - componentDistribution: Record; - files: Array<{ - file: string; - violations: number; - components: FileViolationReport['components']; - }>; -} +import type { GroupForMarkdown } from './types.js'; /** * Generate markdown report for a group diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts index 95a98c9..a4dfcf0 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/stats.utils.ts @@ -3,12 +3,7 @@ import type { AllViolationsComponentReport, FileViolationReport, } from '../models/types.js'; - -export interface ViolationStats { - components: number; - files: number; - lines: number; -} +import type { ViolationStats } from './types.js'; /** * Calculate statistics for single component violations diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/types.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/types.ts new file mode 100644 index 0000000..255357a --- /dev/null +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/types.ts @@ -0,0 +1,60 @@ +import type { FileViolationReport } from '../models/types.js'; + +export interface ViolationStats { + components: number; + files: number; + lines: number; +} + +export interface EnrichedFile extends FileViolationReport { + violations: number; + directory: string; + subdirectory: string; +} + +export interface DirectorySummary { + directory: string; + files: EnrichedFile[]; + fileCount: number; + violations: number; +} + +export interface WorkGroup { + id: number; + name: string; + directories: string[]; + files: EnrichedFile[]; + violations: number; + componentDistribution: Record; +} + +export interface ReportGroup { + id: number; + name: string; + rootPath: string; + directories: string[]; + files: Array<{ + file: string; + violations: number; + components: FileViolationReport['components']; + }>; + statistics: { + fileCount: number; + violationCount: number; + }; + componentDistribution: Record; +} + +export interface GroupForMarkdown { + name: string; + statistics: { + fileCount: number; + violationCount: number; + }; + componentDistribution: Record; + files: Array<{ + file: string; + violations: number; + components: FileViolationReport['components']; + }>; +} diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts index 03d53a8..15a8522 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/utils/work-group.utils.ts @@ -1,32 +1,9 @@ -import type { EnrichedFile } from './file-enrichment.utils.js'; -import type { DirectorySummary } from './directory-grouping.utils.js'; -import type { FileViolationReport } from '../models/types.js'; - -export interface WorkGroup { - id: number; - name: string; - directories: string[]; - files: EnrichedFile[]; - violations: number; - componentDistribution: Record; -} - -export interface ReportGroup { - id: number; - name: string; - rootPath: string; - directories: string[]; - files: Array<{ - file: string; - violations: number; - components: FileViolationReport['components']; - }>; - statistics: { - fileCount: number; - violationCount: number; - }; - componentDistribution: Record; -} +import type { + DirectorySummary, + EnrichedFile, + ReportGroup, + WorkGroup, +} from './types.js'; /** * Map a WorkGroup to the report group format From ccb446c7cf2ada8fe99cfd28415beca14ce84999 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 2 Dec 2025 14:52:02 +0200 Subject: [PATCH 13/13] feat: implement resolveDefaultSaveLocation function for dynamic output path resolution and update related handlers --- docs/tools.md | 12 +++++------ .../builder/build-component-contract.tool.ts | 20 ++++++++++-------- .../diff/diff-component-contract.tool.ts | 21 +++++++++++-------- .../group-violations.tool.ts | 1 - .../ds/report-violations/models/schema.ts | 6 +++--- .../src/lib/tools/ds/shared/constants.ts | 20 ++++++++++++++++++ 6 files changed, 52 insertions(+), 28 deletions(-) diff --git a/docs/tools.md b/docs/tools.md index 721d62e..e6cbf16 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -1,17 +1,17 @@ -# Design System Tools for AI Agents +# Angular MCP Tools for AI Agents -This document provides comprehensive guidance for AI agents working with Angular Design System (DS) migration and analysis tools. Each tool is designed to support automated refactoring, validation, and analysis workflows. +This document provides comprehensive guidance for AI agents working with Angular migration and analysis tools. Each tool is designed to support automated refactoring, validation, and analysis workflows. ## Tool Categories ### 🔍 Project Analysis Tools #### `report-violations` -**Purpose**: Identifies deprecated DS CSS usage patterns for a specific DS component in Angular projects +**Purpose**: Identifies deprecated CSS usage patterns for a specific component in Angular projects **AI Usage**: Use when you need to analyze violations for a specific component before planning refactoring **Key Parameters**: - `directory`: Target analysis directory (use relative paths like `./src/app`) -- `componentName`: DS component class name (e.g., `DsButton`) +- `componentName`: Component class name (e.g., `DsButton`) - `groupBy`: `"file"` or `"folder"` for result organization - `saveAsFile`: Optional boolean - if `true`, saves report to `tmp/.angular-toolkit-mcp/violations-report//-violations.json` **Output**: @@ -20,14 +20,14 @@ This document provides comprehensive guidance for AI agents working with Angular **Best Practice**: Use `saveAsFile: true` when you need to persist results for later processing or grouping workflows #### `report-all-violations` -**Purpose**: Reports all deprecated DS CSS usage for every DS component within a directory +**Purpose**: Reports all deprecated CSS usage for every component within a directory **AI Usage**: Use for a fast, global inventory of violations across the codebase before narrowing to specific components **Key Parameters**: - `directory`: Target analysis directory (use relative paths like `./src/app`) - `groupBy`: `"component"` or `"file"` for result organization (default: `"component"`) - `saveAsFile`: Optional boolean - if `true`, saves report to `tmp/.angular-toolkit-mcp/violations-report/-violations.json` **Output**: -- Default: Structured violation reports grouped by component or file covering all DS components +- Default: Structured violation reports grouped by component or file covering all components - With `saveAsFile: true`: File path and statistics (components, files, lines) **Best Practice**: Use `saveAsFile: true` to persist results for grouping workflows or large-scale migration planning. The saved file can be used as input for work distribution grouping tools. diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts index 43d1cfa..2130ae9 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts @@ -7,9 +7,12 @@ import { buildComponentContract } from './utils/build-contract.js'; import { generateContractSummary } from '../shared/utils/contract-file-ops.js'; import { ContractResult } from './models/types.js'; import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; -import { DEFAULT_OUTPUT_BASE, OUTPUT_SUBDIRS } from '../../shared/constants.js'; +import { + OUTPUT_SUBDIRS, + resolveDefaultSaveLocation, +} from '../../shared/constants.js'; import { createHash } from 'node:crypto'; -import { join, basename, dirname } from 'node:path'; +import { dirname } from 'node:path'; import { mkdir, writeFile } from 'node:fs/promises'; interface BuildComponentContractOptions extends BaseHandlerOptions { @@ -39,13 +42,12 @@ export const buildComponentContractHandler = createHandler< typescriptFile, ); - const defaultSaveLocation = saveLocation - ? saveLocation - : join( - DEFAULT_OUTPUT_BASE, - OUTPUT_SUBDIRS.CONTRACTS, - `${basename(typescriptFile, '.ts')}-contract.json`, - ); + const defaultSaveLocation = resolveDefaultSaveLocation( + saveLocation, + OUTPUT_SUBDIRS.CONTRACTS, + typescriptFile, + '-contract.json', + ); // If templateFile or styleFile are not provided, use the TypeScript file path // This indicates inline template/styles diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts index f86ee08..d31b8ab 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts @@ -6,7 +6,10 @@ import { resolveCrossPlatformPath, normalizePathsInObject, } from '../../shared/utils/cross-platform-path.js'; -import { DEFAULT_OUTPUT_BASE, OUTPUT_SUBDIRS } from '../../shared/constants.js'; +import { + OUTPUT_SUBDIRS, + resolveDefaultSaveLocation, +} from '../../shared/constants.js'; import { diffComponentContractSchema } from './models/schema.js'; import type { DomPathDictionary } from '../shared/models/types.js'; import { loadContract } from '../shared/utils/contract-file-ops.js'; @@ -16,7 +19,7 @@ import { generateDiffSummary, } from './utils/diff-utils.js'; import { writeFile, mkdir } from 'node:fs/promises'; -import { join, basename, dirname } from 'node:path'; +import { dirname } from 'node:path'; import diff from 'microdiff'; interface DiffComponentContractOptions extends BaseHandlerOptions { @@ -48,13 +51,13 @@ export const diffComponentContractHandler = createHandler< ); const effectiveAfterPath = resolveCrossPlatformPath(cwd, contractAfterPath); - const defaultSaveLocation = saveLocation - ? saveLocation - : join( - DEFAULT_OUTPUT_BASE, - OUTPUT_SUBDIRS.CONTRACT_DIFFS, - `${basename(contractBeforePath, '-contract.json')}-diff.json`, - ); + const defaultSaveLocation = resolveDefaultSaveLocation( + saveLocation, + OUTPUT_SUBDIRS.CONTRACT_DIFFS, + contractBeforePath, + '-diff.json', + '-contract.json', + ); const contractBefore = await loadContract(effectiveBeforePath); const contractAfter = await loadContract(effectiveAfterPath); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts index 0aac829..5f59030 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/group-violations.tool.ts @@ -62,7 +62,6 @@ export const groupViolationsHandler = createHandler< let violationsData: AllViolationsReportByFile; if (format === 'component') { - // Convert component-grouped to file-grouped format violationsData = convertComponentToFileFormat( rawData as AllViolationsReport, ); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts index cf0d238..abc2e24 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/schema.ts @@ -10,7 +10,7 @@ import { export const reportViolationsSchema = { name: 'report-violations', - 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).`, + description: `Report deprecated CSS usage for a specific 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({ saveAsFile: { type: 'boolean', @@ -26,13 +26,13 @@ export const reportViolationsSchema = { export const reportAllViolationsSchema = { name: 'report-all-violations', description: - '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.', + 'Scan a directory for all deprecated 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" (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', + 'How to group the results: "component" (default) groups by component showing all files affected by each component, "file" groups by file path showing all components violated in each file', default: 'component', }, saveAsFile: { diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/constants.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/constants.ts index c555c81..d2f9ed7 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/constants.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/constants.ts @@ -1,3 +1,5 @@ +import { join, basename } from 'node:path'; + /** * Default output directory for all generated files. * Used as the base path for contracts, violations reports, and work groups. @@ -13,3 +15,21 @@ export const OUTPUT_SUBDIRS = { VIOLATIONS_REPORT: 'violations-report', VIOLATION_GROUPS: 'violation-groups', } as const; + + +export function resolveDefaultSaveLocation( + saveLocation: string | undefined, + subdir: string, + sourceFile: string, + suffix: string, + stripExtension = '.ts', +): string { + if (saveLocation) { + return saveLocation; + } + return join( + DEFAULT_OUTPUT_BASE, + subdir, + `${basename(sourceFile, stripExtension)}${suffix}`, + ); +}