UNPKG

cmte

Version:

Design by Committee™ except it's just you and LLMs

215 lines (194 loc) 10 kB
/** * A simplified output reference resolver for the Committee framework. * Handles task output references with support for iterations. */ import { logger } from '../../utils/logger.js'; import { getNestedProperty } from '../../utils/nested-property.js'; export class OutputReferenceResolver { constructor() { this.outputs = {}; this.currentIteration = null; this.REFERENCE_REGEX = /^([\w-]+)\.([\w-]+)(?:\[(.*?)\])?(?:\.([\w.]+))?$/; } /** * Sets the current iteration context * @param {string} iterationKey - The current iteration key */ setIterationContext(iterationKey) { this.currentIteration = iterationKey; } /** * Clears the current iteration context */ clearIterationContext() { this.currentIteration = null; } /** * Registers regular task output * @param {string} setName - Name of the task set * @param {string} taskName - Name of the task * @param {any} output - The output to register */ registerOutput(setName, taskName, output) { if (!this.outputs[setName]) { this.outputs[setName] = {}; } this.outputs[setName][taskName] = output; } /** * Registers output for an iterated task * @param {string} setName - Name of the task set * @param {string} taskName - Name of the task * @param {string} iteratorValue - The iteration value * @param {any} output - The output to register */ registerIteratedOutput(setName, taskName, iteratorValue, output) { if (!this.outputs[setName]) { this.outputs[setName] = {}; } // Ensure the entry for the task exists and is an array // If it exists but isn't an array, overwrite it. This might hide errors // where non-iterated and iterated outputs are mixed for the same task, // but simplifies testing setup recovery. if (!this.outputs[setName][taskName] || !Array.isArray(this.outputs[setName][taskName])) { this.outputs[setName][taskName] = []; } this.outputs[setName][taskName].push({ iterator: iteratorValue, output }); } /** * Resolves an output reference string to its stored value. * Handles basic references (setName.taskName.output) and iteration references (setName.taskName[key].output). * @param {string} reference - The output reference string. * @param {string|null} [iterationKeyOverride=null] - Optional override for the iteration context key (used by TemplateRenderer). * @returns {*} The resolved output value. * @throws {Error} If the reference format is invalid, the set/task/output is not found, * or the reference is ambiguous (e.g., basic reference to iterated output). */ resolveReference(reference, iterationKeyOverride = null) { logger.debug(`Resolving reference: ${reference}${iterationKeyOverride ? ` (Iteration Override: ${iterationKeyOverride})` : ''}${this.currentIteration ? ` (Current Context: ${this.currentIteration})` : ''}`); const match = reference.match(this.REFERENCE_REGEX); if (!match) { // logger.debug(`[ORR_DEBUG] Invalid output reference format (will try context): ${reference}`); throw new Error(`Invalid output reference format: ${reference}`); } const [, setName, taskName, iterationLookupKeyRaw, propertyPathRaw] = match; let iterationLookupKey = iterationLookupKeyRaw; const currentIterationContextKey = iterationKeyOverride ?? this.currentIteration; // logger.debug(`[ORR_DEBUG] Using iteration context key: ${currentIterationContextKey || 'undefined'}`); // --- Determine the actual property path to apply (if any) --- let actualPropertyPath = null; if (propertyPathRaw) { // Check if the property path group was captured at all if (propertyPathRaw === 'output') { // Standard .output suffix, means get the whole output actualPropertyPath = null; // logger.debug(`[ORR_DEBUG] Detected standard '.output', no nested path to apply.`); } else if (propertyPathRaw.startsWith('output.')) { // Nested property access like .output.key actualPropertyPath = propertyPathRaw.substring(7); // Get the part after 'output.' // logger.debug(`[ORR_DEBUG] Detected nested property path: '${actualPropertyPath}'`); } else { // Invalid format - path exists but doesn't start with 'output' const errMsg = `Invalid property path in reference '${reference}'. Path must start with '.output'.`; logger.error(errMsg); // Keep error for truly invalid path throw new Error(errMsg); } } // If propertyPathRaw is undefined, actualPropertyPath remains null // --- Check if Set and Task exist --- if (!(setName in this.outputs)) { logger.debug(`[ORR_DEBUG] Set not found for potential reference: ${reference}`); // Keep as debug throw new Error(`Set not found for reference: ${reference}`); } if (!(taskName in this.outputs[setName])) { logger.debug(`[ORR_DEBUG] Task output not found for potential reference: ${reference}`); // Keep as debug throw new Error(`Task output not found for reference: ${reference}`); } const taskOutput = this.outputs[setName][taskName]; // logger.debug(`[ORR_DEBUG] Found task output object/array for ${setName}.${taskName}. Type: ${Array.isArray(taskOutput) ? 'Array' : typeof taskOutput}. Content preview: ${JSON.stringify(taskOutput)?.substring(0, 100)}...`); // --- Resolve Base Output Value --- let resolvedBaseOutput; if (iterationLookupKey !== undefined) { // logger.debug(`[ORR_DEBUG] Handling iteration reference. Key requested: '${iterationLookupKey}'. Using context key: '${currentIterationContextKey || 'none'}'`); if (!Array.isArray(taskOutput)) { const errMsg = `Cannot use iteration key '[${iterationLookupKey}]' on non-iterated output for reference: ${reference}`; logger.error(errMsg); throw new Error(errMsg); } // Special handling for [this] if (iterationLookupKey === 'this') { if (!currentIterationContextKey) { const errMsg = `Cannot use [this] iteration key outside of an iteration context. Reference: ${reference}`; logger.error(errMsg); throw new Error(errMsg); } iterationLookupKey = currentIterationContextKey; // logger.debug(`[ORR_DEBUG] Mapped [this] to actual key: '${iterationLookupKey}'`); } // Handle Wildcard [*] if (iterationLookupKey === '*') { // logger.debug(`[ORR_DEBUG] Resolving wildcard [*] reference.`); const results = taskOutput.map(item => item.output); // logger.debug(`[ORR_DEBUG] Wildcard results: ${JSON.stringify(results)}`); // --- Apply Property Path (if determined) --- if (actualPropertyPath) { // logger.debug(`[ORR_DEBUG] Applying actual property path '${actualPropertyPath}' to EACH item in wildcard results.`); const mappedResults = results.map(res => getNestedProperty(res, actualPropertyPath)); // logger.debug(`[ORR_DEBUG] Result after property path mapping: ${JSON.stringify(mappedResults)}`); return mappedResults; } else { // logger.debug(`[ORR_DEBUG] No property path to apply, returning full wildcard results array.`); return results; // <<< Ensure the array is returned here } } // Find Specific Iteration Item // logger.debug(`[ORR_DEBUG] Searching for specific iterator key: '${iterationLookupKey}' in array of length ${taskOutput.length}`); const item = taskOutput.find(item => item.iterator === iterationLookupKey); // logger.debug(`[ORR_DEBUG] Found item for key '${iterationLookupKey}': ${item ? JSON.stringify(item) : 'NOT FOUND'}`); if (!item) { const availableKeys = taskOutput.map(item => item.iterator).join(', '); const errMsg = `Iteration key '${iterationLookupKey}' not found for ${setName}.${taskName}. Reference: ${reference}. Available keys: ${availableKeys}`; logger.error(errMsg); throw new Error(errMsg); } resolvedBaseOutput = item.output; // logger.debug(`[ORR_DEBUG] Extracted iterated base output. Type: ${typeof resolvedBaseOutput}. Value preview: ${JSON.stringify(resolvedBaseOutput)?.substring(0,100)}...`); } else { // logger.debug(`[ORR_DEBUG] Handling non-iteration reference.`); if (Array.isArray(taskOutput)) { const errMsg = `Ambiguous reference: '${reference}' refers to an iterated output. Specify an iteration key (e.g., [key], [*], [this]) or use a non-iterated task output.`; logger.error(errMsg); throw new Error(errMsg); } // It's a simple, non-iterated value resolvedBaseOutput = taskOutput; // logger.debug(`[ORR_DEBUG] Extracted non-iterated base output. Type: ${typeof resolvedBaseOutput}. Value preview: ${JSON.stringify(resolvedBaseOutput)?.substring(0,100)}...`); } // --- Apply Property Path (if determined) --- if (actualPropertyPath) { // logger.debug(`[ORR_DEBUG] Applying actual property path '${actualPropertyPath}' to base output.`); const finalValue = getNestedProperty(resolvedBaseOutput, actualPropertyPath); // logger.debug(`[ORR_DEBUG] Result after property path: ${JSON.stringify(finalValue)}`); return finalValue; } else { // logger.debug(`[ORR_DEBUG] No property path to apply, returning base output.`); return resolvedBaseOutput; // Return the whole output } } /** * Returns a deep copy of all registered outputs. * @returns {object} A deep copy of the outputs object. */ getAllOutputs() { // Return a deep copy to prevent external modification of internal state return JSON.parse(JSON.stringify(this.outputs)); } /** * Clears all registered outputs and the iteration context. */ clear() { this.outputs = {}; this.currentIteration = null; } }