meld
Version:
Meld: A template language for LLM prompts
636 lines (541 loc) • 31.2 kB
Markdown
Below is a services-based architecture that leverages core Meld libraries (meld-ast, llmxml, meld-spec) and follows SOLID principles. This design ensures compatibility with the Meld ecosystem while maintaining clean separation of concerns.
─────────────────────────────────────────────────────────────────────────
CORE LIBRARIES & TYPES
─────────────────────────────────────────────────────────────────────────
1. meld-spec (Type Definitions)
• Core Node Types:
- MeldNode - Base interface for all AST nodes
- DirectiveNode - AST node for directives
- TextNode - AST node for text content
- CodeFenceNode - AST node for code fences
• Variable Types:
- TextVariable - For @text directives and ${var} interpolation
- DataVariable - For @data directives and #{data.field} interpolation
- PathVariable - For @path directives and $path references
• Command Types:
- CommandDefinition - For @define directives
- CommandMetadata - Command metadata and risk levels
• Validation Types:
- ValidationError - For structured error reporting
- ValidationContext - For validation state
- ValidationResult - For validation outcomes
2. meld-ast (Basic Parsing)
• Provides parse() function for ONLY:
- Converting raw text into basic AST nodes
- Identifying directives, text blocks, and code fences
- Tracking source locations for error reporting
• Does NOT handle:
- Variable interpolation (${var}, #{data}, $path)
- Command resolution ($command(args))
- Any kind of value resolution
• Used only in ParserService for initial AST generation
• Produces raw AST nodes that need further processing
3. llmxml (XML Conversion)
• Handles bidirectional conversion between Markdown and LLM-XML (llm-friendly pseudo-xml)
• Used in OutputService for final formatting
• Handles markdown section extraction with fuzzy matching
• Provides configurable warning system for ambiguous matches
• Includes typed error handling for various failure conditions
─────────────────────────────────────────────────────────────────────────
OVERVIEW & KEY GOALS
─────────────────────────────────────────────────────────────────────────
1. Leverage Core Libraries
• Use meld-spec for ALL type definitions
• Use meld-ast ONLY for parsing text to AST
• Use llmxml for XML/section extraction
• Never reimplement functionality from core libraries
2. Isolate Complex Features
• Each service has a single responsibility
• Services communicate through well-defined interfaces
• Complex operations are delegated to appropriate libraries
3. Clean Directive Logic
• Each directive handler is focused and testable
• Handlers use services for complex operations
• No direct file I/O or parsing in handlers
4. Future-Proof Design
• Easy to add new directives
• Easy to extend existing services
• Clean integration with core libraries
5. Maintainability First
• Clear separation of concerns
• Comprehensive test coverage
• Consistent error handling
─────────────────────────────────────────────────────────────────────────
HIGH-LEVEL FLOW
─────────────────────────────────────────────────────────────────────────
A typical Meld usage scenario:
┌─────────────────────────────────────┐
│ Input Meld Document │
│ (myfile.meld or similar) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ ParserService: Uses meld-ast to │
│ parse text into basic AST nodes │
└─────────────────────────────────────┘
│ Raw AST (MeldNode[])
▼
┌────────────────────────────────────────────────────┐
│ InterpreterService: For each node, route to │
│ the DirectiveService & supporting services │
└────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ DirectiveService: Routes to: │
├─────────────────────────────────────┤
│ Definition Handlers: │──┐
│ • Store raw values from AST │ │
│ • No resolution/interpolation │ │
├─────────────────────────────────────┤ │
│ Execution Handlers: │ │
│ • Use ResolutionService │ │
└─────────────────────────────────────┘ │
│ │
▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────────┐
│ ResolutionService: │ │ StateService: │
│ • ALL variable resolution │◄───│ • Raw variable storage │
│ • ALL command resolution │ │ • No resolution logic │
│ • ALL path resolution │ │ • State hierarchy │
│ • Handles interpolation │ │ • Stores raw AST values │
└─────────────────────────────────┘ └─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ OutputService: Converts final state/AST to desired │
│ format (markdown, llm XML, or others) │
└─────────────────────────────────────────────────────┘
─────────────────────────────────────────────────────────────────────────
CODEBASE STRUCTURE
─────────────────────────────────────────────────────────────────────────
A recommended directory layout that emphasizes core library integration:
project-root/
├─ core/
│ ├─ errors/
│ │ ├─ MeldError.ts # Base custom error classes
│ │ └─ ErrorFactory.ts # Central creation for typed errors
│ ├─ types/
│ │ └─ SpecInterfaces.ts # Re-exports from meld-spec
│ └─ utils/
│ ├─ logger.ts # Winston or other logging
│ └─ helpers.ts # Common small utilities
├─ services/
│ ├─ PathService/
│ │ ├─ PathService.ts # Uses meld-spec PathVariable type
│ │ └─ PathService.test.ts # Unit tests
│ ├─ FileSystemService/
│ │ ├─ FileSystemService.ts # Abstract read/write to disk, mocking
│ │ └─ ...
│ ├─ CircularityService/
│ │ ├─ CircularityService.ts # Tracks imports, detects cycles
│ │ └─ ...
│ ├─ ValidationService/
│ │ ├─ ValidationService.ts # Uses meld-spec for validation
│ │ └─ ...
│ ├─ StateService/
│ │ ├─ StateService.ts # Uses meld-spec variable types
│ │ └─ ...
│ ├─ InterpolationService/
│ │ ├─ InterpolationService.ts # Variable expansion with meld-spec types
│ │ └─ ...
│ ├─ DirectiveService/
│ │ ├─ DirectiveService.ts # Routes directives to handlers
│ │ ├─ handlers/
│ │ │ ├─ TextDirectiveHandler.ts
│ │ │ ├─ DataDirectiveHandler.ts
│ │ │ ├─ EmbedDirectiveHandler.ts
│ │ │ ├─ ImportDirectiveHandler.ts
│ │ │ ├─ PathDirectiveHandler.ts
│ │ │ └─ ...
│ │ └─ ...
│ └─ ...
├─ parser/
│ ├─ ParserService.ts # Wraps meld-ast for parsing
│ └─ ...
├─ interpreter/
│ ├─ InterpreterService.ts # Uses meld-ast nodes
│ └─ ...
├─ output/
│ ├─ OutputService.ts # Uses llmxml for conversions
│ └─ ...
├─ tests/
│ ├─ integration/
│ │ ├─ cli.test.ts
│ │ ├─ sdk.test.ts
│ │ └─ ...
│ ├─ unit/
│ │ └─ (unit tests for each service)
│ └─ ...
├─ cli/
│ ├─ cmd.ts # Command entry
│ └─ ...
├─ sdk/
│ ├─ index.ts # runMeld, parseMeld, ...
│ └─ ...
└─ package.json
─────────────────────────────────────────────────────────────────────────
SERVICE ARCHITECTURE DETAILS
─────────────────────────────────────────────────────────────────────────
Below is a breakdown of key services, their responsibilities, and how they interrelate.
─────────────────────────────────────────────────────────────────────────
1. PathService
─────────────────────────────────────────────────────────────────────────
[See detailed design in service-path-fs.md]
• Responsibility:
- Work with ALREADY RESOLVED paths from ResolutionService
- Validate paths meet security requirements
- Normalize paths across platforms (POSIX/Win32)
- Provide test-mode overrides for easy mocking
• Example Usage in a directive:
```typescript
// ResolutionService handles variable resolution first
const resolvedPath = await resolutionService.resolvePath("$PROJECTPATH/foo.txt");
// PathService validates and normalizes the resolved path
await pathService.validatePath(resolvedPath);
const normalizedPath = pathService.normalizePath(resolvedPath);
```
─────────────────────────────────────────────────────────────────────────
2. FileSystemService
─────────────────────────────────────────────────────────────────────────
[See detailed design in service-path-fs.md]
• Responsibility:
- Abstract raw file operations (read, write, exist checks)
- Provide uniform mocking approach for tests
- Handle error codes, e.g. ENOENT -> MeldError
• Example:
```typescript
// After path resolution and validation:
const content = await fileSystemService.readFile(normalizedPath);
```
─────────────────────────────────────────────────────────────────────────
3. CircularityService
─────────────────────────────────────────────────────────────────────────
[See detailed design in service-circularity.md]
• Responsibility:
- Keep track of which files have been imported
- Detect cycles (File A imports B, B imports A, etc.)
- Provide user-friendly error if a cycle is found
• Example:
"ImportDirectiveHandler" notifies CircularityService.importStarted(filePath),
then if importStarted returns an error, we throw a "Circular reference" MeldError.
─────────────────────────────────────────────────────────────────────────
4. StateService
─────────────────────────────────────────────────────────────────────────
[See detailed design in service-state.md]
• Responsibility:
- Store raw variable values without processing
- Maintain variable type information
- Support variable deletion and updates
- Manage state hierarchy for imports/embeds
- Track imported files
• Example:
"TextDirectiveHandler" -> stateService.setTextVar(name, rawValue)
"DataDirectiveHandler" -> stateService.setDataVar(name, rawObject)
─────────────────────────────────────────────────────────────────────────
5. ResolutionService
─────────────────────────────────────────────────────────────────────────
[See detailed design in service-resolution.md]
• Core Responsibility:
- SOLE resolver for ALL variable types:
• Text variables (${var})
• Data variables and fields (#{data.field})
• Path variables ($path)
• Special path variables ($HOMEPATH/$~, $PROJECTPATH/$.)
• Command references ($command(args))
- Enforce context-specific resolution rules
- Detect variable reference cycles
• Key Components:
1. Dedicated Resolvers:
- TextResolver: Handles ${var}, prevents nested interpolation
- DataResolver: Handles #{data.field}, validates field access
- PathResolver: Handles ALL path variable resolution
• Special variables ($HOMEPATH/$~, $PROJECTPATH/$.)
• Custom path variables from @path directives
• Text variables within paths (${var})
- CommandResolver: Validates parameter types, no data vars in commands
2. Resolution Contexts:
- Path Context:
• Allows path variables ($path)
• Allows text variables (${var})
• Disallows data variables
• Disallows commands
- Command Context: Only text/path vars, no data vars
- Text Context: All variable types, no nesting
- Data Context: Allows field access, no commands
3. Context Factory:
- Pre-defined contexts per directive type
- Enforces grammar rules
- Prevents invalid variable usage
4. Cycle Detection:
- Tracks variable resolution stack
- Detects circular references
- Separate from file import cycles (CircularityService)
• Example Usage:
```typescript
// 1. Get appropriate context
const context = ResolutionContextFactory.forPathDirective();
// 2. Resolve path with variables
const resolvedPath = await resolutionService.resolvePath(
"$PROJECTPATH/docs/${folder}/file.md",
context
);
// Result: "/usr/project/docs/examples/file.md"
// 3. PathService then validates & normalizes
await pathService.validatePath(resolvedPath);
const normalizedPath = pathService.normalizePath(resolvedPath);
// 4. FileSystem handles I/O
const content = await fileSystemService.readFile(normalizedPath);
```
─────────────────────────────────────────────────────────────────────────
6. DirectiveService
─────────────────────────────────────────────────────────────────────────
[See detailed design in service-directive.md]
• Responsibility:
- Route directives to appropriate handlers
- Coordinate between ValidationService and ResolutionService
- Store raw values via StateService
- Manage directive dependencies via ResolutionService
• Organization:
Definition Handlers:
- Store raw values in StateService
- No resolution logic
- Validate directive structure
Execution Handlers:
- Use ResolutionService for all variable resolution
- Pass appropriate resolution context
- Handle resolution errors
• Example:
"@text var = value" -> TextHandler stores raw value
"@run [$cmd(${arg})]" -> RunHandler uses ResolutionService
─────────────────────────────────────────────────────────────────────────
7. ParserService
─────────────────────────────────────────────────────────────────────────
• Responsibility:
- Parse Meld content → AST (MeldNode[]) using meld-ast
- Wrap meld-ast's parse() function
- Provide error location details
─────────────────────────────────────────────────────────────────────────
8. InterpreterService
─────────────────────────────────────────────────────────────────────────
[See detailed design in service-interpreter.md]
• Responsibility:
- Orchestrates the main "interpretation" pipeline:
1) For each AST node:
- If Directive, route to DirectiveService
- If Text, store as raw text or pass along
2) Merge results into StateService
- Provide top-level interpretMeld() function
─────────────────────────────────────────────────────────────────────────
9. OutputService
─────────────────────────────────────────────────────────────────────────
[See detailed design in service-output.md]
• Responsibility:
- Convert final Meld AST/state to desired format
(e.g. Markdown, LLM XML, JSON, etc.)
- Possibly wrap code fences, transform directives to <directive> tags, etc.
─────────────────────────────────────────────────────────────────────────
ARCHITECTURE RELATIONS (ASCII DIAGRAM)
─────────────────────────────────────────────────────────────────────────
+-------------------------+ (1) Parse with meld-ast
| ParserService | get AST nodes
+----------+-------------+
|
(2) For each node +---------v----------+ (2a) If directive:
| InterpreterService | -----> +---------------------+
+---------+----------+ | DirectiveService |
| | Definition vs. |
| | Execution Handlers|
(2b) Node type? +-------+------------+
| |
+----------v-------------+ |
| StateService | <-----------+
| (Raw Value Store) | |
+------------------------+ |
| |
v v
+------------------------+ +-----------------+
| ResolutionService | | Validation |
| • Context Factory | | Service |
| • Type Resolvers | +-----------------+
| • Cycle Detection |
+------------------------+
|
v
+------------------------------------------------+
| OutputService (uses llmxml for conversion) |
+------------------------------------------------+
─────────────────────────────────────────────────────────────────────────
ILLUSTRATION OF A DIRECTIVE'S FLOW (example: @text)
─────────────────────────────────────────────────────────────────────────
1) ParserService sees line "@text greeting = 'Hello, world!'"
-> Creates a DirectiveNode { kind: 'text', ... }
2) InterpreterService processes that node:
-> directiveService.handleDirective(node, interpreterContext)
3) directiveService finds "TextDirectiveHandler" in internal registry
-> textHandler.execute(node, stateService, { interpolationService, validationService, ... })
4) TextDirectiveHandler:
A) validationService.validateTextDirective(...)
B) interpolationService.resolveAll(directive.value) // if needed
C) stateService.setTextVar(name, finalValue)
5) Interpretation continues with next node
─────────────────────────────────────────────────────────────────────────
EXAMPLE: HANDLER SCAFFOLD
─────────────────────────────────────────────────────────────────────────
export class TextDirectiveHandler {
constructor(
private validationService: ValidationService,
private interpolationService: InterpolationService,
private stateService: StateService
) {}
public execute(node: DirectiveNode): void {
// 1) Validate
this.validationService.validateTextDirective(node);
// 2) Extract name/value
const { name, value } = node.directive;
// 3) Possibly do interpolation
const resolvedValue = this.interpolationService.resolveAll(value);
// 4) Store in state
this.stateService.setTextVar(name, resolvedValue);
}
}
─────────────────────────────────────────────────────────────────────────
ADVANTAGES OF THIS DESIGN
─────────────────────────────────────────────────────────────────────────
• Each directive's logic is short & clean—just orchestrating the relevant services.
• Path expansions, filesystem I/O, and state merges are decoupled from directives.
• Clear single responsibility: each service does exactly "one thing."
• Tests become simpler: each service is tested with mocks of its dependencies.
• Better layering: the final pipeline is easy to see in InterpreterService.
─────────────────────────────────────────────────────────────────────────
EXAMPLE SERVICE TYPE USAGE
─────────────────────────────────────────────────────────────────────────
1. ParserService:
```typescript
import { parse } from 'meld-ast';
import { MeldNode, Parser } from 'meld-spec';
export class ParserService implements Parser {
parse(content: string): MeldNode[] {
return parse(content);
}
}
```
2. StateService:
```typescript
import { TextVariable, DataVariable, PathVariable, CommandDefinition } from 'meld-spec';
export class StateService {
private textVars = new Map<string, TextVariable>();
private dataVars = new Map<string, DataVariable>();
private pathVars = new Map<string, PathVariable>();
private commands = new Map<string, CommandDefinition>();
}
```
3. ValidationService:
```typescript
import {
DirectiveNode,
ValidationError,
ValidationContext,
ValidationResult
} from 'meld-spec';
export class ValidationService {
validate(node: DirectiveNode): ValidationResult {
// Validation logic using meld-spec types
}
}
```
4. OutputService:
```typescript
import { createLLMXML } from 'llmxml';
export class OutputService {
private llmxml = createLLMXML({
defaultFuzzyThreshold: 0.8,
warningLevel: 'ambiguous-only'
});
constructor() {
// Register warning handler for ambiguous matches
this.llmxml.on('warning', this.handleWarning);
}
async convertToXML(markdown: string): Promise<string> {
return this.llmxml.toXML(markdown);
}
async extractSection(content: string, sectionName: string) {
try {
return await this.llmxml.getSection(content, sectionName, {
includeNested: true,
fuzzyThreshold: 0.8
});
} catch (error) {
if (error.code === 'SECTION_NOT_FOUND') {
throw new Error(`Section "${sectionName}" not found`);
}
throw error;
}
}
private handleWarning(warning: any) {
if (warning.code === 'AMBIGUOUS_MATCH') {
console.warn('Multiple potential matches found:',
warning.details.matches.map((m: any) => m.title).join(', ')
);
}
}
}
```
─────────────────────────────────────────────────────────────────────────
SUB-TASKS TO FLESH OUT THIS DESIGN
─────────────────────────────────────────────────────────────────────────
1. Set Up Core Library Integration
- Add meld-spec as single source for ALL types
- Add meld-ast ONLY for parsing functionality
- Add llmxml for XML conversion
- Configure TypeScript for proper imports
2. Create Service Interfaces
- Define method signatures using meld-spec types
- Example:
interface IPathService {
resolve(specialPath: string): Promise<string>;
// ...
}
3. Build Error System
- Create error hierarchy extending meld-spec
- Centralize error creation
- Add location tracking
4. Implement ParserService
- Create thin wrapper around meld-ast
- Add error translation
- Add validation hooks
5. Create Core Services
- PathService with meld-spec types
- FileSystemService for I/O
- ValidationService using meld-spec
- StateService with proper types
6. Build Directive System
- Create handler registry
- Implement each directive
- Use services for complex operations
7. Add Integration Tests
- Test full pipeline
- Verify library integration
- Check error handling
─────────────────────────────────────────────────────────────────────────
CONCLUSION & NEXT STEPS
─────────────────────────────────────────────────────────────────────────
With this services-based design:
1. We leverage core Meld libraries:
• meld-ast for parsing
• llmxml for XML conversion
• meld-spec for types
2. Each service has clear responsibilities:
• Minimal, focused interfaces
• Clean dependency injection
• Easy to test and maintain
3. Directives remain simple:
• Use services for complex operations
• Focus on business logic
• Easy to add new ones
4. Testing is straightforward:
• Unit test each service
• Integration test the pipeline
• Mock complex operations
This architecture yields a maintainable codebase that integrates seamlessly with the Meld ecosystem while following SOLID principles.