UNPKG

meld

Version:

Meld: A template language for LLM prompts

295 lines (252 loc) 9.24 kB
import { IStateService } from '@services/state/StateService/IStateService.js'; import { ResolutionContext, ResolutionErrorCode } from '@services/resolution/ResolutionService/IResolutionService.js'; import { ResolutionError } from '@services/resolution/ResolutionService/errors/ResolutionError.js'; import type { MeldNode, DirectiveNode, TextNode, PathVarNode, StructuredPath } from 'meld-spec'; import { MeldResolutionError } from '@core/errors/MeldResolutionError.js'; import { ErrorSeverity } from '@core/errors/MeldError.js'; /** * Handles resolution of path variables ($path) */ export class PathResolver { constructor(private stateService: IStateService) {} /** * Resolve path variables in a node */ async resolve(node: MeldNode, context: ResolutionContext): Promise<string> { // Early return if not a directive node if (node.type !== 'Directive') { return node.type === 'Text' ? (node as TextNode).content : ''; } const directiveNode = node as DirectiveNode; // Validate path variables are allowed if (!context.allowedVariableTypes.path) { throw new MeldResolutionError( 'Path variables are not allowed in this context', { code: ResolutionErrorCode.INVALID_CONTEXT, severity: ErrorSeverity.Fatal, details: { value: directiveNode.directive.value, context: JSON.stringify(context) } } ); } // Validate node type if (directiveNode.directive.kind !== 'path') { throw new MeldResolutionError( 'Invalid node type for path resolution', { code: ResolutionErrorCode.INVALID_NODE_TYPE, severity: ErrorSeverity.Fatal, details: { value: directiveNode.directive.kind } } ); } // Get the variable identifier const identifier = directiveNode.directive.identifier; if (!identifier) { throw new MeldResolutionError( 'Path variable identifier is required', { code: ResolutionErrorCode.SYNTAX_ERROR, severity: ErrorSeverity.Fatal, details: { value: JSON.stringify(directiveNode.directive) } } ); } // Handle special path variables if (identifier === '~' || identifier === 'HOMEPATH') { return this.stateService.getPathVar('HOMEPATH') || ''; } if (identifier === '.' || identifier === 'PROJECTPATH') { return this.stateService.getPathVar('PROJECTPATH') || ''; } // For regular path variables, get value from state const value = this.stateService.getPathVar(identifier); if (value === undefined) { throw new MeldResolutionError( `Undefined path variable: ${identifier}`, { code: ResolutionErrorCode.UNDEFINED_VARIABLE, severity: ErrorSeverity.Recoverable, details: { variableName: identifier, variableType: 'path' } } ); } // Handle structured path objects if (typeof value === 'object' && value !== null && 'raw' in value) { const structuredPath = value as StructuredPath; // Validate path if required if (context.pathValidation) { return this.validatePath(structuredPath, context); } // Use normalized path if available, otherwise use raw return structuredPath.normalized || structuredPath.raw; } // Handle string paths (legacy support) // Validate path if required if (context.pathValidation) { return this.validatePath(value as string, context); } return value as string; } /** * Extract references from a node */ extractReferences(node: MeldNode): string[] { if (node.type !== 'Directive') { return []; } const directiveNode = node as DirectiveNode; if (directiveNode.directive.kind !== 'path') { return []; } const identifier = directiveNode.directive.identifier; if (!identifier) { return []; } // Map special variables to their full names if (identifier === '~') { return ['HOMEPATH']; } if (identifier === '.') { return ['PROJECTPATH']; } // Extract references from structured path if available const value = directiveNode.directive.value; if (value && typeof value === 'object' && 'structured' in value) { const structuredPath = value as StructuredPath; const references = [identifier]; // Always include the path variable itself // Add special variables if (structuredPath.structured.variables.special.length > 0) { references.push(...structuredPath.structured.variables.special); } // Add path variables if (structuredPath.structured.variables.path.length > 0) { references.push(...structuredPath.structured.variables.path); } return references; } return [identifier]; } /** * Validate a resolved path against context requirements */ private validatePath(path: string | StructuredPath, context: ResolutionContext): string { // Convert structured path to string if needed const pathStr = typeof path === 'object' && 'normalized' in path ? (path.normalized || path.raw) : path as string; // Special handling for paths with special variables const hasSpecialVar = pathStr.startsWith('$PROJECTPATH/') || pathStr.startsWith('$./') || pathStr.startsWith('$HOMEPATH/') || pathStr.startsWith('$~/'); // If it has a special variable, we can return it directly if (hasSpecialVar) { return pathStr; } if (context.pathValidation) { // Check if path is absolute or starts with a special variable if (context.pathValidation.requireAbsolute && !pathStr.startsWith('/')) { throw new MeldResolutionError( 'Path must be absolute', { code: ResolutionErrorCode.INVALID_PATH, severity: ErrorSeverity.Fatal, details: { value: pathStr, context: JSON.stringify(context.pathValidation) } } ); } // Check if path starts with an allowed root if (context.pathValidation.allowedRoots?.length) { const hasAllowedRoot = context.pathValidation.allowedRoots.some(root => { const rootVar = this.stateService.getPathVar(root); return rootVar && ( pathStr.startsWith(rootVar + '/') || pathStr === rootVar ); }); if (!hasAllowedRoot) { throw new MeldResolutionError( `Path must start with one of: ${context.pathValidation.allowedRoots.join(', ')}`, { code: ResolutionErrorCode.INVALID_PATH, severity: ErrorSeverity.Fatal, details: { value: pathStr, context: JSON.stringify(context.pathValidation) } } ); } } } return pathStr; } /** * Get all path variables referenced in a node */ getReferencedVariables(node: MeldNode): string[] { // Extract the path variable from the node const pathVar = this.getPathVarFromNode(node); if (!pathVar || pathVar.isSpecial) { return []; } // For structured paths, extract all variables if (node.type === 'Directive' && (node as DirectiveNode).directive.value && typeof (node as DirectiveNode).directive.value === 'object' && 'structured' in (node as DirectiveNode).directive.value) { const structuredPath = (node as DirectiveNode).directive.value as StructuredPath; const references: string[] = [pathVar.identifier]; // Add special variables if (structuredPath.structured.variables.special.length > 0) { references.push(...structuredPath.structured.variables.special); } // Add path variables if (structuredPath.structured.variables.path.length > 0) { references.push(...structuredPath.structured.variables.path); } return references; } return [pathVar.identifier]; } /** * Helper to extract PathVarNode from a node */ private getPathVarFromNode(node: MeldNode): PathVarNode | null { if (node.type !== 'Directive' || (node as DirectiveNode).directive.kind !== 'path') { return null; } // For structured paths, create a synthetic PathVarNode if ((node as DirectiveNode).directive.value && typeof (node as DirectiveNode).directive.value === 'object' && 'structured' in (node as DirectiveNode).directive.value) { const identifier = (node as DirectiveNode).directive.identifier; if (!identifier) return null; // Create a synthetic PathVarNode return { type: 'PathVar', identifier, isSpecial: false }; } const pathVar = (node as DirectiveNode).directive.value as PathVarNode; if (!pathVar || pathVar.type !== 'PathVar') { return null; } return pathVar; } }