meld
Version:
Meld: A template language for LLM prompts
357 lines (301 loc) • 13.3 kB
text/typescript
import { container } from 'tsyringe';
import { IStateService } from '@services/state/StateService/IStateService.js';
import { IParserService } from '@services/pipeline/ParserService/IParserService.js';
import { IInterpreterService } from '@services/pipeline/InterpreterService/IInterpreterService.js';
import { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js';
import { IDirectiveService } from '@services/pipeline/DirectiveService/IDirectiveService.js';
import { MeldResolutionError } from '@core/errors/MeldResolutionError.js';
import path from 'path';
import chalk from 'chalk';
import fs from 'fs/promises';
import { initializeContextDebugger, StateVisualizationService } from '../../tests/utils/debug/index.js';
import { IPathService } from '@services/fs/PathService/IPathService.js';
// Import concrete classes for direct instantiation
import { StateService } from '@services/state/StateService/StateService.js';
import { ParserService } from '@services/pipeline/ParserService/ParserService.js';
import { InterpreterService } from '@services/pipeline/InterpreterService/InterpreterService.js';
import { FileSystemService } from '@services/fs/FileSystemService/FileSystemService.js';
import { DirectiveService } from '@services/pipeline/DirectiveService/DirectiveService.js';
import { PathService } from '@services/fs/PathService/PathService.js';
import { NodeFileSystem } from '@services/fs/FileSystemService/NodeFileSystem.js';
import { PathOperationsService } from '@services/fs/FileSystemService/PathOperationsService.js';
import { ResolutionService } from '@services/resolution/ResolutionService/ResolutionService.js';
import { ValidationService } from '@services/resolution/ValidationService/ValidationService.js';
import { CircularityService } from '@services/resolution/CircularityService/CircularityService.js';
interface DebugTransformOptions {
filePath: string;
directiveType?: string;
outputFormat?: 'text' | 'json' | 'mermaid';
outputFile?: string;
includeContent?: boolean;
}
/**
* Debug node transformations through the pipeline
*/
export async function debugTransformCommand(options: DebugTransformOptions): Promise<void> {
const {
filePath,
directiveType,
outputFormat = 'text',
outputFile,
includeContent = false
} = options;
try {
// Try to get services from DI container (for tests)
let stateService, fileSystemService, parserService, directiveService, interpreterService;
try {
// For tests, try to get services from container
stateService = container.resolve('StateService');
fileSystemService = container.resolve('FileSystemService');
parserService = container.resolve('ParserService');
directiveService = container.resolve('DirectiveService');
interpreterService = container.resolve('InterpreterService');
console.log(chalk.blue('Using services from dependency injection container'));
} catch (error) {
// For runtime use direct instantiation
console.log(chalk.blue('Creating services directly...'));
// Create the path operations service (needed for FileSystemService)
const pathOps = new PathOperationsService();
// Create the node file system implementation
const nodeFs = new NodeFileSystem();
// Create the base services first
stateService = new StateService();
fileSystemService = new FileSystemService(pathOps, nodeFs);
parserService = new ParserService();
const pathService = new PathService();
// Initialize the path service
pathService.initialize(fileSystemService, pathOps);
// Create the resolution service
const resolutionService = new ResolutionService(
stateService,
fileSystemService,
parserService,
pathService
);
// Create the validation service
const validationService = new ValidationService();
// Create the interpreter service first
interpreterService = new InterpreterService();
// Create the circularity service
const circularityService = new CircularityService();
// Create the directive service
directiveService = new DirectiveService();
directiveService.initialize(
validationService,
stateService,
pathService,
fileSystemService,
parserService,
interpreterService, // InterpreterService
circularityService, // CircularityService
resolutionService
);
// Initialize the interpreter service
interpreterService.initialize(directiveService, stateService);
// Register default handlers
directiveService.registerDefaultHandlers();
}
// Set up state with proper paths
const resolvedPath = path.resolve(filePath);
const projectPath = path.dirname(resolvedPath);
console.log(chalk.blue('Project path:'), projectPath);
stateService.setPathVar('PROJECTPATH', projectPath);
stateService.setPathVar('.', projectPath);
stateService.setCurrentFilePath(filePath);
// Try to get the home directory
try {
const homePath = process.env.HOME || process.env.USERPROFILE;
if (homePath) {
stateService.setPathVar('HOMEPATH', homePath);
stateService.setPathVar('~', homePath);
}
} catch (error) {
console.warn(chalk.yellow('Could not set home path variables'));
}
// Enable transformation tracking
if (!interpreterService.canHandleTransformations()) {
console.error(chalk.red('This interpreter does not support transformations'));
console.error(chalk.yellow('Make sure you have built the codebase with "npm run build" before running debug commands'));
return;
}
console.log(chalk.blue(`Debugging transformations for ${filePath}`));
if (directiveType) {
console.log(chalk.blue(`Focusing on directive type: ${directiveType}`));
}
// Read and process the file
if (!await fileSystemService.exists(filePath)) {
console.error(chalk.red(`File not found: ${filePath}`));
return;
}
const fileContent = await fileSystemService.readFile(filePath);
// Use parse instead of parseWithLocations to match test expectations
const nodes = await parserService.parse(fileContent);
// Create a tracking proxy for directiveService
const transformations: Array<{
directiveType: string;
nodeType: string;
originalNode: any;
transformedNode: any;
timestamp: number;
success: boolean;
error?: string;
}> = [];
// Track the transformations by hooking into the processDirective method
const originalProcessDirective = directiveService.processDirective.bind(directiveService);
directiveService.processDirective = async (node, context) => {
const startTime = Date.now();
let success = true;
let error: string | undefined;
let result;
const nodeType = node.type;
const directiveKind = node.directive?.kind || 'unknown';
// Skip if we're filtering by directive type and this doesn't match
if (directiveType && directiveKind !== directiveType) {
return originalProcessDirective(node, context);
}
try {
result = await originalProcessDirective(node, context);
} catch (e) {
success = false;
error = e instanceof Error ? e.message : String(e);
throw e;
} finally {
// Record the transformation attempt
transformations.push({
directiveType: directiveKind,
nodeType,
originalNode: includeContent ? node : { type: nodeType, kind: directiveKind },
transformedNode: includeContent && result && 'replacement' in result
? result.replacement
: { type: 'unknown', replaced: 'replacement' in result },
timestamp: startTime,
success,
error
});
}
return result;
};
console.log(chalk.blue('Processing file to track transformations...'));
// Interpret the file with transformation enabled
await interpreterService.interpret(nodes, {
initialState: stateService,
filePath,
mergeState: true
});
console.log(chalk.green(`Processing complete. Captured ${transformations.length} transformations.`));
// Generate output based on format
let output: string;
if (outputFormat === 'json') {
output = JSON.stringify(transformations, null, 2);
} else if (outputFormat === 'mermaid') {
// Initialize the visualization service for Mermaid output
try {
const visualizationService = initializeContextDebugger().getVisualizationService();
output = visualizationService.transformToMermaid(transformations, {
includeTimestamps: true,
includeDirectiveTypes: true
});
} catch (error) {
console.warn(chalk.yellow('Could not initialize visualization service. Using fallback mermaid generator.'));
output = generateMermaidDiagram(transformations);
}
} else {
// Default text format
output = generateTextReport(transformations);
}
// Output the results
if (outputFile) {
await fs.writeFile(outputFile, output);
console.log(chalk.green(`Transformation results saved to ${outputFile}`));
} else {
console.log(chalk.cyan('\nTransformation Report:\n'));
console.log(output);
}
} catch (error) {
if (error instanceof MeldResolutionError) {
console.error(chalk.red(`Resolution error: ${error.message}`));
if (error.details) {
console.error(chalk.red(`Details: ${JSON.stringify(error.details, null, 2)}`));
}
} else {
console.error(chalk.red(`Error debugging transformations: ${error instanceof Error ? error.message : String(error)}`));
if (error instanceof Error && error.stack) {
console.error(chalk.dim(error.stack));
}
}
console.error(chalk.yellow('If this is a module resolution error, make sure you have built the codebase with "npm run build" before running debug commands'));
}
}
/**
* Generate a text report of the transformations
*/
function generateTextReport(transformations: Array<any>): string {
if (transformations.length === 0) {
return 'No transformations captured.';
}
// Group by directive type
const byDirectiveType = new Map<string, typeof transformations>();
for (const transform of transformations) {
if (!byDirectiveType.has(transform.directiveType)) {
byDirectiveType.set(transform.directiveType, []);
}
byDirectiveType.get(transform.directiveType)!.push(transform);
}
const lines: string[] = [];
for (const [directiveType, transforms] of byDirectiveType.entries()) {
const successCount = transforms.filter(t => t.success).length;
const failCount = transforms.length - successCount;
lines.push(chalk.cyan(`\nDirective Type: ${directiveType}`));
lines.push(chalk.cyan(`Total transformations: ${transforms.length} (${successCount} success, ${failCount} fail)`));
transforms.forEach((transform, i) => {
const statusColor = transform.success ? chalk.green : chalk.red;
const status = transform.success ? 'SUCCESS' : 'FAILED';
lines.push(chalk.dim(`\nTransformation #${i + 1}:`));
lines.push(`Status: ${statusColor(status)}`);
lines.push(`Node Type: ${transform.nodeType}`);
if (transform.success) {
if (transform.transformedNode.type) {
lines.push(`Transformed To: ${transform.transformedNode.type}`);
} else if (transform.transformedNode.replaced) {
lines.push(`Replaced: ${transform.transformedNode.replaced}`);
}
} else if (transform.error) {
lines.push(`Error: ${chalk.red(transform.error)}`);
}
});
}
return lines.join('\n');
}
/**
* Generate a Mermaid diagram of the transformations
*/
function generateMermaidDiagram(transformations: Array<any>): string {
if (transformations.length === 0) {
return 'graph TD\n A[No transformations] --> B[captured]';
}
const lines = ['graph TD'];
// Add nodes for each transformation
transformations.forEach((transform, i) => {
const nodeId = `node_${i}`;
const resultId = `result_${i}`;
const label = `${transform.directiveType}\\n${transform.nodeType}`;
const resultLabel = transform.success
? 'Transformed'
: `Failed\\n${transform.error ? transform.error.substring(0, 20) : 'Error'}`;
lines.push(` ${nodeId}["${label}"]`);
lines.push(` ${resultId}["${resultLabel}"]`);
lines.push(` ${nodeId} --> ${resultId}`);
// Add styling
if (transform.success) {
lines.push(` style ${resultId} fill:#90EE90`);
} else {
lines.push(` style ${resultId} fill:#FFCCCB`);
}
// Connect to next transformation if exists
if (i < transformations.length - 1) {
lines.push(` ${resultId} --> node_${i+1}`);
}
});
return lines.join('\n');
}