erosolar-cli
Version:
Unified AI agent framework for the command line - Multi-provider support with schema-driven tools, code intelligence, and transparent reasoning
888 lines • 34.5 kB
JavaScript
/**
* Comprehensive validation runner for AI software engineering.
* Validates all changes (TypeScript, tests, lint) and provides intelligent error parsing
* with actionable fix suggestions - similar to Claude Code's validation flow.
*/
import { exec } from 'node:child_process';
import { readFile, access } from 'node:fs/promises';
import { join } from 'node:path';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
// ============================================================================
// Error Parsers
// ============================================================================
/**
* Parse TypeScript compiler errors into structured format
*/
export function parseTypeScriptErrors(output) {
const errors = [];
// Match patterns like: src/file.ts(10,5): error TS2322: Type 'string' is not assignable...
// or: src/file.ts:10:5 - error TS2322: Type 'string' is not assignable...
const patterns = [
/^(.+?)\((\d+),(\d+)\):\s*(error|warning)\s+(TS\d+):\s*(.+)$/gm,
/^(.+?):(\d+):(\d+)\s*-\s*(error|warning)\s+(TS\d+):\s*(.+)$/gm,
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(output)) !== null) {
const [, file, line, column, severity, code, message] = match;
const error = {
type: 'typescript',
file,
line: parseInt(line, 10),
column: parseInt(column, 10),
code,
message: message.trim(),
severity: severity === 'error' ? 'error' : 'warning',
suggestedFix: generateTypeScriptFix(code, message.trim(), file),
};
errors.push(error);
}
}
return errors;
}
/**
* Parse Jest/test runner errors into structured format
*/
export function parseTestErrors(output) {
const errors = [];
// Match Jest failure patterns
// FAIL src/tests/foo.test.ts
const failPattern = /FAIL\s+(.+\.(?:test|spec)\.[jt]sx?)/g;
let match;
while ((match = failPattern.exec(output)) !== null) {
const file = match[1];
// Try to extract specific test failure details
const testNamePattern = /✕\s+(.+?)(?:\s+\((\d+)\s*ms\))?$/gm;
let testMatch;
while ((testMatch = testNamePattern.exec(output)) !== null) {
errors.push({
type: 'test',
file,
message: `Test failed: ${testMatch[1]}`,
severity: 'error',
suggestedFix: {
description: 'Review test assertion and fix the code or update the test',
autoFixable: false,
fixType: 'manual',
fixDetails: {
manualSteps: [
`Open ${file}`,
`Find test: "${testMatch[1]}"`,
'Review assertion failure',
'Fix the code or update the expected value',
],
},
},
});
}
}
// Match assertion errors
const assertPattern = /expect\((.+?)\)\.(.+?)\((.+?)\)/g;
while ((match = assertPattern.exec(output)) !== null) {
errors.push({
type: 'test',
message: `Assertion failed: expect(${match[1]}).${match[2]}(${match[3]})`,
severity: 'error',
rawOutput: match[0],
});
}
return errors;
}
/**
* Parse ESLint errors into structured format
*/
export function parseLintErrors(output) {
const errors = [];
// Match ESLint output patterns
// /path/to/file.ts
// 10:5 error 'foo' is defined but never used @typescript-eslint/no-unused-vars
let currentFile = null;
const lines = output.split('\n');
for (const line of lines) {
const fileMatch = line.match(/^([^\s].+\.[jt]sx?)$/);
if (fileMatch) {
currentFile = fileMatch[1];
continue;
}
const errorMatch = line.match(/^\s+(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(.+)$/);
if (errorMatch && currentFile) {
const [, lineNum, column, severity, message, rule] = errorMatch;
errors.push({
type: 'lint',
file: currentFile,
line: parseInt(lineNum, 10),
column: parseInt(column, 10),
code: rule,
message: message.trim(),
severity: severity === 'error' ? 'error' : 'warning',
suggestedFix: generateLintFix(rule, message.trim(), currentFile, parseInt(lineNum, 10)),
});
}
}
return errors;
}
// ============================================================================
// Fix Generators
// ============================================================================
/**
* Generate suggested fix for TypeScript errors
*/
function generateTypeScriptFix(code, _message, file) {
const fixes = {
// Type assignment errors
'TS2322': () => ({
description: 'Type mismatch - update the type annotation or fix the value',
autoFixable: false,
fixType: 'manual',
fixDetails: {
file,
manualSteps: [
'Check if the assigned value is correct',
'Update the type annotation if the value is intentional',
'Or fix the value to match the expected type',
],
},
}),
// Property does not exist
'TS2339': () => ({
description: 'Property does not exist on type',
autoFixable: false,
fixType: 'manual',
fixDetails: {
file,
manualSteps: [
'Check if the property name is spelled correctly',
'Add the property to the interface/type definition',
'Use optional chaining (?.) if property might not exist',
],
},
}),
// Cannot find name
'TS2304': () => ({
description: 'Cannot find name - likely missing import',
autoFixable: true,
fixType: 'command',
fixDetails: {
command: 'Add the missing import statement at the top of the file',
manualSteps: [
'Identify what needs to be imported',
'Add import statement',
'Or declare the variable/type if it should be local',
],
},
}),
// Module not found
'TS2307': () => ({
description: 'Cannot find module - check import path or install package',
autoFixable: false,
fixType: 'manual',
fixDetails: {
manualSteps: [
'Check if the import path is correct',
'If it\'s an npm package, run: npm install <package>',
'If it\'s a local file, verify the path exists',
],
},
}),
// Argument type mismatch
'TS2345': () => ({
description: 'Argument type mismatch',
autoFixable: false,
fixType: 'manual',
fixDetails: {
file,
manualSteps: [
'Check the function signature for expected parameter types',
'Cast the argument if appropriate: (arg as ExpectedType)',
'Or fix the argument value to match expected type',
],
},
}),
// Object comparison warning
'TS2839': () => ({
description: 'Object comparison by reference - use proper comparison',
autoFixable: true,
fixType: 'edit',
fixDetails: {
file,
manualSteps: [
'Extract one side to a variable first',
'Or use JSON.stringify for deep comparison',
'Or use a proper equality function',
],
},
}),
// Unused variable
'TS6133': () => ({
description: 'Variable declared but never used',
autoFixable: true,
fixType: 'edit',
fixDetails: {
file,
manualSteps: [
'Remove the unused variable',
'Or prefix with underscore if intentionally unused: _variable',
'Or use the variable where intended',
],
},
}),
// Implicit any
'TS7006': () => ({
description: 'Parameter implicitly has an any type',
autoFixable: true,
fixType: 'edit',
fixDetails: {
file,
manualSteps: [
'Add explicit type annotation to the parameter',
'Example: (param: string) instead of (param)',
],
},
}),
};
const fixGenerator = fixes[code];
return fixGenerator?.();
}
/**
* Generate suggested fix for ESLint errors
*/
function generateLintFix(rule, _message, file, _line) {
const fixes = {
// Unused variables
'@typescript-eslint/no-unused-vars': () => ({
description: 'Remove unused variable or prefix with underscore',
autoFixable: true,
fixType: 'command',
fixDetails: {
command: `eslint --fix ${file}`,
manualSteps: [
'Remove the unused variable declaration',
'Or prefix with underscore: const _unused = ...',
],
},
}),
// Missing return type
'@typescript-eslint/explicit-function-return-type': () => ({
description: 'Add explicit return type to function',
autoFixable: false,
fixType: 'manual',
fixDetails: {
file,
manualSteps: [
'Add return type annotation after function parameters',
'Example: function foo(): ReturnType { ... }',
],
},
}),
// Prefer const
'prefer-const': () => ({
description: 'Use const instead of let for variables that are never reassigned',
autoFixable: true,
fixType: 'command',
fixDetails: {
command: `eslint --fix ${file}`,
},
}),
// No explicit any
'@typescript-eslint/no-explicit-any': () => ({
description: 'Replace any with a specific type',
autoFixable: false,
fixType: 'manual',
fixDetails: {
file,
manualSteps: [
'Replace `any` with the actual expected type',
'Use `unknown` if type is truly unknown and add type guards',
],
},
}),
};
const fixGenerator = fixes[rule];
return fixGenerator?.();
}
// ============================================================================
// Insight Helpers
// ============================================================================
function analyzeErrorCodes(errors) {
const counts = new Map();
for (const error of errors) {
const key = error.code || error.type;
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return Array.from(counts.entries())
.map(([code, count]) => ({ code, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 5);
}
function analyzeTopFiles(errors) {
const counts = new Map();
for (const error of errors) {
if (!error.file)
continue;
counts.set(error.file, (counts.get(error.file) ?? 0) + 1);
}
return Array.from(counts.entries())
.map(([file, count]) => ({ file, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 5);
}
function buildValidationInsights(errors, warnings) {
const rootCauseHints = new Set();
const recoveryPlan = [];
const hasTypeScriptErrors = errors.some(e => e.type === 'typescript');
const hasTestFailures = errors.some(e => e.type === 'test');
const hasLintErrors = errors.some(e => e.type === 'lint');
const hasBuildErrors = errors.some(e => e.type === 'build');
const autoFixable = errors.filter(e => e.suggestedFix?.autoFixable).length;
const codes = analyzeErrorCodes(errors);
const topFiles = analyzeTopFiles(errors);
const totalErrors = errors.length;
if (totalErrors === 0 && warnings.length === 0) {
return {
rootCauseHints: [],
recoveryPlan: [],
dominantErrorCodes: [],
topFiles: [],
};
}
// Root-cause heuristics
if (errors.some(e => e.code === 'TS2307')) {
rootCauseHints.add('TypeScript cannot find modules (TS2307) - check import paths or install missing dependencies.');
}
if (errors.some(e => e.code === 'TS2304')) {
rootCauseHints.add('Missing identifiers (TS2304) - add imports or definitions before rerunning types.');
}
if (errors.some(e => e.code === '@typescript-eslint/no-unused-vars')) {
rootCauseHints.add('Unused variable lint errors detected - these are usually auto-fixable with eslint --fix.');
}
if (hasTestFailures) {
rootCauseHints.add('Tests are failing - inspect the first failing test output and fix the assertion/code before rerunning all tests.');
}
if (hasBuildErrors) {
rootCauseHints.add('Build failed - resolve TypeScript errors first, then re-run the build to catch bundler issues.');
}
if (warnings.length > 0 && !hasTypeScriptErrors && !hasLintErrors && !hasTestFailures && !hasBuildErrors) {
rootCauseHints.add('Only warnings detected - ensure they are intentional or adjust configurations to silence intentional warnings.');
}
if (totalErrors > 25 && codes[0]) {
rootCauseHints.add(`High error volume (${totalErrors}) - tackle the dominant issue (${codes[0].code}) first to unlock cascading fixes.`);
}
// Recovery plan (ordered, concise)
const topCode = codes.at(0);
if (topCode) {
const { code, count } = topCode;
recoveryPlan.push(`Start with the dominant issue: ${code} (appears ${count}×). Fixing this usually removes many downstream errors.`);
}
if (hasTypeScriptErrors && errors.some(e => e.code === 'TS2307')) {
recoveryPlan.push('Verify import paths and run dependency install if needed (npm install <package>) to resolve module-not-found errors.');
}
if (hasTypeScriptErrors) {
recoveryPlan.push('Iterate with quick_typecheck after each fix to confirm the type surface is clean before running full validation.');
}
if (hasLintErrors && autoFixable > 0) {
recoveryPlan.push('Apply auto-fixes (e.g., npm run lint -- --fix) to clear stylistic/unused issues quickly.');
}
if (hasTestFailures) {
recoveryPlan.push('Re-run the specific failing test file in watch/verbose mode to verify fixes before full test suite (e.g., npm test -- <file>).');
}
if (recoveryPlan.length === 0) {
recoveryPlan.push('Address the first reported error, rerun quick_typecheck, then re-run validate_all_changes once the main blockers are fixed.');
}
return {
rootCauseHints: Array.from(rootCauseHints).slice(0, 6),
recoveryPlan: recoveryPlan.slice(0, 6),
dominantErrorCodes: codes,
topFiles,
};
}
function withInsights(result) {
const insights = buildValidationInsights(result.errors, result.warnings);
return { ...result, insights };
}
// ============================================================================
// Validation Runner
// ============================================================================
export class ValidationRunner {
config;
constructor(config) {
this.config = {
workingDir: config.workingDir,
phases: config.phases ?? ['typescript', 'build', 'test'],
stopOnFirstFailure: config.stopOnFirstFailure ?? false,
timeout: config.timeout ?? 300000,
verbose: config.verbose ?? false,
};
}
/**
* Run all validation phases
*/
async runAll() {
const startTime = Date.now();
const allErrors = [];
const allWarnings = [];
const summaryParts = [];
for (const phase of this.config.phases) {
const result = await this.runPhase(phase);
allErrors.push(...result.errors);
allWarnings.push(...result.warnings);
summaryParts.push(`${phase}: ${result.success ? '✓' : '✗'}`);
if (!result.success && this.config.stopOnFirstFailure) {
break;
}
}
const autoFixableCount = allErrors.filter(e => e.suggestedFix?.autoFixable).length;
return withInsights({
success: allErrors.length === 0,
phase: 'all',
errors: allErrors,
warnings: allWarnings,
summary: summaryParts.join(' | '),
durationMs: Date.now() - startTime,
autoFixableCount,
});
}
/**
* Run a specific validation phase
*/
async runPhase(phase) {
switch (phase) {
case 'typescript':
return this.runTypeScriptValidation();
case 'build':
return this.runBuildValidation();
case 'test':
return this.runTestValidation();
case 'lint':
return this.runLintValidation();
default:
throw new Error(`Unknown validation phase: ${phase}`);
}
}
/**
* Run TypeScript type checking
*/
async runTypeScriptValidation() {
const startTime = Date.now();
try {
// Check if tsconfig.json exists
await access(join(this.config.workingDir, 'tsconfig.json'));
}
catch {
return withInsights({
success: true,
phase: 'typescript',
errors: [],
warnings: [],
summary: 'TypeScript: skipped (no tsconfig.json)',
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
try {
const { stdout, stderr } = await execAsync('npx tsc --noEmit', {
cwd: this.config.workingDir,
timeout: this.config.timeout,
maxBuffer: 10 * 1024 * 1024,
});
const output = stdout + stderr;
const errors = parseTypeScriptErrors(output);
const warnings = errors.filter(e => e.severity === 'warning');
const actualErrors = errors.filter(e => e.severity === 'error');
return withInsights({
success: actualErrors.length === 0,
phase: 'typescript',
errors: actualErrors,
warnings,
summary: `TypeScript: ${actualErrors.length} error(s), ${warnings.length} warning(s)`,
durationMs: Date.now() - startTime,
autoFixableCount: errors.filter(e => e.suggestedFix?.autoFixable).length,
});
}
catch (error) {
const output = (error.stdout || '') + (error.stderr || '');
const errors = parseTypeScriptErrors(output);
if (errors.length === 0) {
errors.push({
type: 'typescript',
message: error.message || 'TypeScript compilation failed',
severity: 'error',
rawOutput: output,
});
}
const actualErrors = errors.filter(e => e.severity === 'error');
const warnings = errors.filter(e => e.severity === 'warning');
return withInsights({
success: false,
phase: 'typescript',
errors: actualErrors,
warnings,
summary: `TypeScript: ${actualErrors.length} error(s)`,
durationMs: Date.now() - startTime,
autoFixableCount: errors.filter(e => e.suggestedFix?.autoFixable).length,
});
}
}
/**
* Run build validation
*/
async runBuildValidation() {
const startTime = Date.now();
try {
// Check if build script exists
const packageJson = await readFile(join(this.config.workingDir, 'package.json'), 'utf-8');
const pkg = JSON.parse(packageJson);
if (!pkg.scripts?.build) {
return withInsights({
success: true,
phase: 'build',
errors: [],
warnings: [],
summary: 'Build: skipped (no build script)',
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
}
catch {
return withInsights({
success: true,
phase: 'build',
errors: [],
warnings: [],
summary: 'Build: skipped (no package.json)',
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
try {
await execAsync('npm run build', {
cwd: this.config.workingDir,
timeout: this.config.timeout,
maxBuffer: 10 * 1024 * 1024,
});
return withInsights({
success: true,
phase: 'build',
errors: [],
warnings: [],
summary: 'Build: ✓ passed',
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
catch (error) {
const output = (error.stdout || '') + (error.stderr || '');
// Try to parse as TypeScript errors first
let errors = parseTypeScriptErrors(output);
if (errors.length === 0) {
errors = [{
type: 'build',
message: 'Build failed',
severity: 'error',
rawOutput: output.slice(0, 2000),
}];
}
return withInsights({
success: false,
phase: 'build',
errors,
warnings: [],
summary: `Build: ✗ failed (${errors.length} error(s))`,
durationMs: Date.now() - startTime,
autoFixableCount: errors.filter(e => e.suggestedFix?.autoFixable).length,
});
}
}
/**
* Run test validation
*/
async runTestValidation() {
const startTime = Date.now();
try {
// Check if test script exists
const packageJson = await readFile(join(this.config.workingDir, 'package.json'), 'utf-8');
const pkg = JSON.parse(packageJson);
if (!pkg.scripts?.test) {
return withInsights({
success: true,
phase: 'test',
errors: [],
warnings: [],
summary: 'Tests: skipped (no test script)',
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
}
catch {
return withInsights({
success: true,
phase: 'test',
errors: [],
warnings: [],
summary: 'Tests: skipped (no package.json)',
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
try {
const { stdout, stderr } = await execAsync('npm test', {
cwd: this.config.workingDir,
timeout: this.config.timeout,
maxBuffer: 10 * 1024 * 1024,
});
const output = stdout + stderr;
// Check for test failures even in "successful" exit
const errors = parseTestErrors(output);
const actualErrors = errors.filter(e => e.severity === 'error');
return withInsights({
success: actualErrors.length === 0,
phase: 'test',
errors: actualErrors,
warnings: [],
summary: `Tests: ${actualErrors.length === 0 ? '✓ passed' : `✗ ${actualErrors.length} failure(s)`}`,
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
catch (error) {
const output = (error.stdout || '') + (error.stderr || '');
const errors = parseTestErrors(output);
if (errors.length === 0) {
errors.push({
type: 'test',
message: 'Tests failed',
severity: 'error',
rawOutput: output.slice(0, 2000),
});
}
return withInsights({
success: false,
phase: 'test',
errors,
warnings: [],
summary: `Tests: ✗ failed (${errors.length} failure(s))`,
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
}
/**
* Run lint validation
*/
async runLintValidation() {
const startTime = Date.now();
try {
// Check if lint script exists
const packageJson = await readFile(join(this.config.workingDir, 'package.json'), 'utf-8');
const pkg = JSON.parse(packageJson);
if (!pkg.scripts?.lint) {
return withInsights({
success: true,
phase: 'lint',
errors: [],
warnings: [],
summary: 'Lint: skipped (no lint script)',
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
}
catch {
return withInsights({
success: true,
phase: 'lint',
errors: [],
warnings: [],
summary: 'Lint: skipped (no package.json)',
durationMs: Date.now() - startTime,
autoFixableCount: 0,
});
}
try {
const { stdout, stderr } = await execAsync('npm run lint', {
cwd: this.config.workingDir,
timeout: this.config.timeout,
maxBuffer: 10 * 1024 * 1024,
});
const output = stdout + stderr;
const errors = parseLintErrors(output);
const actualErrors = errors.filter(e => e.severity === 'error');
const warnings = errors.filter(e => e.severity === 'warning');
return withInsights({
success: actualErrors.length === 0,
phase: 'lint',
errors: actualErrors,
warnings,
summary: `Lint: ${actualErrors.length} error(s), ${warnings.length} warning(s)`,
durationMs: Date.now() - startTime,
autoFixableCount: errors.filter(e => e.suggestedFix?.autoFixable).length,
});
}
catch (error) {
const output = (error.stdout || '') + (error.stderr || '');
const errors = parseLintErrors(output);
if (errors.length === 0) {
errors.push({
type: 'lint',
message: 'Lint check failed',
severity: 'error',
rawOutput: output.slice(0, 2000),
});
}
const actualErrors = errors.filter(e => e.severity === 'error');
const warnings = errors.filter(e => e.severity === 'warning');
return withInsights({
success: false,
phase: 'lint',
errors: actualErrors,
warnings,
summary: `Lint: ✗ ${actualErrors.length} error(s)`,
durationMs: Date.now() - startTime,
autoFixableCount: errors.filter(e => e.suggestedFix?.autoFixable).length,
});
}
}
}
// ============================================================================
// Formatting Helpers
// ============================================================================
/**
* Format validation result for display
*/
export function formatValidationResult(result) {
const lines = [];
lines.push(`## Validation ${result.success ? 'Passed ✓' : 'Failed ✗'}`);
lines.push(`Duration: ${(result.durationMs / 1000).toFixed(1)}s`);
lines.push('');
lines.push(`Summary: ${result.summary}`);
if (result.insights) {
const { rootCauseHints, recoveryPlan, dominantErrorCodes, topFiles } = result.insights;
if (rootCauseHints.length > 0) {
lines.push('');
lines.push('### Root Cause Hints');
for (const hint of rootCauseHints) {
lines.push(`- ${hint}`);
}
}
if (recoveryPlan.length > 0) {
lines.push('');
lines.push('### Recommended Recovery Plan');
recoveryPlan.forEach((step, idx) => {
lines.push(`${idx + 1}. ${step}`);
});
}
if (dominantErrorCodes.length > 0) {
lines.push('');
lines.push('### Dominant Errors');
dominantErrorCodes.slice(0, 3).forEach(({ code, count }) => {
lines.push(`- ${code}: ${count} occurrence(s)`);
});
}
if (topFiles.length > 0) {
lines.push('');
lines.push('### Files With Most Errors');
topFiles.slice(0, 3).forEach(({ file, count }) => {
lines.push(`- ${file}: ${count} issue(s)`);
});
}
}
if (result.errors.length > 0) {
lines.push('');
lines.push(`### Errors (${result.errors.length})`);
for (const error of result.errors.slice(0, 20)) {
const location = error.file
? `${error.file}${error.line ? `:${error.line}` : ''}${error.column ? `:${error.column}` : ''}`
: '';
const code = error.code ? `[${error.code}]` : '';
lines.push(`- ${location} ${code} ${error.message}`);
if (error.suggestedFix) {
lines.push(` Fix: ${error.suggestedFix.description}`);
if (error.suggestedFix.autoFixable) {
lines.push(` (Auto-fixable)`);
}
}
}
if (result.errors.length > 20) {
lines.push(`... and ${result.errors.length - 20} more errors`);
}
}
if (result.warnings.length > 0) {
lines.push('');
lines.push(`### Warnings (${result.warnings.length})`);
for (const warning of result.warnings.slice(0, 10)) {
const location = warning.file ? `${warning.file}:${warning.line || '?'}` : '';
lines.push(`- ${location} ${warning.message}`);
}
if (result.warnings.length > 10) {
lines.push(`... and ${result.warnings.length - 10} more warnings`);
}
}
if (result.autoFixableCount > 0) {
lines.push('');
lines.push(`### Auto-fixable: ${result.autoFixableCount} issue(s)`);
lines.push('Use the auto-fix feature to automatically resolve these issues.');
}
return lines.join('\n');
}
/**
* Format errors for AI consumption (structured for intelligent fixing)
*/
export function formatErrorsForAI(errors, insights) {
const lines = [];
lines.push('# Validation Errors for AI Analysis');
lines.push('');
lines.push('The following errors need to be fixed. For each error:');
lines.push('1. Read the file at the specified location');
lines.push('2. Understand the context around the error');
lines.push('3. Apply the suggested fix or determine the appropriate correction');
lines.push('');
if (insights) {
if (insights.rootCauseHints.length > 0) {
lines.push('## Root Cause Hints');
lines.push('');
for (const hint of insights.rootCauseHints) {
lines.push(`- ${hint}`);
}
lines.push('');
}
if (insights.recoveryPlan.length > 0) {
lines.push('## Recommended Recovery Plan');
lines.push('');
insights.recoveryPlan.forEach((step, idx) => {
lines.push(`${idx + 1}. ${step}`);
});
lines.push('');
}
}
const groupedByFile = new Map();
for (const error of errors) {
const key = error.file || 'unknown';
if (!groupedByFile.has(key)) {
groupedByFile.set(key, []);
}
groupedByFile.get(key).push(error);
}
for (const [file, fileErrors] of groupedByFile) {
lines.push(`## ${file}`);
lines.push('');
for (const error of fileErrors) {
lines.push(`### Line ${error.line || '?'}: ${error.code || error.type}`);
lines.push(`Message: ${error.message}`);
if (error.suggestedFix) {
lines.push(`Suggested fix: ${error.suggestedFix.description}`);
lines.push(`Auto-fixable: ${error.suggestedFix.autoFixable ? 'Yes' : 'No'}`);
if (error.suggestedFix.fixDetails.manualSteps) {
lines.push('Steps:');
for (const step of error.suggestedFix.fixDetails.manualSteps) {
lines.push(` - ${step}`);
}
}
}
lines.push('');
}
}
return lines.join('\n');
}
//# sourceMappingURL=validationRunner.js.map