meld
Version:
Meld: A template language for LLM prompts
1,512 lines (1,279 loc) • 558 kB
Markdown
We need to update our implementation in `api/index.ts` to reflect our more complex services setup. There are some notes on this topic in this file:
\=== NOTES
# SDK Integration Test Patterns
## Test-Implementation Misalignment
We've identified a pattern where SDK integration tests may be making oversimplified assumptions about internal service behavior. This creates potential maintenance challenges and false negatives in our test suite.
### Case Study: Output Service Integration
The `api/api.test.ts` integration tests demonstrate this pattern clearly:
```typescript
// SDK integration test makes simple assumptions
const content = `
Some text content
@run [echo test]
More text
`;
// Expects:
// - Raw text preservation
// - Simple directive handling
// - Direct content matching
```
However, the actual `OutputService` implementation and its unit tests reveal more sophisticated behavior:
1. Transformation Modes
- Non-transformation mode has specific directive handling rules
- Transformation mode replaces directives with results
- Mode selection affects entire output pipeline
2. Format-Specific Behavior
- Each format (markdown, llm) has unique requirements
- LLM XML format has special handling needs
- Directive handling varies by format
3. State Management
- Service tracks transformation state
- Handles state variables differently in different modes
- Complex interaction between state and output
### Impact on Test Reliability
This misalignment causes:
1. False negatives - tests fail despite correct implementation
2. Maintenance burden - fixing "failing" tests can break actual functionality
3. Documentation gaps - simplified tests don't reflect actual behavior
### Recommendations
1. SDK Integration Tests Should:
- Consider transformation modes
- Account for format-specific behavior
- Match documented interface behavior
- Test actual use cases rather than implementation details
2. Documentation Updates:
- Clearly document transformation modes
- Explain format-specific requirements
- Provide SDK usage examples that reflect actual behavior
3. Test Structure:
- Move implementation details to unit tests
- Keep integration tests focused on real-world usage
- Add test cases for different modes and formats
- Document expected behavior in test descriptions
## Implementation Plan
### Phase 1: Test Infrastructure Updates (1-2 hours)
- [ ] Update TestContext initialization
- [ ] Add transformation mode helpers
- [ ] Add format-specific test utilities
- [ ] Update test documentation patterns
### Phase 2: Basic Transformation Tests (2-3 hours)
- [ ] Test transformation mode enabling/disabling
- [ ] Test state variable preservation
- [ ] Test basic directive handling
- [ ] Test content preservation rules
### Phase 3: Format-Specific Tests (2-3 hours)
- [ ] Markdown format tests
- [ ] Headers and formatting
- [ ] Code blocks
- [ ] Directive placeholders
- [ ] LLM format tests
- [ ] XML structure
- [ ] Special characters
- [ ] State representation
### Phase 4: Integration Scenarios (3-4 hours)
- [ ] Full pipeline tests
- [ ] Parse -> Transform -> Output
- [ ] State management
- [ ] Error handling
- [ ] Mixed content tests
- [ ] Multiple directive types
- [ ] Nested transformations
- [ ] State inheritance
- [ ] Edge cases
- [ ] Empty content
- [ ] Invalid directives
- [ ] State conflicts
### Phase 5: Documentation & Examples (2-3 hours)
- [ ] Update test documentation
- [ ] Add example test patterns
- [ ] Document common pitfalls
- [ ] Create test templates
## Action Items
1. Review other SDK integration tests for similar patterns
2. Update test documentation to reflect actual service behavior
3. Consider adding SDK-level transformation mode controls
4. Add integration test examples to SDK documentation
## Risk Assessment
### Low Risk Areas
- Test infrastructure changes (good existing patterns)
- Basic transformation tests (clear requirements)
- Documentation updates (straightforward)
### Medium Risk Areas
- Format-specific edge cases
- State management complexity
- Performance implications
### Mitigation Strategies
1. Incremental implementation
2. Comprehensive test coverage
3. Clear documentation
4. Regular review points
## Timeline
- Total estimated time: 10-15 hours
- Can be implemented incrementally
- Key milestones align with phases
- Regular review points after each phase
## Success Criteria
1. All tests pass consistently
2. No false negatives
3. Clear test patterns documented
4. Easy to maintain and extend
5. Matches actual service behavior
\=== CODE
# api.test.ts
```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { main } from './index.js';
import { TestContext } from '@tests/utils/index.js';
import type { ProcessOptions } from '@core/types/index.js';
import type { NodeFileSystem } from '@services/fs/FileSystemService/NodeFileSystem.js';
// Define the type for main function options
type MainOptions = {
fs?: NodeFileSystem;
format?: 'llm';
services?: any;
};
describe('SDK Integration Tests', () => {
let context: TestContext;
let testFilePath: string;
beforeEach(async () => {
context = new TestContext();
await context.initialize();
testFilePath = 'test.meld';
});
afterEach(async () => {
await context.cleanup();
vi.resetModules();
vi.clearAllMocks();
});
describe('Format Conversion', () => {
it('should handle definition directives correctly', async () => {
await context.fs.writeFile(testFilePath, '@text greeting = "Hello"');
const result = await main(testFilePath, {
fs: context.fs,
services: context.services
});
// Definition directives should be omitted from output
expect(result).toBe('');
});
it('should handle execution directives correctly', async () => {
// Start debug session with enhanced configuration
const debugSessionId = await context.startDebugSession({
captureConfig: {
capturePoints: ['pre-transform', 'post-transform', 'error'],
includeFields: ['nodes', 'transformedNodes', 'variables', 'metadata'],
format: 'full'
},
visualization: {
format: 'mermaid',
includeMetadata: true,
includeTimestamps: true
}
});
try {
await context.fs.writeFile(testFilePath, '@run [echo test]');
// Get initial state ID - FIXED: Remove file path fallback
const initialStateId = context.services.state.getStateId();
if (!initialStateId) {
throw new Error('Failed to get state ID - state not properly initialized');
}
// Enhanced debugging: Generate relationship graph
console.log('Initial State Relationships:');
console.log(await context.services.visualization.generateRelationshipGraph([initialStateId], {
format: 'mermaid',
includeMetadata: true
}));
// Enhanced debugging: Generate initial timeline
console.log('Initial Timeline:');
console.log(await context.services.visualization.generateTimeline([initialStateId], {
format: 'mermaid',
includeTimestamps: true
}));
// Enhanced debugging: Get initial metrics
const startTime = Date.now();
const initialMetrics = await context.services.visualization.getMetrics({
start: startTime - 3600000, // Last hour
end: startTime
});
console.log('Initial State Metrics:', initialMetrics);
console.log('Initial State Hierarchy:');
console.log(await context.services.visualization.generateHierarchyView(initialStateId, {
format: 'mermaid',
includeMetadata: true
}));
// Trace the operation with enhanced error handling
const { result, diagnostics } = await context.services.debugger.traceOperation(
initialStateId,
async () => {
// Enable transformation mode explicitly
context.services.state.enableTransformation(true);
return await main(testFilePath, {
fs: context.fs,
format: 'llm',
services: context.services
} as any);
}
);
// Log diagnostics and state changes
console.log('Operation Diagnostics:', diagnostics);
// Get final state visualization
const finalStateId = context.services.state.getStateId();
if (!finalStateId) {
throw new Error('Failed to get final state ID');
}
// Enhanced debugging: Generate final relationship graph
console.log('Final State Relationships:');
console.log(await context.services.visualization.generateRelationshipGraph([finalStateId], {
format: 'mermaid',
includeMetadata: true
}));
// Enhanced debugging: Generate final timeline
console.log('Final Timeline:');
console.log(await context.services.visualization.generateTimeline([finalStateId], {
format: 'mermaid',
includeTimestamps: true
}));
// Enhanced debugging: Get final metrics
const endTime = Date.now();
const finalMetrics = await context.services.visualization.getMetrics({
start: startTime,
end: endTime
});
console.log('Final State Metrics:', finalMetrics);
console.log('Final State Hierarchy:');
console.log(await context.services.visualization.generateHierarchyView(finalStateId, {
format: 'mermaid',
includeMetadata: true
}));
// Generate transition diagram
console.log('State Transitions:');
console.log(await context.services.visualization.generateTransitionDiagram(finalStateId, {
format: 'mermaid',
includeTimestamps: true
}));
// Add assertions here
expect(result).toBeDefined();
// Add more specific assertions based on expected behavior
} catch (error) {
console.error('Test failed with error:', error);
// Enhanced error reporting
if (context.services.tracking) {
const allStates = await context.services.tracking.getAllStates();
console.log('All tracked states:', allStates);
}
throw error;
}
});
it('should handle complex meld content with mixed directives', async () => {
const content = `
@text greeting = "Hello"
@data config = { "value": 123 }
Some text content
@run [echo test]
More text
`;
await context.fs.writeFile(testFilePath, content);
const result = await main(testFilePath, {
fs: context.fs,
services: context.services
});
// Definition directives should be omitted
expect(result).not.toContain('"identifier": "greeting"');
expect(result).not.toContain('"value": "Hello"');
expect(result).not.toContain('"identifier": "config"');
// Text content should be preserved
expect(result).toContain('Some text content');
expect(result).toContain('More text');
// Execution directives should show placeholder
expect(result).toContain('[run directive output placeholder]');
});
});
describe('Full Pipeline Integration', () => {
it('should handle the complete parse -> interpret -> convert pipeline', async () => {
const content = `
@text greeting = "Hello"
@run [echo test]
Some content
`;
await context.fs.writeFile(testFilePath, content);
const result = await main(testFilePath, {
fs: context.fs,
services: context.services
});
// Definition directive should be omitted
expect(result).not.toContain('"kind": "text"');
expect(result).not.toContain('"identifier": "greeting"');
// Execution directive should show placeholder
expect(result).toContain('[run directive output placeholder]');
// Text content should be preserved
expect(result).toContain('Some content');
});
it('should preserve state and content in transformation mode', async () => {
const content = `
@text first = "First"
@text second = "Second"
@run [echo test]
Content
`;
await context.fs.writeFile(testFilePath, content);
// Enable transformation mode through state service
context.services.state.enableTransformation(true);
const result = await main(testFilePath, {
fs: context.fs,
services: context.services
});
// In transformation mode, directives should be replaced with their results
expect(result).not.toContain('"identifier": "first"');
expect(result).not.toContain('"value": "First"');
expect(result).not.toContain('"identifier": "second"');
// Text content should be preserved
expect(result).toContain('Content');
// Run directive should be transformed (if transformation is working)
expect(result).toContain('test');
});
});
describe('Error Handling', () => {
it('should handle parse errors gracefully', async () => {
await context.fs.writeFile(testFilePath, '@invalid not_a_valid_directive');
await expect(main(testFilePath, {
fs: context.fs,
services: context.services
}))
.rejects
.toThrow(/Parse error/);
});
// TODO: This test will be updated as part of the error handling overhaul
// See dev/ERRORS.md - will be reclassified as a fatal error with improved messaging
it.todo('should handle missing files correctly');
it('should handle empty files', async () => {
await context.fs.writeFile(testFilePath, '');
const result = await main(testFilePath, {
fs: context.fs,
services: context.services
});
expect(result).toBe(''); // Empty input should produce empty output
});
});
describe('Edge Cases', () => {
it.todo('should handle large files efficiently');
it.todo('should handle deeply nested imports');
});
});
```
# index.ts
```typescript
// Core services
export * from '@services/pipeline/InterpreterService/InterpreterService.js';
export * from '@services/pipeline/ParserService/ParserService.js';
export * from '@services/state/StateService/StateService.js';
export * from '@services/resolution/ResolutionService/ResolutionService.js';
export * from '@services/pipeline/DirectiveService/DirectiveService.js';
export * from '@services/resolution/ValidationService/ValidationService.js';
export * from '@services/fs/PathService/PathService.js';
export * from '@services/fs/FileSystemService/FileSystemService.js';
export * from '@services/fs/FileSystemService/PathOperationsService.js';
export * from '@services/pipeline/OutputService/OutputService.js';
export * from '@services/resolution/CircularityService/CircularityService.js';
// Core types and errors
export * from '@core/types/index.js';
export * from '@core/errors/MeldDirectiveError.js';
export * from '@core/errors/MeldInterpreterError.js';
export * from '@core/errors/MeldParseError.js';
import { MeldFileNotFoundError } from '@core/errors/MeldFileNotFoundError.js';
// Import service classes
import { InterpreterService } from '@services/pipeline/InterpreterService/InterpreterService.js';
import { ParserService } from '@services/pipeline/ParserService/ParserService.js';
import { StateService } from '@services/state/StateService/StateService.js';
import { ResolutionService } from '@services/resolution/ResolutionService/ResolutionService.js';
import { DirectiveService } from '@services/pipeline/DirectiveService/DirectiveService.js';
import { ValidationService } from '@services/resolution/ValidationService/ValidationService.js';
import { PathService } from '@services/fs/PathService/PathService.js';
import { FileSystemService } from '@services/fs/FileSystemService/FileSystemService.js';
import { PathOperationsService } from '@services/fs/FileSystemService/PathOperationsService.js';
import { OutputService } from '@services/pipeline/OutputService/OutputService.js';
import { CircularityService } from '@services/resolution/CircularityService/CircularityService.js';
import { NodeFileSystem } from '@services/fs/FileSystemService/NodeFileSystem.js';
import { ProcessOptions } from '@core/types/index.js';
// Package info
export { version } from '@core/version.js';
export async function main(filePath: string, options: ProcessOptions & { services?: any } = {}): Promise<string> {
// Use services from test context if provided, otherwise create new ones
const pathOps = new PathOperationsService();
const fs = options.fs || new NodeFileSystem();
const filesystem = new FileSystemService(pathOps, fs);
if (options.services) {
// Use services from test context
const { parser, interpreter, directive, validation, state, path, circularity, resolution, output } = options.services;
// Initialize services
path.initialize(filesystem);
directive.initialize(
validation,
state,
path,
filesystem,
parser,
interpreter,
circularity,
resolution
);
interpreter.initialize(directive, state);
try {
// Read the file
const content = await filesystem.readFile(filePath);
// Parse the content
const ast = await parser.parse(content);
// Interpret the AST
const resultState = await interpreter.interpret(ast, { filePath, initialState: state });
// Convert to desired format using the updated state
const converted = await output.convert(ast, resultState, options.format || 'llm');
return converted;
} catch (error) {
// If it's a MeldFileNotFoundError, just throw it as is
if (error instanceof MeldFileNotFoundError) {
throw error;
}
// For other Error instances, preserve the error
if (error instanceof Error) {
throw error;
}
// For non-Error objects, convert to string
throw new Error(String(error));
}
} else {
// Create new services
const parser = new ParserService();
const interpreter = new InterpreterService();
const state = new StateService();
const directives = new DirectiveService();
const validation = new ValidationService();
const circularity = new CircularityService();
const resolution = new ResolutionService(state, filesystem, parser);
const path = new PathService();
const output = new OutputService();
// Initialize services
directives.initialize(
validation,
state,
path,
filesystem,
parser,
interpreter,
circularity,
resolution
);
interpreter.initialize(directives, state);
try {
// Read the file
const content = await filesystem.readFile(filePath);
// Parse the content
const ast = await parser.parse(content);
// Interpret the AST
const resultState = await interpreter.interpret(ast, { filePath, initialState: state });
// Convert to desired format using the updated state
const converted = await output.convert(ast, resultState, options.format || 'llm');
return converted;
} catch (error) {
// If it's a MeldFileNotFoundError, just throw it as is
if (error instanceof MeldFileNotFoundError) {
throw error;
}
// For other Error instances, preserve the error
if (error instanceof Error) {
throw error;
}
// For non-Error objects, convert to string
throw new Error(String(error));
}
}
}
```
# DirectiveService.test.ts
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { DirectiveService } from './DirectiveService.js';
import { TestContext } from '@tests/utils/TestContext.js';
import { DirectiveError, DirectiveErrorCode } from './errors/DirectiveError.js';
import type { DirectiveNode } from 'meld-spec';
describe('DirectiveService', () => {
let context: TestContext;
let service: DirectiveService;
beforeEach(async () => {
// Initialize test context
context = new TestContext();
await context.initialize();
// Create service instance
service = new DirectiveService();
// Initialize with real services from context
service.initialize(
context.services.validation,
context.services.state,
context.services.path,
context.services.filesystem,
context.services.parser,
context.services.interpreter,
context.services.circularity,
context.services.resolution
);
// Load test fixtures
await context.fixtures.load('directiveTestProject');
});
afterEach(async () => {
await context.cleanup();
});
describe('Service initialization', () => {
it('should initialize with all required services', () => {
expect(service.getSupportedDirectives()).toContain('text');
expect(service.getSupportedDirectives()).toContain('data');
expect(service.getSupportedDirectives()).toContain('path');
});
it('should throw if used before initialization', async () => {
const uninitializedService = new DirectiveService();
const node = context.factory.createTextDirective('test', '"value"', context.factory.createLocation(1, 1));
const execContext = { currentFilePath: 'test.meld', state: context.services.state };
await expect(uninitializedService.processDirective(node, execContext))
.rejects.toThrow('DirectiveService must be initialized before use');
});
});
describe('Directive processing', () => {
describe('Text directives', () => {
it('should process basic text directive', async () => {
// Verify file exists
const exists = await context.fs.exists('test.meld');
console.log('test.meld exists:', exists);
// Parse the fixture file
const content = await context.fs.readFile('test.meld');
console.log('test.meld content:', content);
const nodes = await context.services.parser.parse(content);
console.log('Parsed nodes:', nodes);
const node = nodes[0] as DirectiveNode;
// Create execution context
const execContext = {
currentFilePath: 'test.meld',
state: context.services.state
};
// Process the directive
const result = await service.processDirective(node, execContext);
// Verify the result
expect(result.getTextVar('greeting')).toBe('Hello');
});
it('should process text directive with variable interpolation', async () => {
// Set up initial state with a variable
const state = context.services.state;
state.setTextVar('name', 'World');
// Parse and process
const content = await context.fs.readFile('test-interpolation.meld');
const nodes = await context.services.parser.parse(content);
const node = nodes[0] as DirectiveNode;
const result = await service.processDirective(node, {
currentFilePath: 'test-interpolation.meld',
state
});
expect(result.getTextVar('greeting')).toBe('Hello World');
});
});
describe('Data directives', () => {
it('should process data directive with object value', async () => {
const content = await context.fs.readFile('test-data.meld');
const nodes = await context.services.parser.parse(content);
const node = nodes[0] as DirectiveNode;
const result = await service.processDirective(node, {
currentFilePath: 'test-data.meld',
state: context.services.state
});
expect(result.getDataVar('config')).toEqual({ key: 'value' });
});
it('should process data directive with variable interpolation', async () => {
// Set up initial state
const state = context.services.state;
state.setTextVar('user', 'Alice');
const content = await context.fs.readFile('test-data-interpolation.meld');
const nodes = await context.services.parser.parse(content);
const node = nodes[0] as DirectiveNode;
const result = await service.processDirective(node, {
currentFilePath: 'test-data-interpolation.meld',
state
});
expect(result.getDataVar('config')).toEqual({ greeting: 'Hello Alice' });
});
});
describe('Import directives', () => {
it('should process basic import', async () => {
// Create import directive node with value property
const node = context.factory.createImportDirective('module.meld', context.factory.createLocation(1, 1));
const result = await service.processDirective(node, {
currentFilePath: 'main.meld',
state: context.services.state
});
expect(result.getTextVar('greeting')).toBe('Hello');
});
it('should handle nested imports', async () => {
// Create import directive node with value property
const node = context.factory.createImportDirective('inner.meld', context.factory.createLocation(1, 1));
const result = await service.processDirective(node, {
currentFilePath: 'middle.meld',
state: context.services.state
});
expect(result.getTextVar('greeting')).toBe('Hello');
});
it('should detect circular imports', async () => {
// Create import directive node with value property
const node = context.factory.createImportDirective('b.meld', context.factory.createLocation(1, 1));
await expect(service.processDirective(node, {
currentFilePath: 'a.meld',
state: context.services.state
})).rejects.toThrow(DirectiveError);
});
});
// ... continue with other directive types and error cases
});
});
```
# DirectiveService.ts
```typescript
import type { DirectiveNode, DirectiveKind, DirectiveData } from 'meld-spec';
import { directiveLogger } from '../../../core/utils/logger.js';
import { IDirectiveService, IDirectiveHandler, DirectiveContext } from './IDirectiveService.js';
import { IValidationService } from '@services/resolution/ValidationService/IValidationService.js';
import { IStateService } from '@services/state/StateService/IStateService.js';
import { IPathService } from '@services/fs/PathService/IPathService.js';
import { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js';
import { IParserService } from '@services/pipeline/ParserService/IParserService.js';
import { IInterpreterService } from '@services/pipeline/InterpreterService/IInterpreterService.js';
import { MeldDirectiveError } from '@core/errors/MeldDirectiveError.js';
import { ICircularityService } from '@services/resolution/CircularityService/ICircularityService.js';
import { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js';
import { DirectiveError, DirectiveErrorCode } from './errors/DirectiveError.js';
import type { ILogger } from './handlers/execution/EmbedDirectiveHandler.js';
// Import all handlers
import { TextDirectiveHandler } from './handlers/definition/TextDirectiveHandler.js';
import { DataDirectiveHandler } from './handlers/definition/DataDirectiveHandler.js';
import { PathDirectiveHandler } from './handlers/definition/PathDirectiveHandler.js';
import { DefineDirectiveHandler } from './handlers/definition/DefineDirectiveHandler.js';
import { RunDirectiveHandler } from './handlers/execution/RunDirectiveHandler.js';
import { EmbedDirectiveHandler } from './handlers/execution/EmbedDirectiveHandler.js';
import { ImportDirectiveHandler } from './handlers/execution/ImportDirectiveHandler.js';
export class MeldLLMXMLError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly details?: any
) {
super(message);
this.name = 'MeldLLMXMLError';
Object.setPrototypeOf(this, MeldLLMXMLError.prototype);
}
}
/**
* Service responsible for handling directives
*/
export class DirectiveService implements IDirectiveService {
private validationService?: IValidationService;
private stateService?: IStateService;
private pathService?: IPathService;
private fileSystemService?: IFileSystemService;
private parserService?: IParserService;
private interpreterService?: IInterpreterService;
private circularityService?: ICircularityService;
private resolutionService?: IResolutionService;
private initialized = false;
private logger: ILogger;
private handlers: Map<string, IDirectiveHandler> = new Map();
constructor(logger?: ILogger) {
this.logger = logger || directiveLogger;
}
initialize(
validationService: IValidationService,
stateService: IStateService,
pathService: IPathService,
fileSystemService: IFileSystemService,
parserService: IParserService,
interpreterService: IInterpreterService,
circularityService: ICircularityService,
resolutionService: IResolutionService
): void {
this.validationService = validationService;
this.stateService = stateService;
this.pathService = pathService;
this.fileSystemService = fileSystemService;
this.parserService = parserService;
this.interpreterService = interpreterService;
this.circularityService = circularityService;
this.resolutionService = resolutionService;
this.initialized = true;
// Register default handlers
this.registerDefaultHandlers();
this.logger.debug('DirectiveService initialized', {
handlers: Array.from(this.handlers.keys())
});
}
/**
* Register all default directive handlers
*/
public registerDefaultHandlers(): void {
// Definition handlers
this.registerHandler(
new TextDirectiveHandler(
this.validationService!,
this.stateService!,
this.resolutionService!
)
);
this.registerHandler(
new DataDirectiveHandler(
this.validationService!,
this.stateService!,
this.resolutionService!
)
);
this.registerHandler(
new PathDirectiveHandler(
this.validationService!,
this.stateService!,
this.resolutionService!
)
);
this.registerHandler(
new DefineDirectiveHandler(
this.validationService!,
this.stateService!,
this.resolutionService!
)
);
// Execution handlers
this.registerHandler(
new RunDirectiveHandler(
this.validationService!,
this.resolutionService!,
this.stateService!,
this.fileSystemService!
)
);
this.registerHandler(
new EmbedDirectiveHandler(
this.validationService!,
this.resolutionService!,
this.stateService!,
this.circularityService!,
this.fileSystemService!,
this.parserService!,
this.interpreterService!,
this.logger
)
);
this.registerHandler(
new ImportDirectiveHandler(
this.validationService!,
this.resolutionService!,
this.stateService!,
this.fileSystemService!,
this.parserService!,
this.interpreterService!,
this.circularityService!
)
);
}
/**
* Register a new directive handler
*/
registerHandler(handler: IDirectiveHandler): void {
if (!this.initialized) {
throw new Error('DirectiveService must be initialized before registering handlers');
}
if (!handler.kind) {
throw new Error('Handler must have a kind property');
}
this.handlers.set(handler.kind, handler);
this.logger.debug(`Registered handler for directive: ${handler.kind}`);
}
/**
* Handle a directive node
*/
public async handleDirective(node: DirectiveNode, context: DirectiveContext): Promise<IStateService> {
return this.processDirective(node, context);
}
/**
* Process multiple directives in sequence
*/
async processDirectives(nodes: DirectiveNode[], parentContext?: DirectiveContext): Promise<IStateService> {
let currentState = parentContext?.state?.clone() || this.stateService!.createChildState();
for (const node of nodes) {
// Create a new context with the current state as parent and a new child state
const nodeContext = {
currentFilePath: parentContext?.currentFilePath || '',
parentState: currentState,
state: currentState.createChildState()
};
// Process directive and get the updated state
const result = await this.processDirective(node, nodeContext);
// If transformation is enabled, we don't merge states since the directive
// will be replaced with a text node and its state will be handled separately
if (!currentState.isTransformationEnabled?.()) {
// result is always an IStateService from processDirective
currentState.mergeChildState(result);
}
}
return currentState;
}
/**
* Create execution context for a directive
*/
private createContext(node: DirectiveNode, parentContext?: DirectiveContext): DirectiveContext {
if (!this.stateService) {
throw new Error('DirectiveService must be initialized before use');
}
const state = parentContext?.state?.clone() || this.stateService.createChildState();
return {
currentFilePath: parentContext?.currentFilePath || '',
parentState: parentContext?.state,
state
};
}
/**
* Update the interpreter service reference
*/
updateInterpreterService(interpreterService: IInterpreterService): void {
this.interpreterService = interpreterService;
this.logger.debug('Updated interpreter service reference');
}
/**
* Check if a handler exists for a directive kind
*/
hasHandler(kind: string): boolean {
return this.handlers.has(kind);
}
/**
* Validate a directive node
*/
async validateDirective(node: DirectiveNode): Promise<void> {
try {
await this.validationService!.validate(node);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorForLog = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to validate directive', {
kind: node.directive.kind,
location: node.location,
error: errorForLog
});
throw new DirectiveError(
errorMessage,
node.directive.kind,
DirectiveErrorCode.VALIDATION_FAILED,
{
node
}
);
}
}
/**
* Create a child context for nested directives
*/
public createChildContext(parentContext: DirectiveContext, filePath: string): DirectiveContext {
return {
currentFilePath: filePath,
state: parentContext.state.createChildState(),
parentState: parentContext.state
};
}
supportsDirective(kind: string): boolean {
return this.handlers.has(kind);
}
getSupportedDirectives(): string[] {
return Array.from(this.handlers.keys());
}
private ensureInitialized(): void {
if (!this.initialized) {
throw new Error('DirectiveService must be initialized before use');
}
}
private async handleTextDirective(node: DirectiveNode): Promise<void> {
const directive = node.directive;
this.logger.debug('Processing text directive', {
identifier: directive.identifier,
location: node.location
});
try {
// Value is already interpolated by meld-ast
await this.stateService!.setTextVar(directive.identifier, directive.value);
this.logger.debug('Text directive processed successfully', {
identifier: directive.identifier,
location: node.location
});
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorForLog = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to process text directive', {
identifier: directive.identifier,
location: node.location,
error: errorForLog
});
throw new MeldDirectiveError(
errorMessage,
'text',
node.location?.start
);
}
}
private async handleDataDirective(node: DirectiveNode): Promise<void> {
const directive = node.directive;
this.logger.debug('Processing data directive', {
identifier: directive.identifier,
location: node.location
});
try {
// Value is already interpolated by meld-ast
let value = directive.value;
if (typeof value === 'string') {
value = JSON.parse(value);
}
await this.stateService!.setDataVar(directive.identifier, value);
this.logger.debug('Data directive processed successfully', {
identifier: directive.identifier,
location: node.location
});
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorForLog = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to process data directive', {
identifier: directive.identifier,
location: node.location,
error: errorForLog
});
throw new MeldDirectiveError(
errorMessage,
'data',
node.location?.start
);
}
}
private async handleImportDirective(node: DirectiveNode): Promise<void> {
const directive = node.directive;
this.logger.debug('Processing import directive', {
path: directive.path,
section: directive.section,
fuzzy: directive.fuzzy,
location: node.location
});
try {
// Path is already interpolated by meld-ast
const fullPath = await this.pathService!.resolvePath(directive.path);
// Check for circular imports
this.circularityService!.beginImport(fullPath);
try {
// Check if file exists
if (!await this.fileSystemService!.exists(fullPath)) {
throw new Error(`Import file not found: ${fullPath}`);
}
// Create a child state for the import
const childState = await this.stateService!.createChildState();
// Read the file content
const content = await this.fileSystemService!.readFile(fullPath);
// If a section is specified, extract it (section name is already interpolated)
let processedContent = content;
if (directive.section) {
processedContent = await this.extractSection(
content,
directive.section,
directive.fuzzy || 0
);
}
// Parse and interpret the content
const parsedNodes = await this.parserService!.parse(processedContent);
await this.interpreterService!.interpret(parsedNodes, {
initialState: childState,
filePath: fullPath,
mergeState: true
});
this.logger.debug('Import content processed', {
path: fullPath,
section: directive.section,
location: node.location
});
} finally {
// Always end import tracking, even if there was an error
this.circularityService!.endImport(fullPath);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorForLog = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to process import directive', {
path: directive.path,
section: directive.section,
location: node.location,
error: errorForLog
});
throw new MeldDirectiveError(
errorMessage,
'import',
node.location?.start
);
}
}
private async extractSection(
content: string,
section: string,
fuzzyMatch: number
): Promise<string> {
try {
// Split content into lines
const lines = content.split('\n');
const headings: { title: string; line: number; level: number }[] = [];
// Find all headings and their levels
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const match = line.match(/^(#{1,6})\s+(.+)$/);
if (match) {
headings.push({
title: match[2],
line: i,
level: match[1].length
});
}
}
// Find best matching heading
let bestMatch: typeof headings[0] | undefined;
let bestScore = 0;
for (const heading of headings) {
const score = this.calculateSimilarity(heading.title, section);
if (score > fuzzyMatch && score > bestScore) {
bestScore = score;
bestMatch = heading;
}
}
if (!bestMatch) {
// Find closest match for error message
let closestMatch = '';
let closestScore = 0;
for (const heading of headings) {
const score = this.calculateSimilarity(heading.title, section);
if (score > closestScore) {
closestScore = score;
closestMatch = heading.title;
}
}
throw new MeldLLMXMLError(
'Section not found',
'SECTION_NOT_FOUND',
{ title: section, bestMatch: closestMatch }
);
}
// Find the end of the section (next heading of same or higher level)
let endLine = lines.length;
for (let i = bestMatch.line + 1; i < lines.length; i++) {
const line = lines[i];
const match = line.match(/^(#{1,6})\s+/);
if (match && match[1].length <= bestMatch.level) {
endLine = i;
break;
}
}
// Extract the section content
return lines.slice(bestMatch.line, endLine).join('\n');
} catch (error) {
if (error instanceof MeldLLMXMLError) {
throw error;
}
throw new MeldLLMXMLError(
error instanceof Error ? error.message : 'Unknown error during section extraction',
'PARSE_ERROR',
error
);
}
}
private calculateSimilarity(str1: string, str2: string): number {
// Convert strings to lowercase for case-insensitive comparison
const s1 = str1.toLowerCase();
const s2 = str2.toLowerCase();
if (s1 === s2) return 1.0;
// Calculate Levenshtein distance
const len1 = s1.length;
const len2 = s2.length;
const matrix: number[][] = [];
for (let i = 0; i <= len1; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= len2; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
);
}
}
// Convert distance to similarity score between 0 and 1
const maxLen = Math.max(len1, len2);
return maxLen === 0 ? 1.0 : 1.0 - matrix[len1][len2] / maxLen;
}
private async handleEmbedDirective(node: DirectiveNode): Promise<void> {
const directive = node.directive;
this.logger.debug('Processing embed directive', {
path: directive.path,
section: directive.section,
fuzzy: directive.fuzzy,
names: directive.names,
location: node.location
});
try {
// Path is already interpolated by meld-ast
const fullPath = await this.pathService!.resolvePath(directive.path);
// Check for circular imports
this.circularityService!.beginImport(fullPath);
try {
// Check if file exists
if (!await this.fileSystemService!.exists(fullPath)) {
throw new Error(`Embed file not found: ${fullPath}`);
}
// Create a child state for the import
const childState = await this.stateService!.createChildState();
// Read the file content
const content = await this.fileSystemService!.readFile(fullPath);
// If a section is specified, extract it (section name is already interpolated)
let processedContent = content;
if (directive.section) {
processedContent = await this.extractSection(
content,
directive.section,
directive.fuzzy || 0
);
}
// Parse and interpret the content
const parsedNodes = await this.parserService!.parse(processedContent);
await this.interpreterService!.interpret(parsedNodes, {
initialState: childState,
filePath: fullPath,
mergeState: true
});
this.logger.debug('Embed content processed', {
path: fullPath,
section: directive.section,
location: node.location
});
} finally {
// Always end import tracking, even if there was an error
this.circularityService!.endImport(fullPath);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorForLog = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to process embed directive', {
path: directive.path,
section: directive.section,
location: node.location,
error: errorForLog
});
throw new MeldDirectiveError(
errorMessage,
'embed',
node.location?.start
);
}
}
/**
* Process a directive node, validating and executing it
* Values in the directive will already be interpolated by meld-ast
* @returns The updated state after directive execution
* @throws {MeldDirectiveError} If directive processing fails
*/
public async processDirective(node: DirectiveNode, context: DirectiveContext): Promise<IStateService> {
// Add initialization check before any other processing
if (!this.initialized) {
throw new Error('DirectiveService must be initialized before use');
}
try {
// Get the handler for this directive kind
const { kind } = node.directive;
const handler = this.handlers.get(kind);
if (!handler) {
throw new DirectiveError(
`No handler found for directive: ${kind}`,
kind,
DirectiveErrorCode.HANDLER_NOT_FOUND,
{ node }
);
}
// Validate directive before handling
await this.validateDirective(node);
// Execute the directive and handle both possible return types
const result = await handler.execute(node, context);
// If result is a DirectiveResult, return its state
if ('state' in result) {
return result.state;
}
// Otherwise, result is already an IStateService
return result;
} catch (error) {
if (error instanceof DirectiveError) {
throw error;
}
// Simplify error messages for common cases
let message = error instanceof Error ? error.message : String(error);
let code = DirectiveErrorCode.EXECUTION_FAILED;
if (message.includes('file not found') || message.includes('no such file')) {
message = `Referenced file not found: ${node.directive.path || node.directive.value}`;
code = DirectiveErrorCode.FILE_NOT_FOUND;
} else if (message.includes('circular import') || message.includes('circular reference')) {
message = 'Circular import detected';
code = DirectiveErrorCode.CIRCULAR_REFERENCE;
} else if (message.includes('parameter count') || message.includes('wrong number of parameters')) {
message = 'Invalid parameter count';
code = DirectiveErrorCode.VALIDATION_FAILED;
} else if (message.includes('invalid path') || message.includes('path validation failed')) {
message = 'Invalid path';
code = DirectiveErrorCode.VALIDATION_FAILED;
}
throw new DirectiveError(
message,
node.directive?.kind || 'unknown',
code,
{ node, cause: error instanceof Error ? error : undefined }
);
}
}
}
```
# IDirectiveService.ts
```typescript
import { DirectiveNode } from 'meld-spec';
import { IStateService } from '@services/state/StateService/IStateService.js';
import type { IValidationService } from '@services/resolution/ValidationService/IValidationService.js';
import type { IPathService } from '@services/fs/PathService/IPathService.js';
import type { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js';
import type { IParserService } from '@services/pipeline/ParserService/IParserService.js';
import type { IInterpreterService } from '@services/pipeline/InterpreterService/IInterpreterService.js';
import type { ICircularityService } from '@services/resolution/CircularityService/ICircularityService.js';
import type { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js';
import type { DirectiveResult } from './types.js';
/**
* Context for directive execution
*/
export interface DirectiveContext {
/** Current file being processed */
currentFilePath?: string;
/** Parent state for nested contexts */
parentState?: IStateService;
/** Current state for this directive */
state: IStateService;
/** Working directory for command execution */
workingDirectory?: string;
}
/**
* Interface for directive handlers
*/
export interface IDirectiveHandler {
/** The directive kind this handler processes */
readonly kind: string;
/**
* Execute the directive
* @returns The updated state after directive execution, or a DirectiveResult containing both state and optional replacement node
*/
execute(
node: DirectiveNode,
context: DirectiveContext
): Promise<DirectiveResult | IStateService>;
}
/**
*