meld
Version:
Meld: A template language for LLM prompts
295 lines (252 loc) • 9.24 kB
text/typescript
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;
}
}