UNPKG

legal-markdown-js

Version:

Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version

219 lines 7.92 kB
/** * Pipeline Builder * * Single source of truth for remark plugin ordering. Uses phase-based * architecture to ensure deterministic execution order and prevent * subtle bugs like variables evaluating after conditionals (Issue #120). * * @module pipeline-builder */ import { PluginOrderValidator } from '../../plugins/remark/plugin-order-validator.js'; import { PipelineError } from '../../errors/index.js'; import { getRuntimeConfig } from '../../config/runtime.js'; import { logger } from '../../utils/logger.js'; /** * Detect appropriate validation mode based on environment * * Returns 'strict' in development/CI, 'warn' in production */ export function detectValidationMode() { const configuredMode = getRuntimeConfig().processing.validationMode; if (configuredMode === 'strict') { return 'strict'; } if (configuredMode === 'permissive') { return 'warn'; } if (process.env.CI === 'true') { return 'strict'; } if (process.env.NODE_ENV === 'production') { return 'warn'; } if (process.env.NODE_ENV === 'test') { return 'warn'; // Don't spam test output } // Default to strict in development return 'strict'; } /** * Group plugins by their assigned processing phase * * @param pluginNames - Array of plugin names to group * @param registry - Plugin metadata registry * @returns Map of phase → plugin names */ export function groupPluginsByPhase(pluginNames, registry) { const byPhase = new Map(); // Initialize all phases for (let phase = 1; phase <= 5; phase++) { byPhase.set(phase, []); } for (const name of pluginNames) { const metadata = registry.get(name); if (!metadata) { throw new PipelineError(`[PipelineBuilder] Plugin "${name}" not found in registry. ` + `Available plugins: ${Array.from(registry.keys()).join(', ')}`); } if (!metadata.phase) { throw new PipelineError(`[PipelineBuilder] Plugin "${name}" has no phase assigned. ` + `All plugins must have a phase field.`); } const phasePlugins = byPhase.get(metadata.phase) || []; phasePlugins.push(name); byPhase.set(metadata.phase, phasePlugins); } return byPhase; } /** * Validate that required capabilities are provided by the pipeline * * Throws an error if any plugin requires a capability that is not * provided by any earlier plugin in the pipeline. * * @param orderedPlugins - Plugin names in execution order * @param registry - Plugin metadata registry * @param debug - Enable debug logging */ export function validateCapabilities(orderedPlugins, registry, debug = false) { const providedCapabilities = new Set(); const errors = []; for (const pluginName of orderedPlugins) { const metadata = registry.get(pluginName); if (!metadata) continue; // Check if this plugin's required capabilities are available if (metadata.requiresCapabilities) { for (const requiredCap of metadata.requiresCapabilities) { if (!providedCapabilities.has(requiredCap)) { errors.push(`Plugin "${pluginName}" requires capability "${requiredCap}" ` + `but no earlier plugin provides it`); } } } // Add this plugin's capabilities to the available set if (metadata.capabilities) { for (const cap of metadata.capabilities) { providedCapabilities.add(cap); if (debug) { logger.debug(`Plugin "${pluginName}" provides capability: ${cap}`); } } } } if (errors.length > 0) { throw new PipelineError('[PipelineBuilder] Capability validation failed:\n' + errors.map(e => ` - ${e}`).join('\n')); } } /** * Build a remark plugin pipeline with phase-based ordering * * This is the single source of truth for plugin ordering. It uses the * phase-based architecture to ensure deterministic execution order and * prevent subtle bugs like variables evaluating after conditionals (Issue #120). * * @param config - Pipeline configuration * @param registry - Plugin metadata registry * @returns Ordered pipeline with validation results * * @example * ```typescript * import { buildRemarkPipeline } from './pipeline-builder.js'; * import { GLOBAL_PLUGIN_REGISTRY } from '../../plugins/remark/plugin-metadata-registry.js'; * * const pipeline = buildRemarkPipeline( * { * enabledPlugins: ['remarkImports', 'remarkTemplateFields', 'remarkHeaders'], * metadata: { author: 'John Doe' }, * options: { debug: true }, * validationMode: 'strict' * }, * GLOBAL_PLUGIN_REGISTRY * ); * * console.log('Plugin order:', pipeline.names); * console.log('By phase:', pipeline.byPhase); * ``` */ export function buildRemarkPipeline(config, registry) { const startTime = new Date(); const validator = new PluginOrderValidator(registry); // Step 1: Determine validation mode const validationMode = config.validationMode || detectValidationMode(); if (config.debug) { logger.debug('Building pipeline with mode', validationMode); logger.debug('Requested plugins', config.enabledPlugins); } // Step 2: Group plugins by phase const byPhase = groupPluginsByPhase(config.enabledPlugins, registry); if (config.debug) { logger.debug('Plugins by phase:'); for (const [phase, plugins] of byPhase.entries()) { if (plugins.length > 0) { logger.debug(` Phase ${phase}: ${plugins.join(', ')}`); } } } // Step 3: Topologically sort within each phase const orderedNames = []; for (const phase of [1, 2, 3, 4, 5]) { const phasePlugins = byPhase.get(phase) || []; if (phasePlugins.length === 0) continue; if (config.debug) { logger.debug(`Sorting phase ${phase} plugins`, phasePlugins); } const sorted = validator.topologicalSort(phasePlugins); orderedNames.push(...sorted); // Update byPhase with sorted order byPhase.set(phase, sorted); if (config.debug) { logger.debug(`Phase ${phase} sorted order`, sorted); } } // Step 4: Validate the final order const validationOptions = { throwOnError: validationMode === 'strict', logWarnings: validationMode === 'warn' || config.debug, strictMode: validationMode === 'strict', debug: config.debug, }; const validation = validator.validate(orderedNames, validationOptions); if (config.debug) { if (validation.valid) { logger.debug('Validation passed'); } else { logger.debug('Validation failed'); if (validation.errors.length > 0) { logger.debug('Errors', validation.errors); } if (validation.warnings.length > 0) { logger.debug('Warnings', validation.warnings); } } } // Step 5: Collect capabilities const capabilities = new Set(); for (const name of orderedNames) { const metadata = registry.get(name); if (metadata?.capabilities) { metadata.capabilities.forEach(cap => capabilities.add(cap)); } } // Step 6: Validate capabilities validateCapabilities(orderedNames, registry, config.debug); if (config.debug) { logger.debug('Final order', orderedNames.join(' → ')); logger.debug('Capabilities provided', Array.from(capabilities)); } return { names: orderedNames, byPhase, validation, capabilities, builtAt: startTime, config, }; } //# sourceMappingURL=pipeline-builder.js.map