[debug] Loading .env file from current working directory: /Users/adam/dev/meld/.env [info] Proceeding with actual execution... [info] FileCollectionManager initialized {"basePath":"/Users/adam/dev/meld/_cmte/define"} [info] Initializing WorkflowExecutor {"workflowPath":"/Users/adam/dev/meld/_cmte/define/workflow.yaml","basePath":"/Users/adam/dev/meld/_cmte/define","savePrompts":false,"dryRun":true,"apiDryRun":false,"lite":false,"useLocalLLM":false,"modelConfig":{"model":"claude-3-7-sonnet-latest"}} [debug] Claude adapter initialized {"model":"claude-3-7-sonnet-latest"} [info] DRY RUN MODE: No API calls will be made [debug] Loading workflow file {"workflowFilePath":"/Users/adam/dev/meld/_cmte/define/workflow.yaml"} [debug] File read: /Users/adam/dev/meld/_cmte/define/workflow.yaml [debug] Loaded workflow {"name":"meld-typespec-define-design","context":[]} [info] Executing workflow: meld-typespec-define-design {"workflowPath":"/Users/adam/dev/meld/_cmte/define/workflow.yaml","dryRun":true,"savePrompts":false,"apiDryRun":false,"lite":false,"outputPathConfig":"output"} [info] Registering file groups defined in workflow.files... [debug] Registering file collection: architectureDocs {"definition":{"include":["../../docs/dev/DI-ARCHITECTURE.md","../../docs/dev/PIPELINE.md"]}} [info] Registered collection 'architectureDocs' with 2 files. [debug] Files for collection 'architectureDocs': {"files":["../../docs/dev/DI-ARCHITECTURE.md","../../docs/dev/PIPELINE.md"]} [debug] Registering file collection: directiveClarityDoc {"definition":{"include":["../../_dev/DEFINE-CLARITY.md"]}} [info] Registered collection 'directiveClarityDoc' with 1 files. [debug] Files for collection 'directiveClarityDoc': {"files":["../../_dev/DEFINE-CLARITY.md"]} [debug] Registering file collection: CoreDirectiveCode {"definition":{"include":["../../services/pipeline/DirectiveService/DirectiveService.ts","../../services/pipeline/DirectiveService/IDirectiveService.ts","../../services/pipeline/DirectiveService/interfaces/DirectiveTypes.ts"]}} [info] Registered collection 'CoreDirectiveCode' with 3 files. [debug] Files for collection 'CoreDirectiveCode': {"files":["../../services/pipeline/DirectiveService/DirectiveService.ts","../../services/pipeline/DirectiveService/IDirectiveService.ts","../../services/pipeline/DirectiveService/interfaces/DirectiveTypes.ts"]} [debug] Registering file collection: DefineHandlerCode {"definition":{"include":["../../services/pipeline/DirectiveService/handlers/definition/DefineDirectiveHandler.ts","../../services/pipeline/ValidationService/validators/DefineDirectiveValidator.ts"]}} [info] Registered collection 'DefineHandlerCode' with 2 files. [debug] Files for collection 'DefineHandlerCode': {"files":["../../services/pipeline/DirectiveService/handlers/definition/DefineDirectiveHandler.ts","../../services/pipeline/ValidationService/validators/DefineDirectiveValidator.ts"]} [debug] Registering file collection: ParserCoreCode {"definition":{"include":["../../services/pipeline/ParserService/ParserService.ts","../../services/pipeline/ParserService/IParserService.ts"]}} [info] Registered collection 'ParserCoreCode' with 2 files. [debug] Files for collection 'ParserCoreCode': {"files":["../../services/pipeline/ParserService/ParserService.ts","../../services/pipeline/ParserService/IParserService.ts"]} [debug] Registering file collection: InterpreterCoreCode {"definition":{"include":["../../services/pipeline/InterpreterService/InterpreterService.ts","../../services/pipeline/InterpreterService/IInterpreterService.ts"]}} [info] Registered collection 'InterpreterCoreCode' with 2 files. [debug] Files for collection 'InterpreterCoreCode': {"files":["../../services/pipeline/InterpreterService/InterpreterService.ts","../../services/pipeline/InterpreterService/IInterpreterService.ts"]} [debug] Registering file collection: ResolutionCoreCode {"definition":{"include":["../../services/pipeline/ResolutionService/ResolutionService.ts","../../services/pipeline/ResolutionService/IResolutionService.ts"]}} [info] Registered collection 'ResolutionCoreCode' with 2 files. [debug] Files for collection 'ResolutionCoreCode': {"files":["../../services/pipeline/ResolutionService/ResolutionService.ts","../../services/pipeline/ResolutionService/IResolutionService.ts"]} [debug] Registering file collection: VariableResolutionCode {"definition":{"include":["../../services/pipeline/ResolutionService/resolvers/VariableReferenceResolver.ts","../../services/pipeline/ResolutionService/resolvers/types.ts"]}} [info] Registered collection 'VariableResolutionCode' with 2 files. [debug] Files for collection 'VariableResolutionCode': {"files":["../../services/pipeline/ResolutionService/resolvers/VariableReferenceResolver.ts","../../services/pipeline/ResolutionService/resolvers/types.ts"]} [debug] Registering file collection: ContentResolutionCode {"definition":{"include":["../../services/pipeline/ResolutionService/resolvers/ContentResolver.ts","../../services/pipeline/ResolutionService/resolvers/StringLiteralHandler.ts"]}} [info] Registered collection 'ContentResolutionCode' with 2 files. [debug] Files for collection 'ContentResolutionCode': {"files":["../../services/pipeline/ResolutionService/resolvers/ContentResolver.ts","../../services/pipeline/ResolutionService/resolvers/StringLiteralHandler.ts"]} [debug] Registering file collection: StateCoreCode {"definition":{"include":["../../services/state/StateService/StateService.ts","../../services/state/StateService/IStateService.ts"]}} [info] Registered collection 'StateCoreCode' with 2 files. [debug] Files for collection 'StateCoreCode': {"files":["../../services/state/StateService/StateService.ts","../../services/state/StateService/IStateService.ts"]} [debug] Registering file collection: FileSystemCoreCode {"definition":{"include":["../../services/fs/FileSystemService/FileSystemService.ts","../../services/fs/FileSystemService/IFileSystemService.ts"]}} [info] Registered collection 'FileSystemCoreCode' with 2 files. [debug] Files for collection 'FileSystemCoreCode': {"files":["../../services/fs/FileSystemService/FileSystemService.ts","../../services/fs/FileSystemService/IFileSystemService.ts"]} [info] Finished registering file groups. [info] Interpolating workflow definitions with file content... [debug] Template reference 'files.architectureDocs' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../docs/dev/DI-ARCHITECTURE.md","absolutePath":"/Users/adam/dev/meld/docs/dev/DI-ARCHITECTURE.md"} [debug] Reading relative file {"relativePath":"../../docs/dev/PIPELINE.md","absolutePath":"/Users/adam/dev/meld/docs/dev/PIPELINE.md"} [debug] Interpolated object key 'overallArchitecture' from '{{ files.architectureDocs }}' to '#### ../../docs/dev/DI-ARCHITECTURE.md ```javascript # Meld Architecture ## INTRODUCTION Meld is a specialized, directive-based scripting language designed for embedding small "@directives" inside an otherwise plain text (e.g., Markdown-like) document. The code in this repository implements: • Meld grammar rules and token types (e.g., text directives, path directives, data directives). • The parsing layer that converts Meld content into an AST (Abstract Syntax Tree). • A directive interpretation layer that processes these AST nodes and manipulates internal "states" to store variables and more. • A resolution layer to handle variable references, path expansions, data manipulations, etc. • Testing utilities and an in-memory FS (memfs) to simulate filesystems for thorough testing. The main idea: 1. Meld code is parsed to an AST. 2. Each directive node is validated and interpreted, updating a shared "state" (variables, data structures, commands, etc.). 3. Optional transformations (e.g., output formatting) generate final representations (Markdown, LLM-friendly XML, etc.). Below is an overview of the directory and service-level architecture, referencing code from this codebase. ## DEPENDENCY INJECTION ARCHITECTURE Meld uses TSyringe for dependency injection, which brings the following benefits: • Decoupled service creation from service usage • Simplified testing with mock injections • Clear dependencies between services • Centralized service configuration ### DI Core Concepts 1. **Service Registration**: Services are registered with the DI container via the `@Service()` decorator, which handles automatic registration with the container. 2. **Dependency Injection**: Services declare their dependencies using constructor parameters with the `@inject()` decorator, allowing the container to provide the correct dependencies. 3. **Container Resolution**: The container automatically resolves dependencies when creating instances, managing the entire dependency tree. 4. **Interface-based Design**: Services follow an interface-first design pattern, where each service implements an interface (e.g., `IFileSystemService`) and dependencies are declared using interface tokens. 5. **Circular Dependency Handling**: Circular dependencies are managed through the Client Factory pattern, which creates focused client interfaces for specific service interactions. ### DI Configuration The core DI configuration is managed in `core/di-config.ts`, which: 1. Configures the global container 2. Registers core services and client factories 3. Connects services via their respective client interfaces 4. Registers remaining services using class registrations ## DIRECTORY & FILE STRUCTURE At a high level, the project is arranged as follows (select key entries included): project-root/ ├─ api/ ← High-level API and tests │ ├─ api.test.ts │ └─ index.ts ├─ bin/ ← CLI entry point │ └─ meld.ts ├─ cli/ ← CLI implementation │ ├─ cli.test.ts │ └─ index.ts ├─ core/ ← Core utilities and types │ ├─ config/ ← Configuration (logging, etc.) │ ├─ errors/ ← Error class definitions │ │ ├─ MeldError.ts │ │ ├─ ServiceInitializationError.ts ← Service initialization errors │ │ └─ ... other errors │ ├─ types/ ← Core type definitions │ │ ├─ dependencies.ts ← Service dependency definitions │ │ └─ index.ts │ ├─ utils/ ← Logging and utility modules │ │ ├─ logger.ts │ │ ├─ serviceValidation.ts ← Service validation utilities │ │ └─ simpleLogger.ts │ └─ ServiceProvider.ts ← DI service provider & helpers ├─ services/ ← Core service implementations │ ├─ pipeline/ ← Main transformation pipeline │ │ ├─ ParserService/ ← Initial parsing │ │ ├─ InterpreterService/← Pipeline orchestration │ │ ├─ DirectiveService/ ← Directive handling │ │ │ ├─ handlers/ │ │ │ │ ├─ definition/ ← Handlers for definition directives │ │ │ │ └─ execution/ ← Handlers for execution directives │ │ │ └─ errors/ │ │ └─ OutputService/ ← Final output generation │ ├─ state/ ← State management │ │ ├─ StateService/ ← Core state management │ │ └─ StateEventService/ ← Core event system │ ├─ resolution/ ← Resolution and validation │ │ ├─ ResolutionService/ ← Variable/path resolution │ │ ├─ ValidationService/ ← Directive validation │ │ └─ CircularityService/← Circular dependency detection │ ├─ fs/ ← File system operations │ │ ├─ FileSystemService/ ← File operations │ │ ├─ PathService/ ← Path handling │ │ └─ PathOperationsService/ ← Path utilities │ └─ cli/ ← Command line interface │ └─ CLIService/ ← CLI entry point ├─ tests/ ← Test infrastructure │ ├─ fixtures/ ← Test fixture data │ ├─ mocks/ ← Test mock implementations │ └─ utils/ ← Test utilities and helpers │ ├─ debug/ ← Test debug utilities │ │ ├─ StateDebuggerService/ │ │ ├─ StateVisualizationService/ │ │ ├─ StateHistoryService/ │ │ └─ StateTrackingService/ │ ├─ di/ ← DI test utilities │ │ ├─ TestContainerHelper.ts ← Container management for tests │ │ └─ TestContextDI.ts ← DI-enabled test context │ ├─ FixtureManager.ts │ ├─ MemfsTestFileSystem.ts │ ├─ ProjectBuilder.ts │ ├─ TestContext.ts │ └─ TestSnapshot.ts ├─ docs/ ← Documentation ├─ package.json ├─ tsconfig.json ├─ tsup.config.ts └─ vitest.config.ts Key subfolders: • services/pipeline/: Core transformation pipeline services (parsing, interpretation, directives, output) • services/state/: State management and event services • services/resolution/: Resolution, validation, and circularity detection services • services/fs/: File system, path handling, and operations services • services/cli/: Command line interface services • core/: Central types, errors, utilities, and DI service provider used throughout the codebase • tests/utils/: Test infrastructure including debug utilities, memfs implementation, fixture management, and test helpers • tests/utils/di/: DI-specific test utilities • api/: High-level public API for using Meld programmatically • cli/: Command line interface for Meld ## CORE LIBRARIES & THEIR ROLE ### meld-ast • parse(content: string): MeldNode[] • Basic parsing that identifies directives vs. text nodes. • Produces an AST which other services manipulate. ### llmxml • Converts content to an LLM-friendly XML format or can parse partially. • OutputService may call it if user requests "llm" format. ### meld-spec • Contains interface definitions for MeldNode, DirectiveNode, TextNode, etc. • Contains directive kind enumerations. ### tsyringe • Provides the dependency injection container • Manages service creation and resolution • Handles dependencies between services ## HIGH-LEVEL FLOW Below is a simplified flow of how Meld content is processed: ┌─────────────────────────────┐ │ Meld Source Document │ └─────────────────────────────┘ │ ▼ ┌─────────────────────────────┐ │ ParserService.parse(...) │ │ → uses meld-ast to parse │ └─────────────────────────────┘ │ AST (MeldNode[]) ▼ ┌─────────────────────────────────────────────────┐ │ InterpreterService.interpret(nodes, options) │ │ → For each node, pass to DirectiveService │ │ → Handles node transformations │ └─────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ DirectiveService │ │ → Routes to correct directive handler │ │ → Handlers can provide replacements │ └──────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────┐ │ StateService + ResolutionService + Others │ │ → Stores variables and transformed nodes │ │ → Path expansions, data lookups, etc. │ └───────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ OutputService │ │ → Uses transformed nodes for output │ │ → Generates clean, directive-free │ │ markdown, LLM XML, or other formats │ └──────────────────────────────────────────┘ This flow is orchestrated through DI, where the container resolves all required services and their dependencies automatically. The DI container handles service creation, ensuring each service gets the dependencies it needs to function properly. ## MAJOR SERVICES (OVERVIEW) Below are the key "services" in the codebase. Each follows the single responsibility principle and is registered with the DI container via the `@Service()` decorator: ### CLIService - Provides command-line interface for running Meld - Handles file watching and reprocessing - Manages format selection and output options - Routes to appropriate services based on CLI flags - Dependencies: ParserService, InterpreterService, OutputService, FileSystemService, PathService, StateService ### ParserService - Wraps the meld-ast parse(content) function - Adds location information with file paths (parseWithLocations) - Produces an array of MeldNode objects - Dependencies: ResolutionServiceClient (for resolving variables during parsing) ### DirectiveService - Routes directives to the correct directive handler - Validates directives using ValidationService - Calls ResolutionService for variable resolution - Updates StateService with directive execution results - Supports node transformation through DirectiveResult interface - Handlers can provide replacement nodes for transformed output - Dependencies: ValidationService, StateService, PathService, FileSystemService, ParserService, InterpreterService, CircularityService, ResolutionService ### InterpreterService - Orchestrates the main interpret(nodes) pipeline - For each AST node: a) If it's text, store it or pass it along b) If it's a directive: - Calls DirectiveService for processing - Handles node transformations if provided - Updates state with transformed nodes - Maintains the top-level process flow - Supports transformation mode through feature flags - Dependencies: DirectiveService, StateService, ParserService, FileSystemService, PathService, CircularityService ### StateService - Stores variables in maps: • textVars (for @text) • dataVars (for @data) • pathVars (for @path) • commands (for @define) - Tracks both original and transformed MeldNodes - Provides transformation capabilities for directive processing - Maintains transformation state during cloning - Provides child states for nested imports - Supports immutability toggles - Dependencies: StateFactory, StateEventService, StateTrackingService ### ResolutionService - Handles all variable interpolation: • Variables ("{{var}}", "{{data.field}}") • Path expansions ("$HOMEPATH/path") • Command references - Context-aware resolution - Circular reference detection - Sub-fragment parsing support - Dependencies: StateService, FileSystemService, ParserServiceClient, PathService ### CircularityService - Prevents infinite import loops - Detects circular variable references - Maintains dependency graphs - Dependencies: ResolutionService ### PathService - Validates and normalizes paths - Enforces path security constraints - Handles path joining and manipulation - Supports test mode for path operations - Dependencies: FileSystemServiceClient (to check if paths exist) ### ValidationService - Validates directive syntax and constraints - Provides extensible validator registration - Throws MeldDirectiveError on validation failures - Tracks available directive kinds - Dependencies: ResolutionService ### FileSystemService - Abstracts file operations (read, write) - Supports both real and test filesystems - Handles path resolution and validation - Dependencies: PathOperationsService, PathServiceClient, IFileSystem ### OutputService - Converts final AST and state to desired format - Uses transformed nodes when available - Supports markdown and LLM XML output - Integrates with llmxml for LLM-friendly formatting - Handles format-specific transformations - Provides clean output without directive definitions - Dependencies: StateService, ResolutionService, VariableReferenceResolverClient ## TESTING INFRASTRUCTURE All tests are heavily reliant on a memory-based filesystem (memfs) for isolation and speed. The major testing utilities include: ### TestContainerHelper - Manages DI containers for tests - Provides isolated container creation - Supports mock registration and service resolution - Handles container cleanup between tests - Detects container state leaks ### TestContextDI - Central test harness that extends TestContext with DI support - Creates a DI container for each test - Provides mock service registration - Supports child context creation - Ensures proper cleanup after tests - Resolves services from the container for testing ### MemfsTestFileSystem - Thin wrapper around memfs - Offers readFile, writeFile, mkdir, etc. with in-memory data - Provides an ephemeral environment for all test IO ### TestContext - Base class for testing environment - Provides references to all major services - Allows writing files, snapshotting the FS, and comparing ### TestSnapshot - Takes "snapshots" of the current Memfs FS, storing a Map - Compares snapshots to detect added/removed/modified files ### ProjectBuilder - Creates mock "projects" in the in-memory FS from JSON structure - Useful for complex, multi-file tests or large fixture-based testing ### Node Factories - Provides helper functions for creating AST nodes in tests - Supports creating directive, text, and code fence nodes - Includes location utilities for source mapping Testing Organization: • tests/utils/: Core test infrastructure (MemFS, snapshots, contexts) • tests/utils/di/: DI-specific test utilities • tests/mocks/: Minimal mocks and test doubles • tests/fixtures/: JSON-based test data • tests/services/: Service-specific integration tests Testing Approach: • Each test uses TestContextDI to create a fresh container • Direct service resolution from the container • Mock registration for dependencies • Isolated container state between tests • Factory functions for creating test nodes and data • Snapshots for tracking filesystem changes ## DEBUGGING INFRASTRUCTURE The codebase includes specialized debugging services located in `tests/utils/debug/` that help diagnose and troubleshoot state-related issues: ### StateDebuggerService - Provides debug session management and diagnostics - Tracks state operations and transformations - Offers operation tracing and analysis - Helps identify state manipulation issues ### StateVisualizationService - Generates visual representations of state - Creates Mermaid/DOT graphs of state relationships - Visualizes state metrics and transformations - Aids in understanding complex state changes ### StateHistoryService - Records chronological state changes - Maintains operation history - Tracks transformation chains - Enables state change replay and analysis ### StateTrackingService - Monitors state relationships and dependencies - Tracks state lineage and inheritance - Records metadata about state changes - Helps debug scope and inheritance issues Debugging Approach: • Services can be enabled selectively in tests • Debug output includes detailed state snapshots • Visual representations help understand complex states • History tracking enables step-by-step analysis These debugging services are particularly useful for: • Troubleshooting complex state transformations • Understanding directive processing chains • Analyzing variable resolution paths • Debugging scope inheritance issues • Visualizing state relationships ## SERVICE RELATIONSHIPS AND DEPENDENCY INJECTION Services in Meld follow a dependency graph managed through the DI container: 1. Base Services: - FileSystemService (depends on PathOperationsService, PathServiceClient) - PathService (depends on FileSystemServiceClient) 2. State Management: - StateEventService (no dependencies) - StateService (depends on StateFactory, StateEventService, StateTrackingService) 3. Core Pipeline: - ParserService (depends on ResolutionServiceClient) - ResolutionService (depends on StateService, FileSystemService, PathService, ParserServiceClient) - ValidationService (depends on ResolutionService) - CircularityService (depends on ResolutionService) 4. Pipeline Orchestration: - DirectiveService (depends on multiple services) - InterpreterService (orchestrates others) 5. Output Generation: - OutputService (depends on StateService, ResolutionService, VariableReferenceResolverClient) 6. Debug Support: - DebuggerService (optional, depends on all) ## Dependency Resolution Patterns ### Circular Dependency Challenges Circular dependencies occur when two or more services depend on each other, creating a dependency cycle: - **FileSystemService ↔ PathService**: FileSystemService needs PathService for path resolution, while PathService needs FileSystemService to check if paths exist - **ParserService ↔ ResolutionService**: ParserService needs ResolutionService to resolve variables, while ResolutionService needs ParserService to parse content - **StateService ↔ StateTrackingService**: Complex bidirectional relationship for state tracking and management ### Client Factory Pattern (Current Approach) The primary approach for handling circular dependencies in Meld is the Client Factory pattern: 1. Create minimal client interfaces that expose only the methods needed by the dependent service 2. Implement factories to create these client interfaces 3. Inject the factories rather than the actual services 4. Use the clients to access only the functionality that's actually needed This pattern follows the Interface Segregation Principle (the "I" in SOLID), ensuring that services depend only on the methods they actually use. #### Example Implementation For the FileSystemService ↔ PathService circular dependency: ```typescript // Minimal interface for what FileSystemService needs from PathService export interface IPathServiceClient { resolvePath(path: string): string; normalizePath(path: string): string; } // Factory to create a client for PathService functionality @injectable() @Service({ description: 'Factory for creating path service clients' }) export class PathServiceClientFactory { constructor(@inject('IPathService') private pathService: IPathService) {} createClient(): IPathServiceClient { return { resolvePath: (path) => this.pathService.resolvePath(path), normalizePath: (path) => this.pathService.normalizePath(path) }; } } // Updated FileSystemService that depends on the factory @injectable() @Service({ description: 'Service for file system operations' }) export class FileSystemService implements IFileSystemService { private pathClient: IPathServiceClient; constructor( @inject('IPathOperationsService') private readonly pathOps: IPathOperationsService, @inject('PathServiceClientFactory') pathClientFactory: PathServiceClientFactory, @inject('IFileSystem') fileSystem: IFileSystem | null = null ) { this.fs = fileSystem || new NodeFileSystem(); this.pathClient = pathClientFactory.createClient(); } // Use the client interface directly private resolvePath(filePath: string): string { return this.pathClient.resolvePath(filePath); } } ``` Similarly, implement the reverse direction with a `FileSystemServiceClient` and `FileSystemServiceClientFactory`. ### Direct Container Resolution (Alternative Approach) For cases where the Client Factory pattern isn't feasible, direct container resolution with lazy loading can be used: ```typescript import { resolveService } from '@core/ServiceProvider'; @injectable() @Service({ description: 'Service with lazy dependency resolution' }) export class OutputService implements IOutputService { private resolverClient?: IVariableReferenceResolverClient; constructor( @inject('IStateService') private readonly stateService: IStateService, @inject('IResolutionService') private readonly resolutionService: IResolutionService ) {} /** * Get a resolver client using direct container resolution * This breaks circular dependencies by deferring resolution until needed */ private getVariableResolver(): IVariableReferenceResolverClient | undefined { // Lazy-load the client only when needed if (!this.resolverClient) { try { // Get the factory from the container using ServiceProvider helper const factory = resolveService( 'VariableReferenceResolverClientFactory' ); // Create the client this.resolverClient = factory.createClient(); logger.debug('Successfully created VariableReferenceResolverClient'); } catch (error) { logger.warn('Failed to create VariableReferenceResolverClient', { error }); } } return this.resolverClient; } // Using the lazy-loaded client async convert(nodes: MeldNode[], state: IStateService, format: string = 'markdown'): Promise { // Get the resolver only when needed const resolver = this.getVariableResolver(); if (resolver && format === 'markdown') { // Process nodes using the resolver for field access return this.nodeToMarkdown(nodes, state, resolver); } // Fallback implementation if resolver isn't available return this.legacyConvert(nodes, state, format); } } ``` This approach: 1. Avoids creating circular dependencies at initialization time 2. Loads dependencies only when they're actually needed 3. Provides fallback mechanisms when resolution fails 4. Uses the ServiceProvider helper `resolveService()` rather than direct container access Key considerations when using direct container resolution: 1. Always include fallback mechanisms 2. Log resolution failures for debugging 3. Cache resolved instances for performance 4. Only resolve what you need, when you need it #### Benefits of Client Factory Pattern 1. **Clear Dependencies**: Services explicitly state what they need through focused interfaces 2. **Interface Segregation**: Services only get access to the specific methods they need 3. **No Null Checks**: Factory creates clients at initialization time, eliminating null checks 4. **Simpler Testing**: Small, focused interfaces are easier to mock 5. **Reduced Tight Coupling**: Services are coupled only to minimal interfaces 6. **Improved Code Readability**: Code intent becomes clearer when using direct method calls 7. **Better Maintainability**: Changes to service interfaces won't affect all dependent services #### Naming Conventions For consistency across the codebase, we follow these naming conventions: - Client Interfaces: `I[ServiceName]Client` (e.g., `IPathServiceClient`) - Factory Classes: `[ServiceName]ClientFactory` (e.g., `PathServiceClientFactory`) - Factory Methods: `createClient()` for consistent API #### Testing with Client Factories Testing becomes more straightforward with the client factory pattern: ```typescript describe('FileSystemService', () => { let context: TestContextDI; let service: IFileSystemService; beforeEach(() => { context = TestContextDI.create(); // Create a mock client const mockPathClient = { resolvePath: vi.fn().mockReturnValue('/resolved/path'), normalizePath: vi.fn().mockReturnValue('normalized/path') }; // Create a mock factory that returns our mock client const mockPathClientFactory = { createClient: vi.fn().mockReturnValue(mockPathClient) }; // Register the mock factory context.registerMock('PathServiceClientFactory', mockPathClientFactory); // Resolve the service service = context.resolveSync('IFileSystemService'); }); afterEach(async () => { await context.cleanup(); }); it('should resolve paths using the path client', async () => { // Test that calling methods on the service uses the client correctly await service.readFile('some/path'); // Verify the path client was used expect(mockPathClient.resolvePath).toHaveBeenCalledWith('some/path'); }); }); ``` For testing services that use direct container resolution, we register mocks directly with the container: ```typescript describe('OutputService', () => { let context: TestContextDI; let service: IOutputService; beforeEach(() => { context = TestContextDI.create(); // Create a mock resolver client const mockResolverClient = { accessFields: vi.fn().mockReturnValue('resolved value'), convertToString: vi.fn().mockReturnValue('formatted string') }; // Create a mock factory that returns our mock client const mockFactory = { createClient: vi.fn().mockReturnValue(mockResolverClient) }; // Register the mock factory with the container context.registerMock('VariableReferenceResolverClientFactory', mockFactory); // Resolve the service service = context.resolveSync('IOutputService'); }); afterEach(async () => { await context.cleanup(); }); it('should convert nodes to markdown with field access', async () => { const result = await service.convert(mockNodes, mockState, 'markdown'); expect(result).toContain('formatted string'); }); }); ``` ## EXAMPLE USAGE SCENARIO 1) Input: A .meld file with lines like: @text greeting = "Hello" @data config = { "value": 123 } @import [ path = "other.meld" ] 2) We load the file from disk. 3) ParserService → parse the content → AST. 4) InterpreterService → interpret(AST). a) For each directive, DirectiveService → validation → resolution → update StateService. b) If an import is encountered, CircularityService ensures no infinite loops. 5) Once done, the final StateService has textVars.greeting = "Hello", dataVars.config = { value: 123 }, etc. 6) OutputService can generate the final text or an LLM-XML representation. With DI, this flow is orchestrated through the container, which resolves all the required services and their dependencies automatically. ## ERROR HANDLING • MeldDirectiveError thrown if a directive fails validation or interpretation. • MeldParseError if the parser cannot parse content. • PathValidationError for invalid paths. • ResolutionError for variable resolution issues. • MeldError as a base class for other specialized errors. • ServiceInitializationError for DI-related initialization failures. These errors typically bubble up to the caller or test. ## CONCLUSION This codebase implements the entire Meld language pipeline: • Parsing Meld documents into an AST. • Validating & interpreting directives. • Storing data in a hierarchical state. • Resolving references (text, data, paths, commands). • (Optionally) generating final formatted output. The codebase uses TSyringe for dependency injection, which helps manage the complex relationships between services. The Client Factory pattern is used to handle circular dependencies between core services, with direct container resolution as an alternative for specific cases. The test environment includes robust DI support with TestContextDI, allowing for isolated container testing, mock registration, and service resolution. The system adheres to SOLID design principles with interface-first design and clear separation of concerns. # Dependency Injection in Meld This document provides guidance on working with the dependency injection (DI) system in the Meld codebase. ## Overview Meld uses [TSyringe](https://github.com/microsoft/tsyringe) for dependency injection. All services are registered and resolved through the DI container, which simplifies service initialization and testing. ## Core Concepts ### 1. Service Registration Services are automatically registered with the DI container when they are decorated with the `@Service()` decorator: ```typescript import { Service } from '@core/ServiceProvider'; @Service({ description: 'Service that provides file system operations' }) export class FileSystemService implements IFileSystemService { // Implementation... } ``` The `@Service()` decorator registers the class with the container and adds some metadata for documentation purposes. ### 2. Dependency Injection Services can inject their dependencies through constructor parameters: ```typescript import { inject } from 'tsyringe'; @Service() export class ResolutionService implements IResolutionService { constructor( @inject('IStateService') private stateService: IStateService, @inject('IFileSystemService') private filesystem: IFileSystemService, @inject('IParserService') private parser: IParserService, @inject('IPathService') private pathService: IPathService ) {} // Implementation... } ``` ### 3. Creating Services Services should be created using the DI container, not with `new`: ```typescript // CORRECT: Let the DI container create the service import { container } from 'tsyringe'; const service = container.resolve(ServiceClass); // CORRECT: Use the ServiceProvider helper import { createService } from '@core/ServiceProvider'; const service = createService(ServiceClass); // INCORRECT: Don't use 'new' directly const service = new ServiceClass(); // Avoid this ``` ## Best Practices ### Service Design 1. **Interface-First Design**: Define an interface for your service before implementing it 2. **Explicit Dependencies**: Always specify dependencies in the constructor 3. **Private Injection**: Use `private` in constructor parameters to store the dependencies 4. **Explicit Return Types**: Always provide return types for methods 5. **Proper Initialization**: Services should be fully initialized after construction ### Example Service ```typescript import { inject } from 'tsyringe'; import { Service } from '@core/ServiceProvider'; // 1. Define the interface export interface IExampleService { process(data: string): Promise; getStatus(): string; } // 2. Implement the service @Service({ description: 'Example service that demonstrates best practices' }) export class ExampleService implements IExampleService { // 3. Constructor injection with explicit dependencies constructor( @inject('IDependencyService') private dependency: IDependencyService, @inject('ILoggerService') private logger: ILoggerService ) {} // 4. Explicit return type async process(data: string): Promise { this.logger.log('Processing data...'); return this.dependency.transform(data); } getStatus(): string { return 'Ready'; } } ``` ## Testing with DI ### Using TestContextDI The `TestContextDI` class provides utilities for testing with DI: ```typescript import { TestContextDI } from '@tests/utils/di/TestContextDI'; describe('MyService', () => { let context: TestContextDI; beforeEach(() => { // Create a test context with DI context = TestContextDI.create(); }); afterEach(async () => { // Clean up resources await context.cleanup(); }); it('should process data correctly', async () => { // Register a mock dependency const mockDependency = { transform: vi.fn().mockReturnValue('transformed') }; context.registerMock('IDependencyService', mockDependency); // Get the service from the container const service = context.container.resolve('IExampleService'); // Test the service const result = await service.process('input'); expect(result).toBe('transformed'); expect(mockDependency.transform).toHaveBeenCalledWith('input'); }); }); ``` ### Mocking Services To register mock implementations: ```typescript // Register a mock instance context.registerMock('IServiceName', mockImplementation); // Register a mock class context.container.registerMockClass('IServiceName', MockClass); ``` ## Common Patterns ### Dual-Mode Constructor Pattern Meld services need to support both DI and non-DI modes. The recommended pattern is: ```typescript /** * Constructor with DI annotations */ constructor( @inject(SomeFactory) factory?: SomeFactory, @inject('IService1') service1?: IService1, @inject('IService2') service2?: IService2 ) { this.initializeFromParams(factory, service1, service2); } /** * Helper that chooses initialization path */ private initializeFromParams( factory?: SomeFactory, service1?: IService1, service2?: IService2 ): void { if (factory) { this.initializeDIMode(factory, service1, service2); } else { this.initializeLegacyMode(service1, service2); } } /** * DI mode initialization */ private initializeDIMode( factory: SomeFactory, service1?: IService1, service2?: IService2 ): void { this.factory = factory; this.service1 = service1; this.service2 = service2; // Additional initialization } /** * Legacy mode initialization */ private initializeLegacyMode( service1?: IService1, service2?: IService2 ): void { // Create default dependencies this.factory = new SomeFactory(); // Additional initialization } ``` This pattern: 1. Keeps the constructor simple 2. Clearly separates DI and non-DI initialization logic 3. Makes maintenance easier 4. Preserves dual-mode functionality 5. Provides a clear path to eventually remove legacy mode See `_dev/issues/features/service-initialization-patterns.md` for more examples. ### Factory Pattern For services that need complex initialization or multiple instances: ```typescript @Service() export class ServiceFactory { constructor( @inject('IDependencyA') private depA: IDependencyA, @inject('IDependencyB') private depB: IDependencyB ) {} createService(config: ServiceConfig): IService { // Create a specialized instance with the given config // The factory can use its injected dependencies return new SpecializedService(this.depA, this.depB, config); } } ``` ### Service Providers For centralized service registration: ```typescript // In a central di-config.ts file: import { container } from 'tsyringe'; // Register core services container.register('FileSystemService', { useClass: FileSystemService }); container.register('IFileSystemService', { useToken: 'FileSystemService' }); ``` ## Troubleshooting ### Circular Dependencies If you have circular dependencies, use `@inject(token)` with a string token instead of a direct class reference: ```typescript // Instead of this (can cause circular dependency issues): constructor(@inject(DependentService) private dependent: DependentService) // Do this: constructor(@inject('IDependentService') private dependent: IDependentService) ``` ### Missing Dependencies If a service fails to resolve with "unregistered dependency token" errors: 1. Check that the service is decorated with `@Service()` 2. Verify that the injected token is registered in the container 3. Check for typos in the injection token string 4. Make sure the services are imported and executed before use ### Testing Issues If tests fail with DI errors: 1. Use `TestContextDI` to create a clean container for each test 2. Register all required mock dependencies before resolving the service 3. Clean up after tests with `context.cleanup()` ``` #### ../../docs/dev/PIPELINE.md ```javascript # Meld Pipeline Flow ## Overview The Meld pipeline processes `.mld` files through several stages to produce either `.xml` or `.md` output. Here's a detailed look at how it works: ```ascii ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Service │ │ Service │ │ Pipeline │ │ Variable │ │ Final │ │Initialization├────►│ Validation ├────►│ Execution ├────►│ Resolution ├────►│ Output │ └─────────────┘ └─────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │Dependencies │ │Validate All │ │Process Input │ │Resolve Vars & │ │Generate Clean│ │ Resolved │ │ Services │ │ Content │ │ References │ │ Output │ └─────────────┘ └─────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ ``` ## Service Organization The pipeline is organized into logical service groups, with strict initialization order and dependency validation: ### Pipeline Services (services/pipeline/) ```ascii ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ Parser │ │ Directive │ │ Interpreter │ │ Output │ │ Service ├────►│ Service ├────►│ Service ├────►│ Service │ └─────────────┘ └─────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │Initialize & │ │Validate & │ │Transform & │ │Format & │ │ Validate │ │Process Dirs │ │Update State │ │Generate Out │ └─────────────┘ └─────────────┘ └──────────────┘ └──────────────┘ ``` ### State Services (services/state/) ```ascii ┌─────────────┐ ┌─────────────┐ │ State │ │ State │ │ Service ├────►│ Event │ └─────────────┘ │ Service │ └─────────────┘ ``` ### Resolution Services (services/resolution/) ```ascii ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ Resolution │ │ Validation │ │ Circularity │ │ Service ├────►│ Service ├────►│ Service │ └─────────────┘ └─────────────┘ └──────────────┘ ``` ### File System Services (services/fs/) ```ascii ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ File │ │ Path │ │ Path │ │ System ├────►│ Service ├────►│ Operations │ │ Service │ │ │ │ Service │ └─────────────┘ └─────────────┘ └──────────────┘ ``` ## Detailed Flow 1. **Service Initialization** (`core/types/dependencies.ts`) ```ascii ┌─────────────┐ │Load Service │ │Dependencies │ └─────┬───────┘ │ ▼ ┌─────────────┐ │Initialize in│ │ Order │ └─────┬───────┘ │ ▼ ┌─────────────┐ │ Validate │ │ Services │ └─────────────┘ ``` - Resolves service dependencies - Initializes in correct order - Validates service configuration - Enables transformation if requested 2. **Input Processing** (`CLIService`) - User runs `meld prompt.mld` - `CLIService` handles command line options - Default output is `.xml` format - Can specify `--format markdown` for `.md` output - Supports `--stdout` for direct console output 3. **Parsing** (`ParserService`) ```ascii ┌─────────────┐ │ Raw Text │ │ Input │ └─────┬───────┘ │ ▼ ┌─────────────┐ │ meld-ast │ │ Parser │ └─────┬───────┘ │ ▼ ┌─────────────┐ │ MeldNode[] │ │ AST │ └─────────────┘ ``` - Reads the input file content - Parses into AST using `meld-ast` - Identifies directives and text nodes - Adds source location information 4. **Interpretation** (`InterpreterService`) ```ascii ┌─────────────┐ ┌─────────────┐ │ MeldNode[] │ │ Directive │ │ AST ├────►│ Service │ └─────────────┘ └──────┬──────┘ │ ▼ ┌─────────────┐ ┌─────────────┐ │ Resolution │◄────┤ Handler │ │ Service │ │(with node │ └──────┬──────┘ │replacements)│ │ └─────────────┘ ▼ ┌─────────────┐ │ State │ │ Service │ │(Original & │ │Transformed) │ └─────────────┘ ``` - Processes each AST node sequentially - Routes directives to appropriate handlers - Handlers can provide replacement nodes - Maintains both original and transformed states - Resolves variables and references - Handles file imports and embedding 5. **Variable Resolution** (`ResolutionService`) ```ascii ┌─────────────┐ ┌─────────────┐ │Text Nodes & │ │ Resolution │ │ Directives ├────►│ Service │ └─────────────┘ └──────┬──────┘ │ ▼ ┌─────────────┐ ┌─────────────┐ │ Field │ │ State with │ │ Access │◄────┤ Variables │ │ Utility │ │ │ └──────┬──────┘ └─────────────┘ │ ▼ ┌─────────────┐ │ Resolved │ │ Variables │ │ & References│ └─────────────┘ ``` - Resolves variable references like `{{variable}}` - Handles field access with dot and bracket notation - Supports nested object and array access - Manages path variable resolution and prefixing - Provides two architecture models: - Traditional: OutputService handles resolution - Delegated: ResolutionService handles resolution 6. **Output Generation** (`OutputService`) ```ascii ┌─────────────┐ ┌─────────────┐ │Transformed │ │ Format │ │ Nodes & ├────►│ Converter │ │ State │ └──────┬──────┘ │ ▼ ┌─────────────┐ ┌─────────────┐ │Clean Output │◄────┤ Formatted │ │(No Directive│ │ Output │ │Definitions) │ └─────────────┘ └─────────────┘ ``` - Takes transformed nodes and state - Converts to requested format: - `llm`: Uses `llmxml` library for LLM-friendly XML - `markdown`: Clean markdown without directive definitions - Writes output to file or stdout ## Transformation Mode and Variable Resolution When transformation mode is enabled, the pipeline handles directives and variables in a special way. Understanding this flow is critical for debugging and enhancing directive handlers: ```ascii ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ Directive │ │Interpretation│ │ Variable │ │ Output │ │ Handlers ├────►│ & Node ├────►│ Resolution ├────►│ Generation │ │(with replace│ │Transformation│ │ │ │ │ │ nodes) │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └──────────────┘ └──────────────┘ ``` ### Key Transformation Pipeline Concepts 1. **Directive Handler Replacement Nodes** - Directive handlers can return replacement nodes when in transformation mode - The InterpreterService must properly apply these replacements in the transformed nodes array - For import directives, the replacement is typically an empty text node - For embed directives, the replacement node contains the embedded content 2. **State Propagation Across Boundaries** - Variables must be explicitly copied between parent and child states - When importing files, variables must be copied from imported state to parent state - The ImportDirectiveHandler must ensure all variable types (text, data, path, commands) are copied 3. **Variable Resolution Process** - Variables can be resolved at multiple stages: - During directive processing - During node transformation - During final output generation - During post-processing in the main function - Meld supports two distinct architectures for variable resolution: - **Traditional Architecture**: OutputService resolves variables directly during final rendering - **Delegated Architecture**: OutputService delegates to ResolutionService for variable resolution - Architecture is controlled by the `resolveVariablesInOutput` feature flag or the `MELD_DISABLE_OUTPUT_VARIABLE_RESOLUTION` environment variable 4. **State Management for Transformation** - The StateService maintains both original and transformed node arrays - Transformed nodes must be explicitly initialized - The transformNode method is used to replace directive nodes with their outputs - State must keep track of transformation options to determine which directives to transform ## Service Responsibilities ### Pipeline Services 1. **ParserService** (`services/pipeline/ParserService/`) - Wraps meld-ast parser - Produces AST nodes - Adds file location information 2. **InterpreterService** (`services/pipeline/InterpreterService/`) - Orchestrates directive processing - Handles node transformations - Maintains interpretation state - Handles imports and embedding - **Critical for transformation:** Applies directive handler replacement nodes to transformed node array - **State propagation:** Ensures proper variable inheritance between parent and child states 3. **DirectiveService** (`services/pipeline/DirectiveService/`) - Routes directives to handlers - Validates directive syntax - Supports node transformation - Updates state based on directive results - **Directive handlers:** Can return replacement nodes in transformation mode - **Handler context:** Includes parent state for proper variable propagation 4. **OutputService** (`services/pipeline/OutputService/`) - Uses transformed nodes for clean output - Supports markdown and LLM XML - Generates directive-free output - Handles formatting options - **Variable resolution:** Resolves variable references in text nodes during output generation - **Transformation handling:** Uses special processing for variable references in transformation mode ### State Services 1. **StateService** (`services/state/StateService/`) - Stores variables and commands - Maintains original and transformed nodes - Manages scope and inheritance - Tracks file dependencies - **Transformation support:** Keeps track of both original and transformed node arrays - **Variable copying:** Must explicitly copy variables between parent and child states - **Transformation options:** Supports selective transformation of different directive types 2. **StateEventService** (`services/state/StateEventService/`) - Handles state change events - Manages state updates - Provides event hooks - Supports state tracking ### Resolution Services 1. **ResolutionService** (`services/resolution/ResolutionService/`) - Resolves variables and references - Handles path expansions - Manages circular dependencies 2. **ValidationService** (`services/resolution/ValidationService/`) - Validates directive syntax and constraints - Provides extensible validator registration - Throws MeldDirectiveError on validation failures - Tracks available directive kinds 3. **CircularityService** (`services/resolution/CircularityService/`) - Prevents infinite import loops - Detects circular variable references - Maintains dependency graphs ### File System Services 1. **FileSystemService** (`services/fs/FileSystemService/`) - Abstracts file operations (read, write) - Supports both real and test filesystems - Handles path resolution and validation 2. **PathService** (`services/fs/PathService/`) - Validates and normalizes paths - Enforces path security constraints - Handles path joining and manipulation - Supports test mode for path operations 3. **PathOperationsService** (`services/fs/PathOperationsService/`) - Handles complex path operations - Provides path utilities - Manages path transformations ```' [debug] Template reference 'files.directiveClarityDoc' requesting content for 1 files... [debug] Reading relative file {"relativePath":"../../_dev/DEFINE-CLARITY.md","absolutePath":"/Users/adam/dev/meld/_dev/DEFINE-CLARITY.md"} [debug] Interpolated object key 'directiveClarityContent' from '{{ files.directiveClarityDoc }}' to '#### ../../_dev/DEFINE-CLARITY.md ```javascript # @define Directive: Understanding and Implementation ## Core Concept: Creating Reusable Command Templates The `@define` directive allows you to create named, reusable templates for runnable commands (both shell commands and language scripts). These templates can accept parameters, making them function like simple macros or functions within Meld. Defined commands are invoked using the `@run $commandName(...)` syntax. ## Syntax There are two primary forms: **1. Defining Basic Commands (Shell Commands):** ```meld @define commandName(param1, param2) = @run [command template with {{param1}} and {{param2}}] // Or for multiline commands: @define multiCmd(arg) = @run [[ echo "Starting script with {{arg}}" ./run_script.sh {{arg}} ]] ``` - **`commandName`**: The identifier (no `$`) used to invoke the command later with `@run $commandName(...)`. - **`(param1, param2)`**: An optional list of parameter names, acting as placeholders within the command template. - **`=`**: Separator. - **`@run [...]` or `@run [[...]]`**: The right-hand side **must** be a `BasicCommand` `@run` directive. This defines the shell command template to be executed. **2. Defining Language Commands (JS, Python, Bash):** ```meld @define jsCommand(name, value) = @run js(name, value) [[ // Raw JavaScript code using parameters name & value console.log(`Processing ${name}: ${value}`); // Note: {{variables}} are NOT interpolated here ]] @define pyCommand(inputPath) = @run python(inputPath) [[ # Raw Python code import sys input_file = sys.argv[1] print(f"Processing {input_file}") # ... ]] ``` - **`commandName`**: Identifier for the language command. - **`(param1, ...)`**: Parameters expected by the language script. - **`@run language(...) [[...]]`**: The right-hand side **must** be a `LanguageCommand` `@run` directive. This defines the language, the parameters it accepts, and the *raw code block* to be executed. ## Command Template Body (for Basic Commands) When defining a Basic Command template (`@run [...]` or `@run [[...]]`): - **Shell Command**: It should be a valid shell command string. - **Parameter Placeholders**: It can contain `{{param1}}`, `{{param2}}`, etc., corresponding to the parameters defined in the parentheses `(...)`. These will be replaced by the arguments provided when the command is invoked via `@run $commandName(...)`. - **Other Variables**: It can also contain standard Meld variable references (`{{globalVar}}`, `$pathVar`). These are *not* resolved when `@define` is processed; they are resolved *at the time the command is executed* via `@run`. - **Multiline Syntax (`[[...]]`)**: If using double brackets, the first newline immediately following `[[` is ignored. ## Language Code Block (for Language Commands) When defining a Language Command template (`@run language(...) [[...]]`): - **Raw Code**: The content within `[[...]]` is treated as **raw source code** for the specified language (js, python, bash). - **NO Interpolation**: Variables (`{{var}}`, `$path`) inside the `[[...]]` block are **NOT** interpolated. The code is passed directly to the language interpreter. - **Parameters**: The parameters defined in `@run language(param1, ...)` are passed to the script at runtime (e.g., as command-line arguments). ## Core Implementation (`DefineDirectiveHandler`) The `@define` handler primarily acts as a storage mechanism: 1. **Validate Syntax**: Checks the overall `@define name(...) = @run ...` structure. 2. **Extract Components**: Parses the directive to get the `commandName` (without `$`), the list of `parameters` (e.g., `["param1", "param2"]`), and the details of the right-hand `@run` directive (its kind - basic or language, the command template string or code block, the language if applicable). 3. **Store Definition**: Creates a `CommandDefinition` object containing the `parameters` array and the necessary details from the `@run` directive (e.g., the literal command template string for basic commands, or the language and raw code block for language commands). 4. **Update State**: Stores this `CommandDefinition` object in the current execution state using `state.setCommand(commandName, commandDefinition)`. Metadata can also be stored. **Important**: The `@define` handler does *not* execute anything or resolve variables within the template/code block. It simply stores the definition. ## Interaction with `@run $commandName(...)` The execution logic resides within the `RunDirectiveHandler` and its `DefinedCommandHandler` subtype: 1. **Invocation**: `@run $myCmd("argValue1", {{variableArg2}})` 2. **Retrieve Definition**: Fetches the `CommandDefinition` for `myCmd` from the state. 3. **Resolve Arguments**: Resolves the arguments (`"argValue1"`, `{{variableArg2}}`) provided in the `@run` call. 4. **Execution based on Definition Type**: * **If Basic Command Definition**: Substitutes the resolved arguments *positionally* into the stored command template string (replacing `{{param1}}`, `{{param2}}`, etc.). The resulting command string is then executed, resolving any other variables (`{{globalVar}}`, `$pathVar`) at that time. * **If Language Command Definition**: Passes the resolved arguments to the stored language script (e.g., as command-line arguments `sys.argv` in Python, `process.argv` in Node). The raw code block stored in the definition is executed by the appropriate language interpreter. ## Key Implementation Aspects & Considerations * **Positional Parameters**: Substitution/passing relies strictly on the order in the `@define` parameter list and the `@run` argument list. * **Delayed Resolution (Basic Commands)**: Variables (`{{globalVar}}`, `$pathVar`) within a basic command template are resolved only when invoked via `@run`. * **No Interpolation (Language Commands)**: The code block for language commands is executed raw; use the defined parameters to pass data into the script. * **No Direct Output**: `@define` only modifies state. ## Validation Criteria A correct `@define` implementation ensures: - Both basic and language command definitions are correctly parsed and stored. - Invocation via `@run` correctly retrieves the definition and identifies its type. - Arguments passed to `@run` are resolved correctly. - For basic commands: Positional parameter substitution into the template works reliably, and the final command executes correctly. - For language commands: Resolved arguments are passed correctly to the script, and the stored code block is executed by the correct interpreter. ```' [debug] Template reference 'files.CoreDirectiveCode' requesting content for 3 files... [debug] Reading relative file {"relativePath":"../../services/pipeline/DirectiveService/DirectiveService.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/DirectiveService/DirectiveService.ts"} [debug] Reading relative file {"relativePath":"../../services/pipeline/DirectiveService/IDirectiveService.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/DirectiveService/IDirectiveService.ts"} [debug] Reading relative file {"relativePath":"../../services/pipeline/DirectiveService/interfaces/DirectiveTypes.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/DirectiveService/interfaces/DirectiveTypes.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.CoreDirectiveCode }}' to '#### ../../services/pipeline/DirectiveService/DirectiveService.ts ```javascript import type { DirectiveNode, DirectiveKind, DirectiveData } from '@core/syntax/types/index.js'; import { directiveLogger } from '@core/utils/logger.js'; import { IDirectiveService, IDirectiveHandler, DirectiveContext } from '@services/pipeline/DirectiveService/IDirectiveService.js'; import type { ValidationServiceLike, StateServiceLike, PathServiceLike, FileSystemLike, ParserServiceLike, InterpreterServiceLike, CircularityServiceLike, ResolutionServiceLike, DirectiveServiceLike } from '@core/shared-service-types.js'; import { MeldDirectiveError } from '@core/errors/MeldDirectiveError.js'; import { DirectiveError, DirectiveErrorCode, DirectiveErrorSeverity } from '@services/pipeline/DirectiveService/errors/DirectiveError.js'; import { ErrorSeverity } from '@core/errors/MeldError.js'; import type { ILogger } from '@services/pipeline/DirectiveService/handlers/execution/EmbedDirectiveHandler.js'; import { Service } from '@core/ServiceProvider.js'; import { inject, delay, injectable } from 'tsyringe'; import { container } from 'tsyringe'; import type { IResolutionServiceClientForDirective } from '@services/pipeline/ResolutionService/interfaces/IResolutionServiceClientForDirective.js'; import { ResolutionServiceClientForDirectiveFactory } from '@services/pipeline/ResolutionService/factories/ResolutionServiceClientForDirectiveFactory.js'; import { InterpreterServiceClientFactory } from '@services/pipeline/InterpreterService/factories/InterpreterServiceClientFactory.js'; import type { IInterpreterServiceClient } from '@services/pipeline/InterpreterService/interfaces/IInterpreterServiceClient.js'; import { DirectiveResult } from './interfaces/DirectiveTypes.js'; // Import all handlers import { TextDirectiveHandler } from '@services/pipeline/DirectiveService/handlers/definition/TextDirectiveHandler.js'; import { DataDirectiveHandler } from '@services/pipeline/DirectiveService/handlers/definition/DataDirectiveHandler.js'; import { PathDirectiveHandler } from '@services/pipeline/DirectiveService/handlers/definition/PathDirectiveHandler.js'; import { DefineDirectiveHandler } from '@services/pipeline/DirectiveService/handlers/definition/DefineDirectiveHandler.js'; import { RunDirectiveHandler } from '@services/pipeline/DirectiveService/handlers/execution/RunDirectiveHandler.js'; import { EmbedDirectiveHandler } from '@services/pipeline/DirectiveService/handlers/execution/EmbedDirectiveHandler.js'; import { ImportDirectiveHandler } from '@services/pipeline/DirectiveService/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 */ @injectable() @Service({ description: 'Service responsible for handling and processing directives', dependencies: [ { token: 'IValidationService', name: 'validationService' }, { token: 'IStateService', name: 'stateService' }, { token: 'IPathService', name: 'pathService' }, { token: 'IFileSystemService', name: 'fileSystemService' }, { token: 'IParserService', name: 'parserService' }, { token: 'InterpreterServiceClientFactory', name: 'interpreterServiceClientFactory' }, { token: 'ICircularityService', name: 'circularityService' }, { token: 'IResolutionService', name: 'resolutionService' } ] }) export class DirectiveService implements IDirectiveService, DirectiveServiceLike { private validationService!: ValidationServiceLike; private stateService!: StateServiceLike; private pathService!: PathServiceLike; private fileSystemService!: FileSystemLike; private parserService!: ParserServiceLike; private interpreterService?: InterpreterServiceLike; // Legacy reference private interpreterClient?: IInterpreterServiceClient; // Client from factory pattern private interpreterClientFactory?: InterpreterServiceClientFactory; private circularityService!: CircularityServiceLike; private resolutionService!: ResolutionServiceLike; private resolutionClient?: IResolutionServiceClientForDirective; private resolutionClientFactory?: ResolutionServiceClientForDirectiveFactory; private factoryInitialized: boolean = false; private interpreterFactoryInitialized: boolean = false; private initialized = false; private logger: ILogger; private handlers: Map = new Map(); /** * Creates a new DirectiveService instance. * Uses dependency injection for service dependencies. * * @param validationService Validation service for directives (injected) * @param stateService State service for managing variables (injected) * @param pathService Path service for handling file paths (injected) * @param fileSystemService File system service for file operations (injected) * @param parserService Parser service for parsing Meld files (injected) * @param interpreterServiceClientFactory Factory for creating interpreter clients (injected) * @param circularityService Circularity service for detecting circular imports (injected) * @param resolutionService Resolution service for variable resolution (injected) * @param logger Logger for directive operations (optional) */ constructor( @inject('IValidationService') validationService?: ValidationServiceLike, @inject('IStateService') stateService?: StateServiceLike, @inject('IPathService') pathService?: PathServiceLike, @inject('IFileSystemService') fileSystemService?: FileSystemLike, @inject('IParserService') parserService?: ParserServiceLike, @inject('InterpreterServiceClientFactory') interpreterServiceClientFactory?: InterpreterServiceClientFactory, @inject('ICircularityService') circularityService?: CircularityServiceLike, @inject('IResolutionService') resolutionService?: ResolutionServiceLike, @inject('DirectiveLogger') logger?: ILogger ) { // Always ensure we have a logger (both in DI and non-DI modes) this.logger = logger || directiveLogger; // Skip initialization if we're in DI mode and not all required services are provided if (validationService && stateService && pathService && fileSystemService && parserService && circularityService && resolutionService) { this.initializeFromParams( validationService, stateService, pathService, fileSystemService, parserService, undefined, // Replaced by interpreterServiceClientFactory circularityService, resolutionService ); } else { // In non-DI mode or when not fully initialized, just set up the logger this.logger.debug('DirectiveService constructed but not fully initialized, call initialize() manually'); } // Initialize interpreter client factory this.interpreterClientFactory = interpreterServiceClientFactory; if (this.interpreterClientFactory) { this.interpreterFactoryInitialized = true; this.initializeInterpreterClient(); } // Set initialized to true before registering handlers this.initialized = true; // Register default handlers this.registerDefaultHandlers(); } /** * Initialize this service with the given parameters. * Uses DI-only mode for initialization. */ private initializeFromParams( validationService?: ValidationServiceLike, stateService?: StateServiceLike, pathService?: PathServiceLike, fileSystemService?: FileSystemLike, parserService?: ParserServiceLike, interpreterServiceClientFactory?: InterpreterServiceClientFactory, circularityService?: CircularityServiceLike, resolutionService?: ResolutionServiceLike ): void { // Verify that required services are provided if (!validationService || !stateService || !pathService || !fileSystemService || !parserService || !circularityService || !resolutionService) { this.logger.warn('DirectiveService initialized with missing dependencies'); return; } // Initialize all services this.validationService = validationService; this.stateService = stateService; this.pathService = pathService; this.fileSystemService = fileSystemService; this.parserService = parserService; this.circularityService = circularityService; this.resolutionService = resolutionService; // Set initialized to true this.initialized = true; // Handle the circular dependency with InterpreterService // We'll set this later in updateInterpreterService() // but use the delay-injected service if available if (interpreterServiceClientFactory) { // Use setTimeout to ensure all services are fully initialized setTimeout(() => { this.registerDefaultHandlers(); this.logger.debug('DirectiveService initialized via DI', { handlers: Array.from(this.handlers.keys()) }); }, 0); } } /** * Explicitly initialize the service with all required dependencies. * @deprecated This method is maintained for backward compatibility. * The service is automatically initialized via dependency injection. */ initialize( validationService: ValidationServiceLike, stateService: StateServiceLike, pathService: PathServiceLike, fileSystemService: FileSystemLike, parserService: ParserServiceLike, interpreterServiceClientFactory: InterpreterServiceClientFactory, circularityService: CircularityServiceLike, resolutionService: ResolutionServiceLike ): void { this.validationService = validationService; this.stateService = stateService; this.pathService = pathService; this.fileSystemService = fileSystemService; this.parserService = parserService; this.circularityService = circularityService; this.resolutionService = resolutionService; this.initialized = true; // Initialize interpreter client factory this.interpreterClientFactory = interpreterServiceClientFactory; if (this.interpreterClientFactory) { this.interpreterFactoryInitialized = true; this.initializeInterpreterClient(); } // Register default handlers this.registerDefaultHandlers(); this.logger.debug('DirectiveService initialized manually', { handlers: Array.from(this.handlers.keys()) }); } /** * Register all default directive handlers * This is public to allow tests to explicitly initialize handlers in both DI and non-DI modes */ public registerDefaultHandlers(): void { // Add debug logging to help diagnose DI issues this.logger.debug('Registering default handlers', { hasValidationService: !!this.validationService, hasStateService: !!this.stateService, hasResolutionService: !!this.resolutionService, hasFileSystemService: !!this.fileSystemService, stateTransformationEnabled: this.stateService?.isTransformationEnabled?.(), state: this.stateService ? { hasTrackingService: !!(this.stateService as any).trackingService, hasEventService: !!(this.stateService as any).eventService } : 'undefined' }); try { // Definition handlers const textHandler = new TextDirectiveHandler( this.validationService!, this.stateService!, this.resolutionService! ); // Set FileSystemService if available if (this.fileSystemService) { textHandler.setFileSystemService(this.fileSystemService); } this.registerHandler(textHandler); this.registerHandler( new DataDirectiveHandler( this.validationService!, this.stateService!, this.resolutionService! ) ); } catch (error) { this.logger.error('Error registering directive handlers', { error }); throw error; } this.registerHandler( new PathDirectiveHandler( this.validationService!, this.stateService!, this.resolutionService! ) ); this.registerHandler( new DefineDirectiveHandler( this.validationService!, this.stateService!, this.resolutionService! ) ); // Execution handlers try { // Try to get CommandExecutionService from the container let commandExecutionService: any; try { commandExecutionService = container.resolve('ICommandExecutionService'); this.logger.debug('Resolved CommandExecutionService from container'); } catch (resolveError) { this.logger.warn('CommandExecutionService not found in container, creating mock for testing'); // Create RunFeedbackManager for the mock const mockFeedbackManager = { showRunningCommandFeedback: (command: string) => { this.logger.debug(`Running command: ${command}`); }, clearCommandFeedback: () => {}, startCommandAnimation: (message: string) => { this.logger.debug(message || 'Running command...'); return () => {}; } }; // Create a mock CommandExecutionService commandExecutionService = { executeShellCommand: async (command: string, options?: any) => { this.logger.debug(`Mock executing shell command: ${command}`); return { stdout: `Mock output for: ${command}`, stderr: '', exitCode: 0 }; }, executeLanguageCode: async (code: string, language: string, options?: any) => { this.logger.debug(`Mock executing ${language} code`); return { stdout: `Mock output for ${language} code`, stderr: '', exitCode: 0 }; } }; // Register the mock for future resolution try { container.registerInstance('ICommandExecutionService', commandExecutionService); container.registerInstance('RunFeedbackManager', mockFeedbackManager); this.logger.debug('Registered mock CommandExecutionService in container'); } catch (registerError) { this.logger.warn('Failed to register mock CommandExecutionService', { error: registerError }); } } this.registerHandler( new RunDirectiveHandler( this.validationService!, this.resolutionService!, this.stateService!, this.fileSystemService!, commandExecutionService ) ); } catch (error) { this.logger.error('Failed to create RunDirectiveHandler', { error }); throw error; } this.registerHandler( new EmbedDirectiveHandler( this.validationService!, this.resolutionService!, this.stateService!, this.circularityService!, this.fileSystemService!, this.parserService!, this.interpreterClientFactory!, this.logger ) ); this.registerHandler( new ImportDirectiveHandler( this.validationService!, this.resolutionService!, this.stateService!, this.fileSystemService!, this.parserService!, this.interpreterClientFactory!, 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 { return this.processDirective(node, context); } /** * Process multiple directives in sequence */ async processDirectives(nodes: DirectiveNode[], parentContext?: DirectiveContext): Promise { let currentState = parentContext?.state?.clone() || this.stateService!.createChildState(); // Inherit or create initial formatting context let currentFormattingContext = parentContext?.formattingContext ? { ...parentContext.formattingContext } : { isOutputLiteral: currentState.isTransformationEnabled(), contextType: 'block' as 'inline' | 'block', nodeType: 'Text' }; for (const node of nodes) { // Create a new context with the current state as both parent and state // This ensures that subsequent directives can see variables defined by previous directives const nodeContext = { currentFilePath: parentContext?.currentFilePath || '', parentState: currentState, state: currentState.clone(), formattingContext: { ...currentFormattingContext, nodeType: node.type, parentContext: currentFormattingContext } } as DirectiveContext; // Process directive and get the updated state const result = await this.processDirective(node, nodeContext); // Update formatting context for the next directive // This ensures consistent newline handling between directives if (nodeContext.formattingContext) { currentFormattingContext = nodeContext.formattingContext; } // 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?.()) { // Update currentState directly with the result so next directives have access to it currentState = result; } else { // Even if transformation is enabled, we need to make sure variables defined in one directive // are available to subsequent directives if (result !== nodeContext.state) { // Only apply the new state if it actually changed (as a result of directive execution) currentState = result; } } } return currentState; } /** * Create execution context for a directive */ private createContext(node: DirectiveNode, parentContext?: DirectiveContext): DirectiveContext { // Create a new state or clone parent state const state = parentContext?.state?.clone() || this.stateService!.createChildState(); // Create a new resolution context or inherit from parent const resolutionContext = parentContext?.resolutionContext || {}; // Set the default formatting context based on node type const formattingContext = { isOutputLiteral: state.isTransformationEnabled?.() || false, contextType: 'block' as 'inline' | 'block', nodeType: node.type, parentContext: parentContext?.formattingContext }; // Determine working directory - use parent's or default to current directory const workingDirectory = parentContext?.workingDirectory || this.fileSystemService?.getCwd() || process.cwd(); // Return the complete context return { state, parentState: parentContext?.state, currentFilePath: parentContext?.currentFilePath || this.stateService?.getCurrentFilePath() || '', workingDirectory, resolutionContext, formattingContext } as DirectiveContext; } /** * Update the interpreter service reference */ updateInterpreterService(interpreterService: InterpreterServiceLike): 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 { 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 { // Create a child state that inherits from parent const childState = parentContext.state.createChildState(); // Set the file path in the child state if (filePath) { childState.setCurrentFilePath(filePath); } // Create a new resolution context - inherit from parent with updated state const resolutionContext = { ...(parentContext.resolutionContext || {}), state: childState, currentFilePath: filePath }; // Inherit or create formatting context const formattingContext = { isOutputLiteral: parentContext.formattingContext?.isOutputLiteral ?? childState.isTransformationEnabled(), parentContext: parentContext.formattingContext, contextType: (parentContext.formattingContext?.contextType || 'block') as 'inline' | 'block', nodeType: parentContext.formattingContext?.nodeType || 'Text', atLineStart: parentContext.formattingContext?.atLineStart, atLineEnd: parentContext.formattingContext?.atLineEnd }; // Return the complete child context return { state: childState, parentState: parentContext.state, currentFilePath: filePath, workingDirectory: parentContext.workingDirectory, resolutionContext, formattingContext } as DirectiveContext; } 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 { 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', { location: node.location?.start } ); } } private async handleDataDirective(node: DirectiveNode): Promise { 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', { location: node.location?.start } ); } } private async handleImportDirective(node: DirectiveNode): Promise { 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.callInterpreterInterpret(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', { location: node.location?.start } ); } } private async extractSection( content: string, section: string, fuzzyMatch: number ): Promise { 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 { 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.callInterpreterInterpret(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', { location: 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 { // Add initialization check before any other processing if (!this.initialized) { throw new MeldDirectiveError( 'DirectiveService must be initialized before use', 'initialization', { severity: ErrorSeverity.Fatal } ); } 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 with formatting context, update context for propagation if ('state' in result) { // If the directive returned a formatting context, update the context if ((result as DirectiveResult).formattingContext && context.formattingContext) { Object.assign(context.formattingContext, (result as DirectiveResult).formattingContext); } return result.state; } // Otherwise, result is already an IStateService return result; } catch (error) { // If it's already a DirectiveError or MeldDirectiveError, just rethrow if (error instanceof DirectiveError || error instanceof MeldDirectiveError) { throw error; } // Simplify error messages for common cases let message = error instanceof Error ? error.message : String(error); let code = DirectiveErrorCode.EXECUTION_FAILED; let severity = ErrorSeverity.Recoverable; 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; severity = DirectiveErrorSeverity[code]; } else if (message.includes('circular import') || message.includes('circular reference')) { message = 'Circular import detected'; code = DirectiveErrorCode.CIRCULAR_REFERENCE; severity = DirectiveErrorSeverity[code]; } else if (message.includes('parameter count') || message.includes('wrong number of parameters')) { message = 'Invalid parameter count'; code = DirectiveErrorCode.VALIDATION_FAILED; severity = DirectiveErrorSeverity[code]; } else if (message.includes('invalid path') || message.includes('path validation failed')) { message = 'Invalid path'; code = DirectiveErrorCode.VALIDATION_FAILED; severity = DirectiveErrorSeverity[code]; } throw new DirectiveError( message, node.directive?.kind || 'unknown', code, { node, context, cause: error instanceof Error ? error : undefined } ); } } /** * Lazily initialize the ResolutionServiceClientForDirective factory * This is called only when needed to avoid circular dependencies */ private ensureFactoryInitialized(): void { if (this.factoryInitialized) { return; } this.factoryInitialized = true; try { this.resolutionClientFactory = container.resolve('ResolutionServiceClientForDirectiveFactory'); this.initializeResolutionClient(); } catch (error) { // Factory not available, will use direct reference this.logger.debug('ResolutionServiceClientForDirectiveFactory not available, using direct reference for resolution operations'); } } /** * Initialize the ResolutionServiceClientForDirective using the factory */ private initializeResolutionClient(): void { if (!this.resolutionClientFactory) { return; } try { this.resolutionClient = this.resolutionClientFactory.createClient(); this.logger.debug('Successfully created ResolutionServiceClientForDirective using factory'); } catch (error) { this.logger.warn('Failed to create ResolutionServiceClientForDirective, falling back to direct reference', { error }); this.resolutionClient = undefined; } } /** * Resolve text using the resolution service * @private */ private async resolveText(text: string, context: DirectiveContext): Promise { this.ensureFactoryInitialized(); if (this.resolutionClient) { try { return await this.resolutionClient.resolveText(text, context.resolutionContext || { currentFilePath: context.currentFilePath, workingDirectory: context.workingDirectory }); } catch (error) { directiveLogger.warn('Error using resolutionClient.resolveText', { error }); } } // Fallback to direct resolution service return this.resolutionService.resolveText(text, { currentFilePath: context.currentFilePath, workingDirectory: context.workingDirectory }); } /** * Resolve data using the resolution service * @private */ private async resolveData(ref: string, context: DirectiveContext): Promise { this.ensureFactoryInitialized(); if (this.resolutionClient) { try { return await this.resolutionClient.resolveData(ref, context.resolutionContext || { currentFilePath: context.currentFilePath, workingDirectory: context.workingDirectory }); } catch (error) { directiveLogger.warn('Error using resolutionClient.resolveData', { error }); } } // Fallback to direct resolution service return this.resolutionService.resolveData(ref, { currentFilePath: context.currentFilePath, workingDirectory: context.workingDirectory }); } /** * Resolve path using the resolution service * @private */ private async resolvePath(path: string, context: DirectiveContext): Promise { this.ensureFactoryInitialized(); if (this.resolutionClient) { try { return await this.resolutionClient.resolvePath(path, context.resolutionContext || { currentFilePath: context.currentFilePath, workingDirectory: context.workingDirectory }); } catch (error) { directiveLogger.warn('Error using resolutionClient.resolvePath', { error }); } } // Fallback to direct resolution service return this.resolutionService.resolvePath(path, { currentFilePath: context.currentFilePath, workingDirectory: context.workingDirectory }); } /** * Initialize the interpreterClient using the factory */ private initializeInterpreterClient(): void { if (!this.interpreterClientFactory) { return; } try { this.interpreterClient = this.interpreterClientFactory.createClient(); this.logger.debug('Successfully created InterpreterServiceClient using factory'); } catch (error) { this.logger.warn('Failed to create InterpreterServiceClient', { error }); this.interpreterClient = undefined; } } /** * Lazily initialize the InterpreterServiceClient factory * This is called only when needed to avoid circular dependencies */ private ensureInterpreterFactoryInitialized(): void { if (this.interpreterFactoryInitialized) { return; } this.interpreterFactoryInitialized = true; try { this.interpreterClientFactory = container.resolve('InterpreterServiceClientFactory'); this.initializeInterpreterClient(); } catch (error) { // Factory not available, will use direct service this.logger.debug('InterpreterServiceClientFactory not available, will use direct service if available'); } } /** * Calls the interpret method on the interpreter service * Uses the client if available, falls back to direct service reference */ private async callInterpreterInterpret(nodes: any[], options?: any): Promise { // Ensure factory is initialized this.ensureInterpreterFactoryInitialized(); // Try to use the client from factory first if (this.interpreterClient) { try { return await this.interpreterClient.interpret(nodes, options); } catch (error) { this.logger.warn('Error using interpreterClient.interpret, falling back to direct service', { error }); } } // Fall back to direct service reference if (this.interpreterService) { return await this.interpreterService.interpret(nodes, options); } throw new Error('No interpreter service available'); } /** * Calls the createChildContext method on the interpreter service * Uses the client if available, falls back to direct service reference */ private async callInterpreterCreateChildContext(parentState: StateServiceLike, filePath?: string, options?: any): Promise { // Ensure factory is initialized this.ensureInterpreterFactoryInitialized(); // Try to use the client from factory first if (this.interpreterClient) { try { return await this.interpreterClient.createChildContext(parentState, filePath, options); } catch (error) { this.logger.warn('Error using interpreterClient.createChildContext, falling back to direct service', { error }); } } // Fall back to direct service reference if (this.interpreterService) { return await this.interpreterService.createChildContext(parentState, filePath, options); } throw new Error('No interpreter service available'); } } ``` #### ../../services/pipeline/DirectiveService/IDirectiveService.ts ```javascript import type { DirectiveNode } from '@core/syntax/types/index.js'; import type { DirectiveContextBase } from '@core/shared/types.js'; import type { StateServiceLike, ValidationServiceLike, PathServiceLike, FileSystemLike, ParserServiceLike, CircularityServiceLike, ResolutionServiceLike, DirectiveServiceLike } from '@core/shared-service-types.js'; import type { DirectiveResult } from '@services/pipeline/DirectiveService/types.js'; /** * Context for directive execution * Extends the base context with state-specific fields */ export interface DirectiveContext extends DirectiveContextBase { /** Parent state for nested contexts */ parentState?: StateServiceLike; /** Current state for this directive */ state: StateServiceLike; /** Current file being processed */ currentFilePath?: string; /** Working directory for command execution */ workingDirectory?: string; /** Resolution context for variable resolution */ resolutionContext?: any; /** Formatting context for output generation - propagates formatting preferences across service boundaries */ formattingContext?: { /** Whether in output-literal mode (formerly transformation mode) */ isOutputLiteral: boolean; /** Whether this is an inline or block context */ contextType: 'inline' | 'block'; /** Current node type being processed */ nodeType: string; /** Whether at start of line */ atLineStart?: boolean; /** Whether at end of line */ atLineEnd?: boolean; /** Parent formatting context for inheritance */ parentContext?: any; }; } /** * Interface for directive handlers */ export interface IDirectiveHandler { /** The directive kind this handler processes */ readonly kind: string; /** * Execute the directive * * @param node - The directive node to execute * @param context - The execution context * @returns The updated state after directive execution, or a DirectiveResult containing both state and optional replacement node * @throws {MeldDirectiveError} If directive execution fails */ execute( node: DirectiveNode, context: DirectiveContext ): Promise; } /** * Service responsible for handling Meld directives in the processing pipeline. * Orchestrates directive validation, execution, and transformation. * * @remarks * The DirectiveService acts as the core orchestrator for directive processing. * It maintains a registry of directive handlers, validates directive syntax and * semantics, and routes directives to the appropriate handler for execution. * * This service is central to the Meld pipeline and interacts with nearly all * other services to process directives effectively. * * Dependencies: * - ValidationServiceLike: For directive syntax and semantic validation * - StateServiceLike: For maintaining state during directive execution * - PathServiceLike: For path resolution in file-related directives * - FileSystemLike: For file operations in import and other directives * - ParserServiceLike: For parsing content in imports and fragments * - InterpreterServiceClientFactory: For nested interpretation in imports * - CircularityServiceLike: For detecting circular imports and references * - ResolutionServiceLike: For variable resolution in directive content */ export interface IDirectiveService extends DirectiveServiceLike { /** * Initialize the DirectiveService with required dependencies * * @param validationService - Service for validating directive syntax and semantics * @param stateService - Service for maintaining state during execution * @param pathService - Service for path resolution * @param fileSystemService - Service for file operations * @param parserService - Service for parsing content * @param interpreterServiceClientFactory - Factory for creating interpreter service clients * @param circularityService - Service for detecting circular references * @param resolutionService - Service for variable resolution */ initialize( validationService: ValidationServiceLike, stateService: StateServiceLike, pathService: PathServiceLike, fileSystemService: FileSystemLike, parserService: ParserServiceLike, interpreterServiceClientFactory: any, // Use 'any' to allow both IInterpreterService and InterpreterServiceClientFactory circularityService: CircularityServiceLike, resolutionService: ResolutionServiceLike ): void; /** * Update the interpreter service reference * This is needed to handle circular dependencies in initialization * * @param interpreterService - The interpreter service to use * @deprecated Use interpreterServiceClientFactory instead */ updateInterpreterService(interpreterService: any): void; /** * Handle a directive node * * @param node - The directive node to handle * @param context - The execution context * @returns The updated state after directive execution * @throws {MeldDirectiveError} If directive handling fails */ handleDirective( node: DirectiveNode, context: DirectiveContext ): Promise; /** * Register a new directive handler * * @param handler - The handler to register * @throws {MeldServiceError} If a handler for the directive kind already exists */ registerHandler(handler: IDirectiveHandler): void; /** * Check if a handler exists for a directive kind * * @param kind - The directive kind to check * @returns true if a handler exists, false otherwise */ hasHandler(kind: string): boolean; /** * Validate a directive node * * @param node - The directive node to validate * @throws {MeldDirectiveError} If validation fails */ validateDirective(node: DirectiveNode): Promise; /** * Create a child context for nested directives * * @param parentContext - The parent execution context * @param filePath - The file path for the child context * @returns A new directive context with a child state */ createChildContext( parentContext: DirectiveContext, filePath: string ): DirectiveContext; /** * Process a directive node, validating and executing it * Values in the directive will already be interpolated by meld-ast * * @param node - The directive node to process * @param parentContext - Optional parent context for nested directives * @returns The updated state after directive execution * @throws {MeldDirectiveError} If directive processing fails * * @example * ```ts * const node = { * type: 'Directive', * kind: 'text', * name: 'greeting', * value: 'Hello, world!', * // ... other properties * }; * const state = await directiveService.processDirective(node); * ``` */ processDirective(node: DirectiveNode, parentContext?: DirectiveContext): Promise; /** * Process multiple directive nodes in sequence * * @param nodes - The directive nodes to process * @param parentContext - Optional parent context for nested directives * @returns The final state after processing all directives * @throws {MeldDirectiveError} If any directive processing fails */ processDirectives(nodes: DirectiveNode[], parentContext?: DirectiveContext): Promise; /** * Check if a directive kind is supported * * @param kind - The directive kind to check * @returns true if the directive kind is supported, false otherwise */ supportsDirective(kind: string): boolean; /** * Get a list of all supported directive kinds * * @returns An array of supported directive kinds */ getSupportedDirectives(): string[]; } ``` #### ../../services/pipeline/DirectiveService/interfaces/DirectiveTypes.ts ```javascript import { StateServiceLike } from '@core/shared-service-types.js'; import { MeldNode } from '@core/syntax/types/index.js'; /** * The result of executing a directive * Contains the updated state and optionally a replacement node to use in transformation mode */ export interface DirectiveResult { /** * The updated state after processing the directive */ state: StateServiceLike; /** * In transformation mode, this is the replacement node that the directive's node should be transformed into */ replacement?: MeldNode; /** * Optional formatting context that should be propagated to further processing. * This helps maintain consistent newline handling and formatting. */ formattingContext?: { isOutputLiteral?: boolean; contextType?: 'inline' | 'block'; nodeType?: string; [key: string]: any; }; } ```' [debug] Template reference 'files.DefineHandlerCode' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../services/pipeline/DirectiveService/handlers/definition/DefineDirectiveHandler.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/DirectiveService/handlers/definition/DefineDirectiveHandler.ts"} [debug] Reading relative file {"relativePath":"../../services/pipeline/ValidationService/validators/DefineDirectiveValidator.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ValidationService/validators/DefineDirectiveValidator.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.DefineHandlerCode }}' to '#### ../../services/pipeline/DirectiveService/handlers/definition/DefineDirectiveHandler.ts ```javascript import { IDirectiveHandler, DirectiveContext } from '@services/pipeline/DirectiveService/IDirectiveService.js'; import type { IValidationService } from '@services/pipeline/ValidationService/IValidationService.js'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import type { IResolutionService } from '@services/pipeline/ResolutionService/IResolutionService.js'; import { DirectiveNode, DefineDirectiveData } from '@core/syntax/types.js'; import { DirectiveError, DirectiveErrorCode, DirectiveErrorSeverity } from '@services/pipeline/DirectiveService/errors/DirectiveError.js'; import { directiveLogger as logger } from '@core/utils/logger.js'; import { ErrorSeverity } from '@core/errors/MeldError.js'; import { inject, injectable } from 'tsyringe'; import { Service } from '@core/ServiceProvider.js'; interface CommandDefinition { parameters: string[]; command: string; metadata?: { risk?: 'high' | 'med' | 'low'; about?: string; meta?: Record; }; } @injectable() @Service({ description: 'Handler for @define directives' }) export class DefineDirectiveHandler implements IDirectiveHandler { public readonly kind = 'define'; constructor( @inject('IValidationService') private validationService: IValidationService, @inject('IStateService') private stateService: IStateService, @inject('IResolutionService') private resolutionService: IResolutionService ) {} async execute(node: DirectiveNode, context: DirectiveContext): Promise { try { // 1. Validate directive structure await this.validationService.validate(node); // 2. Extract name, parameters, and command from directive const directive = node.directive as DefineDirectiveData; const { name, parameters, command } = directive; // Parse any metadata from the name const nameMetadata = this.parseIdentifier(name); // 3. Create command definition const commandDef: Omit = { parameters: parameters || [], command: command.kind === 'run' ? command.command : '', // Explicitly add an empty parameters array if none was provided ...(parameters === undefined && { parameters: [] }) }; // 4. Create new state for modifications const newState = context.state.clone(); // 5. Store command with metadata newState.setCommand(nameMetadata.name, { ...commandDef, ...(nameMetadata.metadata && { metadata: nameMetadata.metadata }) }); return newState; } catch (error) { // Wrap in DirectiveError if needed if (error instanceof DirectiveError) { // Ensure location is set by creating a new error if needed if (!error.details?.location && node.location) { const wrappedError = new DirectiveError( error.message, error.kind, error.code, { ...error.details, location: node.location, severity: error.details?.severity || DirectiveErrorSeverity[error.code] } ); throw wrappedError; } throw error; } // Handle resolution errors const resolutionError = new DirectiveError( error instanceof Error ? error.message : 'Unknown error in define directive', this.kind, DirectiveErrorCode.RESOLUTION_FAILED, { node, context, cause: error instanceof Error ? error : undefined, location: node.location, severity: DirectiveErrorSeverity[DirectiveErrorCode.RESOLUTION_FAILED] } ); throw resolutionError; } } private parseIdentifier(identifier: string): { name: string; metadata?: CommandDefinition['metadata'] } { // Check for metadata fields const parts = identifier.split('.'); const name = parts[0]; if (!name) { throw new DirectiveError( 'Define directive requires a valid identifier', this.kind, DirectiveErrorCode.VALIDATION_FAILED, { severity: DirectiveErrorSeverity[DirectiveErrorCode.VALIDATION_FAILED] } ); } // Handle metadata if present if (parts.length > 1) { const metaType = parts[1]; const metaValue = parts[2]; if (metaType === 'risk') { if (!['high', 'med', 'low'].includes(metaValue)) { throw new DirectiveError( 'Invalid risk level. Must be high, med, or low', this.kind, DirectiveErrorCode.VALIDATION_FAILED, { severity: DirectiveErrorSeverity[DirectiveErrorCode.VALIDATION_FAILED] } ); } return { name, metadata: { risk: metaValue as 'high' | 'med' | 'low' } }; } if (metaType === 'about') { return { name, metadata: { about: 'This is a description' } }; } throw new DirectiveError( 'Invalid metadata field. Only risk and about are supported', this.kind, DirectiveErrorCode.VALIDATION_FAILED, { severity: DirectiveErrorSeverity[DirectiveErrorCode.VALIDATION_FAILED] } ); } return { name }; } /** * Extract parameter references from a command string * This method is kept for backward compatibility with tests */ private extractParameterReferences(command: string): string[] { const paramPattern = /\${(\w+)}/g; const params = new Set(); let match; while ((match = paramPattern.exec(command)) !== null) { params.add(match[1]); } return Array.from(params); } } ``` #### ../../services/pipeline/ValidationService/validators/DefineDirectiveValidator.ts ```javascript import { DirectiveNode } from '@core/syntax/types.js'; import { MeldDirectiveError } from '@core/errors/MeldDirectiveError.js'; import { DirectiveErrorCode } from '@services/pipeline/DirectiveService/errors/DirectiveError.js'; /** * Validates @define directives */ export function validateDefineDirective(node: DirectiveNode): void { const directive = node.directive; // Validate name if (!directive.name || typeof directive.name !== 'string') { throw new MeldDirectiveError( 'Define directive requires a "name" property (string)', 'define', { location: node.location?.start, code: DirectiveErrorCode.VALIDATION_FAILED } ); } // Check if it's a basic name or a name with risk annotation const nameParts = directive.name.split('.'); // The AST has already validated the function name format // If there are extensions (like risk annotations), validate them if (nameParts.length > 1) { // First extension must be 'risk' or 'about' if (nameParts[1] !== 'risk' && nameParts[1] !== 'about') { throw new MeldDirectiveError( 'Define directive name extension must be "risk" or "about"', 'define', { location: node.location?.start, code: DirectiveErrorCode.VALIDATION_FAILED } ); } // If there's a third part (risk level), it must be high, med, or low if (nameParts.length > 2 && !['high', 'med', 'low'].includes(nameParts[2])) { throw new MeldDirectiveError( 'Risk level must be "high", "med", or "low"', 'define', { location: node.location?.start, code: DirectiveErrorCode.VALIDATION_FAILED } ); } // No more than 3 parts allowed if (nameParts.length > 3) { throw new MeldDirectiveError( 'Define directive name cannot have more than 3 parts', 'define', { location: node.location?.start, code: DirectiveErrorCode.VALIDATION_FAILED } ); } } // Validate command exists if (!directive.command || typeof directive.command !== 'object') { throw new MeldDirectiveError( 'Define directive requires a "command" property (object)', 'define', { location: node.location?.start, code: DirectiveErrorCode.VALIDATION_FAILED } ); } // Validate command structure if (directive.command.kind !== 'run' || typeof directive.command.command !== 'string') { throw new MeldDirectiveError( 'Define directive command must have a kind="run" and a command string', 'define', { location: node.location?.start, code: DirectiveErrorCode.VALIDATION_FAILED } ); } // Validate command is not empty if (!directive.command.command.trim()) { throw new MeldDirectiveError( 'Command cannot be empty', 'define', { location: node.location?.start, code: DirectiveErrorCode.VALIDATION_FAILED } ); } } ```' [debug] Template reference 'files.ParserCoreCode' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../services/pipeline/ParserService/ParserService.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ParserService/ParserService.ts"} [debug] Reading relative file {"relativePath":"../../services/pipeline/ParserService/IParserService.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ParserService/IParserService.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.ParserCoreCode }}' to '#### ../../services/pipeline/ParserService/ParserService.ts ```javascript import { injectable, singleton, container, inject } from 'tsyringe'; import { Service } from '@core/ServiceProvider.js'; import { parserLogger as logger } from '@core/utils/logger.js'; import { MeldParseError } from '@core/errors/MeldParseError.js'; import { ErrorSeverity } from '@core/errors/MeldError.js'; import { ResolutionServiceClientFactory } from '@services/pipeline/ResolutionService/factories/ResolutionServiceClientFactory.js'; import type { IResolutionServiceClient } from '@services/pipeline/ResolutionService/interfaces/IResolutionServiceClient.js'; import type { MeldNode } from '@core/syntax/types/index.js'; import type { IParserService } from '@services/pipeline/ParserService/IParserService.js'; import type { CodeFenceNode, TextNode, DirectiveNode, DirectiveKind, SourceLocation, Position } from '@core/syntax/types/index.js'; import type { IVariableReference } from '@core/syntax/types/interfaces/IVariableReference.js'; import { parse } from '@core/ast/index.js'; // Import the parse function directly import type { Location } from '@core/types/index.js'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import type { IResolutionService } from '@services/pipeline/ResolutionService/IResolutionService.js'; import type { ResolutionContext } from '@services/pipeline/ResolutionService/IResolutionService.js'; import { VariableNodeFactory } from '@core/syntax/types/factories/VariableNodeFactory.js'; // Define our own ParseError type since it's not exported from meld-ast interface ParseError { message: string; location: { start: { line: number; column: number }; end: { line: number; column: number }; }; } interface MeldAstError extends Error { message: string; name: string; location?: { start: { line: number; column: number }; end: { line: number; column: number }; }; toString(): string; } // Updated to recognize both the old MeldAstError and our new core/ast MeldAstError function isMeldAstError(error: unknown): error is MeldAstError { return ( typeof error === 'object' && error !== null && 'message' in error && 'name' in error && typeof (error as any).toString === 'function' && ( (error as any).name === 'MeldAstError' || ((error as any).name === 'Error' && 'location' in error && 'code' in error) ) ); } @injectable() @Service({ description: 'Service responsible for parsing Meld syntax into AST nodes' }) export class ParserService implements IParserService { private resolutionClient?: IResolutionServiceClient; private resolutionClientFactory?: ResolutionServiceClientFactory; private factoryInitialized: boolean = false; private variableNodeFactory?: VariableNodeFactory; /** * Creates a new instance of the ParserService */ constructor( @inject(VariableNodeFactory) variableNodeFactory?: VariableNodeFactory ) { // We'll initialize the factory lazily to avoid circular dependencies if (process.env.DEBUG === 'true') { console.log('ParserService: Initialized'); } // Initialize the variable node factory or fall back to container resolution this.variableNodeFactory = variableNodeFactory || container.resolve(VariableNodeFactory); } /** * Lazily initialize the ResolutionServiceClient factory * This is called only when needed to avoid circular dependencies */ private ensureFactoryInitialized(): void { if (this.factoryInitialized) { return; } this.factoryInitialized = true; try { this.resolutionClientFactory = container.resolve('ResolutionServiceClientFactory'); this.initializeResolutionClient(); } catch (error) { // Factory not available logger.debug('ResolutionServiceClientFactory not available'); } } /** * Initialize the ResolutionServiceClient using the factory */ private initializeResolutionClient(): void { if (!this.resolutionClientFactory) { return; } try { this.resolutionClient = this.resolutionClientFactory.createClient(); logger.debug('Successfully created ResolutionServiceClient using factory'); } catch (error) { logger.warn('Failed to create ResolutionServiceClient', { error }); this.resolutionClient = undefined; } } /** * Transform old variable node types into the consolidated VariableReferenceNode type * Also recursively processes nested nodes */ private transformVariableNode(node: MeldNode): MeldNode { if (!node || typeof node !== 'object') { return node; } // Using type assertion since we need to access properties not in base MeldNode const anyNode = node as any; // First transform arrays recursively if (Array.isArray(anyNode)) { return anyNode.map(item => this.transformVariableNode(item)) as any; } // Handle variable node types if (anyNode.type === 'TextVar' || anyNode.type === 'DataVar' || anyNode.type === 'PathVar') { // Determine the valueType based on the original node type let valueType: 'text' | 'data' | 'path'; if (anyNode.type === 'TextVar') { valueType = 'text'; } else if (anyNode.type === 'DataVar') { valueType = 'data'; } else { // PathVar valueType = 'path'; } // Get identifier from the appropriate property const identifier = anyNode.identifier || anyNode.value || ''; // Get fields or empty array const fields = anyNode.fields || []; // Get format if it exists const format = anyNode.format; // Get location if it exists const location = anyNode.location; // Use factory to create variable reference node if (this.variableNodeFactory) { return this.variableNodeFactory.createVariableReferenceNode( identifier, valueType, fields, format, location ) as MeldNode; } else { // Fallback to direct creation if factory is unavailable logger.warn('VariableNodeFactory not available, falling back to direct creation'); return { type: 'VariableReference', identifier, valueType, fields, isVariableReference: true, ...(format && { format }), ...(location && { location }) } as MeldNode; } } // Process other node types that might contain variable nodes in their properties // For example, a Directive node might contain variable references in its values if (anyNode.type === 'Directive' && anyNode.directive) { // Clone the directive data and recursively transform any variables it contains const transformedDirective = { ...anyNode.directive }; // Check for specific properties that might contain variable references if (transformedDirective.value && typeof transformedDirective.value === 'object') { transformedDirective.value = this.transformVariableNode(transformedDirective.value); } return { ...anyNode, directive: transformedDirective }; } return anyNode; } private async parseContent(content: string, filePath?: string): Promise { try { // parse is already imported at the top of the file const options = { failFast: true, trackLocations: true, validateNodes: true, preserveCodeFences: true, validateCodeFences: true, structuredPaths: true, onError: (error: unknown) => { if (isMeldAstError(error)) { logger.warn('Parse warning', { error: error.toString() }); } } }; // Register the content with source mapping service if a filePath is provided if (filePath) { try { const { registerSource } = require('@core/utils/sourceMapUtils.js'); registerSource(filePath, content); logger.debug(`Registered content for source mapping: ${filePath}`); } catch (err) { // Source mapping is optional, so just log a debug message if it fails logger.debug('Source mapping not available, skipping registration', { error: err }); } } // Parse the content const result = await parse(content, options); // Transform old variable node types into consolidated type const transformedAst = (result.ast || []).map((node: MeldNode) => this.transformVariableNode(node)); // Validate code fence nesting this.validateCodeFences(transformedAst); // Log any non-fatal errors if (result.errors && result.errors.length > 0) { result.errors.forEach((error: unknown) => { if (isMeldAstError(error)) { // Don't log warnings directly - we'll handle them through the error display service logger.debug('Parse warning detected', { errorMessage: error.toString() }); } }); } return transformedAst; } catch (error) { if (isMeldAstError(error)) { // Create a MeldParseError with the original error information const errorLocation = error.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }; // Always use the provided filePath if we have one, don't rely on what's in the error const actualFilePath = filePath; const locationWithPath = { ...errorLocation, filePath: actualFilePath }; const parseError = new MeldParseError( error.message, locationWithPath, { filePath: actualFilePath, // Directly set filePath in the error options cause: isMeldAstError(error) ? error : undefined, // Set the original error as the cause only if it's a proper Error context: { originalError: error, sourceLocation: { filePath: actualFilePath, line: errorLocation.start.line, column: errorLocation.start.column }, location: locationWithPath, // Add the file path in the context for the error display service to use errorFilePath: actualFilePath } } ); // Try to enhance with source mapping information if (filePath) { try { const { enhanceMeldErrorWithSourceInfo } = require('@core/utils/sourceMapUtils.js'); const enhancedError = enhanceMeldErrorWithSourceInfo(parseError); logger.debug('Enhanced parse error with source mapping', { original: parseError.message, enhanced: enhancedError.message, sourceLocation: enhancedError.context?.sourceLocation }); throw enhancedError; } catch (enhancementError) { // If enhancement fails, throw the original error logger.debug('Failed to enhance parse error with source mapping', { error: enhancementError }); throw parseError; } } throw parseError; } // For unknown errors, provide a generic message with proper location information const actualFilePath = filePath; const locationWithPath = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 }, filePath: actualFilePath }; const genericError = new MeldParseError( 'Parse error: Unknown error occurred', locationWithPath, { filePath: actualFilePath, // Directly set filePath in the error options cause: isMeldAstError(error) ? error : undefined, // Set the original error as the cause only if it's a proper Error context: { originalError: error, sourceLocation: { filePath: actualFilePath, line: 1, column: 1 }, location: locationWithPath, // Add the file path in the context for the error display service to use errorFilePath: actualFilePath } } ); // Try to enhance with source mapping information if (filePath) { try { const { enhanceMeldErrorWithSourceInfo } = require('@core/utils/sourceMapUtils.js'); throw enhanceMeldErrorWithSourceInfo(genericError); } catch (enhancementError) { // If enhancement fails, throw the original error throw genericError; } } throw genericError; } } public async parse(content: string, filePath?: string): Promise { return this.parseContent(content, filePath); } /** * Parse a string into AST nodes (alias for parse to match ParserServiceLike interface) * * @param content - The content to parse * @param options - Optional parsing options * @returns A promise that resolves with the parsed AST nodes */ public async parseString(content: string, options?: { filePath?: string }): Promise { return this.parse(content, options?.filePath); } /** * Parse a file into AST nodes * * @param filePath - The path to the file to parse * @returns A promise that resolves with the parsed AST nodes */ public async parseFile(filePath: string): Promise { try { // Use the resolution client to read the file this.ensureFactoryInitialized(); if (this.resolutionClient) { const content = await this.resolutionClient.resolveFile(filePath); return this.parse(content, filePath); } // If no resolution client, throw an error throw new MeldParseError(`Cannot parse file: ${filePath} - No file resolution service available`); } catch (error) { throw new MeldParseError( `Failed to parse file: ${filePath}`, undefined, { cause: error instanceof Error ? error : new Error(String(error)), context: { error: error instanceof Error ? error.message : String(error) } } ); } } public async parseWithLocations(content: string, filePath?: string): Promise { const nodes = await this.parseContent(content, filePath); if (!filePath) { return nodes; } return nodes.map(node => { if (node.location) { // Preserve exact column numbers from original location return { ...node, location: { ...node.location, // Preserve all original location properties filePath // Only add filePath } }; } return node; }); } private isParseError(error: unknown): error is ParseError { return ( typeof error === 'object' && error !== null && 'message' in error && 'location' in error && typeof error.location === 'object' && error.location !== null && 'start' in error.location && 'end' in error.location ); } /** * Check if a node is a variable reference node using the factory */ private isVariableReferenceNode(node: any): node is IVariableReference { if (this.variableNodeFactory) { return this.variableNodeFactory.isVariableReferenceNode(node); } // Fallback to direct checking return ( node?.type === 'VariableReference' && typeof node?.identifier === 'string' && typeof node?.valueType === 'string' ); } private validateCodeFences(nodes: MeldNode[]): void { // Since we're using the meld-ast parser with validateNodes=true and preserveCodeFences=true, // we can trust that the code fences are already valid. // This is just an extra validation layer to ensure code fence integrity for (const node of nodes) { if (node.type === 'CodeFence') { const codeFence = node as CodeFenceNode; const content = codeFence.content; // Skip empty code fences (should be rare but possible) if (!content) { continue; } // Split the content by lines const lines = content.split('\n'); if (lines.length < 2) { throw new MeldParseError( 'Invalid code fence: must have at least an opening and closing line', node.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } ); } // Get the first line (opening fence) and count backticks const firstLine = lines[0]; let openTickCount = 0; for (let i = 0; i < firstLine.length; i++) { if (firstLine[i] === '`') { openTickCount++; } else { break; } } // Get the last line (closing fence) and count backticks const lastLine = lines[lines.length - 1]; let closeTickCount = 0; for (let i = 0; i < lastLine.length; i++) { if (lastLine[i] === '`') { closeTickCount++; } else { break; } } if (openTickCount === 0) { throw new MeldParseError( 'Invalid code fence: missing opening backticks', node.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } ); } if (closeTickCount === 0) { throw new MeldParseError( 'Invalid code fence: missing closing backticks', node.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } ); } if (openTickCount !== closeTickCount) { throw new MeldParseError( `Code fence must be closed with exactly ${openTickCount} backticks, got ${closeTickCount}`, node.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } ); } } } } /** * Resolve a variable reference node * @param node - The variable reference node to resolve * @param context - The resolution context * @returns The resolved node */ async resolveVariableReference(node: IVariableReference, context: ResolutionContext): Promise { try { // Ensure factory is initialized this.ensureFactoryInitialized(); // Try to use the resolution client if (this.resolutionClient) { try { // Convert the node to string format for the client const nodeStr = `{{${node.valueType}.${node.identifier}${node.fields ? '.' + node.fields.map(f => f.value).join('.') : ''}}}`; // Use resolveVariableReference method which is in the interface const resolvedStr = await this.resolutionClient.resolveVariableReference(nodeStr, context); // Return the original node with updated information // Use type assertion since we're adding a property that's not in the interface return { ...node, resolvedValue: resolvedStr } as IVariableReference & { resolvedValue: string }; } catch (error) { logger.warn('Error using resolutionClient.resolve', { error, node }); } } // If we get here, we couldn't resolve the variable logger.warn('No resolution client available for variable transformation'); return node; } catch (error) { logger.warn('Failed to transform variable node', { error, node }); return node; } } } ``` #### ../../services/pipeline/ParserService/IParserService.ts ```javascript import type { MeldNode } from '@core/syntax/types/index.js'; import type { ParserServiceLike } from '@core/shared-service-types.js'; /** * Service responsible for parsing Meld content into an Abstract Syntax Tree (AST). * Provides methods to parse and analyze Meld documents and fragments. * * @remarks * The ParserService wraps the meld-ast parser to provide a consistent interface * for parsing Meld content. It adds location information to nodes and handles * error reporting. * * This service is typically used as the first step in the Meld document processing * pipeline, converting raw text content into a structured AST that can be further * processed by other services. * * Dependencies: * - meld-ast: For the underlying parsing functionality */ interface IParserService extends ParserServiceLike { /** * Parse Meld content into an AST using meld-ast. * * @param content - The Meld content to parse * @returns A promise that resolves to an array of MeldNodes representing the AST * @throws {MeldParseError} If the content cannot be parsed * * @example * ```ts * const content = '@text greeting = "Hello, world!"'; * const nodes = await parserService.parse(content); * // nodes contains a DirectiveNode representing the @text directive * ``` */ parse(content: string): Promise; /** * Parse Meld content and provide location information for each node. * This is useful for error reporting and source mapping. * * @param content - The Meld content to parse * @param filePath - Optional file path for better error messages * @returns A promise that resolves to an array of MeldNodes with location information * @throws {MeldParseError} If the content cannot be parsed * * @example * ```ts * const content = '@text greeting = "Hello, world!"'; * const nodes = await parserService.parseWithLocations(content, 'example.meld'); * // nodes contains a DirectiveNode with location information * ``` */ parseWithLocations(content: string, filePath?: string): Promise; } export type { IParserService }; ```' [debug] Template reference 'files.InterpreterCoreCode' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../services/pipeline/InterpreterService/InterpreterService.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/InterpreterService/InterpreterService.ts"} [debug] Reading relative file {"relativePath":"../../services/pipeline/InterpreterService/IInterpreterService.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/InterpreterService/IInterpreterService.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.InterpreterCoreCode }}' to '#### ../../services/pipeline/InterpreterService/InterpreterService.ts ```javascript import type { MeldNode, SourceLocation, DirectiveNode } from '@core/syntax/types/index.js'; import { interpreterLogger as logger } from '@core/utils/logger.js'; import type { IInterpreterService, InterpreterOptions } from '@services/pipeline/InterpreterService/IInterpreterService.js'; import type { DirectiveServiceLike, StateServiceLike, InterpreterServiceLike } from '@core/shared-service-types.js'; import { MeldInterpreterError, type InterpreterLocation } from '@core/errors/MeldInterpreterError.js'; import { MeldError, ErrorSeverity } from '@core/errors/MeldError.js'; import { StateVariableCopier } from '@services/state/utilities/StateVariableCopier.js'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import { Service } from '@core/ServiceProvider.js'; import { inject, injectable, delay, container } from 'tsyringe'; import { DirectiveServiceClientFactory } from '@services/pipeline/DirectiveService/factories/DirectiveServiceClientFactory.js'; import { IDirectiveServiceClient } from '@services/pipeline/DirectiveService/interfaces/IDirectiveServiceClient.js'; const DEFAULT_OPTIONS: Required> = { filePath: '', mergeState: true, importFilter: [], strict: true }; function convertLocation(loc?: SourceLocation): InterpreterLocation | undefined { if (!loc) return undefined; return { line: loc.start.line, column: loc.start.column, }; } function getErrorMessage(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; return 'Unknown error'; } /** * Service for interpreting Meld AST and executing directives */ @injectable() @Service({ description: 'Service for interpreting Meld AST nodes and executing directives', dependencies: [ { token: 'DirectiveServiceClientFactory', name: 'directiveServiceClientFactory' }, { token: 'IStateService', name: 'stateService' } ] }) export class InterpreterService implements IInterpreterService, InterpreterServiceLike { private directiveService?: DirectiveServiceLike; // Legacy reference private directiveClient?: IDirectiveServiceClient; // Client from factory pattern private directiveClientFactory?: DirectiveServiceClientFactory; private stateService?: StateServiceLike; private initialized = false; private stateVariableCopier = new StateVariableCopier(); private initializationPromise: Promise | null = null; private factoryInitialized: boolean = false; /** * Creates a new InterpreterService * * @param directiveServiceClientFactory - Factory for creating directive service clients * @param stateService - Service for state management */ constructor( @inject('DirectiveServiceClientFactory') directiveServiceClientFactory?: DirectiveServiceClientFactory, @inject('IStateService') stateService?: StateServiceLike ) { this.directiveClientFactory = directiveServiceClientFactory; this.stateService = stateService; logger.debug('InterpreterService constructor', { hasDirectiveFactory: !!this.directiveClientFactory, hasStateService: !!this.stateService }); // If we have dependencies, initialize if (this.directiveClientFactory && this.stateService) { // Create a promise that resolves when initialization completes this.initializationPromise = new Promise((resolve) => { this.initializeDirectiveClient(); this.initialized = true; logger.debug('InterpreterService initialized via DI'); resolve(); }); } } /** * Initialize the directiveClient using the factory */ private initializeDirectiveClient(): void { if (!this.directiveClientFactory) { return; } try { this.directiveClient = this.directiveClientFactory.createClient(); logger.debug('Successfully created DirectiveServiceClient using factory'); } catch (error) { logger.warn('Failed to create DirectiveServiceClient', { error }); this.directiveClient = undefined; } } /** * Lazily initialize the DirectiveServiceClient factory * This is called only when needed to avoid circular dependencies */ private ensureFactoryInitialized(): void { if (this.factoryInitialized) { return; } this.factoryInitialized = true; try { this.directiveClientFactory = container.resolve('DirectiveServiceClientFactory'); this.initializeDirectiveClient(); } catch (error) { logger.warn('Failed to resolve DirectiveServiceClientFactory', { error }); } } /** * Ensure the service is initialized before use * @private */ private ensureInitialized(): void { if (!this.initialized) { throw new MeldInterpreterError( 'InterpreterService must be initialized before use', 'initialization', undefined, // No location information { severity: ErrorSeverity.Fatal } ); } } /** * Calls the directive service to handle a directive node * Uses the client if available, falls back to direct service reference */ private async callDirectiveHandleDirective(node: DirectiveNode, context: any): Promise { this.ensureFactoryInitialized(); if (this.directiveClient && this.directiveClient.handleDirective) { try { return await this.directiveClient.handleDirective(node, context); } catch (error) { logger.warn('Error using directiveClient.handleDirective, falling back to direct service', { error }); } } if (this.directiveService) { return await this.directiveService.handleDirective(node, context); } throw new MeldInterpreterError( 'No directive service available to handle directive', 'directive_handling', undefined, // No location information { severity: ErrorSeverity.Fatal } ); } /** * Calls the directive service to check if it supports a directive kind * Uses the client if available, falls back to direct service reference */ private callDirectiveSupportsDirective(kind: string): boolean { this.ensureFactoryInitialized(); if (this.directiveClient) { try { return this.directiveClient.supportsDirective(kind); } catch (error) { logger.warn('Error using directiveClient.supportsDirective, falling back to direct service', { error }); } } if (this.directiveService) { return this.directiveService.supportsDirective(kind); } return false; } /** * Returns whether this service can handle transformations * Required by the pipeline validation system */ public canHandleTransformations(): boolean { return true; } /** * Explicitly initialize the service with all required dependencies. * @deprecated This method is maintained for backward compatibility. * The service is automatically initialized via dependency injection. */ initialize( directiveService: DirectiveServiceLike, stateService: StateServiceLike ): void { // Store the direct reference for backward compatibility this.directiveService = directiveService; this.stateService = stateService; this.initialized = true; this.initializationPromise = Promise.resolve(); logger.debug('InterpreterService initialized manually'); } /** * Handle errors based on severity and options * In strict mode, all errors throw * In permissive mode, recoverable errors become warnings */ private handleError(error: Error, options: Required> & Pick): void { // If it's not a MeldError, wrap it const meldError = error instanceof MeldError ? error : MeldError.wrap(error); logger.error('Error in InterpreterService', { error: meldError }); // In strict mode, or if it's a fatal error, throw it if (options.strict || !meldError.canBeWarning()) { throw meldError; } // In permissive mode with recoverable errors, use the error handler or log a warning if (options.errorHandler) { options.errorHandler(meldError); } else { logger.warn(`Warning: ${meldError.message}`, { code: meldError.code, filePath: meldError.filePath, severity: meldError.severity }); } } async interpret( nodes: MeldNode[], options?: InterpreterOptions ): Promise { // Ensure we're initialized before processing this.ensureInitialized(); if (!nodes) { throw new MeldInterpreterError( 'No nodes provided for interpretation', 'interpretation', undefined, { severity: ErrorSeverity.Fatal } ); } if (!Array.isArray(nodes)) { throw new MeldInterpreterError( 'Invalid nodes provided for interpretation: expected array', 'interpretation', undefined, { severity: ErrorSeverity.Fatal } ); } const opts = { ...DEFAULT_OPTIONS, ...options }; let currentState: StateServiceLike; try { // Initialize state if (opts.initialState) { if (opts.mergeState) { // When mergeState is true, create child state from initial state currentState = opts.initialState.createChildState(); } else { // When mergeState is false, create completely isolated state currentState = this.stateService!.createChildState(); } } else { // No initial state, create fresh state currentState = this.stateService!.createChildState(); } if (!currentState) { throw new MeldInterpreterError( 'Failed to initialize state for interpretation', 'initialization', undefined, { severity: ErrorSeverity.Fatal } ); } if (opts.filePath) { currentState.setCurrentFilePath(opts.filePath); } // Take a snapshot of initial state for rollback const initialSnapshot = currentState.clone(); let lastGoodState = initialSnapshot; logger.debug('Starting interpretation', { nodeCount: nodes?.length ?? 0, filePath: opts.filePath, mergeState: opts.mergeState }); for (const node of nodes) { try { currentState = await this.interpretNode(node, currentState, opts); // Update last good state after successful interpretation lastGoodState = currentState.clone(); } catch (error) { // Handle errors based on severity and options try { this.handleError(error instanceof Error ? error : new Error(String(error)), opts); // If we get here, the error was handled as a warning // Continue with the last good state currentState = lastGoodState.clone(); } catch (fatalError) { // If we get here, the error was fatal and should be propagated // Restore to initial state before rethrowing if (opts.initialState && opts.mergeState) { // Only attempt to merge back if we have a parent and mergeState is true opts.initialState.mergeChildState(initialSnapshot); } throw fatalError; } } } // Merge state back to parent if requested if (opts.initialState && opts.mergeState) { opts.initialState.mergeChildState(currentState); } logger.debug('Interpretation completed successfully', { nodeCount: nodes?.length ?? 0, filePath: currentState.getCurrentFilePath(), finalStateNodes: currentState.getNodes()?.length ?? 0, mergedToParent: opts.mergeState && opts.initialState }); return currentState; } catch (error) { // Wrap any unexpected errors const wrappedError = error instanceof Error ? error : new MeldInterpreterError( `Unexpected error during interpretation: ${String(error)}`, 'interpretation', undefined, { severity: ErrorSeverity.Fatal, cause: error instanceof Error ? error : undefined } ); throw wrappedError; } } async interpretNode( node: MeldNode, state: StateServiceLike, options?: InterpreterOptions ): Promise { this.ensureInitialized(); if (!node) { throw new MeldInterpreterError( 'No node provided for interpretation', 'interpretation' ); } if (!state) { throw new MeldInterpreterError( 'No state provided for node interpretation', 'interpretation' ); } if (!node.type) { throw new MeldInterpreterError( 'Unknown node type', 'interpretation', convertLocation(node.location) ); } logger.debug('Interpreting node', { type: node.type, location: node.location, filePath: state.getCurrentFilePath() }); const opts = { ...DEFAULT_OPTIONS, ...options }; try { // Take a snapshot before processing const preNodeState = state.clone(); let currentState = preNodeState; // Process based on node type switch (node.type) { case 'Text': // Create new state for text node const textState = currentState.clone(); textState.addNode(node); currentState = textState; break; case 'CodeFence': // Handle CodeFence nodes similar to Text nodes - preserve them exactly const codeFenceState = currentState.clone(); codeFenceState.addNode(node); currentState = codeFenceState; break; case 'VariableReference': // Handle variable reference nodes if ((node as any).valueType === 'text') { // Handle TextVar nodes similar to Text nodes const textVarState = currentState.clone(); textVarState.addNode(node); currentState = textVarState; } else if ((node as any).valueType === 'data') { // Handle DataVar nodes similar to Text/TextVar nodes const dataVarState = currentState.clone(); dataVarState.addNode(node); currentState = dataVarState; } break; // Note: Legacy TextVar and DataVar cases are kept for backward compatibility case 'TextVar' as any: // Handle TextVar nodes similar to Text nodes const textVarState = currentState.clone(); textVarState.addNode(node); currentState = textVarState; break; case 'DataVar' as any: // Handle DataVar nodes similar to Text/TextVar nodes const dataVarState = currentState.clone(); dataVarState.addNode(node); currentState = dataVarState; break; case 'Comment': // Comments are ignored during interpretation break; case 'Directive': // Process directive with cloned state to maintain immutability const directiveState = currentState.clone(); // Add the node first to maintain order directiveState.addNode(node); if (node.type !== 'Directive' || !('directive' in node) || !node.directive) { throw new MeldInterpreterError( 'Invalid directive node', 'invalid_directive', convertLocation(node.location) ); } const directiveNode = node as DirectiveNode; // Capture the original state for importing directives in transformation mode const originalState = state; const isImportDirective = directiveNode.directive.kind === 'import'; // Create formatting context for consistent newline handling across service boundaries const formattingContext = { isOutputLiteral: state.isTransformationEnabled?.() || false, contextType: 'block' as 'inline' | 'block', // Default to block context nodeType: node.type, atLineStart: true, // Default assumption atLineEnd: false // Default assumption }; // Store the directive result to check for replacement nodes const directiveResult = await this.callDirectiveHandleDirective(directiveNode, { state: directiveState, parentState: currentState, currentFilePath: state.getCurrentFilePath() ?? undefined, formattingContext // Add formatting context for cross-service propagation }); // Update current state with the result currentState = directiveResult; // Capture any updates to formatting context from the directive handler if (directiveResult.getFormattingContext) { const updatedContext = directiveResult.getFormattingContext(); if (updatedContext) { logger.debug('Formatting context updated by directive', { directiveKind: directiveNode.directive.kind, contextType: updatedContext.contextType, isOutputLiteral: updatedContext.isOutputLiteral }); } } // Check if the directive handler returned a replacement node // This happens when the handler implements the DirectiveResult interface // with a replacement property if (directiveResult && 'replacement' in directiveResult && 'state' in directiveResult) { // We need to extract the replacement node and state from the result const result = directiveResult as unknown as { replacement: MeldNode; state: StateServiceLike; }; const replacement = result.replacement; const resultState = result.state; // Update current state with the result state currentState = resultState; // Special handling for imports in transformation mode: // Copy all variables from the imported file to the original state if (isImportDirective && currentState.isTransformationEnabled && currentState.isTransformationEnabled()) { try { logger.debug('Import directive in transformation mode, copying variables to original state'); // Use the state variable copier utility to copy all variables this.stateVariableCopier.copyAllVariables( currentState as unknown as IStateService, originalState as unknown as IStateService, { skipExisting: false, trackContextBoundary: false, // No tracking service in the interpreter trackVariableCrossing: false } ); } catch (e) { logger.debug('Error copying variables from import to original state', { error: e }); } } // If transformation is enabled and we have a replacement node, // we need to apply it to the transformed nodes if (currentState.isTransformationEnabled && currentState.isTransformationEnabled()) { logger.debug('Applying replacement node from directive handler', { originalType: node.type, replacementType: replacement.type, directiveKind: directiveNode.directive.kind, isVarReference: directiveNode.directive.kind === 'embed' && typeof directiveNode.directive.path === 'object' && directiveNode.directive.path !== null && 'isVariableReference' in directiveNode.directive.path }); // Apply the transformation by replacing the directive node with the replacement try { // Ensure we have the transformed nodes array initialized if (!currentState.getTransformedNodes || !currentState.getTransformedNodes()) { // Initialize transformed nodes if needed const originalNodes = currentState.getNodes(); if (originalNodes && currentState.setTransformedNodes) { currentState.setTransformedNodes([...originalNodes]); logger.debug('Initialized transformed nodes array', { nodesCount: originalNodes.length }); } } // Special handling for variable-based embed directives if (directiveNode.directive.kind === 'embed' && typeof directiveNode.directive.path === 'object' && directiveNode.directive.path !== null && 'isVariableReference' in directiveNode.directive.path) { logger.debug('Processing variable-based embed transformation', { path: directiveNode.directive.path, hasReplacement: !!replacement }); // Make sure all variables are copied properly try { this.stateVariableCopier.copyAllVariables( currentState as unknown as IStateService, originalState as unknown as IStateService, { skipExisting: false, trackContextBoundary: false, trackVariableCrossing: false } ); } catch (e) { logger.debug('Error copying variables from variable-based embed to original state', { error: e }); } } // Apply the transformation currentState.transformNode(node, replacement as MeldNode); } catch (transformError) { logger.error('Error applying transformation', { error: transformError, directiveKind: directiveNode.directive.kind }); // Continue execution despite transformation error } } } break; default: throw new MeldInterpreterError( `Unknown node type: ${node.type}`, 'unknown_node', convertLocation(node.location) ); } return currentState; } catch (error) { // Preserve MeldInterpreterError or wrap other errors if (error instanceof MeldInterpreterError) { throw error; } throw new MeldInterpreterError( getErrorMessage(error), node.type, convertLocation(node.location), { cause: error instanceof Error ? error : undefined, context: { nodeType: node.type, location: convertLocation(node.location), state: { filePath: state.getCurrentFilePath() ?? undefined } } } ); } } async createChildContext( parentState: StateServiceLike, filePath?: string, options?: InterpreterOptions ): Promise { this.ensureInitialized(); if (!parentState) { throw new MeldInterpreterError( 'No parent state provided for child context creation', 'context_creation' ); } try { // Create child state from parent const childState = parentState.createChildState(); if (!childState) { throw new MeldInterpreterError( 'Failed to create child state', 'context_creation', undefined, { context: { parentFilePath: parentState.getCurrentFilePath() ?? undefined } } ); } // Set file path if provided if (filePath) { childState.setCurrentFilePath(filePath); } logger.debug('Created child context', { parentFilePath: parentState.getCurrentFilePath(), childFilePath: filePath, hasParent: true }); return childState; } catch (error) { logger.error('Failed to create child context', { parentFilePath: parentState.getCurrentFilePath(), childFilePath: filePath, error }); // Preserve MeldInterpreterError or wrap other errors if (error instanceof MeldInterpreterError) { throw error; } throw new MeldInterpreterError( getErrorMessage(error), 'context_creation', undefined, { cause: error instanceof Error ? error : undefined, context: { parentFilePath: parentState.getCurrentFilePath() ?? undefined, childFilePath: filePath, state: { filePath: parentState.getCurrentFilePath() ?? undefined } } } ); } } } ``` #### ../../services/pipeline/InterpreterService/IInterpreterService.ts ```javascript import type { MeldNode } from '@core/syntax/types/index.js'; import type { DirectiveServiceLike, StateServiceLike } from '@core/shared-service-types.js'; import type { MeldError } from '@core/errors/MeldError.js'; /** * Error handler function type for handling Meld errors during interpretation. * * @param error - The error to handle */ interface ErrorHandler { (error: MeldError): void; } /** * Options for configuring the interpreter behavior. */ interface InterpreterOptions { /** * Initial state to use for interpretation. * If not provided, a new state will be created. */ initialState?: StateServiceLike; /** * Current file path for error reporting and path resolution. */ filePath?: string; /** * Whether to merge the final state back to the parent. * @default true */ mergeState?: boolean; /** * List of variables to import. * If undefined, all variables are imported. * If empty array, no variables are imported. */ importFilter?: string[]; /** * Whether to run in strict mode. * In strict mode, all errors throw. * In permissive mode, recoverable errors become warnings. * @default true */ strict?: boolean; /** * Custom error handler. * If provided, will be called for all errors. * In permissive mode, recoverable errors will be passed to this handler instead of throwing. */ errorHandler?: ErrorHandler; } /** * Service responsible for interpreting Meld AST nodes and orchestrating the processing pipeline. * Acts as the core orchestration layer for the Meld execution lifecycle. * * @remarks * The InterpreterService is the primary entry point for processing Meld content. * It coordinates the entire pipeline, from directive handling to state management. * It maintains contextual information during execution and manages error handling, * state transitions, and transformation tracking. * * Dependencies: * - DirectiveServiceLike: For processing directive nodes * - StateServiceLike: For maintaining state during interpretation */ interface IInterpreterService { /** * Check if this service can handle transformations. * * @returns true if transformations are supported, false otherwise */ canHandleTransformations(): boolean; /** * Initialize the InterpreterService with required dependencies. * * @param directiveService - Service for handling directives * @param stateService - Service for maintaining state */ initialize( directiveService: DirectiveServiceLike, stateService: StateServiceLike ): void; /** * Interpret a sequence of Meld nodes. * Processes each node in order, updating state as necessary. * * @param nodes - The nodes to interpret * @param options - Optional configuration options * @returns The final state after interpretation * @throws {MeldInterpreterError} If interpretation fails * * @example * ```ts * const content = '@text greeting = "Hello, world!"'; * const nodes = await parserService.parse(content); * const state = await interpreterService.interpret(nodes, { * filePath: 'example.meld', * strict: true * }); * ``` */ interpret( nodes: MeldNode[], options?: InterpreterOptions ): Promise; /** * Interpret a single Meld node. * * @param node - The node to interpret * @param state - The current state * @param options - Optional configuration options * @returns The state after interpretation * @throws {MeldInterpreterError} If interpretation fails */ interpretNode( node: MeldNode, state: StateServiceLike, options?: InterpreterOptions ): Promise; /** * Create a new interpreter context with a child state. * Useful for nested interpretation (import/embed). * * @param parentState - The parent state to inherit from * @param filePath - Optional file path for the child context * @param options - Optional configuration options * @returns A child state initialized for interpretation * * @example * ```ts * // Create a child context for processing an imported file * const childState = await interpreterService.createChildContext( * parentState, * 'imported.meld', * { importFilter: ['greeting', 'username'] } * ); * ``` */ createChildContext( parentState: StateServiceLike, filePath?: string, options?: InterpreterOptions ): Promise; } export type { ErrorHandler, InterpreterOptions, IInterpreterService }; ```' [debug] Template reference 'files.ResolutionCoreCode' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../services/pipeline/ResolutionService/ResolutionService.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ResolutionService/ResolutionService.ts"} [debug] Reading relative file {"relativePath":"../../services/pipeline/ResolutionService/IResolutionService.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ResolutionService/IResolutionService.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.ResolutionCoreCode }}' to '#### ../../services/pipeline/ResolutionService/ResolutionService.ts ```javascript import * as path from 'path'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import type { IResolutionService, ResolutionContext } from '@services/pipeline/ResolutionService/IResolutionService.js'; import { ResolutionErrorCode } from '@services/pipeline/ResolutionService/IResolutionService.js'; import { TextResolver } from '@services/pipeline/ResolutionService/resolvers/TextResolver.js'; import { DataResolver } from '@services/pipeline/ResolutionService/resolvers/DataResolver.js'; import { PathResolver } from '@services/pipeline/ResolutionService/resolvers/PathResolver.js'; import { CommandResolver } from '@services/pipeline/ResolutionService/resolvers/CommandResolver.js'; import { ContentResolver } from '@services/pipeline/ResolutionService/resolvers/ContentResolver.js'; import { VariableReferenceResolver } from '@services/pipeline/ResolutionService/resolvers/VariableReferenceResolver.js'; import { FieldAccessUtility } from '@services/resolution/utilities/index.js'; import { resolutionLogger as logger } from '@core/utils/logger.js'; import type { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js'; import type { MeldNode, DirectiveNode, TextNode, DirectiveKind, CodeFenceNode } from '@core/syntax/types/index.js'; import { MeldFileNotFoundError } from '@core/errors/MeldFileNotFoundError.js'; import { MeldResolutionError } from '@core/errors/MeldResolutionError.js'; import { ErrorSeverity } from '@core/errors/MeldError.js'; import { inject, singleton, container } from 'tsyringe'; import type { IPathService } from '@services/fs/PathService/IPathService.js'; import { VariableResolutionTracker, ResolutionTrackingConfig } from '@tests/utils/debug/VariableResolutionTracker/index.js'; import { Service } from '@core/ServiceProvider.js'; import type { IParserServiceClient } from '@services/pipeline/ParserService/interfaces/IParserServiceClient.js'; import { ParserServiceClientFactory } from '@services/pipeline/ParserService/factories/ParserServiceClientFactory.js'; import { IVariableReferenceResolverClient } from '@services/pipeline/ResolutionService/interfaces/IVariableReferenceResolverClient.js'; import { VariableReferenceResolverClientFactory } from '@services/pipeline/ResolutionService/factories/VariableReferenceResolverClientFactory.js'; import { IDirectiveServiceClient } from '@services/pipeline/DirectiveService/interfaces/IDirectiveServiceClient.js'; import { DirectiveServiceClientFactory } from '@services/pipeline/DirectiveService/factories/DirectiveServiceClientFactory.js'; import type { IFileSystemServiceClient } from '@services/fs/FileSystemService/interfaces/IFileSystemServiceClient.js'; import { FileSystemServiceClientFactory } from '@services/fs/FileSystemService/factories/FileSystemServiceClientFactory.js'; import type { IParserService } from '@services/pipeline/ParserService/IParserService.js'; import { VariableResolutionErrorFactory } from '@services/pipeline/ResolutionService/resolvers/error-factory.js'; /** * Interface matching the StructuredPath expected from meld-spec */ interface StructuredPath { raw: string; structured: { segments: string[]; variables?: { special?: string[]; path?: string[]; }; cwd?: boolean; }; normalized?: string; } /** * Internal type for heading nodes in the ResolutionService * This is converted from TextNode when we detect a heading pattern */ interface InternalHeadingNode { content: string; level: number; } /** * Convert a TextNode to an InternalHeadingNode if it matches heading pattern * Returns null if the node is not a heading */ function parseHeadingNode(node: TextNode): InternalHeadingNode | null { // Instead of using regex, check the AST properties if (!node.content.startsWith('#')) { return null; } // Count the number of # characters at the start let level = 0; for (let i = 0; i < node.content.length && i < 6; i++) { if (node.content[i] === '#') { level++; } else { break; } } // Validate level and check for space after #s if (level === 0 || level > 6 || node.content[level] !== ' ') { return null; } // Extract the content after the # characters const content = node.content.substring(level + 1).trim(); if (!content) { return null; } return { level, content }; } /** * Check if a node is a text node that represents a heading */ function isHeadingTextNode(node: MeldNode): node is TextNode { if (node.type !== 'Text') { return false; } const textNode = node as TextNode; // Must start with at least one # and at most 6 if (!textNode.content.startsWith('#')) { return false; } // Count the number of # characters let hashCount = 0; for (let i = 0; i < textNode.content.length && i < 6; i++) { if (textNode.content[i] === '#') { hashCount++; } else { break; } } // Valid heading must have: // 1. Between 1-6 hash characters // 2. A space after the hash characters // 3. Content after the space return ( hashCount >= 1 && hashCount <= 6 && textNode.content.length > hashCount && textNode.content[hashCount] === ' ' && textNode.content.substring(hashCount + 1).trim().length > 0 ); } /** * Service responsible for resolving variables, commands, and paths in different contexts */ @singleton() @Service({ description: 'Service responsible for resolving variable references and other dynamic content' }) export class ResolutionService implements IResolutionService { private textResolver: TextResolver = null!; private dataResolver: DataResolver = null!; private pathResolver: PathResolver = null!; private commandResolver: CommandResolver = null!; private contentResolver: ContentResolver = null!; private variableReferenceResolver: VariableReferenceResolver = null!; private resolutionTracker?: VariableResolutionTracker; private stateService: IStateService = null!; private fileSystemService: IFileSystemService = null!; private pathService: IPathService = null!; private parserService?: IParserService; private parserClient?: IParserServiceClient; private parserClientFactory?: ParserServiceClientFactory; private variableResolverClient?: IVariableReferenceResolverClient; private variableResolverClientFactory?: VariableReferenceResolverClientFactory; private directiveClient?: IDirectiveServiceClient; private directiveClientFactory?: DirectiveServiceClientFactory; private fsClient?: IFileSystemServiceClient; private fsClientFactory?: FileSystemServiceClientFactory; private factoryInitialized: boolean = false; /** * Creates a new instance of the ResolutionService * @param stateService - State service for variable management * @param fileSystemService - File system service for file operations * @param pathService - Path service for path operations * @param parserService - Parser service for parsing strings */ constructor( @inject('IStateService') stateService?: IStateService, @inject('IFileSystemService') fileSystemService?: IFileSystemService, @inject('IPathService') pathService?: IPathService, @inject('IParserService') parserService?: IParserService ) { this.initializeFromParams(stateService, fileSystemService, pathService, parserService); // We'll initialize the factory lazily to avoid circular dependencies if (process.env.DEBUG === 'true') { console.log('ResolutionService: Initialized with', { hasStateService: !!this.stateService, hasFileSystemService: !!this.fileSystemService, hasPathService: !!this.pathService, hasParserService: !!this.parserService }); } } /** * Lazily initialize all factories * This is called only when needed to avoid circular dependencies */ private ensureFactoryInitialized(): void { if (this.factoryInitialized) { return; } this.factoryInitialized = true; // Initialize parser client factory try { this.parserClientFactory = container.resolve('ParserServiceClientFactory'); this.initializeParserClient(); } catch (error) { // In test environment, we need to work even without factories logger.debug(`ParserServiceClientFactory not available: ${(error as Error).message}`); } // Initialize variable resolver client factory try { this.variableResolverClientFactory = container.resolve('VariableReferenceResolverClientFactory'); this.initializeVariableResolverClient(); } catch (error) { // In test environment, we need to work even without factories logger.debug(`VariableReferenceResolverClientFactory not available: ${(error as Error).message}`); } // Initialize directive client factory try { this.directiveClientFactory = container.resolve('DirectiveServiceClientFactory'); this.initializeDirectiveClient(); } catch (error) { // In test environment, we need to work even without factories logger.debug(`DirectiveServiceClientFactory not available: ${(error as Error).message}`); } // Initialize file system client factory try { this.fsClientFactory = container.resolve('FileSystemServiceClientFactory'); this.initializeFsClient(); } catch (error) { // In test environment, we need to work even without factories logger.debug(`FileSystemServiceClientFactory not available: ${(error as Error).message}`); } } /** * Initialize the ParserServiceClient using the factory */ private initializeParserClient(): void { if (!this.parserClientFactory) { throw new Error('ParserServiceClientFactory is not initialized'); } try { this.parserClient = this.parserClientFactory.createClient(); logger.debug('Successfully created ParserServiceClient using factory'); } catch (error) { throw new Error(`Failed to create ParserServiceClient: ${(error as Error).message}`); } } /** * Initialize the VariableResolverClient using the factory */ private initializeVariableResolverClient(): void { if (!this.variableResolverClientFactory) { throw new Error('VariableReferenceResolverClientFactory is not initialized'); } try { this.variableResolverClient = this.variableResolverClientFactory.createClient(); logger.debug('Successfully created VariableReferenceResolverClient using factory'); } catch (error) { throw new Error(`Failed to create VariableReferenceResolverClient: ${(error as Error).message}`); } } /** * Initialize the DirectiveServiceClient using the factory */ private initializeDirectiveClient(): void { if (!this.directiveClientFactory) { throw new Error('DirectiveServiceClientFactory is not initialized'); } try { this.directiveClient = this.directiveClientFactory.createClient(); logger.debug('Successfully created DirectiveServiceClient using factory'); } catch (error) { throw new Error(`Failed to create DirectiveServiceClient: ${(error as Error).message}`); } } /** * Initialize the FileSystemServiceClient using the factory */ private initializeFsClient(): void { if (!this.fsClientFactory) { throw new Error('FileSystemServiceClientFactory is not initialized'); } try { this.fsClient = this.fsClientFactory.createClient(); logger.debug('Successfully created FileSystemServiceClient using factory'); } catch (error) { throw new Error(`Failed to create FileSystemServiceClient: ${(error as Error).message}`); } } /** * Initialize this service with the given parameters * Using DI-only mode */ private initializeFromParams( stateService?: IStateService, fileSystemService?: IFileSystemService, pathService?: IPathService, parserService?: IParserService ): void { // Verify required dependencies if (!stateService) { throw new Error('StateService is required for ResolutionService'); } // Initialize services this.stateService = stateService; this.fileSystemService = fileSystemService || this.createDefaultFileSystemService(); this.pathService = pathService || this.createDefaultPathService(); this.parserService = parserService; // Initialize resolvers this.initializeResolvers(); } /** * Create a default file system service if not provided * Used as fallback in case dependency injection fails */ private createDefaultFileSystemService(): IFileSystemService { logger.warn('Using default FileSystemService - this should only happen in tests'); // Use unknown as an intermediate cast to avoid strict type checking return { readFile: async (): Promise => '', exists: async (): Promise => false, writeFile: async (): Promise => {}, stat: async (): Promise => ({ isDirectory: () => false }), isFile: async (): Promise => false, readDir: async (): Promise => [], ensureDir: async (): Promise => {}, isDirectory: async (): Promise => false, getCwd: (): string => '', dirname: (filePath: string): string => '', watch: (): any => ({ [Symbol.asyncIterator]: () => ({ next: async () => ({ done: true, value: undefined }) }) }), executeCommand: async (): Promise => ({ stdout: '', stderr: '' }), mkdir: async (): Promise => {}, } as unknown as IFileSystemService; } /** * Create a default path service if not provided * Used as fallback in case dependency injection fails */ private createDefaultPathService(): IPathService { logger.warn('Using default PathService - this should only happen in tests'); // Use unknown as an intermediate cast to avoid strict type checking return { validatePath: async (path: string | StructuredPath): Promise => path, resolvePath: (path: string | StructuredPath): string | StructuredPath => path, normalizePath: (path: string): string => path, initialize: (): void => {}, enableTestMode: (): void => {}, disableTestMode: (): void => {}, isTestMode: (): boolean => false, setTestMode: (): void => {}, getHomePath: (): string => '', getProjectPath: (): string => '', setProjectPath: (): void => {}, dirname: (): string => '', isAbsolute: (): boolean => false, // Minimal implementation for fallback } as unknown as IPathService; } /** * Initialize the resolver components used by this service */ private initializeResolvers(): void { this.textResolver = new TextResolver(this.stateService); this.dataResolver = new DataResolver(this.stateService); this.pathResolver = new PathResolver(this.stateService); this.commandResolver = new CommandResolver(this.stateService); this.contentResolver = new ContentResolver(this.stateService); this.variableReferenceResolver = new VariableReferenceResolver( this.stateService, this ); } /** * Parse a string into AST nodes for resolution */ private async parseForResolution(value: string): Promise { try { // Ensure factory is initialized before trying to use it this.ensureFactoryInitialized(); // Use the parser client if available if (this.parserClient) { try { const nodes = await this.parserClient.parseString(value); return nodes || []; } catch (error) { logger.error('Error using parserClient.parseString', { error, valueLength: value.length }); } } // Last resort fallback to direct parsing in tests logger.warn('No parser client available - falling back to direct import or mock parser'); // Try using directly injected parser service if available (for tests) if (this.parserService) { try { const nodes = await this.parserService.parse(value); return nodes || []; } catch (error) { logger.warn('Error using injected parser service', { error }); } } // Finally, try using require try { // Use require for better build compatibility const coreAst = require('@core/ast'); const result = await coreAst.parse(value, { trackLocations: true }); return result.ast || []; } catch (error) { // In a test environment, create a fallback text node logger.warn('Last resort - creating fallback text node', { value }); return [{ type: 'Text', content: value } as TextNode]; } } catch (error) { logger.error('Error parsing content for resolution', { error }); return []; } } /** * Resolve text variables in a string */ async resolveText(text: string, context: ResolutionContext): Promise { const nodes = await this.parseForResolution(text); return this.textResolver.resolve(nodes[0] as DirectiveNode, context); } /** * Resolve data variables and fields */ async resolveData(ref: string, context: ResolutionContext): Promise { const nodes = await this.parseForResolution(ref); return this.dataResolver.resolve(nodes[0] as DirectiveNode, context); } /** * Resolve path variables */ async resolvePath(path: string, context: ResolutionContext): Promise { const nodes = await this.parseForResolution(path); return this.pathResolver.resolve(nodes[0] as DirectiveNode, context); } /** * Resolve command references */ async resolveCommand(cmd: string, args: string[], context: ResolutionContext): Promise { const node: DirectiveNode = { type: 'Directive', directive: { kind: 'run', name: cmd, identifier: cmd, args } }; return this.commandResolver.resolve(node, context); } /** * Resolve content from a file path */ async resolveFile(path: string): Promise { try { // Ensure factory is initialized this.ensureFactoryInitialized(); // Try to use the file system client if available if (this.fsClient) { try { // The IFileSystemServiceClient interface doesn't include readFile // so we need to directly use the fileSystemService instead return await this.fileSystemService.readFile(path); } catch (error) { logger.warn('Error reading file with fileSystemService', { error: error instanceof Error ? error.message : 'Unknown error', path }); } } // Fall back to direct file system service return await this.fileSystemService.readFile(path); } catch (error) { throw new MeldFileNotFoundError(`Failed to read file: ${path}`, { cause: error instanceof Error ? error : new Error(String(error)) }); } } /** * Resolve raw content nodes, preserving formatting but skipping comments */ async resolveContent(nodes: MeldNode[], context: ResolutionContext): Promise { if (!Array.isArray(nodes)) { // If a string path is provided, read the file const path = String(nodes); if (!await this.fileSystemService.exists(path)) { throw new MeldResolutionError( `File not found: ${path}`, { code: ResolutionErrorCode.INVALID_PATH, details: { value: path }, severity: ErrorSeverity.Fatal } ); } return this.fileSystemService.readFile(path); } // Otherwise, process the nodes return this.contentResolver.resolve(nodes, context); } /** * Resolve any value based on the provided context rules */ async resolveInContext(value: string | StructuredPath, context?: ResolutionContext): Promise { // If no context is provided, create a default one const resolveContext = context || { allowedVariableTypes: { text: true, data: true, path: true, command: true }, pathValidation: { requireAbsolute: false, allowedRoots: [] }, currentFilePath: undefined, state: this.stateService }; // Add debug logging for debugging path handling issues logger.debug('ResolutionService.resolveInContext', { value: typeof value === 'string' ? value : value.raw, allowedVariableTypes: resolveContext.allowedVariableTypes, pathValidation: resolveContext.pathValidation, stateExists: !!resolveContext.state, specialPathVars: resolveContext.state ? { PROJECTPATH: resolveContext.state.getPathVar('PROJECTPATH'), HOMEPATH: resolveContext.state.getPathVar('HOMEPATH') } : 'state not available' }); // Handle structured path objects by delegating to the dedicated method if (typeof value === 'object' && value !== null && 'raw' in value) { return this.resolveStructuredPath(value, resolveContext); } // Handle string values if (typeof value === 'string') { // Check for special direct path variable references if (value === '$HOMEPATH' || value === '$~') { const homePath = resolveContext.state?.getPathVar('HOMEPATH') || this.stateService.getPathVar('HOMEPATH'); return homePath || ''; } if (value === '$PROJECTPATH' || value === '$.') { const projectPath = resolveContext.state?.getPathVar('PROJECTPATH') || this.stateService.getPathVar('PROJECTPATH'); return projectPath || ''; } // Check for command references in the format $command(args) const commandRegex = /^\$(\w+)\(([^)]*)\)$/; const commandMatch = value.match(commandRegex); if (commandMatch) { const [, cmdName, argsStr] = commandMatch; // Parse args, splitting by comma but respecting quoted strings const args = argsStr.split(',').map(arg => arg.trim()); try { logger.debug('Resolving command reference', { cmdName, args }); const result = await this.resolveCommand(cmdName, args, resolveContext); return result; } catch (error) { logger.warn('Command execution failed', { cmdName, args, error }); // Fall back to the command name and args, joining with spaces return `${cmdName} ${args.join(' ')}`; } } // Try to parse the string as a path using the parser service try { // Only attempt parsing if the string contains path variable indicators if (value.includes('$.') || value.includes('$~') || value.includes('$/') || value.includes('$')) { const nodes = await this.parseForResolution(value); const pathNode = nodes.find(node => (node as any).type === 'PathVar' || (node.type === 'Directive' && (node as any).directive?.kind === 'path') ); if (pathNode) { // Extract the structured path from the node let structPath: StructuredPath; if ((pathNode as any).type === 'PathVar' && (pathNode as any).value) { structPath = (pathNode as any).value as StructuredPath; // Recursive call with the structured path return this.resolveStructuredPath(structPath, resolveContext); } else if (pathNode.type === 'Directive') { const directiveNode = pathNode as any; if (directiveNode.directive.value && typeof directiveNode.directive.value === 'object' && 'raw' in directiveNode.directive.value) { structPath = directiveNode.directive.value as StructuredPath; // Recursive call with the structured path return this.resolveStructuredPath(structPath, resolveContext); } } } } } catch (error) { // If parsing fails, fall back to variable resolution logger.debug('Path parsing failed, falling back to variable resolution', { error: (error as Error).message }); } } // Handle string values return this.resolveVariables(value as string, resolveContext); } /** * Resolve variables within a string value * @internal Used by resolveInContext */ private async resolveVariables(value: string, context: ResolutionContext): Promise { // Check if the string contains variable references if (value.includes('{{') || value.includes('${') || value.includes('$')) { logger.debug('Resolving variables in string:', { value }); // Special handling for path variables with $ prefix (like $temp) // Uncomment when adding path variable support // value = await this.resolvePathVariablesInText(value, context); // Ensure factory is initialized before trying to use it this.ensureFactoryInitialized(); // Try new approach first (factory pattern) if (this.variableResolverClient) { try { return await this.variableResolverClient.resolve(value, context); } catch (error) { logger.warn('Error using variableResolverClient.resolve, falling back to direct reference', { error, value }); } } // Fall back to direct reference return this.variableReferenceResolver.resolve(value, context); } return value; } /** * Validate that resolution is allowed in the given context */ async validateResolution(value: string | StructuredPath, context?: ResolutionContext): Promise { // If no context is provided, create a default one const resolveContext = context || { allowedVariableTypes: { text: true, data: true, path: true, command: true }, pathValidation: { requireAbsolute: false, allowedRoots: [] }, currentFilePath: undefined, state: this.stateService }; // Convert StructuredPath to string if needed const stringValue = typeof value === 'string' ? value : value.raw; // Parse the value to check for variable types const nodes = await this.parseForResolution(stringValue); for (const node of nodes) { if (node.type !== 'Directive') continue; const directiveNode = node as DirectiveNode; // Check if the directive type is allowed switch (directiveNode.directive.kind) { case 'text': if (!resolveContext.allowedVariableTypes.text) { const errorMessage = 'Text variables are not allowed in this context'; const errorDetails = { value: typeof value === 'string' ? value : value.raw, context: JSON.stringify(context) }; const error = new MeldResolutionError( errorMessage, { code: ResolutionErrorCode.INVALID_CONTEXT, details: errorDetails, severity: ErrorSeverity.Fatal } ); logger.error('Validation error in ResolutionService', { error }); throw error; } break; case 'data': if (!resolveContext.allowedVariableTypes.data) { const errorMessage = 'Data variables are not allowed in this context'; const errorDetails = { value: typeof value === 'string' ? value : value.raw, context: JSON.stringify(context) }; const error = new MeldResolutionError( errorMessage, { code: ResolutionErrorCode.INVALID_CONTEXT, details: errorDetails, severity: ErrorSeverity.Fatal } ); logger.error('Validation error in ResolutionService', { error }); throw error; } break; case 'path': if (!resolveContext.allowedVariableTypes.path) { const errorMessage = 'Path variables are not allowed in this context'; const errorDetails = { value: typeof value === 'string' ? value : value.raw, context: JSON.stringify(context) }; const error = new MeldResolutionError( errorMessage, { code: ResolutionErrorCode.INVALID_CONTEXT, details: errorDetails, severity: ErrorSeverity.Fatal } ); logger.error('Validation error in ResolutionService', { error }); throw error; } break; case 'run': if (!resolveContext.allowedVariableTypes.command) { const errorMessage = 'Command references are not allowed in this context'; const errorDetails = { value: typeof value === 'string' ? value : value.raw, context: JSON.stringify(context) }; const error = new MeldResolutionError( errorMessage, { code: ResolutionErrorCode.INVALID_CONTEXT, details: errorDetails, severity: ErrorSeverity.Fatal } ); logger.error('Validation error in ResolutionService', { error }); throw error; } break; } } } /** * Check for circular variable references */ async detectCircularReferences(value: string): Promise { const visited = new Set(); const stack: string[] = []; const checkReferences = async (text: string, currentRef?: string) => { // Parse the text to get variable references const nodes = await this.parseForResolution(text); if (!nodes || !Array.isArray(nodes)) { throw new MeldResolutionError( 'Invalid parse result', { code: ResolutionErrorCode.SYNTAX_ERROR, details: { value: text }, severity: ErrorSeverity.Fatal } ); } for (const node of nodes) { if (node.type !== 'Directive') continue; const directiveNode = node as DirectiveNode; const ref = directiveNode.directive.identifier; if (!ref) continue; // Skip if this is a direct reference to the current variable if (ref === currentRef) continue; if (stack.includes(ref)) { // Create the circular reference path const path = [...stack, ref].join(' -> '); throw new MeldResolutionError( `Circular reference detected: ${path}`, { code: ResolutionErrorCode.CIRCULAR_REFERENCE, details: { value: text, variableName: ref }, severity: ErrorSeverity.Fatal } ); } if (!visited.has(ref)) { visited.add(ref); stack.push(ref); let refValue: string | undefined; switch (directiveNode.directive.kind) { case 'text': refValue = this.stateService.getTextVar(ref); break; case 'data': const dataValue = this.stateService.getDataVar(ref); if (dataValue && typeof dataValue === 'string') { refValue = dataValue; } break; case 'path': refValue = this.stateService.getPathVar(ref); break; case 'run': const cmdValue = this.stateService.getCommand(ref); if (cmdValue) { refValue = cmdValue.command; } break; } if (refValue) { await checkReferences(refValue, ref); } // Remove from stack after checking stack.pop(); } } }; await checkReferences(value); } /** * Extract a section from content by its heading * @param content The content to extract the section from * @param heading The heading text to search for * @param fuzzy Optional fuzzy matching threshold (0-1, where 1 is exact match, defaults to 0.7) */ async extractSection(content: string, heading: string, fuzzy?: number): Promise { logger.debug('Extracting section from content', { headingToFind: heading, contentLength: content.length, fuzzyThreshold: fuzzy }); try { // Use llmxml for section extraction with new improved API const { createLLMXML } = await import('llmxml'); const llmxml = createLLMXML({ warningLevel: 'none' }); // Extract the section directly from markdown using per-call configuration const section = await llmxml.getSection(content, heading, { includeNested: true, fuzzyThreshold: fuzzy !== undefined ? fuzzy : 0.7 }); logger.debug('Found section using llmxml', { heading, sectionLength: section.length }); return section; } catch (error) { if (error instanceof MeldResolutionError) { throw error; } // Handle error from llmxml, which now provides detailed diagnostic information if (error && typeof error === 'object' && 'code' in error) { const llmError = error as any; if (llmError.code === 'SECTION_NOT_FOUND') { // Get available headings and closest matches from the error details const availableHeadings = llmError.details?.availableHeadings?.map((h: any) => h.title) || []; const closestMatches = llmError.details?.closestMatches?.map((m: any) => `${m.title} (${Math.round(m.similarity * 100)}%)` ) || []; logger.warn('Section not found', { heading, availableHeadings, closestMatches }); throw new MeldResolutionError( 'Section not found: ' + heading, { code: ResolutionErrorCode.SECTION_NOT_FOUND, details: { value: heading, contentPreview: content.substring(0, 100) + '...', availableHeadings: availableHeadings.join(', '), suggestions: closestMatches.join(', ') }, severity: ErrorSeverity.Recoverable } ); } } // For other errors, log and rethrow with additional context logger.error('Error extracting section', { heading, error: error instanceof Error ? error.message : String(error) }); throw new MeldResolutionError( `Failed to extract section: ${error instanceof Error ? error.message : String(error)}`, { code: ResolutionErrorCode.SECTION_EXTRACTION_FAILED, details: { value: heading }, severity: ErrorSeverity.Recoverable } ); } } /** * Extract all headings from content using regex * Since llmxml API compatibility issues, we'll use a simple regex approach * @private */ private async extractHeadingsFromContent(content: string): Promise<{ title: string; level: number; path: string[] }[]> { try { // Simple regex to extract markdown headings const headingRegex = /^(#{1,6})\s+(.+?)(?:\s+#+)?$/gm; const matches = [...content.matchAll(headingRegex)]; // Transform regex matches into structured heading objects const headings: { title: string; level: number; path: string[] }[] = []; const pathMap: Map = new Map(); // Level -> current path at that level for (const match of matches) { const level = match[1].length; // Number of # characters const title = match[2].trim(); // Create a path array by inheriting from parent levels const path: string[] = []; for (let i = 1; i < level; i++) { const parentPath = pathMap.get(i); if (parentPath && parentPath.length > 0) { path.push(...parentPath); } } path.push(title); // Update the path map for this level pathMap.set(level, [title]); // Add to headings array headings.push({ title, level, path }); } return headings; } catch (error) { logger.warn('Error extracting headings', { error: error instanceof Error ? error.message : String(error) }); return []; } } private nodesToString(nodes: MeldNode[]): string { return nodes.map(node => { switch (node.type) { case 'Text': return (node as TextNode).content; case 'CodeFence': const codeFence = node as CodeFenceNode; return '```' + (codeFence.language || '') + '\n' + codeFence.content + '\n```'; case 'Directive': const directive = node as DirectiveNode; return `@${directive.directive.kind} ${directive.directive.value || ''}`; default: return ''; } }).join('\n'); } /** * Resolve a structured path to an absolute path * @private */ private async resolveStructuredPath(path: StructuredPath, context?: ResolutionContext): Promise { // If no context is provided, create a default one const resolveContext = context || { allowedVariableTypes: { text: true, data: true, path: true, command: true }, pathValidation: { requireAbsolute: false, allowedRoots: [] }, currentFilePath: undefined, state: this.stateService }; // IMPORTANT FIX: Check for special flags that indicate we should skip path resolution // This prevents directory paths from being added to variable content in embeds if ((resolveContext as any).isVariableEmbed === true || (resolveContext as any).disablePathPrefixing === true) { logger.debug('Path prefixing disabled for this context (variable embed)', { raw: path.raw, isVariableEmbed: (resolveContext as any).isVariableEmbed, disablePathPrefixing: (resolveContext as any).disablePathPrefixing }); // For variable embeds, return the raw value without path resolution if (typeof path === 'string') { return path; } return path.raw; } const { structured, raw } = path; // Get base directory from context if available (use currentFilePath if available) const baseDir = resolveContext.currentFilePath ? this.pathService.dirname(resolveContext.currentFilePath) : process.cwd(); // Add detailed debug logging for path resolution logger.debug('Resolving structured path', { raw: path.raw, structured: path.structured, baseDir, currentFilePath: resolveContext.currentFilePath, home: process.env.HOME, cwd: process.cwd() }); // Add specific logging for home path resolution if (structured.variables?.special?.includes('HOMEPATH')) { const homePath = this.pathService.getHomePath(); if (process.env.DEBUG === 'true') { console.log('Resolving home path in structured path:', { raw, homePath, segments: structured.segments, baseDir }); } } try { // Use the PathService to resolve the structured path // This handles all special variables and path normalization const resolvedPath = this.pathService.resolvePath(path, baseDir); // Log the final resolved path for debugging if (process.env.DEBUG === 'true') { console.log('Path resolved successfully:', { raw, resolvedPath, exists: await this.fileSystemService.exists(resolvedPath) }); } return resolvedPath; } catch (error) { // Log detailed error information if (process.env.DEBUG === 'true') { console.error('Path resolution failed:', { raw, structured, baseDir, error: (error as Error).message }); } // Handle error based on severity throw new MeldResolutionError( `Failed to resolve path: ${(error as Error).message}`, { code: ResolutionErrorCode.INVALID_PATH, details: { value: raw }, severity: ErrorSeverity.Recoverable } ); } } /** * Get the variable reference resolver */ getVariableResolver(): VariableReferenceResolver { return this.variableReferenceResolver; } /** * Enable tracking of variable resolution attempts * @param config Configuration for the resolution tracker */ enableResolutionTracking(config: Partial): void { // Import and create the tracker if it doesn't exist if (!this.resolutionTracker) { this.resolutionTracker = new VariableResolutionTracker(); } // Configure the tracker this.resolutionTracker.configure({ enabled: true, ...config }); // Set it on the variable reference resolver this.variableReferenceResolver.setResolutionTracker(this.resolutionTracker); } /** * Get the resolution tracker for debugging * @returns The current resolution tracker or undefined if not enabled */ getResolutionTracker(): VariableResolutionTracker | undefined { return this.resolutionTracker; } /** * Validate a path for security and existence * * @param path - The path to validate * @param context - The resolution context with state and allowed variable types * @returns A promise that resolves to true if the path is valid, false otherwise */ async validatePath(path: string, context: ResolutionContext): Promise { try { // First resolve the path to handle any variables const resolvedPath = await this.resolvePath(path, context); // Then validate the resolved path using the PathService await this.pathService.validatePath(resolvedPath, { mustExist: true }); return true; } catch (error) { logger.debug('Path validation failed', { path, error: (error as Error).message }); return false; } } /** * Resolves a field access on a variable (e.g., variable.field.subfield) * * @param variableName - The base variable name * @param fieldPath - The path to the specific field * @param context - The resolution context with state and allowed variable types * @returns The resolved field value * @throws {MeldResolutionError} If field access fails */ async resolveFieldAccess(variableName: string, fieldPath: string, context?: ResolutionContext): Promise { logger.debug(`Resolving field access: ${variableName}.${fieldPath}`); if (!context || !context.state) { throw new MeldResolutionError( `Cannot resolve field access without state context`, { code: ResolutionErrorCode.INVALID_CONTEXT, severity: ErrorSeverity.Fatal } ); } // Get the base variable value const baseValue = context.state.getDataVar(variableName); if (baseValue === undefined) { throw VariableResolutionErrorFactory.variableNotFound(variableName); } try { // Extract field access options from the context if available const fieldAccessOptions = (context as any).fieldAccessOptions || {}; // Use the shared FieldAccessUtility for consistent field access across the codebase const result = FieldAccessUtility.accessFieldsByPath( baseValue, fieldPath, { // Always enable both notation types for maximum compatibility arrayNotation: true, numericIndexing: true, // Preserve types by default, unless explicitly turned off preserveType: fieldAccessOptions.preserveType !== false, // Pass through formatting context if available formattingContext: fieldAccessOptions.formattingContext }, variableName, context.strict !== false // Use strict mode unless explicitly disabled ); logger.debug(`Successfully resolved field access ${variableName}.${fieldPath}`, { resultType: typeof result, isArray: Array.isArray(result) }); return result; } catch (error) { logger.error(`Error resolving field access ${variableName}.${fieldPath}`, { error }); throw VariableResolutionErrorFactory.fieldAccessError( `Error accessing field "${fieldPath}" of variable "${variableName}": ${error instanceof Error ? error.message : String(error)}`, variableName ); } } /** * Convert a value to a formatted string based on the provided formatting context. * Delegates to the VariableReferenceResolverClient when available. * * @param value - The value to convert to a string * @param options - Formatting options including context information * @returns The formatted string representation of the value */ async convertToFormattedString(value: any, options?: any): Promise { // First try to use the client for proper formatting if (!this.variableResolverClient) { this.initializeVariableResolverClient(); } if (this.variableResolverClient) { try { return this.variableResolverClient.convertToString(value, options); } catch (error) { logger.warn('Error using variableResolverClient.convertToString, falling back to basic formatting', { error: error instanceof Error ? error.message : String(error) }); } } // Fall back to basic formatting if (value === undefined || value === null) { return ''; } else if (typeof value === 'string') { return value; } else if (typeof value === 'object') { try { // Check if this is a block context from options const isBlock = options?.formattingContext?.isBlock === true; const isTransformation = options?.formattingContext?.isTransformation === true; // For objects in block context or transformation mode, use pretty printing if ((isBlock || isTransformation) && (Array.isArray(value) || Object.keys(value).length > 0)) { return JSON.stringify(value, null, 2); } // For inline contexts, use compact representation return JSON.stringify(value); } catch (error) { logger.warn('Error formatting object to string', { error: error instanceof Error ? error.message : String(error) }); return String(value); } } else { // For primitive values, just use String() return String(value); } } } ``` #### ../../services/pipeline/ResolutionService/IResolutionService.ts ```javascript import type { MeldNode } from '@core/syntax/types/index.js'; import type { StateServiceLike, StructuredPath } from '@core/shared-service-types.js'; import { VariableResolutionTracker, ResolutionTrackingConfig } from '@tests/utils/debug/VariableResolutionTracker/index.js'; /** * Context for variable resolution, specifying what types of variables and operations are allowed. * Controls the behavior of resolution operations for security and validation. */ interface ResolutionContext { /** Current file being processed, for error reporting */ currentFilePath?: string; /** What types of variables are allowed in this context */ allowedVariableTypes: { /** Allow text variables {{var}} (formerly ${var}) */ text: boolean; /** Allow data variables {{data}} (formerly #{data}) */ data: boolean; /** Allow path variables $path */ path: boolean; /** Allow command interpolation $command */ command: boolean; }; /** Path validation rules when resolving paths */ pathValidation?: { /** Whether paths must be absolute */ requireAbsolute: boolean; /** List of allowed path roots e.g. [$HOMEPATH, $PROJECTPATH] */ allowedRoots: string[]; }; /** Whether field access is allowed for data variables (e.g., data.field) */ allowDataFields?: boolean; /** Whether to throw errors on resolution failures (true) or attempt to recover (false) */ strict?: boolean; /** Whether nested variable references are allowed */ allowNested?: boolean; /** The state service to use for variable resolution */ state: StateServiceLike; /** Flag indicating this is a variable embed context for special handling */ isVariableEmbed?: boolean; /** Flag to disable path prefixing */ disablePathPrefixing?: boolean; /** Flag to actively prevent path prefixing from occurring */ preventPathPrefixing?: boolean; /** Field access options for enhanced field resolution */ fieldAccessOptions?: { preserveType?: boolean; formattingContext?: any; arrayNotation?: boolean; numericIndexing?: boolean; variableName?: string; }; } /** * Error codes for resolution failures to enable precise error handling */ enum ResolutionErrorCode { /** Variable is undefined in the current context */ UNDEFINED_VARIABLE = 'UNDEFINED_VARIABLE', /** Circular reference detected in variable resolution */ CIRCULAR_REFERENCE = 'CIRCULAR_REFERENCE', /** Resolution context is invalid or missing required properties */ INVALID_CONTEXT = 'INVALID_CONTEXT', /** Variable type is not allowed in the current context */ INVALID_VARIABLE_TYPE = 'INVALID_VARIABLE_TYPE', /** Path format is invalid or violates path security rules */ INVALID_PATH = 'INVALID_PATH', /** Maximum iteration count exceeded during resolution */ MAX_ITERATIONS_EXCEEDED = 'MAX_ITERATIONS_EXCEEDED', /** Variable reference has invalid syntax */ SYNTAX_ERROR = 'SYNTAX_ERROR', /** Error accessing fields in a data variable */ FIELD_ACCESS_ERROR = 'FIELD_ACCESS_ERROR', /** Maximum recursion depth exceeded during resolution */ MAX_DEPTH_EXCEEDED = 'MAX_DEPTH_EXCEEDED', /** General resolution failure */ RESOLUTION_FAILED = 'RESOLUTION_FAILED', /** Node type is invalid for the requested operation */ INVALID_NODE_TYPE = 'INVALID_NODE_TYPE', /** Command reference is invalid */ INVALID_COMMAND = 'INVALID_COMMAND', /** Variable not found in the current state */ VARIABLE_NOT_FOUND = 'VARIABLE_NOT_FOUND', /** Field does not exist in the data variable */ INVALID_FIELD = 'INVALID_FIELD', /** Command not found in the current state */ COMMAND_NOT_FOUND = 'COMMAND_NOT_FOUND', /** Section not found in the content */ SECTION_NOT_FOUND = 'SECTION_NOT_FOUND', /** Section extraction failed */ SECTION_EXTRACTION_FAILED = 'SECTION_EXTRACTION_FAILED', /** Specific field not found in variable */ FIELD_NOT_FOUND = 'FIELD_NOT_FOUND', /** Invalid access pattern (e.g., array access on non-array) */ INVALID_ACCESS = 'INVALID_ACCESS' } /** * Service responsible for resolving variables, commands, and paths in Meld content. * Handles all interpolation and reference resolution while enforcing security constraints. * * @remarks * The ResolutionService is a core service that handles all variable interpolation and * reference resolution in Meld. It's responsible for replacing variables like {{var}}, * resolving paths with special variables ($HOMEPATH, $PROJECTPATH), executing commands * via $command references, and extracting sections from content. * * This service implements safety checks to prevent security issues like circular references * and unauthorized path access, while providing rich error information for debugging. * * Dependencies: * - IStateService: For retrieving variable values * - IPathService: For path validation and resolution * - IFileSystemService: For file access * - ICircularityService: For detecting circular references */ interface IResolutionService { /** * Resolve text variables ({{var}}) in a string. * * @param text - The string containing text variables to resolve * @param context - The resolution context with state and allowed variable types * @returns The string with all variables resolved * @throws {MeldResolutionError} If resolution fails and strict mode is enabled * * @example * ```ts * const resolved = await resolutionService.resolveText( * "Hello, {{name}}! Welcome to {{company}}.", * { allowedVariableTypes: { text: true, data: false, path: false, command: false }, state } * ); * ``` */ resolveText(text: string, context: ResolutionContext): Promise; /** * Resolve data variables and fields ({{data.field}}) to their values. * * @param ref - The data variable reference to resolve * @param context - The resolution context with state and allowed variable types * @returns The resolved data value * @throws {MeldResolutionError} If resolution fails and strict mode is enabled * * @example * ```ts * const data = await resolutionService.resolveData( * "user.profile.name", * { allowedVariableTypes: { text: false, data: true, path: false, command: false }, * allowDataFields: true, state } * ); * ``` */ resolveData(ref: string, context: ResolutionContext): Promise; /** * Resolve path variables ($path) to absolute paths. * Handles $HOMEPATH/$~ and $PROJECTPATH/$. resolution. * * @param path - The path with variables to resolve * @param context - The resolution context with state and allowed variable types * @returns The resolved absolute path * @throws {MeldResolutionError} If resolution fails and strict mode is enabled * @throws {PathValidationError} If the path violates path security rules * * @example * ```ts * const absPath = await resolutionService.resolvePath( * "$./src/config/$environment.json", * { allowedVariableTypes: { text: true, data: false, path: true, command: false }, state } * ); * ``` */ resolvePath(path: string, context: ResolutionContext): Promise; /** * Resolve command references ($command(args)) to their results. * * @param cmd - The command name to resolve * @param args - The arguments to pass to the command * @param context - The resolution context with state and allowed variable types * @returns The command execution result * @throws {MeldResolutionError} If resolution fails and strict mode is enabled * * @example * ```ts * const result = await resolutionService.resolveCommand( * "listFiles", * ["*.js", "--recursive"], * { allowedVariableTypes: { text: true, data: true, path: true, command: true }, state } * ); * ``` */ resolveCommand(cmd: string, args: string[], context: ResolutionContext): Promise; /** * Resolve content from a file path. * * @param path - The path to the file to read * @returns The file content as a string * @throws {MeldFileSystemError} If the file cannot be read */ resolveFile(path: string): Promise; /** * Resolve raw content nodes, preserving formatting but skipping comments. * * @param nodes - The AST nodes to convert to text * @param context - The resolution context with state and allowed variable types * @returns The resolved content as a string * @throws {MeldResolutionError} If resolution fails and strict mode is enabled */ resolveContent(nodes: MeldNode[], context: ResolutionContext): Promise; /** * Resolve any value based on the provided context rules. * This is a general-purpose resolution method that handles different types of values. * * @param value - The string or structured path to resolve * @param context - The resolution context with state and allowed variable types * @returns The resolved value as a string * @throws {MeldResolutionError} If resolution fails and strict mode is enabled */ resolveInContext(value: string | StructuredPath, context?: ResolutionContext): Promise; /** * Resolves a field access on a variable (e.g., variable.field.subfield) * * @param variableName - The base variable name * @param fieldPath - The path to the specific field * @param context - The resolution context with state and allowed variable types * @returns The resolved field value * @throws {MeldResolutionError} If field access fails */ resolveFieldAccess(variableName: string, fieldPath: string, context?: ResolutionContext): Promise; /** * Validate that a value can be resolved with the given context * @throws {MeldResolutionError} If validation fails */ validateResolution(value: string | StructuredPath, context?: ResolutionContext): Promise; /** * Extract a section from content by its heading. * Useful for retrieving specific parts of markdown or other structured text. * * @param content - The content to extract the section from * @param section - The heading text to search for * @param fuzzy - Optional fuzzy matching threshold (0-1, where 1 is exact match, defaults to 0.7) * @returns The extracted section content * @throws {MeldResolutionError} With code SECTION_NOT_FOUND if the section cannot be found * * @example * ```ts * const apiDocs = await resolutionService.extractSection( * readme, * "API Documentation", * 0.8 // 80% match threshold * ); * ``` */ extractSection(content: string, section: string, fuzzy?: number): Promise; /** * Check for circular variable references. * * @param value - The string to check for circular references * @throws {MeldResolutionError} With code CIRCULAR_REFERENCE if circular references are detected */ detectCircularReferences(value: string): Promise; /** * Convert a value to a formatted string based on the provided formatting context. * This is particularly useful for handling data variables in different output contexts. * * @param value - The value to convert to a string * @param options - Formatting options including context information * @returns The formatted string representation of the value * * @example * ```ts * const formatted = await resolutionService.convertToFormattedString( * dataValue, * { * formattingContext: { * isBlock: true, * nodeType: 'embed', * isTransformation: true * } * } * ); * ``` */ convertToFormattedString(value: any, options?: any): Promise; /** * Enable tracking of variable resolution attempts. * This is primarily used for debugging and visualization. * * @param config - Configuration for the resolution tracker */ enableResolutionTracking(config: Partial): void; /** * Get the resolution tracker for debugging. * * @returns The current resolution tracker or undefined if not enabled */ getResolutionTracker(): VariableResolutionTracker | undefined; } export type { ResolutionContext, IResolutionService }; export { ResolutionErrorCode }; ```' [debug] Template reference 'files.VariableResolutionCode' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../services/pipeline/ResolutionService/resolvers/VariableReferenceResolver.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ResolutionService/resolvers/VariableReferenceResolver.ts"} [debug] Reading relative file {"relativePath":"../../services/pipeline/ResolutionService/resolvers/types.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ResolutionService/resolvers/types.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.VariableResolutionCode }}' to '#### ../../services/pipeline/ResolutionService/resolvers/VariableReferenceResolver.ts ```javascript import type { IStateService } from '@services/state/StateService/IStateService.js'; import type { ResolutionContext } from '@services/pipeline/ResolutionService/IResolutionService.js'; import { ResolutionErrorCode } from '@services/pipeline/ResolutionService/IResolutionService.js'; import { MeldResolutionError } from '@core/errors/MeldResolutionError.js'; import { ErrorSeverity } from '@core/errors/MeldError.js'; import type { IResolutionService } from '@services/pipeline/ResolutionService/IResolutionService.js'; import type { MeldNode, TextNode, DirectiveNode, NodeType } from '@core/syntax/types/index.js'; import { resolutionLogger as logger } from '@core/utils/logger.js'; import { VariableResolutionTracker } from '@tests/utils/debug/VariableResolutionTracker/index.js'; import { container, inject, injectable } from 'tsyringe'; import type { IResolutionServiceClient } from '@services/pipeline/ResolutionService/interfaces/IResolutionServiceClient.js'; import { ResolutionServiceClientFactory } from '@services/pipeline/ResolutionService/factories/ResolutionServiceClientFactory.js'; import type { IParserServiceClient } from '@services/pipeline/ParserService/interfaces/IParserServiceClient.js'; import { ParserServiceClientFactory } from '@services/pipeline/ParserService/factories/ParserServiceClientFactory.js'; import type { IParserService } from '@services/pipeline/ParserService/IParserService.js'; import { VariableType, Field, IVariableReference } from '@core/syntax/types/interfaces/index.js'; import { VariableNodeFactory } from '@core/syntax/types/factories/index.js'; // Keep legacy imports for backward compatibility during transition import { SPECIAL_PATH_VARS, ENV_VAR_PREFIX, VAR_PATTERNS } from '@core/syntax/types/variables.js'; import { VariableResolutionErrorFactory } from '@services/pipeline/ResolutionService/resolvers/error-factory.js'; // Define a format context type type FormatContext = 'inline' | 'block'; // Type guard functions function isTextNode(node: MeldNode): node is TextNode { return node.type === 'Text' && 'content' in node; } function isDirectiveNode(node: MeldNode): node is DirectiveNode { return node.type === 'Directive' && 'directive' in node; } /** * Local type guard function that uses the factory pattern internally * @param node Node to check * @returns True if the node is a variable reference node */ function isVariableReferenceNode(node: any): node is IVariableReference { // Try to use factory pattern first if available in the container try { const factory = container.resolve(VariableNodeFactory); return factory.isVariableReferenceNode(node); } catch (error) { // Fallback to direct type checking (same logic as legacy function) return ( node.type === 'VariableReference' && typeof node.identifier === 'string' && typeof node.valueType === 'string' ); } } /** * Handles resolution of variable references ({{var}}) * Previously used ${var} for text and #{var} for data, now unified as {{var}} */ export class VariableReferenceResolver { private readonly MAX_RESOLUTION_DEPTH = 20; private readonly MAX_ITERATIONS = 100; private resolutionTracker?: VariableResolutionTracker; private resolutionClient?: IResolutionServiceClient; private resolutionClientFactory?: ResolutionServiceClientFactory; private parserClient?: IParserServiceClient; private parserClientFactory?: ParserServiceClientFactory; private factoryInitialized: boolean = false; /** * Creates a new instance of the VariableReferenceResolver * @param stateService - State service for variable management * @param resolutionService - Resolution service for resolving variables * @param parserService - Parser service for parsing content with variables */ constructor( private readonly stateService: IStateService, private readonly resolutionService?: IResolutionService, private readonly parserService?: IParserService, @inject(VariableNodeFactory) private readonly variableNodeFactory?: VariableNodeFactory ) { // Initialize the factory if it wasn't injected (for backward compatibility) if (!this.variableNodeFactory) { logger.debug('VariableNodeFactory not injected, resolving from container'); try { this.variableNodeFactory = container.resolve(VariableNodeFactory); } catch (error) { logger.warn('Failed to resolve VariableNodeFactory from container', { error: error instanceof Error ? error.message : String(error) }); // We'll fall back to legacy functions if needed } } } /** * Lazily initialize the service client factories * This is called only when needed to avoid circular dependencies * @throws Error if factory initialization fails */ private ensureFactoryInitialized(): void { // If already initialized, return early if (this.factoryInitialized) { return; } // Mark as initialized to prevent recursive calls this.factoryInitialized = true; // Initialize resolution client factory if needed if (!this.resolutionService && !this.resolutionClient) { try { // Resolve the factory from the container this.resolutionClientFactory = container.resolve('ResolutionServiceClientFactory'); this.initializeResolutionClient(); logger.debug('Initialized ResolutionServiceClient via factory'); } catch (error) { // Log error but don't fail - we might be able to continue with other mechanisms logger.warn('Failed to initialize ResolutionServiceClient', { error: error instanceof Error ? error.message : String(error) }); } } else { logger.debug('Using directly injected ResolutionService, skipping client factory'); } // Initialize parser client factory if needed if (!this.parserService && !this.parserClient) { try { // Resolve the factory from the container this.parserClientFactory = container.resolve('ParserServiceClientFactory'); this.initializeParserClient(); logger.debug('Initialized ParserServiceClient via factory'); } catch (error) { // Log error but don't fail - we'll fall back to regex parsing logger.warn('Failed to initialize ParserServiceClient, will use regex fallback', { error: error instanceof Error ? error.message : String(error) }); } } else { logger.debug('Using directly injected ParserService, skipping client factory'); } } /** * Initialize the ResolutionServiceClient using the factory * @throws Error if client creation fails */ private initializeResolutionClient(): void { if (!this.resolutionClientFactory) { logger.warn('ResolutionServiceClientFactory not available, some functionality may be limited'); return; } try { this.resolutionClient = this.resolutionClientFactory.createClient(); logger.debug('Successfully created ResolutionServiceClient'); } catch (error) { // Don't throw, just log the error and continue without the client logger.warn('Failed to create ResolutionServiceClient', { error: error instanceof Error ? error.message : String(error) }); } } /** * Initialize the ParserServiceClient using the factory * @throws Error if client creation fails */ private initializeParserClient(): void { if (!this.parserClientFactory) { logger.warn('ParserServiceClientFactory not available, will use regex fallback for parsing'); return; } try { this.parserClient = this.parserClientFactory.createClient(); logger.debug('Successfully created ParserServiceClient'); } catch (error) { // Don't throw, just log the error and continue without the client logger.warn('Failed to create ParserServiceClient, will use regex fallback', { error: error instanceof Error ? error.message : String(error) }); } } /** * Set the resolution tracker for debugging * @internal */ setResolutionTracker(tracker: VariableResolutionTracker): void { this.resolutionTracker = tracker; } /** * Resolves all variable references in the given text * @param text Text containing variable references like {{varName}} * @param context Resolution context * @returns Resolved text with all variables replaced with their values */ async resolve(content: string, context: ResolutionContext): Promise { logger.debug(`Resolving content: ${content}`, { content, contextStrict: context.strict }); // Initialize result let result = ''; // Provide a new context with depth tracking to prevent circular references const newContext: ResolutionContext & { depth?: number } = { ...context, depth: (context as any).depth !== undefined ? (context as any).depth + 1 : 1 }; // Check for max depth if (newContext.depth && newContext.depth > this.MAX_RESOLUTION_DEPTH) { // Special handling for excessive nesting if (context.isVariableEmbed) { // For variable embeds, we log a warning but don't fail completely logger.warn(`Maximum resolution depth exceeded in variable embed context. Depth: ${newContext.depth}`); // Return the original content without further resolution return content; } else { throw new MeldResolutionError( 'Maximum resolution depth exceeded', { code: ResolutionErrorCode.MAX_DEPTH_EXCEEDED, severity: ErrorSeverity.Fatal } ); } } // Parse the content into nodes const nodes = await this.parseContent(content); logger.debug(`Parsed ${nodes.length} nodes`, { nodeTypes: nodes.map(n => n.type).join(', ') }); // Process each node for (const node of nodes) { // Handle text nodes if (isTextNode(node)) { logger.debug(`Processing text node: ${node.content.substring(0, 50)}${node.content.length > 50 ? '...' : ''}`); result += node.content; continue; } // Handle variable reference nodes if (isVariableReferenceNode(node)) { logger.debug(`Processing variable reference node: ${node.identifier}`, { valueType: node.valueType, hasFields: !!node.fields, fields: node.fields ? JSON.stringify(node.fields) : 'none' }); try { // Get the variable value let value = await this.getVariable(node.identifier, newContext); // If the variable has fields, access them if (node.fields && node.fields.length > 0) { logger.debug(`Accessing fields for variable ${node.identifier}`, { fields: JSON.stringify(node.fields) }); try { value = await this.accessFields(value, node.fields, newContext, node.identifier); logger.debug(`Field access result for ${node.identifier}:`, { valueType: typeof value, isArray: Array.isArray(value), value: typeof value === 'object' ? JSON.stringify(value).substring(0, 100) : String(value) }); } catch (fieldError: any) { // In non-strict mode, return empty string for field access errors if (!newContext.strict) { logger.warn(`Field access error in non-strict mode, returning empty string: ${fieldError.message}`); value = ''; } else { // In strict mode, rethrow the error throw fieldError; } } } // Convert the value to string let stringValue; if (Array.isArray(value) && this.isArrayOfObjects(value)) { // For arrays of objects (like the complexArray in parent-object-reference.test.ts), // force pretty printing with indentation stringValue = JSON.stringify(value, null, 2); logger.debug(`Pretty-printed array for ${node.identifier} with indentation`); } else { // Normal string conversion for all other types stringValue = this.convertToString(value); } logger.debug(`Converted ${node.identifier} to string: ${stringValue.substring(0, 100)}${stringValue.length > 100 ? '...' : ''}`); // Add to result result += stringValue; } catch (error) { // In non-strict mode, replace with empty string if (!newContext.strict) { logger.warn(`Error resolving variable ${node.identifier} in non-strict mode, using empty string`, { error: error instanceof Error ? error.message : String(error) }); result += ''; } else { // In strict mode, rethrow the error throw error; } } continue; } // Handle directive nodes (should not happen in normal operation) if (isDirectiveNode(node)) { logger.warn(`Unexpected directive node in variable resolution: ${node.directive.kind}`); // Skip directive nodes continue; } // Unknown node type logger.warn(`Unknown node type in variable resolution: ${node.type}`); } // If the result still contains variable references, resolve them recursively if (this.containsVariableReferences(result)) { logger.debug(`Result still contains variable references, resolving recursively: ${result}`); return this.resolve(result, newContext); } logger.debug(`Final resolved result: ${result}`); return result; } /** * Resolve a field access expression like varName.field1.field2 * Enhanced version with type preservation options * * @param varName Base variable name * @param fieldPath Dot-notation field path (e.g., "field1.field2") * @param context Resolution context * @param preserveType Whether to preserve the type of the result (vs. string conversion) * @returns Resolved field value (type preserved if preserveType is true) */ async resolveFieldAccess( varName: string, fieldPath: string, context: ResolutionContext, preserveType: boolean = false ): Promise { // Log additional debug information for variable embed handling const isVariableEmbed = (context as any).isVariableEmbed === true; const disablePathPrefixing = (context as any).disablePathPrefixing === true; if (isVariableEmbed || disablePathPrefixing) { logger.debug(`Field access in variable embed context: ${varName}.${fieldPath}`, { isVariableEmbed, disablePathPrefixing, preserveType }); } try { // Get the base variable const value = await this.getVariable(varName, context); if (value === undefined) { throw VariableResolutionErrorFactory.variableNotFound(varName); } // No fields to access - return base variable if (!fieldPath) { // If preserveType is false, convert to string if (!preserveType) { return this.convertToString(value); } return value; } // Split the field path const fields = fieldPath.split('.').map(field => { // Check if this is a numeric index const numIndex = parseInt(field, 10); if (!isNaN(numIndex)) { return { type: 'index' as const, value: numIndex }; } // Otherwise it's a field name return { type: 'field' as const, value: field }; }); // Access the fields // Use the internal accessFields method with proper error handling const result = await this.accessFields(value, fields, context, varName); // If preserveType is false, convert to string if (!preserveType) { return this.convertToString(result); } // Otherwise return the raw value with deep cloning to preserve type if (result !== null && result !== undefined) { if (Array.isArray(result)) { return [...result]; // Return a copy of the array } else if (typeof result === 'object') { return { ...result as Record }; // Return a copy of the object } } return result; } catch (error) { // Log the error for diagnostic purposes logger.error('Error in resolveFieldAccess', { varName, fieldPath, preserveType, error: error instanceof Error ? error.message : String(error) }); // Track resolution error if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( 'field-access-error', JSON.stringify({ varName, fieldPath, preserveType, context: JSON.stringify(context) }), false, undefined, error instanceof Error ? error.message : String(error) ); } // Rethrow to maintain behavior throw error; } } /** * Debug version of field access that returns detailed information * This is used for testing and debugging */ debugFieldAccess(obj: any, fields: string[], context: ResolutionContext): any { // Start with the base value let current = obj; const path = []; try { // Handle each field for (let i = 0; i < fields.length; i++) { const field = fields[i]; path.push(field); // Check if current value exists if (current === undefined || current === null) { return { success: false, error: `Cannot access field ${field} in undefined value`, path: path.join('.'), result: undefined }; } // Try to parse as number for array access const numIndex = parseInt(field, 10); if (!isNaN(numIndex)) { // Array access if (Array.isArray(current)) { if (numIndex >= 0 && numIndex < current.length) { current = current[numIndex]; } else { return { success: false, error: `Array index ${numIndex} out of bounds for array of length ${current.length}`, path: path.join('.'), result: undefined }; } } else { return { success: false, error: `Cannot access array index in non-array value`, path: path.join('.'), type: typeof current, result: undefined }; } } else { // Field access if (typeof current === 'object' && current !== null) { if (field in current) { current = current[field]; } else { return { success: false, error: `Field ${field} not found in object`, path: path.join('.'), result: undefined }; } } else { return { success: false, error: `Cannot access field in non-object value`, path: path.join('.'), type: typeof current, result: undefined }; } } } // Success return { success: true, path: path.join('.'), result: current }; } catch (error) { // Unexpected error return { success: false, error: error instanceof Error ? error.message : String(error), path: path.join('.'), result: undefined }; } } /** * Check if the content contains variable references */ private containsVariableReferences(content: string): boolean { // Check for {{variable}} syntax const hasStandardVars = content.includes('{{') && content.includes('}}'); // Check for $variable path syntax const hasPathVars = content.includes('$') && /\$[a-zA-Z0-9_]+/.test(content); return hasStandardVars || hasPathVars; } /** * Parse the content to extract variable references */ private async parseContent(content: string): Promise { // Try to use parser service first if (this.parserService) { try { return await this.parserService.parse(content); } catch (error) { logger.debug('Error using parser service, falling back to regex', { error }); } } // Try parser client if available if (this.parserClient) { try { return await this.parserClient.parseString(content); } catch (error) { logger.debug('Error using parser client, falling back to regex', { error }); } } // Fall back to regex parsing return this.parseWithRegex(content); } /** * Get a variable value by name */ private async getVariable(name: string, context: ResolutionContext): Promise { logger.debug(`Getting variable '${name}'`, { variableName: name, contextStrict: context.strict, allowedTypes: context.allowedVariableTypes }); // Track resolution attempt if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackAttemptStart(name, 'getVariable'); } // Check if this is a nested variable reference if (name.includes('{{')) { logger.debug(`Resolving nested variable reference: ${name}`); const result = await this.resolveNestedVariableReference('{{' + name + '}}', context); // Track the resolution result if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( name, 'nested-variable-reference', result !== undefined, result, result === undefined ? 'Nested variable not found' : undefined ); } return result; } // First try as text variable const textValue = context.state.getTextVar(name); if (textValue !== undefined) { logger.debug(`Found text variable '${name}'`, { value: typeof textValue === 'string' ? textValue : JSON.stringify(textValue), type: typeof textValue }); // Track the resolution result if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( name, 'text-variable', true, textValue, undefined ); } return textValue; } // Then try as data variable - check if we're getting an actual object // or a stringified object which can happen in some test cases let dataValue = context.state.getDataVar(name); if (dataValue !== undefined) { logger.debug(`Found data variable '${name}'`, { valueType: typeof dataValue, isArray: Array.isArray(dataValue), preview: typeof dataValue === 'object' ? JSON.stringify(dataValue).substring(0, 100) : String(dataValue), rawValue: dataValue }); // If dataValue is a string but looks like JSON, try to parse it if (typeof dataValue === 'string' && (dataValue.startsWith('{') || dataValue.startsWith('['))) { try { const parsedData = JSON.parse(dataValue); logger.debug(`Parsed JSON string data variable '${name}'`, { parsedType: typeof parsedData, isArray: Array.isArray(parsedData), parsedPreview: JSON.stringify(parsedData).substring(0, 100) }); // Track the resolution result if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( name, 'data-variable-parsed-from-string', true, parsedData, undefined ); } return parsedData; } catch (e) { // If parsing fails, just use the string value logger.debug(`Failed to parse data variable '${name}' as JSON, using as string`, { error: e instanceof Error ? e.message : String(e) }); // Track the failed parsing if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( name, 'data-variable-parse-failed', true, dataValue, e instanceof Error ? e.message : String(e) ); } } } // Track the resolution result if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( name, 'data-variable', true, dataValue, undefined ); } return dataValue; } // Finally try as path variable const pathValue = context.state.getPathVar(name); if (pathValue !== undefined) { logger.debug(`Found path variable '${name}'`, { value: typeof pathValue === 'string' ? pathValue : JSON.stringify(pathValue), type: typeof pathValue }); // Track the resolution result if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( name, 'path-variable', true, pathValue, undefined ); } return pathValue; } // Variable not found - always throw in strict mode if (context.strict) { logger.warn(`Variable '${name}' not found and strict mode is enabled, throwing error`); // Track the failed resolution if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( name, 'variable-not-found', false, undefined, `Variable '${name}' not found` ); } throw VariableResolutionErrorFactory.variableNotFound(name); } logger.warn(`Variable '${name}' not found but strict mode is disabled, returning undefined`); // Track the missing variable in non-strict mode if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( name, 'variable-not-found-non-strict', false, undefined, `Variable '${name}' not found` ); } return undefined; // Return undefined instead of '' to signal missing variable } /** * Convert a value to a string representation * * @param value The value to convert to string * @param formattingContext Optional formatting context to control output format * @returns Formatted string representation */ convertToString( value: any, formattingContext?: { isBlock?: boolean; nodeType?: string; linePosition?: 'start' | 'middle' | 'end'; isTransformation?: boolean; } ): string { // Handle null and undefined if (value === null || value === undefined) { return ''; } // Handle text nodes if (isTextNode(value)) { return value.content; } // Handle variable reference nodes if (isVariableReferenceNode(value)) { return value.identifier; } // Determine the formatting context const formatContext: FormatContext = formattingContext?.isBlock ? 'block' : 'inline'; const formatOutput = formattingContext?.isBlock || formattingContext?.nodeType === 'embed' || !formattingContext; // Default to formatted output for direct reference // Delegate to the enhanced private method return this.formatValueAsString(value, formatOutput, formatContext); } /** * Convert a value to string representation with context-aware formatting * Private helper method used by convertToString * * @param value The value to convert to string * @param formatOutput Whether to format the output (true for block format, false for inline) * @param formatContext Optional formatting context to control output format * @returns Formatted string representation */ private formatValueAsString(value: any, formatOutput = false, formatContext: FormatContext = 'inline'): string { if (value === undefined || value === null) { return ''; } if (typeof value === 'string') { return value; } if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { return String(value); } // Handle array values with standardized formatting if (Array.isArray(value)) { // Block context formatting (for multiline output) if (formatContext === 'block') { // For arrays containing objects, use proper JSON indentation if (this.isArrayOfObjects(value)) { return JSON.stringify(value, null, 2); } // For arrays with nested arrays or complex structures, use proper JSON indentation if (this.hasComplexStructure(value)) { return JSON.stringify(value, null, 2); } // If formatOutput is true (for variable embeds), use pretty JSON format if (formatOutput) { return JSON.stringify(value, null, 2); } // Simple arrays (strings, numbers, etc.) in block context get comma-space formatting return value.map(item => this.formatValueAsString(item, formatOutput, 'inline')).join(', '); } // Inline context - always use comma-space formatting return value.map(item => this.formatValueAsString(item, formatOutput, 'inline')).join(', '); } // Handle object values with standardized formatting if (typeof value === 'object') { try { // Block context formatting (for multiline output) if (formatContext === 'block') { // Use 2-space indentation for pretty printing return JSON.stringify(value, null, 2); } // Inline context - compact JSON without whitespace return JSON.stringify(value); } catch (error) { // Just in case JSON.stringify fails return '[Object]'; } } // Default fallback for other types return String(value); } private shouldArrayBePrettyPrinted(arr: any[]): boolean { // Arrays should be pretty-printed if: // 1. They contain objects - especially for the enhanced-field-access test if (arr.length > 0 && arr.every(item => typeof item === 'object' && item !== null && !Array.isArray(item))) { return true; } // 2. They contain nested arrays // 3. They are longer than 5 items if (arr.length > 5) { return true; } return arr.some(item => { if (typeof item === 'object' && item !== null) { return true; } if (Array.isArray(item)) { return true; } const stringified = String(item); return stringified.length > 20; }); } // Helper to identify arrays of objects (for proper JSON indentation) private isArrayOfObjects(arr: any[]): boolean { if (arr.length === 0) { return false; } // Check if all items in the array are objects (not null, not arrays) return arr.some(item => typeof item === 'object' && item !== null && !Array.isArray(item) && Object.keys(item).length > 0 ); } /** * Format a JSON string to be more readable in inline context * Ensures spaces after colons and commas */ private formatJsonString(jsonStr: string): string { return jsonStr .replace(/,"/g, ', "') // Add space after commas .replace(/:{/g, ': {') // Add space after colons followed by object .replace(/:\[/g, ': [') // Add space after colons followed by array .replace(/":"/g, '": "'); // Add space after colon in key-value pairs } /** * Extract all variable references from the given text * @param text The text to extract references from * @returns Array of variable references */ extractReferences(text: string): string[] { // Extract all variable references using regex const references = new Set(); const variableRegex = /\{\{([^}]+)\}\}/g; let match; while ((match = variableRegex.exec(text)) !== null) { const reference = match[1]; // For field access, only include the base variable name const baseName = reference.split('.')[0]; references.add(baseName); } return Array.from(references); } /** * Extract variable references asynchronously using AST when possible * @param text The text to extract references from * @returns Array of variable references and information */ async extractReferencesAsync(text: string): Promise { try { // Parse the text into nodes const nodes = await this.parseContent(text); return this.extractVariableReferencesFromNodes(nodes); } catch (error) { // Fall back to regex-based extraction logger.debug('Error parsing content for reference extraction, falling back to regex', { error }); return this.extractReferences(text); } } /** * Extract variable references from AST nodes * @param nodes The AST nodes to extract references from * @returns Array of variable references */ private extractVariableReferencesFromNodes(nodes: MeldNode[]): string[] { const references = new Set(); for (const node of nodes) { if (isVariableReferenceNode(node)) { references.add(node.identifier); } } return Array.from(references); } /** * Parse a variable reference into base name and fields */ private parseVariableReference(reference: string): { baseName: string, fields: Field[] } { // Check if this is a field access if (reference.includes('.')) { const parts = reference.split('.'); const baseName = parts[0]; const fields = parts.slice(1).map(field => { // Check if this is a numeric index const numIndex = parseInt(field, 10); if (!isNaN(numIndex)) { return { type: 'index' as const, value: numIndex }; } // Otherwise it's a field name return { type: 'field' as const, value: field }; }) as Field[]; return { baseName, fields }; } // Simple variable reference return { baseName: reference, fields: [] }; } /** * Access fields in a nested object */ private async accessFields(obj: any, fields: any[], context: ResolutionContext, variableName: string): Promise { let current = obj; // Check if this is a variable embed context and log details const isVariableEmbed = (context as any).isVariableEmbed === true; const disablePathPrefixing = (context as any).disablePathPrefixing === true; if (isVariableEmbed || disablePathPrefixing) { logger.debug(`Access fields in variable embed context: ${variableName}`, { fields: JSON.stringify(fields), isVariableEmbed, disablePathPrefixing, objectType: typeof obj, isArray: Array.isArray(obj) }); } // Try to parse stringified JSON if needed if (typeof current === 'string' && (current.startsWith('{') || current.startsWith('['))) { try { const parsed = JSON.parse(current); logger.debug(`Successfully parsed stringified JSON for variable '${variableName}'`, { originalType: 'string', parsedType: typeof parsed, isArray: Array.isArray(parsed) }); current = parsed; } catch (error) { // Not valid JSON, continue with the string value logger.debug(`Failed to parse string as JSON for variable '${variableName}'`, { error: error instanceof Error ? error.message : String(error), value: current }); } } // Log debug information to help with troubleshooting logger.debug(`Accessing fields for variable '${variableName}'`, { initialObjectType: typeof current, isArray: Array.isArray(current), rawValue: current, numFields: fields?.length, fields: JSON.stringify(fields) }); // Track field access attempt if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackAttemptStart( `${variableName}.fields`, 'field-access', { initialType: typeof current, isArray: Array.isArray(current), fields: JSON.stringify(fields) } ); } // Process fields in order for (let i = 0; i < fields.length; i++) { const field = fields[i]; const fieldValue = field.value !== undefined ? field.value : field; const fieldType = field.type || 'field'; const fieldPath = fields.slice(0, i + 1).map(f => f.type === 'index' ? `[${f.value}]` : `.${f.value}` ).join('').replace(/^\./, ''); // Make sure we have a valid object to access fields on if (current === null || current === undefined) { const errorMessage = `Cannot access field '${fieldValue}' of ${current} for variable '${variableName}'`; const detailedMessage = `Cannot access field '${fieldValue}' at path '${fieldPath}' because the parent value is ${current}`; logger.error(errorMessage, { fieldPath, parentValue: current }); // Track the failed field access if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( `${variableName}.${fieldValue}`, 'field-access-null-undefined', false, undefined, detailedMessage ); } throw VariableResolutionErrorFactory.invalidAccess( variableName, detailedMessage ); } // Log debug information about the current field access logger.debug(`Processing field ${i}`, { fieldType, fieldValue, currentType: typeof current, isArray: Array.isArray(current), currentValue: current }); // If current is not an object or array and we're trying to access a property, throw error if (typeof current !== 'object' && !Array.isArray(current)) { const errorMessage = `Cannot access field '${fieldValue}' of non-object value (type: ${typeof current}) for variable '${variableName}'`; const detailedMessage = `Cannot access field '${fieldValue}' at path '${fieldPath}' because the parent value is of type '${typeof current}' (${String(current).substring(0, 50)}${String(current).length > 50 ? '...' : ''})`; logger.error(errorMessage, { fieldPath, parentType: typeof current, parentValue: current }); // Track the failed field access if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( `${variableName}.${fieldValue}`, 'field-access-non-object', false, undefined, detailedMessage ); } throw VariableResolutionErrorFactory.invalidAccess( variableName, detailedMessage ); } // Field access (regular property or array index) if (fieldType === 'index' && Array.isArray(current)) { // Array index access const index = typeof fieldValue === 'number' ? fieldValue : parseInt(fieldValue as string, 10); if (isNaN(index)) { const errorMessage = `Invalid array index: '${fieldValue}' is not a number for variable '${variableName}'`; const detailedMessage = `Invalid array index: '${fieldValue}' at path '${fieldPath}' is not a valid number`; logger.error(errorMessage, { fieldPath, fieldValue }); // Track the failed field access if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( `${variableName}[${fieldValue}]`, 'field-access-invalid-index', false, undefined, detailedMessage ); } throw VariableResolutionErrorFactory.invalidAccess( variableName, detailedMessage ); } if (index < 0 || index >= current.length) { const errorMessage = `Array index ${index} out of bounds [0-${current.length-1}] for variable '${variableName}'`; const detailedMessage = `Array index ${index} at path '${fieldPath}' is out of bounds [0-${current.length-1}]`; logger.error(errorMessage, { fieldPath, index, arrayLength: current.length }); // Track the failed field access if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( `${variableName}[${index}]`, 'field-access-index-out-of-bounds', false, undefined, detailedMessage ); } throw VariableResolutionErrorFactory.indexOutOfBounds( variableName, index, current.length ); } current = current[index]; logger.debug(`Accessed array index ${index}`, { resultType: typeof current, isResultArray: Array.isArray(current), value: current }); } else { // Regular property access const propName = String(fieldValue); if (!(propName in current)) { // Check if we have a stringified JSON object that needs parsing if (typeof current === 'string' && (current.startsWith('{') || current.startsWith('['))) { try { const parsed = JSON.parse(current); if (propName in parsed) { logger.debug(`Found property '${propName}' in parsed JSON string`, { parsedType: typeof parsed, isArray: Array.isArray(parsed) }); current = parsed; current = current[propName]; continue; } } catch (error) { // Not valid JSON, continue with normal error handling logger.debug(`Failed to parse string as JSON for property access`, { error: error instanceof Error ? error.message : String(error) }); } } const errorMessage = `Field '${propName}' not found in variable '${variableName}'`; const detailedMessage = `Field '${propName}' at path '${fieldPath}' not found in variable '${variableName}'`; const availableKeys = typeof current === 'object' && current !== null ? Object.keys(current) : []; logger.error(errorMessage, { fieldPath, availableKeys, parentType: typeof current, parentValue: current }); if (context.strict) { // Track the failed field access if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( `${variableName}.${propName}`, 'field-access-not-found', false, undefined, detailedMessage ); } // Create a more detailed error message that includes available keys const keysInfo = availableKeys.length > 0 ? `Available keys: ${availableKeys.join(', ')}` : 'No keys available'; throw VariableResolutionErrorFactory.fieldNotFound( variableName, `${propName} (${keysInfo})` ); } else { logger.warn(`Field '${propName}' not found in variable '${variableName}', returning empty string (strict mode off)`); // Track the failed field access in non-strict mode if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( `${variableName}.${propName}`, 'field-access-not-found-non-strict', false, '', detailedMessage ); } return ''; } } current = current[propName]; logger.debug(`Accessed property ${propName}`, { resultType: typeof current, isResultArray: Array.isArray(current), value: current }); } } // Track successful field access if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( `${variableName}.fields`, 'field-access-success', true, current, undefined ); } return current; } /** * Resolve a variable reference in a field name * This allows for dynamic field access like obj[varName] */ private async resolveVariableInFieldName(fieldName: string, context: ResolutionContext): Promise { // Check if the field name contains variable references if (fieldName.includes('{{')) { // Resolve any variable references in the field name const resolvedName = await this.resolve(fieldName, context); // Try to convert to number if it looks like an array index const numIndex = parseInt(resolvedName, 10); if (!isNaN(numIndex)) { return numIndex; } return resolvedName; } // No variables to resolve return fieldName; } /** * Resolves a variable reference that contains another variable reference * For example: {{var_{{nested}}}} * @param reference The reference containing nested variables * @param context Resolution context * @returns Resolved variable value * @throws MeldResolutionError if resolution fails */ private async resolveNestedVariableReference(reference: string, context: ResolutionContext): Promise { try { // First try to use directly injected resolution service if (this.resolutionService) { try { return await this.resolutionService.resolveInContext(reference, context); } catch (error) { logger.debug('Error using injected resolutionService.resolveInContext, trying fallback', { error: error instanceof Error ? error.message : String(error), reference }); // If this fails and strict mode is on, rethrow if (context.strict) { throw error; } // Otherwise continue with fallback approaches } } // Ensure factory is initialized for client approach this.ensureFactoryInitialized(); // Try to use the resolution client if (this.resolutionClient) { try { if (this.resolutionClient.resolveInContext) { return await this.resolutionClient.resolveInContext(reference, context); } // Fallback to regular resolveVariables return await this.resolutionClient.resolveVariables(reference, context); } catch (error) { // Check if this is already a MeldResolutionError (like circular reference detection) if (error instanceof MeldResolutionError) { if (context.strict) { throw error; } } logger.debug('Error using resolutionClient.resolveInContext', { error: error instanceof Error ? error.message : String(error), reference }); // If strict mode is on, rethrow if (context.strict) { if (error instanceof Error) { throw error; } throw new Error(String(error)); } } } // If all else fails, try to resolve directly const nodes = await this.parseContent(reference); if (nodes.length === 1 && isVariableReferenceNode(nodes[0])) { const node = nodes[0] as any; // Cast to any to avoid type errors const varName = node.identifier; // Get variable value const value = await this.getVariable(varName, context); if (value === undefined) { if (context.strict) { throw VariableResolutionErrorFactory.variableNotFound(varName); } return ''; } // Handle fields access for both new and legacy node types if ((node.type === 'VariableReference' && node.fields && node.fields.length > 0) || (node.type === 'DataVar' && node.fields && node.fields.length > 0) || (node.type === 'TextVar' && node.fields && node.fields.length > 0)) { const fields = node.fields; try { const fieldValue = await this.accessFields(value, fields, context, varName); return this.convertToString(fieldValue); } catch (error) { if (context.strict) { throw error; } return ''; } } return this.convertToString(value); } // Not a variable reference return reference; } catch (error) { // Log the error for diagnostic purposes logger.error('Error in resolveNestedVariableReference', { reference, error: error instanceof Error ? error.message : String(error) }); // Track resolution error if tracking is enabled if (this.resolutionTracker) { this.resolutionTracker.trackResolutionAttempt( 'nested-variable-resolution-error', JSON.stringify({ reference, context: JSON.stringify(context) }), false, undefined, error instanceof Error ? error.message : String(error) ); } // Always propagate errors if strict mode is enabled if (context.strict) { throw error; } // In non-strict mode, return empty string for errors return ''; } } /** * Parse content using regex-based approach as a fallback * This is used when AST-based parsing fails */ private parseWithRegex(content: string): MeldNode[] { const result: MeldNode[] = []; // Simple implementation to avoid edge cases in the regex approach let remaining = content; let startIndex = remaining.indexOf('{{'); while (startIndex !== -1) { // Add text before the variable if (startIndex > 0) { result.push({ type: 'Text', content: remaining.substring(0, startIndex) } as TextNode); } const endIndex = remaining.indexOf('}}', startIndex); if (endIndex === -1) { // No closing braces - treat the rest as text result.push({ type: 'Text', content: remaining } as TextNode); break; } const varContent = remaining.substring(startIndex + 2, endIndex); // Check if the variable reference contains another variable reference if (varContent.includes('{{')) { // This is a nested reference - keep it as text for later processing result.push({ type: 'Text', content: remaining.substring(startIndex, endIndex + 2) } as TextNode); } else { // Parse as a regular variable reference const { baseName, fields } = this.parseVariableReference(varContent); const valueType = fields && fields.length > 0 ? 'data' : 'text'; // Create variable reference node using factory if available if (this.variableNodeFactory) { // Use factory pattern result.push(this.variableNodeFactory.createVariableReferenceNode(baseName, valueType, fields)); } else { // Legacy fallback - import is maintained at the top during transition logger.warn('VariableNodeFactory not available, using legacy function'); // We kept the legacy import as fallback const legacyCreateNode = require('@core/syntax/types/variables.js').createVariableReferenceNode; result.push(legacyCreateNode(baseName, valueType, fields)); } } // Move to the next position remaining = remaining.substring(endIndex + 2); startIndex = remaining.indexOf('{{'); } // Add any remaining text if (remaining.length > 0) { result.push({ type: 'Text', content: remaining } as TextNode); } return result; } /** * Determines if an array has complex structure that would benefit from pretty-printing as JSON * @param arr The array to check * @returns True if the array contains nested arrays, long items, or is a long array */ private hasComplexStructure(arr: any[]): boolean { // Arrays have complex structure if: // 1. They contain nested arrays const hasNestedArrays = arr.some(item => Array.isArray(item)); if (hasNestedArrays) { return true; } // 2. They are longer than 5 items if (arr.length > 5) { return true; } // 3. They contain long values return arr.some(item => { // Handle objects with multiple properties if (typeof item === 'object' && item !== null && !Array.isArray(item)) { return Object.keys(item).length > 1; } // Handle long string values if (typeof item === 'string' && item.length > 20) { return true; } return false; }); } } ``` #### ../../services/pipeline/ResolutionService/resolvers/types.ts ```javascript import type { MeldNode } from '@core/syntax/types.js'; /** * Represents a field access in a variable reference * Examples: object.field, array[0] */ export interface Field { type: 'field' | 'index'; value: string | number; } /** * Text node containing static content */ export interface TextNode extends MeldNode { type: 'Text'; value: string; content?: string; // Support legacy property name } /** * Base node type for variable references */ export interface VariableReferenceNode extends MeldNode { type: 'VariableReference'; identifier: string; fields?: Field[]; isVariableReference: boolean; } /** * Text variable reference (previously ${var}) */ export interface TextVarNode extends VariableReferenceNode { valueType?: 'text'; } /** * Data variable reference (previously #{data}) */ export interface DataVarNode extends VariableReferenceNode { valueType?: 'data'; } /** * Directive node (@directive) */ export interface DirectiveNode extends MeldNode { type: 'Directive'; directive: { kind: string; identifier: string; value?: string; [key: string]: any; }; } /** * Type guard for text nodes */ export function isTextNode(node: MeldNode): node is TextNode { return node.type === 'Text'; } /** * Type guard for variable reference nodes */ export function isVariableReferenceNode(node: MeldNode): node is VariableReferenceNode { return node.type === 'VariableReference'; } /** * Type guard for text variable nodes */ export function isTextVarNode(node: MeldNode): node is TextVarNode { return node.type === 'VariableReference' && (!('valueType' in node) || (node as any).valueType === 'text'); } /** * Type guard for data variable nodes */ export function isDataVarNode(node: MeldNode): node is DataVarNode { return node.type === 'VariableReference' && 'valueType' in node && (node as any).valueType === 'data'; } /** * Type guard for directive nodes */ export function isDirectiveNode(node: MeldNode): node is DirectiveNode { return node.type === 'Directive'; } ```' [debug] Template reference 'files.ContentResolutionCode' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../services/pipeline/ResolutionService/resolvers/ContentResolver.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ResolutionService/resolvers/ContentResolver.ts"} [debug] Reading relative file {"relativePath":"../../services/pipeline/ResolutionService/resolvers/StringLiteralHandler.ts","absolutePath":"/Users/adam/dev/meld/services/pipeline/ResolutionService/resolvers/StringLiteralHandler.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.ContentResolutionCode }}' to '#### ../../services/pipeline/ResolutionService/resolvers/ContentResolver.ts ```javascript import type { IStateService } from '@services/state/StateService/IStateService.js'; import { ResolutionContext } from '@services/pipeline/ResolutionService/IResolutionService.js'; import type { MeldNode, TextNode, CodeFenceNode, CommentNode } from '@core/syntax/types.js'; /** * Handles resolution of raw content (text, code blocks, comments) * Preserves original document formatting while skipping comments and directives */ export class ContentResolver { constructor(private stateService: IStateService) {} /** * Resolve content nodes, preserving original formatting but skipping comments and directives */ async resolve(nodes: MeldNode[], context: ResolutionContext): Promise { const resolvedParts: string[] = []; for (const node of nodes) { // Skip comments and directives if (node.type === 'Comment' || node.type === 'Directive') { continue; } switch (node.type) { case 'Text': // Regular text - output as is resolvedParts.push((node as TextNode).content); break; case 'CodeFence': // For code fences, directly use the content from the node // meld-ast handles all code fence formatting resolvedParts.push((node as CodeFenceNode).content); break; } } // Join parts without adding any additional whitespace return resolvedParts .filter(part => part !== undefined) .join(''); } } ``` #### ../../services/pipeline/ResolutionService/resolvers/StringLiteralHandler.ts ```javascript import { ResolutionError } from '@services/pipeline/ResolutionService/errors/ResolutionError.js'; import type { IParserService } from '@services/pipeline/ParserService/IParserService.js'; import type { MeldNode, TextNode } from '@core/syntax/types.js'; /** * Handles validation and parsing of string literals in text directives */ export class StringLiteralHandler { private readonly QUOTE_TYPES = ["'", '"', '`'] as const; private readonly MIN_CONTENT_LENGTH = 1; private silentMode: boolean = false; constructor(private parserService?: IParserService) {} /** * Enable silent mode to suppress warning messages (useful for tests) */ setSilentMode(silent: boolean): void { this.silentMode = silent; } /** * Checks if a value appears to be a string literal * This is a preliminary check before full validation */ async isStringLiteralWithAst(value: string): Promise { if (!this.parserService) { return this.isStringLiteral(value); } try { // Wrap the string in a directive to ensure proper parsing const wrappedValue = `@text test = ${value}`; // Parse with AST const nodes = await this.parserService.parse(wrappedValue); // Look for directive nodes const directiveNode = nodes.find(node => node.type === 'Directive' && (node as any).directive?.kind === 'text' ); if (directiveNode) { // In the test environment, the mock parser doesn't create a StringLiteral type // but just passes the value through, so we need to check both formats const directiveValue = (directiveNode as any).directive?.value; // Check if it's a StringLiteral node in the AST if (directiveValue && typeof directiveValue === 'object' && directiveValue.type === 'StringLiteral') { return true; } // Check if it's a string value that looks like a string literal if (typeof directiveValue === 'string') { return this.isStringLiteral(directiveValue); } } return false; } catch (error) { if (!this.silentMode) { console.error('Failed to check string literal with AST, falling back to manual check:', error); } return this.isStringLiteral(value); } } /** * Checks if a value appears to be a string literal * This is a preliminary check before full validation */ isStringLiteral(value: string): boolean { if (!value || value.length < 2) { return false; } const firstChar = value[0]; const lastChar = value[value.length - 1]; // Check for matching quotes if (!this.QUOTE_TYPES.includes(firstChar as any) || firstChar !== lastChar) { return false; } // Check for unclosed quotes let isEscaped = false; for (let i = 1; i < value.length - 1; i++) { if (value[i] === '\\') { isEscaped = !isEscaped; } else if (value[i] === firstChar && !isEscaped) { return false; // Found an unescaped quote in the middle } else { isEscaped = false; } } return true; } /** * Validates a string literal for proper quoting and content * @throws ResolutionError if the literal is invalid */ async validateLiteralWithAst(value: string): Promise { if (!this.parserService) { return this.validateLiteral(value); } try { // Wrap the string in a directive to ensure proper parsing const wrappedValue = `@text test = ${value}`; // Parse with AST const nodes = await this.parserService.parse(wrappedValue); // If parsing succeeds without errors, the literal is valid // Just check if it's actually a string literal node const directiveNode = nodes.find(node => node.type === 'Directive' && (node as any).directive?.kind === 'text' ); if (!directiveNode) { throw new ResolutionError( 'Failed to validate string literal with AST', { value } ); } const directiveValue = (directiveNode as any).directive?.value; // In the test environment, the mock parser doesn't create a StringLiteral type // but just passes the value through, so we need to check both formats if (directiveValue && typeof directiveValue === 'object' && directiveValue.type === 'StringLiteral') { // Valid string literal object return; } else if (typeof directiveValue === 'string') { // Validate the string value as a string literal return this.validateLiteral(directiveValue); } throw new ResolutionError( 'String literal is invalid', { value } ); } catch (error) { // If parsing fails, fall back to manual validation if (!this.silentMode) { console.error('Failed to validate string literal with AST, falling back to manual validation:', error); } return this.validateLiteral(value); } } /** * Validates a string literal for proper quoting and content * @throws ResolutionError if the literal is invalid */ validateLiteral(value: string): void { if (!value || value.length < 2) { throw new ResolutionError( 'String literal is empty or too short', { value } ); } const firstChar = value[0]; const lastChar = value[value.length - 1]; // Check if starts with a valid quote if (!this.QUOTE_TYPES.includes(firstChar as any)) { throw new ResolutionError( 'String literal must start with a quote (\', ", or `)', { value } ); } // Check if quotes match if (firstChar !== lastChar) { throw new ResolutionError( 'String literal has mismatched quotes', { value } ); } // Check for mixed quotes const otherQuotes = this.QUOTE_TYPES.filter(q => q !== firstChar); const content = value.slice(1, -1); for (const quote of otherQuotes) { if (content.includes(quote) && !this.isEscaped(content, quote)) { throw new ResolutionError( 'String literal contains unescaped mixed quotes', { value } ); } } // Check content length if (content.length < this.MIN_CONTENT_LENGTH) { throw new ResolutionError( 'String literal content is empty', { value } ); } // Check for newlines in single/double quoted strings if (firstChar !== '`' && content.includes('\n')) { throw new ResolutionError( 'Single and double quoted strings cannot contain newlines', { value } ); } } /** * Parses a string literal, removing quotes and handling escapes * @throws ResolutionError if the literal is invalid */ async parseLiteralWithAst(value: string): Promise { if (!this.parserService) { return this.parseLiteral(value); } try { // Validate first await this.validateLiteralWithAst(value); // Wrap the string in a directive to ensure proper parsing const wrappedValue = `@text test = ${value}`; // Parse with AST const nodes = await this.parserService.parse(wrappedValue); // Extract the string literal value const directiveNode = nodes.find(node => node.type === 'Directive' && (node as any).directive?.kind === 'text' ); if (directiveNode) { const directiveValue = (directiveNode as any).directive?.value; if (directiveValue && typeof directiveValue === 'object' && directiveValue.type === 'StringLiteral') { // The parser has already handled quote escaping return directiveValue.value; } else if (typeof directiveValue === 'string') { // Parse as string literal return this.parseLiteral(directiveValue); } } // Fall back to manual parsing return this.parseLiteral(value); } catch (error) { // If parsing fails, fall back to manual parsing if (!this.silentMode) { console.error('Failed to parse string literal with AST, falling back to manual parsing:', error); } return this.parseLiteral(value); } } /** * Parses a string literal, removing quotes and handling escapes * @throws ResolutionError if the literal is invalid */ parseLiteral(value: string): string { // First validate the literal this.validateLiteral(value); // Get the content between quotes const content = value.slice(1, -1); // Handle escaped quotes based on quote type const quoteType = value[0]; return this.unescapeQuotes(content, quoteType as typeof this.QUOTE_TYPES[number]); } /** * Checks if a character at a given position is escaped */ private isEscaped(str: string, char: string, pos?: number): boolean { if (pos === undefined) { // If no position given, check all occurrences let escaped = false; for (let i = 0; i < str.length; i++) { if (str[i] === char && !this.isEscaped(str, char, i)) { return false; } } return true; } // Count backslashes before the character let backslashCount = 0; let i = pos - 1; while (i >= 0 && str[i] === '\\') { backslashCount++; i--; } return backslashCount % 2 === 1; } /** * Unescapes quotes in the content based on quote type */ private unescapeQuotes(content: string, quoteType: typeof this.QUOTE_TYPES[number]): string { // Replace escaped quotes with actual quotes return content.replace( new RegExp(`\\\\${quoteType}`, 'g'), quoteType ); } } ```' [debug] Template reference 'files.StateCoreCode' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../services/state/StateService/StateService.ts","absolutePath":"/Users/adam/dev/meld/services/state/StateService/StateService.ts"} [debug] Reading relative file {"relativePath":"../../services/state/StateService/IStateService.ts","absolutePath":"/Users/adam/dev/meld/services/state/StateService/IStateService.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.StateCoreCode }}' to '#### ../../services/state/StateService/StateService.ts ```javascript import type { MeldNode, TextNode } from '@core/syntax/types/index.js'; import { stateLogger as logger } from '@core/utils/logger.js'; import type { IStateService, TransformationOptions } from '@services/state/StateService/IStateService.js'; import type { StateNode, CommandDefinition } from '@services/state/StateService/types.js'; import { StateFactory } from '@services/state/StateService/StateFactory.js'; import type { IStateEventService, StateEvent } from '@services/state/StateEventService/IStateEventService.js'; import type { IStateTrackingService } from '@tests/utils/debug/StateTrackingService/IStateTrackingService.js'; import { inject, container, injectable } from 'tsyringe'; import { Service } from '@core/ServiceProvider.js'; import { StateTrackingServiceClientFactory } from '@services/state/StateTrackingService/factories/StateTrackingServiceClientFactory.js'; import type { IStateTrackingServiceClient } from '@services/state/StateTrackingService/interfaces/IStateTrackingServiceClient.js'; import { randomUUID } from 'crypto'; // Helper function to get the container function getContainer() { return container; } /** * Service for managing state in Meld files * * Handles variables, imports, commands, nodes, and state transformations */ @injectable() @Service({ description: 'Service responsible for managing state in Meld files' }) export class StateService implements IStateService { // Initialize with default or it will be set in initialization methods private stateFactory: StateFactory = new StateFactory(); private currentState!: StateNode; private _isImmutable: boolean = false; private _transformationEnabled: boolean = false; private _transformationOptions: TransformationOptions = { variables: false, directives: false, commands: false, imports: false }; private eventService?: IStateEventService; private trackingService?: IStateTrackingService; // Factory pattern properties private trackingServiceClientFactory?: StateTrackingServiceClientFactory; private trackingClient?: IStateTrackingServiceClient; private factoryInitialized: boolean = false; /** * Creates a new StateService instance using dependency injection * * @param stateFactory - Factory for creating state nodes and managing state operations * @param eventService - Service for handling state events and notifications * @param trackingServiceClientFactory - Factory for creating tracking service clients * @param parentState - Optional parent state to inherit from (used for nested imports) */ constructor( @inject(StateFactory) stateFactory?: StateFactory, @inject('IStateEventService') eventService?: IStateEventService, @inject('StateTrackingServiceClientFactory') trackingServiceClientFactory?: StateTrackingServiceClientFactory, parentState?: IStateService ) { if (stateFactory) { this.stateFactory = stateFactory; this.eventService = eventService; // Initialize tracking client factory this.trackingServiceClientFactory = trackingServiceClientFactory; if (this.trackingServiceClientFactory) { this.factoryInitialized = true; this.initializeTrackingClient(); } this.initializeState(parentState); } else { // Fallback for non-DI initialization logger.warn('StateService initialized without factory in DI-only mode'); this.stateFactory = new StateFactory(); // Use proper type guards to determine service type if (eventService && this.isStateEventService(eventService)) { this.eventService = eventService; } // Initialize state with parent if provided const actualParentState = parentState || (eventService && this.isStateService(eventService) ? eventService : undefined); this.initializeState(actualParentState); } } /** * Lazily initialize the StateTrackingServiceClient factory * This is called only when needed to avoid circular dependencies */ private ensureFactoryInitialized(): void { if (this.factoryInitialized) { return; } this.factoryInitialized = true; try { this.trackingServiceClientFactory = container.resolve('StateTrackingServiceClientFactory'); this.initializeTrackingClient(); } catch (error) { // Factory not available, will use direct service logger.debug('StateTrackingServiceClientFactory not available, will use direct service if available'); } } /** * Initialize the StateTrackingServiceClient using the factory */ private initializeTrackingClient(): void { if (!this.trackingServiceClientFactory) { return; } try { this.trackingClient = this.trackingServiceClientFactory.createClient(); logger.debug('Successfully created StateTrackingServiceClient using factory'); } catch (error) { logger.warn('Failed to create StateTrackingServiceClient, will use direct service if available', { error }); this.trackingClient = undefined; } } /** * Initialize the service or re-initialize it * Can be used to reset the service to initial state * * @deprecated Use constructor injection instead. This method will be removed in a future version. * @param eventService - Optional event service to use * @param parentState - Optional parent state to inherit from */ initialize(eventService?: IStateEventService, parentState?: IStateService): void { logger.warn('StateService.initialize is deprecated. Use constructor injection instead.'); // For backward compatibility, if the eventService was provided, use it if (eventService && this.isStateEventService(eventService)) { this.eventService = eventService; } // Initialize state with parent state this.initializeState(parentState); } /** * Initialize the state, either as a fresh state or as a child of a parent state */ private initializeState(parentState?: IStateService): void { this.currentState = this.stateFactory.createState({ source: 'new', parentState: parentState ? (parentState as StateService).currentState : undefined }); // If parent has services, inherit them if (parentState) { const parent = parentState as StateService; // Inherit services if not already set if (!this.eventService && parent.eventService) { this.eventService = parent.eventService; } if (!this.trackingService && parent.trackingService) { this.trackingService = parent.trackingService; } } // Register state with tracking service if available // Ensure factory is initialized before trying to use it this.ensureFactoryInitialized(); const parentId = parentState ? (parentState as StateService).currentState.stateId : undefined; // Try to use the client from the factory first if (this.trackingClient) { try { // Register the state with the pre-generated ID this.trackingClient.registerState({ id: this.currentState.stateId, parentId, filePath: this.currentState.filePath, createdAt: Date.now(), transformationEnabled: this._transformationEnabled, source: 'child' }); // Explicitly register parent-child relationship if parent exists if (parentState && parentId) { this.trackingClient.registerRelationship({ sourceId: parentId, targetId: this.currentState.stateId, type: 'parent-child', timestamp: Date.now(), source: 'child' }); } return; // Successfully used the client, no need to try other methods } catch (error) { logger.warn('Error using trackingClient.registerState, will fall back to direct service if available', { error }); } } // Fall back to direct tracking service if available if (this.trackingService) { // Register the state with the pre-generated ID this.trackingService.registerState({ id: this.currentState.stateId, parentId, filePath: this.currentState.filePath, createdAt: Date.now(), transformationEnabled: this._transformationEnabled, source: 'child' }); // Explicitly register parent-child relationship if parent exists if (parentState && parentId) { this.trackingService.registerRelationship({ sourceId: parentId, targetId: this.currentState.stateId, type: 'parent-child', timestamp: Date.now(), source: 'child' }); } } } setEventService(eventService: IStateEventService): void { this.eventService = eventService; } private async emitEvent(event: StateEvent): Promise { if (this.eventService) { await this.eventService.emit(event); } } // Text variables getTextVar(name: string): string | undefined { return this.currentState.variables.text.get(name); } setTextVar(name: string, value: string): void { this.checkMutable(); const text = new Map(this.currentState.variables.text); text.set(name, value); this.updateState({ variables: { ...this.currentState.variables, text } }, `setTextVar:${name}`); } getAllTextVars(): Map { return new Map(this.currentState.variables.text); } getLocalTextVars(): Map { return new Map(this.currentState.variables.text); } // Data variables getDataVar(name: string): unknown { return this.currentState.variables.data.get(name); } setDataVar(name: string, value: unknown): void { this.checkMutable(); const data = new Map(this.currentState.variables.data); data.set(name, value); this.updateState({ variables: { ...this.currentState.variables, data } }, `setDataVar:${name}`); } getAllDataVars(): Map { return new Map(this.currentState.variables.data); } getLocalDataVars(): Map { return new Map(this.currentState.variables.data); } // Path variables getPathVar(name: string): string | undefined { return this.currentState.variables.path.get(name); } setPathVar(name: string, value: string): void { this.checkMutable(); const path = new Map(this.currentState.variables.path); path.set(name, value); this.updateState({ variables: { ...this.currentState.variables, path } }, `setPathVar:${name}`); } getAllPathVars(): Map { return new Map(this.currentState.variables.path); } // Commands getCommand(name: string): CommandDefinition | undefined { return this.currentState.commands.get(name); } setCommand(name: string, command: string | CommandDefinition): void { this.checkMutable(); const commands = new Map(this.currentState.commands); const commandDef = typeof command === 'string' ? { command } : command; commands.set(name, commandDef); this.updateState({ commands }, `setCommand:${name}`); } getAllCommands(): Map { return new Map(this.currentState.commands); } // Nodes getNodes(): MeldNode[] { return [...this.currentState.nodes]; } getTransformedNodes(): MeldNode[] { if (this._transformationEnabled) { return this.currentState.transformedNodes ? [...this.currentState.transformedNodes] : [...this.currentState.nodes]; } return [...this.currentState.nodes]; } setTransformedNodes(nodes: MeldNode[]): void { this.checkMutable(); this.updateState({ transformedNodes: nodes }, 'setTransformedNodes'); } addNode(node: MeldNode): void { this.checkMutable(); const nodes = [...this.currentState.nodes, node]; const transformedNodes = this._transformationEnabled ? (this.currentState.transformedNodes ? [...this.currentState.transformedNodes, node] : [...nodes]) : undefined; this.updateState({ nodes, transformedNodes }, 'addNode'); } transformNode(original: MeldNode, transformed: MeldNode): void { this.checkMutable(); if (!this._transformationEnabled) { return; } // Initialize transformed nodes if needed let transformedNodes = this.currentState.transformedNodes ? [...this.currentState.transformedNodes] : [...this.currentState.nodes]; // First try direct reference comparison let index = transformedNodes.findIndex(node => node === original); // If not found by reference, try matching by location if (index === -1 && original.location && transformed.location) { index = transformedNodes.findIndex(node => node.location?.start?.line === original.location?.start?.line && node.location?.start?.column === original.location?.start?.column && node.location?.end?.line === original.location?.end?.line && node.location?.end?.column === original.location?.end?.column ); } if (index !== -1) { // Replace the node at the found index transformedNodes[index] = transformed; } else { // If not found in transformed nodes, check original nodes const originalIndex = this.currentState.nodes.findIndex(node => { if (!node.location || !original.location) return false; return ( node.location.start.line === original.location.start.line && node.location.start.column === original.location.start.column && node.location.end.line === original.location.end.line && node.location.end.column === original.location.end.column ); }); if (originalIndex === -1) { throw new Error('Cannot transform node: original node not found'); } // Replace the node at the original index transformedNodes[originalIndex] = transformed; } this.updateState({ transformedNodes }, 'transformNode'); } isTransformationEnabled(): boolean { return this._transformationEnabled; } /** * Check if a specific transformation type is enabled * @param type The transformation type to check (variables, directives, commands, imports) * @returns Whether the specified transformation type is enabled */ shouldTransform(type: keyof TransformationOptions): boolean { return this._transformationEnabled && Boolean(this._transformationOptions[type]); } /** * Enable transformation with specific options * @param options Options for selective transformation, or true/false for all */ enableTransformation(options?: TransformationOptions | boolean): void { if (typeof options === 'boolean') { // Legacy behavior - all on or all off this._transformationEnabled = options; this._transformationOptions = options ? { variables: true, directives: true, commands: true, imports: true } : { variables: false, directives: false, commands: false, imports: false }; } else { // Selective transformation this._transformationEnabled = true; this._transformationOptions = { ...{ variables: true, directives: true, commands: true, imports: true }, ...options }; } if (this._transformationEnabled && !this.currentState.transformedNodes) { // Initialize transformed nodes with current nodes when enabling transformation this.updateState({ transformedNodes: [...this.currentState.nodes] }, 'enableTransformation'); } } /** * Get the current transformation options * @returns The current transformation options */ getTransformationOptions(): TransformationOptions { return { ...this._transformationOptions }; } appendContent(content: string): void { this.checkMutable(); // Create a text node and add it const textNode: TextNode = { type: 'Text', content, location: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } } }; this.addNode(textNode); } // Imports addImport(path: string): void { this.checkMutable(); const imports = new Set(this.currentState.imports); imports.add(path); this.updateState({ imports }, `addImport:${path}`); } removeImport(path: string): void { this.checkMutable(); const imports = new Set(this.currentState.imports); imports.delete(path); this.updateState({ imports }, `removeImport:${path}`); } hasImport(path: string): boolean { return this.currentState.imports.has(path); } getImports(): Set { return new Set(this.currentState.imports); } // File path getCurrentFilePath(): string | null { return this.currentState.filePath ?? null; } setCurrentFilePath(path: string): void { this.checkMutable(); this.updateState({ filePath: path }, 'setCurrentFilePath'); } // State management /** * In the immutable state model, any non-empty state is considered to have local changes. * This is a deliberate design choice - each state represents a complete snapshot, * so the entire state is considered "changed" from its creation. * * @returns Always returns true to indicate the state has changes */ hasLocalChanges(): boolean { return true; // In immutable model, any non-empty state has local changes } /** * Returns a list of changed elements in the state. In the immutable state model, * the entire state is considered changed from creation, so this always returns * ['state'] to indicate the complete state has changed. * * This is a deliberate design choice that aligns with the immutable state model * where each state is a complete snapshot. * * @returns Always returns ['state'] to indicate the entire state has changed */ getLocalChanges(): string[] { return ['state']; // In immutable model, the entire state is considered changed } setImmutable(): void { this._isImmutable = true; } get isImmutable(): boolean { return this._isImmutable; } /** * Creates a new child state that inherits from this state. * Used for import resolution to maintain variable scope. */ createChildState(): IStateService { this.checkMutable(); // Create a new StateService instance that inherits from this one // Use factory pattern consistently - pass the trackingServiceClientFactory instead of service const childState = new StateService( this.stateFactory, this.eventService, this.trackingServiceClientFactory ); // Transfer parent variables to child // Copy text variables this.getAllTextVars().forEach((value, key) => { childState.setTextVar(key, value); }); // Copy data variables this.getAllDataVars().forEach((value, key) => { childState.setDataVar(key, value); }); // Copy path variables this.getAllPathVars().forEach((value, key) => { childState.setPathVar(key, value); }); // Copy commands this.getAllCommands().forEach((command, name) => { childState.setCommand(name, command); }); // Copy import info this.getImports().forEach(importPath => { childState.addImport(importPath); }); // Copy current file path const filePath = this.getCurrentFilePath(); if (filePath) { childState.setCurrentFilePath(filePath); } // Set child state to transform if parent is transforming if (this._transformationEnabled) { childState.enableTransformation(this._transformationOptions); } // Track child state creation // Ensure factory is initialized before trying to use it this.ensureFactoryInitialized(); if (this.trackingClient) { try { // Register the parent-child relationship this.trackingClient.registerRelationship({ sourceId: this.currentState.stateId, targetId: (childState as StateService).currentState.stateId, type: 'parent-child', timestamp: Date.now(), source: 'parent' }); // Register a "created" event for the child state if (this.trackingClient.registerEvent) { this.trackingClient.registerEvent({ stateId: this.currentState.stateId, type: 'created-child', timestamp: Date.now(), details: { childId: (childState as StateService).currentState.stateId }, source: 'parent' }); } } catch (error) { logger.warn('Failed to register child state creation with tracking client', { error }); } } else if (this.trackingService) { // Fall back to direct service // Register the parent-child relationship try { this.trackingService.addRelationship( this.currentState.stateId, (childState as StateService).currentState.stateId, 'parent-child' ); } catch (error) { logger.warn('Failed to register parent-child relationship with tracking service', { error }); } } return childState; } mergeChildState(childState: IStateService): void { this.checkMutable(); const child = childState as StateService; this.currentState = this.stateFactory.mergeStates(this.currentState, child.currentState); // Add merge relationship if tracking enabled if (this.currentState.stateId && child.currentState.stateId) { // Ensure factory is initialized before trying to use it this.ensureFactoryInitialized(); // Try to use the client from the factory first if (this.trackingClient) { try { // Make sure parent-child relationship exists this.trackingClient.addRelationship( this.currentState.stateId, child.currentState.stateId, 'parent-child' ); // Add merge-source relationship this.trackingClient.addRelationship( this.currentState.stateId, child.currentState.stateId, 'merge-source' ); // Successfully used the client, proceed to emit event } catch (error) { logger.warn('Error using trackingClient in mergeChildState, falling back to direct service', { error }); // Fall back to direct tracking service if available if (this.trackingService) { // Make sure parent-child relationship exists this.trackingService.addRelationship( this.currentState.stateId, child.currentState.stateId, 'parent-child' ); // Add merge-source relationship this.trackingService.addRelationship( this.currentState.stateId, child.currentState.stateId, 'merge-source' ); } } } else if (this.trackingService) { // Fall back to direct tracking service if client not available // Make sure parent-child relationship exists this.trackingService.addRelationship( this.currentState.stateId, child.currentState.stateId, 'parent-child' ); // Add merge-source relationship this.trackingService.addRelationship( this.currentState.stateId, child.currentState.stateId, 'merge-source' ); } } // Emit merge event this.emitEvent({ type: 'merge', stateId: this.currentState.stateId || 'unknown', source: 'mergeChildState', timestamp: Date.now(), location: { file: this.getCurrentFilePath() || undefined } }); } /** * Creates a deep clone of this state service */ clone(): IStateService { // Create a new StateService with the same factory, eventService and trackingServiceFactory const cloned = new StateService( this.stateFactory, this.eventService, this.trackingServiceClientFactory ); // Use the factory to create a cloned state with all properties correctly initialized (cloned as StateService).currentState = this.stateFactory.createClonedState( this.currentState, { source: 'clone', filePath: this.currentState.filePath } ); // Copy transformation settings (cloned as StateService)._transformationEnabled = this._transformationEnabled; (cloned as StateService)._transformationOptions = { ...this._transformationOptions }; (cloned as StateService)._isImmutable = this._isImmutable; // Track cloning // Ensure factory is initialized before trying to use it this.ensureFactoryInitialized(); if (this.trackingClient) { try { // Register the clone-original relationship this.trackingClient.registerRelationship({ sourceId: this.currentState.stateId, targetId: (cloned as StateService).currentState.stateId, type: 'clone-original', timestamp: Date.now(), source: 'original' }); // Register a "cloned" event for the state if (this.trackingClient.registerEvent) { this.trackingClient.registerEvent({ stateId: this.currentState.stateId, type: 'cloned', timestamp: Date.now(), details: { cloneId: (cloned as StateService).currentState.stateId }, source: 'original' }); } } catch (error) { logger.warn('Failed to register clone with tracking client', { error }); } } else if (this.trackingService) { // Fall back to direct service try { // Register the clone-original relationship with type assertion since it's valid in the client interface this.trackingService.addRelationship( this.currentState.stateId, (cloned as StateService).currentState.stateId, 'parent-child' // Use 'parent-child' as fallback for direct service ); } catch (error) { logger.warn('Failed to register clone-original relationship with tracking service', { error }); } } return cloned; } private checkMutable(): void { if (this._isImmutable) { throw new Error('Cannot modify immutable state'); } } private updateState(updates: Partial, source: string): void { this.currentState = this.stateFactory.updateState(this.currentState, updates); // Emit transform event for state updates this.emitEvent({ type: 'transform', stateId: this.currentState.stateId || 'unknown', source, timestamp: Date.now(), location: { file: this.getCurrentFilePath() || undefined } }); } getStateId(): string | undefined { return this.currentState.stateId; } /** * Sets the state ID and establishes parent-child relationships for tracking */ setStateId(params: { parentId?: string, source: string }): void { // If no stateId exists yet, generate a new UUID const stateId = this.currentState.stateId || (randomUUID ? randomUUID() : crypto.randomUUID()); this.currentState.stateId = stateId; // Use type assertion to allow string assignment to the enum-like type this.currentState.source = params.source as any; // Register with tracking service if available // Ensure factory is initialized before trying to use it this.ensureFactoryInitialized(); // Try to use the client from the factory first if (this.trackingClient) { try { this.trackingClient.registerState({ id: stateId, source: params.source as any, // Type assertion to handle string vs enum-like type filePath: this.getCurrentFilePath() || undefined, transformationEnabled: this._transformationEnabled }); // Add parent-child relationship if parentId provided if (params.parentId) { this.trackingClient.addRelationship( params.parentId, stateId, 'parent-child' ); } return; // Successfully used the client, no need to try other methods } catch (error) { logger.warn('Error using trackingClient in setStateId, falling back to direct service', { error }); } } // Fall back to direct tracking service if available if (this.trackingService) { try { this.trackingService.registerState({ id: stateId, source: params.source as any, // Type assertion to handle string vs enum-like type filePath: this.getCurrentFilePath() || undefined, transformationEnabled: this._transformationEnabled }); // Add parent-child relationship if parentId provided if (params.parentId) { this.trackingService.addRelationship( params.parentId, stateId, 'parent-child' ); } } catch (error) { console.warn('Failed to register state ID with tracking service', { error, stateId }); } } } getCommandOutput(command: string): string | undefined { if (!this._transformationEnabled || !this.currentState.transformedNodes) { return undefined; } // Find the transformed node that matches this command const transformedNode = this.currentState.transformedNodes.find(node => { if (node.type !== 'Text') return false; return (node as TextNode).content === command; }); return transformedNode?.type === 'Text' ? (transformedNode as TextNode).content : undefined; } hasTransformationSupport(): boolean { return true; } /** * Reset the state service to initial state * Used primarily for testing */ reset(): void { // Reset to a fresh state this.initializeState(); // Reset flags this._isImmutable = false; this._transformationEnabled = false; this._transformationOptions = { variables: false, directives: false, commands: false, imports: false }; } /** * Type guard to check if a service is an IStateEventService * @param service The service to check * @returns True if the service is an IStateEventService */ private isStateEventService(service: unknown): service is IStateEventService { return ( typeof service === 'object' && service !== null && 'on' in service && 'off' in service && 'emit' in service && !('createChildState' in service) ); } /** * Type guard to check if a service is an IStateService * @param service The service to check * @returns True if the service is an IStateService */ private isStateService(service: unknown): service is IStateService { return ( typeof service === 'object' && service !== null && 'createChildState' in service && 'getTextVar' in service && 'setTextVar' in service ); } // Add back the setTrackingService method setTrackingService(trackingService: IStateTrackingService): void { this.trackingService = trackingService; // Register existing state if not already registered if (this.currentState.stateId) { // Ensure factory is initialized before trying to use it this.ensureFactoryInitialized(); // Try to use the client from the factory first if (this.trackingClient) { try { this.trackingClient.registerState({ id: this.currentState.stateId, source: this.currentState.source || 'new', // Use original source or default to 'new' filePath: this.getCurrentFilePath() || undefined, transformationEnabled: this._transformationEnabled, createdAt: Date.now() }); return; // Successfully used the client, no need to try other methods } catch (error) { logger.warn('Error using trackingClient in setTrackingService, will fall back to direct service', { error }); } } // Fall back to direct tracking service try { this.trackingService.registerState({ id: this.currentState.stateId, source: this.currentState.source || 'new', // Use original source or default to 'new' filePath: this.getCurrentFilePath() || undefined, transformationEnabled: this._transformationEnabled, createdAt: Date.now() }); } catch (error) { logger.warn('Failed to register existing state with tracking service', { error, stateId: this.currentState.stateId }); } } } /** * Checks if a variable with the given name and type exists * * @param type - The type of variable ('text', 'data', or 'path') * @param name - The name of the variable to check * @returns true if the variable exists, false otherwise */ hasVariable(type: string, name: string): boolean { switch (type.toLowerCase()) { case 'text': return this.getTextVar(name) !== undefined; case 'data': return this.getDataVar(name) !== undefined; case 'path': return this.getPathVar(name) !== undefined; default: return false; } } } ``` #### ../../services/state/StateService/IStateService.ts ```javascript import type { MeldNode } from '@core/syntax/types/index.js'; import type { StateServiceBase, StateTransformationOptions } from '@core/shared/types.js'; import type { IStateEventService } from '@services/state/StateEventService/IStateEventService.js'; import type { IStateTrackingService } from '@tests/utils/debug/StateTrackingService/IStateTrackingService.js'; /** * Options for selective transformation */ interface TransformationOptions extends StateTransformationOptions {} /** * Service responsible for managing state in Meld documents. * Acts as a central store for variables, commands, and document nodes. * Manages state hierarchy, transformation, and immutability controls. * * @remarks * StateService is a core service that maintains all state information during * Meld document processing. It handles variable storage, command registration, * node tracking, transformation state, and parent-child relationships. * * Dependencies: * - IStateEventService: For state change event notifications * - IStateTrackingService: For debugging and tracking state operations */ interface IStateService extends StateServiceBase { /** * Sets the event service for state change notifications. * * @param eventService - The event service to use */ setEventService(eventService: IStateEventService): void; /** * Sets the tracking service for state debugging and analysis. * * @param trackingService - The tracking service to use */ setTrackingService(trackingService: IStateTrackingService): void; /** * Gets the unique identifier for this state instance. * * @returns The state ID, if assigned, or undefined */ getStateId(): string | undefined; /** * Gets a text variable by name. * * @param name - The name of the variable to retrieve * @returns The variable value, or undefined if not found */ getTextVar(name: string): string | undefined; /** * Sets a text variable. * * @param name - The name of the variable to set * @param value - The value to assign to the variable * @throws {MeldStateError} If the state is immutable */ setTextVar(name: string, value: string): void; /** * Gets all text variables, including inherited ones from parent states. * * @returns A map of all text variables */ getAllTextVars(): Map; /** * Gets only locally defined text variables (not inherited from parent states). * * @returns A map of local text variables */ getLocalTextVars(): Map; /** * Gets a data variable by name. * * @param name - The name of the variable to retrieve * @returns The variable value, or undefined if not found */ getDataVar(name: string): unknown; /** * Sets a data variable. * * @param name - The name of the variable to set * @param value - The value to assign to the variable * @throws {MeldStateError} If the state is immutable */ setDataVar(name: string, value: unknown): void; /** * Gets all data variables, including inherited ones from parent states. * * @returns A map of all data variables */ getAllDataVars(): Map; /** * Gets only locally defined data variables (not inherited from parent states). * * @returns A map of local data variables */ getLocalDataVars(): Map; /** * Gets a path variable by name. * * @param name - The name of the variable to retrieve * @returns The variable value, or undefined if not found */ getPathVar(name: string): string | undefined; /** * Sets a path variable. * * @param name - The name of the variable to set * @param value - The value to assign to the variable * @throws {MeldStateError} If the state is immutable */ setPathVar(name: string, value: string): void; /** * Gets all path variables, including inherited ones from parent states. * * @returns A map of all path variables */ getAllPathVars(): Map; /** * Gets a command by name. * * @param name - The name of the command to retrieve * @returns The command details, or undefined if not found */ getCommand(name: string): { command: string; options?: Record } | undefined; /** * Sets a command with optional options. * * @param name - The name of the command to set * @param command - The command string or command object with options * @throws {MeldStateError} If the state is immutable */ setCommand(name: string, command: string | { command: string; options?: Record }): void; /** * Gets all commands, including inherited ones from parent states. * * @returns A map of all commands */ getAllCommands(): Map }>; /** * Gets all original document nodes in order. * * @returns An array of document nodes */ getNodes(): MeldNode[]; /** * Adds a node to the document. * * @param node - The node to add * @throws {MeldStateError} If the state is immutable */ addNode(node: MeldNode): void; /** * Appends raw content to the document. * * @param content - The content to append * @throws {MeldStateError} If the state is immutable */ appendContent(content: string): void; /** * Gets transformed nodes for output generation. * * @returns An array of transformed nodes */ getTransformedNodes(): MeldNode[]; /** * Sets the complete array of transformed nodes. * * @param nodes - The transformed nodes to set * @throws {MeldStateError} If the state is immutable */ setTransformedNodes(nodes: MeldNode[]): void; /** * Records a transformation relationship between nodes. * * @param original - The original node * @param transformed - The transformed node * @throws {MeldStateError} If the state is immutable */ transformNode(original: MeldNode, transformed: MeldNode): void; /** * Checks if transformation is enabled. * * @returns Always returns true as transformation is now always enabled * @deprecated Maintained for backward compatibility; transformation is always enabled */ isTransformationEnabled(): boolean; /** * Enables transformation with optional settings. * * @param options - Transformation options or boolean to enable/disable all * @deprecated Maintained for backward compatibility; transformation is always enabled */ enableTransformation(options?: TransformationOptions | boolean): void; /** * Checks if a specific transformation type is enabled. * * @param type - The transformation type to check * @returns Always returns true as all transformation types are now enabled * @deprecated Maintained for backward compatibility; all transformations are always enabled */ shouldTransform(type: keyof TransformationOptions): boolean; /** * Gets the current transformation options. * * @returns Current transformation options (all enabled) * @deprecated Maintained for backward compatibility; all transformations are always enabled */ getTransformationOptions(): TransformationOptions; /** * Gets the output of a previously executed command. * * @param command - The command to get output for * @returns The command output, or undefined if not found */ getCommandOutput(command: string): string | undefined; /** * Checks if the state implementation supports transformation. * * @returns true if transformation is supported, false otherwise */ hasTransformationSupport(): boolean; /** * Registers an imported file path. * * @param path - The path of the imported file * @throws {MeldStateError} If the state is immutable */ addImport(path: string): void; /** * Removes an imported file path. * * @param path - The path of the imported file to remove * @throws {MeldStateError} If the state is immutable */ removeImport(path: string): void; /** * Checks if a file has been imported. * * @param path - The path to check * @returns true if the file has been imported, false otherwise */ hasImport(path: string): boolean; /** * Gets all imported file paths. * * @returns A set of all imported file paths */ getImports(): Set; /** * Gets the path of the current file being processed. * * @returns The current file path, or null if not set */ getCurrentFilePath(): string | null; /** * Sets the path of the current file being processed. * * @param path - The current file path */ setCurrentFilePath(path: string): void; /** * Checks if the state has local changes that haven't been merged. * * @returns true if there are local changes, false otherwise */ hasLocalChanges(): boolean; /** * Gets a list of local changes. * * @returns An array of change descriptions */ getLocalChanges(): string[]; /** * Makes the state immutable, preventing further changes. */ setImmutable(): void; /** * Whether the state is immutable. */ readonly isImmutable: boolean; /** * Creates a child state that inherits from this state. * * @returns A new child state */ createChildState(): IStateService; /** * Merges changes from a child state into this state. * * @param childState - The child state to merge * @throws {MeldStateError} If the state is immutable or the child state is invalid */ mergeChildState(childState: IStateService): void; /** * Creates a deep clone of this state. * * @returns A new state with the same values */ clone(): IStateService; } export type { TransformationOptions, IStateService }; ```' [debug] Template reference 'files.FileSystemCoreCode' requesting content for 2 files... [debug] Reading relative file {"relativePath":"../../services/fs/FileSystemService/FileSystemService.ts","absolutePath":"/Users/adam/dev/meld/services/fs/FileSystemService/FileSystemService.ts"} [debug] Reading relative file {"relativePath":"../../services/fs/FileSystemService/IFileSystemService.ts","absolutePath":"/Users/adam/dev/meld/services/fs/FileSystemService/IFileSystemService.ts"} [debug] Interpolated object key 'codeContent' from '{{ files.FileSystemCoreCode }}' to '#### ../../services/fs/FileSystemService/FileSystemService.ts ```javascript import * as fsExtra from 'fs-extra'; import { filesystemLogger as logger } from '@core/utils/logger.js'; import type { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js'; import type { IPathOperationsService } from '@services/fs/FileSystemService/IPathOperationsService.js'; import type { IFileSystem } from '@services/fs/FileSystemService/IFileSystem.js'; import { NodeFileSystem } from '@services/fs/FileSystemService/NodeFileSystem.js'; import { MeldError } from '@core/errors/MeldError.js'; import { MeldFileNotFoundError } from '@core/errors/MeldFileNotFoundError.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import { MeldFileSystemError } from '@core/errors/MeldFileSystemError.js'; import { injectable, inject } from 'tsyringe'; import { Service } from '@core/ServiceProvider.js'; import type { IPathService } from '@services/fs/PathService/IPathService.js'; import type { IPathServiceClient } from '@services/fs/PathService/interfaces/IPathServiceClient.js'; import { PathServiceClientFactory } from '@services/fs/PathService/factories/PathServiceClientFactory.js'; const execAsync = promisify(exec); interface FileOperationContext { operation: string; path: string; details?: Record; [key: string]: unknown; } @injectable() @Service({ description: 'Service for file system operations' }) export class FileSystemService implements IFileSystemService { private fs: IFileSystem; private pathClient?: IPathServiceClient; private factoryInitialized: boolean = false; /** * Creates a new instance of the FileSystemService * * @param pathOps - Service for handling path operations and normalization * @param fileSystem - File system implementation to use (optional, defaults to NodeFileSystem) * @param pathClientFactory - Factory for creating PathServiceClient instances */ constructor( @inject('IPathOperationsService') private readonly pathOps: IPathOperationsService, @inject('IFileSystem') fileSystem?: IFileSystem, @inject('PathServiceClientFactory') private readonly pathClientFactory?: PathServiceClientFactory ) { // Set file system implementation this.fs = fileSystem || new NodeFileSystem(); // Initialize factory if available - REMOVED to avoid circular dependency // this.ensureFactoryInitialized(); if (process.env.DEBUG === 'true') { console.log('FileSystemService: Initialized with', { hasPathOps: !!this.pathOps, hasPathClientFactory: !!this.pathClientFactory, hasPathClient: !!this.pathClient, fileSystemType: this.fs.constructor.name }); } } /** * Lazily initialize the PathServiceClient factory * This is called only when needed to avoid circular dependencies */ private ensureFactoryInitialized(): void { if (this.factoryInitialized) { return; } this.factoryInitialized = true; // Use factory if available if (this.pathClientFactory && typeof this.pathClientFactory.createClient === 'function') { try { this.pathClient = this.pathClientFactory.createClient(); logger.debug('Successfully created PathServiceClient using factory'); } catch (error) { logger.warn('Failed to create PathServiceClient', { error }); // For test environments, don't throw to allow tests to work if (process.env.NODE_ENV !== 'test') { throw new MeldError('Failed to create PathServiceClient - factory pattern required', { cause: error as Error }); } } } else { logger.warn('PathServiceClientFactory not available or invalid - factory pattern required'); // For test environments, don't throw to allow tests to work if (process.env.NODE_ENV !== 'test') { throw new MeldError('PathServiceClientFactory not available - factory pattern required'); } } } /** * Sets the file system implementation * @deprecated This method is deprecated and will be removed in a future version. * Use dependency injection instead by registering the file system implementation with the DI container. * @param fileSystem - The file system implementation to use */ setFileSystem(fileSystem: IFileSystem): void { logger.warn('FileSystemService.setFileSystem is deprecated. Use dependency injection instead.'); this.fs = fileSystem; } /** * Gets the current file system implementation * @returns The current file system implementation */ getFileSystem(): IFileSystem { return this.fs; } /** * Resolves a path to an absolute path * * @param filePath - Path to resolve * @returns The resolved absolute path */ resolvePath(filePath: string): string { try { // Use the path client if available if (this.pathClient) { try { return this.pathClient.resolvePath(filePath); } catch (error) { logger.warn('Error using pathClient.resolvePath, falling back to pathOps', { error: error instanceof Error ? error.message : String(error), filePath }); } } // Fall back to path operations service return this.pathOps.resolvePath(filePath); } catch (error) { logger.warn('Error resolving path', { path: filePath, error: error instanceof Error ? error.message : String(error) }); // Last resort fallback return filePath; } } // File operations async readFile(filePath: string): Promise { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'readFile', path: filePath, resolvedPath }; try { logger.debug('Reading file', context); // Use the correct signature with just the path const content = await this.fs.readFile(resolvedPath); logger.debug('Successfully read file', { ...context, contentLength: content.length }); return content; } catch (error) { const err = error as Error; if (err.message.includes('ENOENT')) { logger.error('File not found', { ...context, error: err }); throw new MeldFileNotFoundError(filePath, { cause: err }); } logger.error('Error reading file', { ...context, error: err }); throw new MeldFileSystemError(`Error reading file: ${filePath}`, { cause: err, // Don't include resolvedPath since it's not in the error options type filePath }); } } async writeFile(filePath: string, content: string): Promise { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'writeFile', path: filePath, resolvedPath, details: { contentLength: content.length } }; try { logger.debug('Writing file', context); await this.ensureDir(this.pathOps.dirname(resolvedPath)); await this.fs.writeFile(resolvedPath, content); logger.debug('Successfully wrote file', context); } catch (error) { const err = error as Error; logger.error('Failed to write file', { ...context, error: err }); throw new MeldError(`Failed to write file: ${filePath}`, { cause: err, filePath }); } } async exists(filePath: string): Promise { try { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'exists', path: resolvedPath }; logger.debug('Checking if path exists', context); return await this.fs.exists(resolvedPath); } catch (error) { logger.warn('Error checking if path exists', { path: filePath, error: error instanceof Error ? error.message : String(error) }); return false; } } /** * Checks if a file exists (combines exists and isFile checks) * * @param filePath - Path to check * @returns A promise that resolves with true if the path exists and is a file, false otherwise */ async fileExists(filePath: string): Promise { try { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'fileExists', path: resolvedPath }; logger.debug('Checking if file exists', context); const exists = await this.exists(resolvedPath); if (!exists) { return false; } return await this.isFile(resolvedPath); } catch (error) { logger.warn('Error checking if file exists', { path: filePath, error: error instanceof Error ? error.message : String(error) }); return false; } } async stat(filePath: string): Promise { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'stat', path: filePath, resolvedPath }; try { logger.debug('Getting file stats', context); const stats = await this.fs.stat(resolvedPath); logger.debug('Successfully got file stats', { ...context, isDirectory: stats.isDirectory() }); return stats; } catch (error) { const err = error as Error; logger.error('Failed to get file stats', { ...context, error: err }); throw new MeldError(`Failed to get file stats: ${filePath}`, { cause: err, filePath }); } } // Directory operations async readDir(dirPath: string): Promise { const resolvedPath = this.resolvePath(dirPath); const context: FileOperationContext = { operation: 'readDir', path: dirPath, resolvedPath }; try { logger.debug('Reading directory', context); const files = await this.fs.readDir(resolvedPath); logger.debug('Successfully read directory', { ...context, fileCount: files.length }); return files; } catch (error) { const err = error as Error; logger.error('Failed to read directory', { ...context, error: err }); throw new MeldError(`Failed to read directory: ${dirPath}`, { cause: err, filePath: dirPath }); } } async ensureDir(dirPath: string): Promise { const resolvedPath = this.resolvePath(dirPath); const context: FileOperationContext = { operation: 'ensureDir', path: dirPath, resolvedPath }; try { logger.debug('Ensuring directory exists', context); await this.fs.mkdir(resolvedPath); logger.debug('Successfully ensured directory exists', context); } catch (error) { const err = error as Error; logger.error('Failed to ensure directory exists', { ...context, error: err }); throw new MeldError(`Failed to ensure directory exists: ${dirPath}`, { cause: err, filePath: dirPath }); } } async isDirectory(filePath: string): Promise { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'isDirectory', path: filePath, resolvedPath }; try { logger.debug('Checking if path is directory', context); const isDir = await this.fs.isDirectory(resolvedPath); logger.debug('Path directory check complete', { ...context, isDirectory: isDir }); return isDir; } catch (error) { const err = error as Error; logger.error('Failed to check if path is directory', { ...context, error: err }); throw new MeldError(`Failed to check if path is directory: ${filePath}`, { cause: err, filePath }); } } async isFile(filePath: string): Promise { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'isFile', path: filePath, resolvedPath }; try { logger.debug('Checking if path is file', context); const isFile = await this.fs.isFile(resolvedPath); logger.debug('Path file check complete', { ...context, isFile }); return isFile; } catch (error) { const err = error as Error; logger.error('Failed to check if path is file', { ...context, error: err }); throw new MeldError(`Failed to check if path is file: ${filePath}`, { cause: err, filePath }); } } getCwd(): string { return process.cwd(); } // Add dirname method that delegates to PathOperationsService dirname(filePath: string): string { return this.pathOps.dirname(filePath); } watch(path: string, options?: { recursive?: boolean }): AsyncIterableIterator<{ filename: string; eventType: string }> { const resolvedPath = this.resolvePath(path); const context: FileOperationContext = { operation: 'watch', path, resolvedPath, details: { options } }; try { logger.debug('Starting file watch', context); return this.fs.watch(resolvedPath, options); } catch (error) { const err = error as Error; logger.error('Failed to watch file', { ...context, error: err }); throw new MeldError(`Failed to watch file: ${path}`, { cause: err, filePath: path }); } } async executeCommand(command: string, options?: { cwd?: string }): Promise<{ stdout: string; stderr: string }> { // We don't need to resolve paths for command execution const context = { operation: 'executeCommand', command, cwd: options?.cwd }; try { logger.debug('Executing command', context); const { stdout, stderr } = await this.fs.executeCommand(command, options); logger.debug('Command executed successfully', { ...context, stdout, stderr }); return { stdout, stderr }; } catch (error) { const err = error as Error; logger.error('Failed to execute command', { ...context, error: err }); throw new MeldFileSystemError(`Failed to execute command: ${command}`, { cause: err, command }); } } /** * Creates a directory and any necessary parent directories. * * @deprecated Use `ensureDir` instead. This method will be removed in a future version. * @param dirPath - Path to the directory to create * @param options - Options for directory creation * @param options.recursive - Whether to create parent directories if they don't exist * @returns A promise that resolves when the directory is created * @throws {MeldFileSystemError} If the directory cannot be created */ async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise { logger.warn('FileSystemService.mkdir is deprecated. Use ensureDir instead.'); return this.ensureDir(dirPath); } } ``` #### ../../services/fs/FileSystemService/IFileSystemService.ts ```javascript import type { Stats } from 'fs-extra'; import type { IFileSystem } from '@services/fs/FileSystemService/IFileSystem.js'; import { FileSystemBase } from '@core/shared/types.js'; /** * Service responsible for file system operations. * Provides methods for reading, writing, and manipulating files and directories. * Abstracts underlying file system implementation to support both real and test environments. * * @remarks * This is a high-level service interface that should be used by application code. * It provides validation, path resolution, and error handling on top of the basic * filesystem operations provided by IFileSystem. * * Dependencies: * - IFileSystem: For low-level filesystem operations * - IPathService: For path validation and resolution */ interface IFileSystemService extends FileSystemBase { /** * Reads the content of a file as a string. * * @param filePath - Path to the file to read * @returns A promise that resolves with the file content as a string * @throws {MeldFileSystemError} If the file cannot be read or does not exist * * @example * ```ts * const content = await fileSystemService.readFile('/path/to/file.txt'); * console.log(content); * ``` */ readFile(filePath: string): Promise; /** * Writes content to a file. * Creates the file if it doesn't exist, and overwrites it if it does. * * @param filePath - Path to the file to write * @param content - Content to write to the file * @returns A promise that resolves when the write operation is complete * @throws {MeldFileSystemError} If the file cannot be written * * @example * ```ts * await fileSystemService.writeFile('/path/to/file.txt', 'Hello, world!'); * ``` */ writeFile(filePath: string, content: string): Promise; /** * Checks if a file or directory exists. * * @param filePath - Path to check * @returns A promise that resolves with true if the path exists, false otherwise */ exists(filePath: string): Promise; /** * Gets information about a file or directory. * * @param filePath - Path to get information about * @returns A promise that resolves with a Stats object containing file information * @throws {MeldFileSystemError} If the path cannot be accessed */ stat(filePath: string): Promise; /** * Checks if a path points to a file. * * @param filePath - Path to check * @returns A promise that resolves with true if the path is a file, false otherwise */ isFile(filePath: string): Promise; /** * Lists the contents of a directory. * * @param dirPath - Path to the directory to read * @returns A promise that resolves with an array of filenames in the directory * @throws {MeldFileSystemError} If the directory cannot be read or does not exist */ readDir(dirPath: string): Promise; /** * Creates a directory and any necessary parent directories. * * @param dirPath - Path to the directory to create * @returns A promise that resolves when the directory is created * @throws {MeldFileSystemError} If the directory cannot be created */ ensureDir(dirPath: string): Promise; /** * Checks if a path points to a directory. * * @param filePath - Path to check * @returns A promise that resolves with true if the path is a directory, false otherwise */ isDirectory(filePath: string): Promise; /** * Watches a file or directory for changes. * * @param path - Path to watch * @param options - Watch options * @param options.recursive - Whether to watch subdirectories recursively * @returns An async iterator that yields file change events * * @example * ```ts * const watcher = fileSystemService.watch('/path/to/dir', { recursive: true }); * for await (const event of watcher) { * console.log(`${event.filename} ${event.eventType}`); * } * ``` */ watch(path: string, options?: { recursive?: boolean }): AsyncIterableIterator<{ filename: string; eventType: string }>; /** * Gets the current working directory. * * @returns The current working directory path */ getCwd(): string; /** * Gets the directory name of a path. * * @param filePath - Path to get the directory name from * @returns The directory part of the path */ dirname(filePath: string): string; /** * Executes a shell command. * * @param command - Command to execute * @param options - Command options * @param options.cwd - Working directory for the command * @returns A promise that resolves with the command output * @throws {MeldCommandError} If the command fails * * @example * ```ts * const result = await fileSystemService.executeCommand('ls -la', { cwd: '/path/to/dir' }); * console.log(result.stdout); * ``` */ executeCommand(command: string, options?: { cwd?: string }): Promise<{ stdout: string; stderr: string }>; /** * Sets the file system implementation to use. * This is primarily used for testing to inject a mock filesystem. * * @param fileSystem - The filesystem implementation to use */ setFileSystem(fileSystem: IFileSystem): void; /** * Gets the current file system implementation. * * @returns The current filesystem implementation */ getFileSystem(): IFileSystem; /** * Creates a directory and any necessary parent directories. * * @deprecated Use `ensureDir` instead. This method will be removed in a future version. * @param dirPath - Path to the directory to create * @param options - Options for directory creation * @param options.recursive - Whether to create parent directories if they don't exist * @returns A promise that resolves when the directory is created * @throws {MeldFileSystemError} If the directory cannot be created */ mkdir(dirPath: string, options?: { recursive?: boolean }): Promise; } export type { IFileSystemService }; ```' [info] Finished interpolating workflow definitions. [debug] Initial context AFTER interpolation {"contextKeys":["overallArchitecture","directiveName","directiveClarityContent","workflow"],"globalVariableKeys":["overallArchitecture","directiveName","directiveClarityContent"],"iterableObjectKeys":["services"]} [info] Executing 5 sets sequentially... [info] Starting set: gather-requirements [debug] File read: /Users/adam/dev/meld/_cmte/define/sets/gather-requirements.set.yaml [info] Processing for_each target: services for set gather-requirements [debug] Resolved for_each target 'services' from workflow iterable_objects. [info] Iterating over object target 'services' (converted to key-value pairs). [info] Iterating over 9 items for set gather-requirements [debug] SetExecutor initialized {"baseSetName":"gather-requirements","dryRun":false} [debug] Executing iteration 0 with key: CoreDirective {"baseSetName":"gather-requirements"} [debug] Executing set logic: gather-requirements {"setName":"gather-requirements[CoreDirective]"} [debug] Executing task: analyze-service-needs-for-directive {"setName":"gather-requirements[CoreDirective]"} [debug] File read: /Users/adam/dev/meld/_cmte/define/tasks/analyze-service-needs-for-directive.md [debug] Loaded and cached task from markdown: analyze-service-needs-for-directive {"path":"/Users/adam/dev/meld/_cmte/define/tasks/analyze-service-needs-for-directive.md"} [debug] Successfully resolved 'item.key' as context variable. [debug] Successfully resolved 'directiveName' as context variable. [debug] Successfully resolved 'item.key' as context variable. [debug] Successfully resolved 'directiveName' as context variable. [debug] Successfully resolved 'directiveName' as context variable. [debug] Successfully resolved 'overallArchitecture' as context variable. [debug] Successfully resolved 'directiveName' as context variable. [debug] Successfully resolved 'directiveClarityContent' as context variable. [debug] Successfully resolved 'item.key' as context variable. [debug] Successfully resolved 'item.value.codeContent' as context variable. [debug] Successfully resolved 'directiveName' as context variable. [debug] Successfully resolved 'item.value.codeContent' as context variable. [debug] Successfully resolved 'directiveName' as context variable. [debug] Successfully resolved 'directiveName' as context variable. [debug] Successfully resolved 'item.key' as context variable. [debug] Successfully converted Markdown prompt to LLMXML {"taskName":"analyze-service-needs-for-directive","setName":"gather-requirements[CoreDirective]"} [debug] Sending message to Claude model: claude-3-7-sonnet-latest