cognitive-modules-cli 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1265 @@
1
+ /**
2
+ * Composition Engine - Module Composition and Orchestration
3
+ *
4
+ * Implements COMPOSITION.md specification:
5
+ * - Sequential Composition: A → B → C
6
+ * - Parallel Composition: A → [B, C, D] → Aggregator
7
+ * - Conditional Composition: A → (condition) → B or C
8
+ * - Iterative Composition: A → (check) → A → ... → Done
9
+ * - Dataflow Mapping: JSONPath-like expressions
10
+ * - Aggregation Strategies: merge, array, first, custom
11
+ * - Dependency Resolution with fallbacks
12
+ * - Timeout handling
13
+ * - Circular dependency detection
14
+ */
15
+ import { findModule, getDefaultSearchPaths } from './loader.js';
16
+ import { runModule } from './runner.js';
17
+ // =============================================================================
18
+ // Error Codes for Composition
19
+ // =============================================================================
20
+ export const COMPOSITION_ERRORS = {
21
+ E4004: 'CIRCULAR_DEPENDENCY',
22
+ E4005: 'MAX_DEPTH_EXCEEDED',
23
+ E4008: 'COMPOSITION_TIMEOUT',
24
+ E4009: 'DEPENDENCY_NOT_FOUND',
25
+ E4010: 'DATAFLOW_ERROR',
26
+ E4011: 'CONDITION_EVAL_ERROR',
27
+ E4012: 'AGGREGATION_ERROR',
28
+ E4013: 'ITERATION_LIMIT_EXCEEDED',
29
+ };
30
+ // =============================================================================
31
+ // JSONPath-like Expression Parser
32
+ // =============================================================================
33
+ /**
34
+ * Parse and evaluate JSONPath-like expressions.
35
+ *
36
+ * Supported syntax:
37
+ * - $.field - Root field access
38
+ * - $.nested.field - Nested access
39
+ * - $.array[0] - Array index
40
+ * - $.array[*].field - Array map
41
+ * - $ - Entire object
42
+ */
43
+ export function evaluateJsonPath(expression, data) {
44
+ if (!expression.startsWith('$')) {
45
+ return expression; // Literal value
46
+ }
47
+ if (expression === '$') {
48
+ return data;
49
+ }
50
+ const path = expression.slice(1); // Remove leading $
51
+ const segments = parsePathSegments(path);
52
+ let current = data;
53
+ for (const segment of segments) {
54
+ if (current === null || current === undefined) {
55
+ return undefined;
56
+ }
57
+ if (segment.type === 'field' && segment.name !== undefined) {
58
+ if (typeof current !== 'object') {
59
+ return undefined;
60
+ }
61
+ current = current[segment.name];
62
+ }
63
+ else if (segment.type === 'index' && segment.index !== undefined) {
64
+ if (!Array.isArray(current)) {
65
+ return undefined;
66
+ }
67
+ current = current[segment.index];
68
+ }
69
+ else if (segment.type === 'wildcard') {
70
+ if (!Array.isArray(current)) {
71
+ return undefined;
72
+ }
73
+ // Map over array
74
+ const remainingSegments = segment.remaining ?? [];
75
+ current = current.map(item => {
76
+ let result = item;
77
+ for (const remainingSegment of remainingSegments) {
78
+ if (result === null || result === undefined) {
79
+ return undefined;
80
+ }
81
+ if (remainingSegment.type === 'field' && remainingSegment.name !== undefined) {
82
+ result = result[remainingSegment.name];
83
+ }
84
+ }
85
+ return result;
86
+ });
87
+ break; // Wildcard consumes remaining path
88
+ }
89
+ }
90
+ return current;
91
+ }
92
+ function parsePathSegments(path) {
93
+ const segments = [];
94
+ let remaining = path;
95
+ while (remaining.length > 0) {
96
+ // Remove leading dot
97
+ if (remaining.startsWith('.')) {
98
+ remaining = remaining.slice(1);
99
+ }
100
+ // Array index: [0]
101
+ const indexMatch = remaining.match(/^\[(\d+)\]/);
102
+ if (indexMatch) {
103
+ segments.push({ type: 'index', index: parseInt(indexMatch[1], 10) });
104
+ remaining = remaining.slice(indexMatch[0].length);
105
+ continue;
106
+ }
107
+ // Array wildcard: [*]
108
+ const wildcardMatch = remaining.match(/^\[\*\]/);
109
+ if (wildcardMatch) {
110
+ remaining = remaining.slice(wildcardMatch[0].length);
111
+ // Parse remaining path for wildcard
112
+ const remainingSegments = parsePathSegments(remaining);
113
+ segments.push({ type: 'wildcard', remaining: remainingSegments });
114
+ break; // Wildcard consumes the rest
115
+ }
116
+ // Field name (support hyphens in field names like quick-check)
117
+ const fieldMatch = remaining.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/);
118
+ if (fieldMatch) {
119
+ segments.push({ type: 'field', name: fieldMatch[1] });
120
+ remaining = remaining.slice(fieldMatch[0].length);
121
+ continue;
122
+ }
123
+ // Unknown segment, break
124
+ break;
125
+ }
126
+ return segments;
127
+ }
128
+ /**
129
+ * Apply dataflow mapping to transform data
130
+ */
131
+ export function applyMapping(mapping, sourceData) {
132
+ const result = {};
133
+ for (const [targetField, sourceExpr] of Object.entries(mapping)) {
134
+ result[targetField] = evaluateJsonPath(sourceExpr, sourceData);
135
+ }
136
+ return result;
137
+ }
138
+ // =============================================================================
139
+ // Condition Expression Evaluator
140
+ // =============================================================================
141
+ /**
142
+ * Evaluate condition expressions.
143
+ *
144
+ * Supported operators:
145
+ * - Comparison: ==, !=, >, <, >=, <=
146
+ * - Logical: &&, ||, !
147
+ * - Existence: exists($.field)
148
+ * - String: contains($.field, "value")
149
+ */
150
+ export function evaluateCondition(expression, data) {
151
+ try {
152
+ // Handle exists() function
153
+ const existsMatch = expression.match(/exists\(([^)]+)\)/);
154
+ if (existsMatch) {
155
+ const value = evaluateJsonPath(existsMatch[1].trim(), data);
156
+ const exists = value !== undefined && value !== null;
157
+ const remainingExpr = expression.replace(existsMatch[0], exists ? 'true' : 'false');
158
+ if (remainingExpr.trim() === 'true' || remainingExpr.trim() === 'false') {
159
+ return remainingExpr.trim() === 'true';
160
+ }
161
+ return evaluateCondition(remainingExpr, data);
162
+ }
163
+ // Handle contains() function - supports both string and array
164
+ const containsMatch = expression.match(/contains\(([^,]+),\s*["']([^"']+)["']\)/);
165
+ if (containsMatch) {
166
+ const value = evaluateJsonPath(containsMatch[1].trim(), data);
167
+ const search = containsMatch[2];
168
+ let contains = false;
169
+ if (typeof value === 'string') {
170
+ contains = value.includes(search);
171
+ }
172
+ else if (Array.isArray(value)) {
173
+ contains = value.includes(search);
174
+ }
175
+ const remainingExpr = expression.replace(containsMatch[0], contains ? 'true' : 'false');
176
+ return evaluateCondition(remainingExpr, data);
177
+ }
178
+ // Handle length property (support hyphens in field names)
179
+ const lengthMatch = expression.match(/(\$[a-zA-Z0-9._\[\]*-]+)\.length/g);
180
+ if (lengthMatch) {
181
+ let processedExpr = expression;
182
+ for (const match of lengthMatch) {
183
+ const pathPart = match.replace('.length', '');
184
+ const value = evaluateJsonPath(pathPart, data);
185
+ const length = Array.isArray(value) ? value.length :
186
+ typeof value === 'string' ? value.length : 0;
187
+ processedExpr = processedExpr.replace(match, String(length));
188
+ }
189
+ expression = processedExpr;
190
+ }
191
+ // Replace JSONPath expressions with values (support hyphens in field names)
192
+ const jsonPathMatches = expression.match(/\$[a-zA-Z0-9._\[\]*-]+/g);
193
+ if (jsonPathMatches) {
194
+ let processedExpr = expression;
195
+ for (const match of jsonPathMatches) {
196
+ const value = evaluateJsonPath(match, data);
197
+ let replacement;
198
+ if (value === undefined || value === null) {
199
+ replacement = 'null';
200
+ }
201
+ else if (typeof value === 'string') {
202
+ replacement = `"${value}"`;
203
+ }
204
+ else if (typeof value === 'boolean') {
205
+ replacement = value ? 'true' : 'false';
206
+ }
207
+ else if (typeof value === 'number') {
208
+ replacement = String(value);
209
+ }
210
+ else {
211
+ replacement = JSON.stringify(value);
212
+ }
213
+ processedExpr = processedExpr.replace(match, replacement);
214
+ }
215
+ expression = processedExpr;
216
+ }
217
+ // Evaluate the expression safely
218
+ return safeEval(expression);
219
+ }
220
+ catch (error) {
221
+ console.error(`Failed to evaluate condition: ${expression}`, error);
222
+ return false;
223
+ }
224
+ }
225
+ /**
226
+ * Safe expression evaluator (no eval())
227
+ */
228
+ function safeEval(expression) {
229
+ // Remove whitespace
230
+ expression = expression.trim();
231
+ // Handle logical operators (lowest precedence)
232
+ // Handle || first
233
+ const orParts = splitByOperator(expression, '||');
234
+ if (orParts.length > 1) {
235
+ return orParts.some(part => safeEval(part));
236
+ }
237
+ // Handle &&
238
+ const andParts = splitByOperator(expression, '&&');
239
+ if (andParts.length > 1) {
240
+ return andParts.every(part => safeEval(part));
241
+ }
242
+ // Handle ! (not)
243
+ if (expression.startsWith('!')) {
244
+ return !safeEval(expression.slice(1));
245
+ }
246
+ // Handle parentheses
247
+ if (expression.startsWith('(') && expression.endsWith(')')) {
248
+ return safeEval(expression.slice(1, -1));
249
+ }
250
+ // Handle comparison operators
251
+ const comparisonOps = ['!==', '===', '!=', '==', '>=', '<=', '>', '<'];
252
+ for (const op of comparisonOps) {
253
+ const opIndex = expression.indexOf(op);
254
+ if (opIndex !== -1) {
255
+ const left = parseValue(expression.slice(0, opIndex).trim());
256
+ const right = parseValue(expression.slice(opIndex + op.length).trim());
257
+ switch (op) {
258
+ case '===':
259
+ case '==':
260
+ return left === right;
261
+ case '!==':
262
+ case '!=':
263
+ return left !== right;
264
+ case '>':
265
+ return left > right;
266
+ case '<':
267
+ return left < right;
268
+ case '>=':
269
+ return left >= right;
270
+ case '<=':
271
+ return left <= right;
272
+ }
273
+ }
274
+ }
275
+ // Handle boolean literals
276
+ if (expression === 'true')
277
+ return true;
278
+ if (expression === 'false')
279
+ return false;
280
+ // Handle truthy/falsy
281
+ const value = parseValue(expression);
282
+ return Boolean(value);
283
+ }
284
+ function splitByOperator(expression, operator) {
285
+ const parts = [];
286
+ let current = '';
287
+ let depth = 0;
288
+ let inString = false;
289
+ let stringChar = '';
290
+ for (let i = 0; i < expression.length; i++) {
291
+ const char = expression[i];
292
+ // Handle string literals
293
+ if ((char === '"' || char === "'") && expression[i - 1] !== '\\') {
294
+ if (!inString) {
295
+ inString = true;
296
+ stringChar = char;
297
+ }
298
+ else if (char === stringChar) {
299
+ inString = false;
300
+ }
301
+ }
302
+ // Handle parentheses depth
303
+ if (!inString) {
304
+ if (char === '(')
305
+ depth++;
306
+ if (char === ')')
307
+ depth--;
308
+ }
309
+ // Check for operator
310
+ if (!inString && depth === 0 && expression.slice(i, i + operator.length) === operator) {
311
+ parts.push(current.trim());
312
+ current = '';
313
+ i += operator.length - 1;
314
+ continue;
315
+ }
316
+ current += char;
317
+ }
318
+ if (current.trim()) {
319
+ parts.push(current.trim());
320
+ }
321
+ return parts;
322
+ }
323
+ function parseValue(str) {
324
+ str = str.trim();
325
+ // Null
326
+ if (str === 'null')
327
+ return null;
328
+ // Boolean
329
+ if (str === 'true')
330
+ return true;
331
+ if (str === 'false')
332
+ return false;
333
+ // String (quoted)
334
+ if ((str.startsWith('"') && str.endsWith('"')) ||
335
+ (str.startsWith("'") && str.endsWith("'"))) {
336
+ return str.slice(1, -1);
337
+ }
338
+ // Number
339
+ const num = Number(str);
340
+ if (!isNaN(num))
341
+ return num;
342
+ // Return as string
343
+ return str;
344
+ }
345
+ // =============================================================================
346
+ // Aggregation Strategies
347
+ // =============================================================================
348
+ /**
349
+ * Deep merge two objects (later wins on conflict)
350
+ */
351
+ function deepMerge(target, source) {
352
+ if (source === null || source === undefined) {
353
+ return target;
354
+ }
355
+ if (typeof source !== 'object' || typeof target !== 'object') {
356
+ return source;
357
+ }
358
+ if (Array.isArray(source)) {
359
+ return source;
360
+ }
361
+ const result = { ...target };
362
+ for (const [key, value] of Object.entries(source)) {
363
+ if (key in result && typeof result[key] === 'object' && typeof value === 'object') {
364
+ result[key] = deepMerge(result[key], value);
365
+ }
366
+ else {
367
+ result[key] = value;
368
+ }
369
+ }
370
+ return result;
371
+ }
372
+ /**
373
+ * Aggregate multiple results using specified strategy
374
+ */
375
+ export function aggregateResults(results, strategy) {
376
+ if (results.length === 0) {
377
+ return {
378
+ ok: false,
379
+ meta: { confidence: 0, risk: 'high', explain: 'No results to aggregate' },
380
+ error: { code: 'E4012', message: 'No results to aggregate' }
381
+ };
382
+ }
383
+ if (results.length === 1) {
384
+ return results[0];
385
+ }
386
+ switch (strategy) {
387
+ case 'first': {
388
+ // Return first non-null successful result
389
+ const firstSuccess = results.find(r => r.ok);
390
+ return firstSuccess ?? results[0];
391
+ }
392
+ case 'array': {
393
+ // Collect all results into an array
394
+ const allData = results
395
+ .filter(r => r.ok && 'data' in r)
396
+ .map(r => r.data);
397
+ const allMeta = results
398
+ .filter(r => 'meta' in r)
399
+ .map(r => r.meta);
400
+ // Compute aggregate meta
401
+ const avgConfidence = allMeta.length > 0
402
+ ? allMeta.reduce((sum, m) => sum + m.confidence, 0) / allMeta.length
403
+ : 0.5;
404
+ const maxRisk = allMeta.length > 0
405
+ ? ['none', 'low', 'medium', 'high'][Math.max(...allMeta.map(m => ['none', 'low', 'medium', 'high'].indexOf(m.risk)))]
406
+ : 'medium';
407
+ return {
408
+ ok: true,
409
+ meta: {
410
+ confidence: avgConfidence,
411
+ risk: maxRisk,
412
+ explain: `Aggregated ${allData.length} results`
413
+ },
414
+ data: {
415
+ results: allData,
416
+ rationale: `Combined ${allData.length} module outputs into array`
417
+ }
418
+ };
419
+ }
420
+ case 'merge':
421
+ default: {
422
+ // Deep merge all results (later wins)
423
+ let mergedData = {};
424
+ let mergedMeta = {
425
+ confidence: 0.5,
426
+ risk: 'medium',
427
+ explain: ''
428
+ };
429
+ const explains = [];
430
+ let totalConfidence = 0;
431
+ let maxRiskLevel = 0;
432
+ const riskLevels = { none: 0, low: 1, medium: 2, high: 3 };
433
+ const riskNames = ['none', 'low', 'medium', 'high'];
434
+ for (const result of results) {
435
+ if (result.ok && 'data' in result) {
436
+ mergedData = deepMerge(mergedData, result.data);
437
+ }
438
+ if ('meta' in result) {
439
+ const meta = result.meta;
440
+ totalConfidence += meta.confidence;
441
+ maxRiskLevel = Math.max(maxRiskLevel, riskLevels[meta.risk] ?? 2);
442
+ if (meta.explain) {
443
+ explains.push(meta.explain);
444
+ }
445
+ }
446
+ }
447
+ mergedMeta = {
448
+ confidence: totalConfidence / results.length,
449
+ risk: riskNames[maxRiskLevel],
450
+ explain: explains.join('; ').slice(0, 280)
451
+ };
452
+ return {
453
+ ok: true,
454
+ meta: mergedMeta,
455
+ data: {
456
+ ...mergedData,
457
+ rationale: `Merged ${results.length} module outputs`
458
+ }
459
+ };
460
+ }
461
+ }
462
+ }
463
+ // =============================================================================
464
+ // Dependency Resolution
465
+ // =============================================================================
466
+ /**
467
+ * Check if version matches pattern (simplified semver)
468
+ */
469
+ export function versionMatches(version, pattern) {
470
+ if (!pattern || pattern === '*') {
471
+ return true;
472
+ }
473
+ // Parse version into parts
474
+ const parseVersion = (v) => {
475
+ return v.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
476
+ };
477
+ const vParts = parseVersion(version);
478
+ // Exact match
479
+ if (!pattern.startsWith('^') && !pattern.startsWith('~') && !pattern.startsWith('>') && !pattern.startsWith('<')) {
480
+ const pParts = parseVersion(pattern);
481
+ return vParts[0] === pParts[0] && vParts[1] === pParts[1] && vParts[2] === pParts[2];
482
+ }
483
+ // >= match
484
+ if (pattern.startsWith('>=')) {
485
+ const pParts = parseVersion(pattern.slice(2));
486
+ for (let i = 0; i < 3; i++) {
487
+ if (vParts[i] > pParts[i])
488
+ return true;
489
+ if (vParts[i] < pParts[i])
490
+ return false;
491
+ }
492
+ return true;
493
+ }
494
+ // > match
495
+ if (pattern.startsWith('>') && !pattern.startsWith('>=')) {
496
+ const pParts = parseVersion(pattern.slice(1));
497
+ for (let i = 0; i < 3; i++) {
498
+ if (vParts[i] > pParts[i])
499
+ return true;
500
+ if (vParts[i] < pParts[i])
501
+ return false;
502
+ }
503
+ return false;
504
+ }
505
+ // ^ (compatible) - same major
506
+ if (pattern.startsWith('^')) {
507
+ const pParts = parseVersion(pattern.slice(1));
508
+ return vParts[0] === pParts[0] &&
509
+ (vParts[1] > pParts[1] || (vParts[1] === pParts[1] && vParts[2] >= pParts[2]));
510
+ }
511
+ // ~ (patch only) - same major.minor
512
+ if (pattern.startsWith('~')) {
513
+ const pParts = parseVersion(pattern.slice(1));
514
+ return vParts[0] === pParts[0] && vParts[1] === pParts[1] && vParts[2] >= pParts[2];
515
+ }
516
+ return true;
517
+ }
518
+ /**
519
+ * Resolve a dependency, checking version and trying fallbacks
520
+ */
521
+ export async function resolveDependency(dep, searchPaths) {
522
+ // Try primary module
523
+ const module = await findModule(dep.name, searchPaths);
524
+ if (module) {
525
+ // Check version if specified
526
+ if (dep.version && !versionMatches(module.version, dep.version)) {
527
+ console.warn(`Module ${dep.name} version ${module.version} does not match ${dep.version}`);
528
+ if (!dep.optional) {
529
+ // Try fallback
530
+ if (dep.fallback) {
531
+ return findModule(dep.fallback, searchPaths);
532
+ }
533
+ return null;
534
+ }
535
+ }
536
+ return module;
537
+ }
538
+ // Try fallback
539
+ if (dep.fallback) {
540
+ return findModule(dep.fallback, searchPaths);
541
+ }
542
+ // Optional dependency not found
543
+ if (dep.optional) {
544
+ return null;
545
+ }
546
+ throw new Error(`Required dependency not found: ${dep.name}`);
547
+ }
548
+ // =============================================================================
549
+ // Composition Orchestrator
550
+ // =============================================================================
551
+ export class CompositionOrchestrator {
552
+ provider;
553
+ cwd;
554
+ searchPaths;
555
+ constructor(provider, cwd = process.cwd()) {
556
+ this.provider = provider;
557
+ this.cwd = cwd;
558
+ this.searchPaths = getDefaultSearchPaths(cwd);
559
+ }
560
+ /**
561
+ * Execute a composed module workflow
562
+ */
563
+ async execute(moduleName, input, options = {}) {
564
+ const startTime = Date.now();
565
+ const trace = [];
566
+ const moduleResults = {};
567
+ // Create context
568
+ const context = {
569
+ depth: 0,
570
+ maxDepth: options.maxDepth ?? 5,
571
+ results: {},
572
+ input,
573
+ running: new Set(),
574
+ startTime,
575
+ timeoutMs: options.timeoutMs,
576
+ iterationCount: 0
577
+ };
578
+ try {
579
+ // Load the main module
580
+ const module = await findModule(moduleName, this.searchPaths);
581
+ if (!module) {
582
+ return {
583
+ ok: false,
584
+ moduleResults: {},
585
+ trace: [],
586
+ totalTimeMs: Date.now() - startTime,
587
+ error: {
588
+ code: COMPOSITION_ERRORS.E4009,
589
+ message: `Module not found: ${moduleName}`
590
+ }
591
+ };
592
+ }
593
+ // Check if module has composition config
594
+ const composition = this.getCompositionConfig(module);
595
+ let result;
596
+ if (composition) {
597
+ // Execute composition workflow
598
+ result = await this.executeComposition(module, composition, context, trace);
599
+ }
600
+ else {
601
+ // Simple execution (no composition)
602
+ result = await this.executeModule(module, input, context, trace);
603
+ }
604
+ // Collect all results
605
+ for (const [name, res] of Object.entries(context.results)) {
606
+ moduleResults[name] = res;
607
+ }
608
+ moduleResults[moduleName] = result;
609
+ return {
610
+ ok: result.ok,
611
+ result,
612
+ moduleResults,
613
+ trace,
614
+ totalTimeMs: Date.now() - startTime
615
+ };
616
+ }
617
+ catch (error) {
618
+ return {
619
+ ok: false,
620
+ moduleResults,
621
+ trace,
622
+ totalTimeMs: Date.now() - startTime,
623
+ error: {
624
+ code: 'E4000',
625
+ message: error.message
626
+ }
627
+ };
628
+ }
629
+ }
630
+ /**
631
+ * Get composition config from module (if exists)
632
+ */
633
+ getCompositionConfig(module) {
634
+ // Check if module has composition in its metadata
635
+ const raw = module.composition;
636
+ return raw ?? null;
637
+ }
638
+ /**
639
+ * Execute composition based on pattern
640
+ */
641
+ async executeComposition(module, composition, context, trace) {
642
+ // Check timeout
643
+ if (this.isTimedOut(context)) {
644
+ return this.timeoutError(context);
645
+ }
646
+ // Check depth
647
+ if (context.depth > context.maxDepth) {
648
+ return {
649
+ ok: false,
650
+ meta: { confidence: 0, risk: 'high', explain: 'Max composition depth exceeded' },
651
+ error: { code: COMPOSITION_ERRORS.E4005, message: `Maximum composition depth (${context.maxDepth}) exceeded` }
652
+ };
653
+ }
654
+ // Resolve dependencies first
655
+ if (composition.requires) {
656
+ for (const dep of composition.requires) {
657
+ const resolved = await resolveDependency(dep, this.searchPaths);
658
+ if (!resolved && !dep.optional) {
659
+ return {
660
+ ok: false,
661
+ meta: { confidence: 0, risk: 'high', explain: `Dependency not found: ${dep.name}` },
662
+ error: { code: COMPOSITION_ERRORS.E4009, message: `Required dependency not found: ${dep.name}` }
663
+ };
664
+ }
665
+ }
666
+ }
667
+ // Execute based on pattern
668
+ switch (composition.pattern) {
669
+ case 'sequential':
670
+ return this.executeSequential(module, composition, context, trace);
671
+ case 'parallel':
672
+ return this.executeParallel(module, composition, context, trace);
673
+ case 'conditional':
674
+ return this.executeConditional(module, composition, context, trace);
675
+ case 'iterative':
676
+ return this.executeIterative(module, composition, context, trace);
677
+ default:
678
+ // Default to sequential
679
+ return this.executeSequential(module, composition, context, trace);
680
+ }
681
+ }
682
+ /**
683
+ * Execute sequential composition: A → B → C
684
+ */
685
+ async executeSequential(module, composition, context, trace) {
686
+ const dataflow = composition.dataflow ?? [];
687
+ let currentData = context.input;
688
+ let lastResult = null;
689
+ for (const step of dataflow) {
690
+ // Check timeout
691
+ if (this.isTimedOut(context)) {
692
+ return this.timeoutError(context);
693
+ }
694
+ // Check condition
695
+ if (step.condition) {
696
+ const conditionData = {
697
+ input: context.input,
698
+ ...context.results,
699
+ current: currentData
700
+ };
701
+ if (!evaluateCondition(step.condition, conditionData)) {
702
+ trace.push({
703
+ module: Array.isArray(step.to) ? step.to.join(',') : step.to,
704
+ startTime: Date.now(),
705
+ endTime: Date.now(),
706
+ durationMs: 0,
707
+ success: true,
708
+ skipped: true,
709
+ reason: `Condition not met: ${step.condition}`
710
+ });
711
+ continue;
712
+ }
713
+ }
714
+ // Get source data
715
+ const sources = Array.isArray(step.from) ? step.from : [step.from];
716
+ const sourceDataArray = [];
717
+ for (const source of sources) {
718
+ if (source === 'input') {
719
+ sourceDataArray.push(context.input);
720
+ }
721
+ else if (source.endsWith('.output')) {
722
+ const moduleName = source.replace('.output', '');
723
+ const moduleResult = context.results[moduleName];
724
+ if (moduleResult && 'data' in moduleResult) {
725
+ sourceDataArray.push(moduleResult.data);
726
+ }
727
+ }
728
+ else {
729
+ // Assume it's a module name
730
+ const moduleResult = context.results[source];
731
+ if (moduleResult && 'data' in moduleResult) {
732
+ sourceDataArray.push(moduleResult.data);
733
+ }
734
+ }
735
+ }
736
+ // Aggregate sources if multiple
737
+ let sourceData;
738
+ if (sourceDataArray.length === 1) {
739
+ sourceData = sourceDataArray[0];
740
+ }
741
+ else if (sourceDataArray.length > 1) {
742
+ sourceData = { sources: sourceDataArray };
743
+ }
744
+ else {
745
+ sourceData = currentData;
746
+ }
747
+ // Apply mapping
748
+ if (step.mapping) {
749
+ currentData = applyMapping(step.mapping, sourceData);
750
+ }
751
+ else {
752
+ currentData = sourceData;
753
+ }
754
+ // Execute target(s)
755
+ const targets = Array.isArray(step.to) ? step.to : [step.to];
756
+ if (targets.length === 1 && targets[0] === 'output') {
757
+ // Final output, no module to execute
758
+ continue;
759
+ }
760
+ for (const target of targets) {
761
+ if (target === 'output')
762
+ continue;
763
+ // Find and execute target module
764
+ const targetModule = await findModule(target, this.searchPaths);
765
+ if (!targetModule) {
766
+ return {
767
+ ok: false,
768
+ meta: { confidence: 0, risk: 'high', explain: `Target module not found: ${target}` },
769
+ error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${target}` }
770
+ };
771
+ }
772
+ // Get timeout for this dependency
773
+ const depConfig = composition.requires?.find(d => d.name === target);
774
+ const depTimeout = depConfig?.timeout_ms;
775
+ // Execute with timeout
776
+ lastResult = await this.executeModuleWithTimeout(targetModule, currentData, context, trace, depTimeout);
777
+ // Store result
778
+ context.results[target] = lastResult;
779
+ if (!lastResult.ok) {
780
+ // Check if we should use fallback
781
+ if (depConfig?.fallback) {
782
+ const fallbackModule = await findModule(depConfig.fallback, this.searchPaths);
783
+ if (fallbackModule) {
784
+ lastResult = await this.executeModuleWithTimeout(fallbackModule, currentData, context, trace, depTimeout);
785
+ context.results[depConfig.fallback] = lastResult;
786
+ }
787
+ }
788
+ if (!lastResult.ok && !depConfig?.optional) {
789
+ return lastResult;
790
+ }
791
+ }
792
+ // Update current data with result
793
+ if (lastResult.ok && 'data' in lastResult) {
794
+ currentData = lastResult.data;
795
+ }
796
+ }
797
+ }
798
+ // Return last result or execute main module
799
+ if (lastResult) {
800
+ return lastResult;
801
+ }
802
+ // Execute the main module with composed input
803
+ return this.executeModule(module, currentData, context, trace);
804
+ }
805
+ /**
806
+ * Execute parallel composition: A → [B, C, D] → Aggregator
807
+ */
808
+ async executeParallel(module, composition, context, trace) {
809
+ const dataflow = composition.dataflow ?? [];
810
+ // Find parallel execution steps (where 'to' is an array)
811
+ for (const step of dataflow) {
812
+ if (this.isTimedOut(context)) {
813
+ return this.timeoutError(context);
814
+ }
815
+ // Get source data
816
+ let sourceData = context.input;
817
+ if (step.from) {
818
+ const sources = Array.isArray(step.from) ? step.from : [step.from];
819
+ for (const source of sources) {
820
+ if (source === 'input') {
821
+ sourceData = context.input;
822
+ }
823
+ else {
824
+ const moduleName = source.replace('.output', '');
825
+ const moduleResult = context.results[moduleName];
826
+ if (moduleResult && 'data' in moduleResult) {
827
+ sourceData = moduleResult.data;
828
+ }
829
+ }
830
+ }
831
+ }
832
+ // Apply mapping
833
+ if (step.mapping) {
834
+ sourceData = applyMapping(step.mapping, sourceData);
835
+ }
836
+ // Get targets
837
+ const targets = Array.isArray(step.to) ? step.to : [step.to];
838
+ if (targets.every(t => t === 'output')) {
839
+ continue;
840
+ }
841
+ // Execute targets in parallel
842
+ const parallelPromises = [];
843
+ const parallelModules = [];
844
+ for (const target of targets) {
845
+ if (target === 'output')
846
+ continue;
847
+ const targetModule = await findModule(target, this.searchPaths);
848
+ if (!targetModule) {
849
+ if (!composition.requires?.find(d => d.name === target)?.optional) {
850
+ return {
851
+ ok: false,
852
+ meta: { confidence: 0, risk: 'high', explain: `Module not found: ${target}` },
853
+ error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${target}` }
854
+ };
855
+ }
856
+ continue;
857
+ }
858
+ const depConfig = composition.requires?.find(d => d.name === target);
859
+ const depTimeout = depConfig?.timeout_ms;
860
+ parallelModules.push(target);
861
+ parallelPromises.push(this.executeModuleWithTimeout(targetModule, sourceData, { ...context, running: new Set(context.running) }, trace, depTimeout));
862
+ }
863
+ // Wait for all parallel executions
864
+ const parallelResults = await Promise.all(parallelPromises);
865
+ // Store results
866
+ for (let i = 0; i < parallelModules.length; i++) {
867
+ context.results[parallelModules[i]] = parallelResults[i];
868
+ }
869
+ // Check for failures
870
+ const failures = parallelResults.filter(r => !r.ok);
871
+ if (failures.length > 0) {
872
+ // Check if all failed modules are optional
873
+ const allOptional = parallelModules.every((name, i) => {
874
+ if (parallelResults[i].ok)
875
+ return true;
876
+ return composition.requires?.find(d => d.name === name)?.optional;
877
+ });
878
+ if (!allOptional) {
879
+ return failures[0];
880
+ }
881
+ }
882
+ }
883
+ // Aggregate results
884
+ const aggregateStep = dataflow.find(s => {
885
+ const targets = Array.isArray(s.to) ? s.to : [s.to];
886
+ return targets.includes('output');
887
+ });
888
+ if (aggregateStep && Array.isArray(aggregateStep.from)) {
889
+ const resultsToAggregate = [];
890
+ for (const source of aggregateStep.from) {
891
+ const moduleName = source.replace('.output', '');
892
+ const result = context.results[moduleName];
893
+ if (result) {
894
+ resultsToAggregate.push(result);
895
+ }
896
+ }
897
+ const strategy = aggregateStep.aggregate ?? 'merge';
898
+ return aggregateResults(resultsToAggregate, strategy);
899
+ }
900
+ // Execute main module with all results
901
+ return this.executeModule(module, context.input, context, trace);
902
+ }
903
+ /**
904
+ * Execute conditional composition: A → (condition) → B or C
905
+ */
906
+ async executeConditional(module, composition, context, trace) {
907
+ const routing = composition.routing ?? [];
908
+ // First, execute the initial module to get data for conditions
909
+ const dataflow = composition.dataflow ?? [];
910
+ let conditionData = context.input;
911
+ // Execute initial steps
912
+ for (const step of dataflow) {
913
+ if (this.isTimedOut(context)) {
914
+ return this.timeoutError(context);
915
+ }
916
+ const targets = Array.isArray(step.to) ? step.to : [step.to];
917
+ // Execute non-routing targets
918
+ for (const target of targets) {
919
+ if (target === 'output')
920
+ continue;
921
+ // Check if this target is involved in routing
922
+ const isRoutingTarget = routing.some(r => r.next === target);
923
+ if (isRoutingTarget)
924
+ continue;
925
+ const targetModule = await findModule(target, this.searchPaths);
926
+ if (!targetModule)
927
+ continue;
928
+ // Get source data
929
+ let sourceData = context.input;
930
+ if (step.from) {
931
+ const source = Array.isArray(step.from) ? step.from[0] : step.from;
932
+ if (source !== 'input') {
933
+ const moduleName = source.replace('.output', '');
934
+ const result = context.results[moduleName];
935
+ if (result && 'data' in result) {
936
+ sourceData = result.data;
937
+ }
938
+ }
939
+ }
940
+ if (step.mapping) {
941
+ sourceData = applyMapping(step.mapping, sourceData);
942
+ }
943
+ const result = await this.executeModule(targetModule, sourceData, context, trace);
944
+ context.results[target] = result;
945
+ // Update condition data - include full result (meta + data) for routing conditions
946
+ conditionData = {
947
+ input: context.input,
948
+ ...Object.fromEntries(Object.entries(context.results).map(([k, v]) => [
949
+ k,
950
+ v // Keep full result including meta and data
951
+ ]))
952
+ };
953
+ }
954
+ }
955
+ // Evaluate routing conditions
956
+ // Build condition data with proper structure for accessing $.module-name.meta.confidence
957
+ const routingConditionData = {
958
+ input: context.input,
959
+ ...Object.fromEntries(Object.entries(context.results).map(([k, v]) => [k, v]))
960
+ };
961
+ for (const rule of routing) {
962
+ const matches = evaluateCondition(rule.condition, routingConditionData);
963
+ if (matches) {
964
+ if (rule.next === null) {
965
+ // Use current result directly
966
+ const lastResult = Object.values(context.results).pop();
967
+ return lastResult ?? this.executeModule(module, context.input, context, trace);
968
+ }
969
+ // Execute the next module
970
+ const nextModule = await findModule(rule.next, this.searchPaths);
971
+ if (!nextModule) {
972
+ return {
973
+ ok: false,
974
+ meta: { confidence: 0, risk: 'high', explain: `Routing target not found: ${rule.next}` },
975
+ error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${rule.next}` }
976
+ };
977
+ }
978
+ // Pass through the data
979
+ let nextInput = context.input;
980
+ const lastDataflowStep = dataflow[dataflow.length - 1];
981
+ if (lastDataflowStep?.mapping) {
982
+ nextInput = applyMapping(lastDataflowStep.mapping, conditionData);
983
+ }
984
+ return this.executeModule(nextModule, nextInput, context, trace);
985
+ }
986
+ }
987
+ // No routing matched, execute main module
988
+ return this.executeModule(module, context.input, context, trace);
989
+ }
990
+ /**
991
+ * Execute iterative composition: A → (check) → A → ... → Done
992
+ */
993
+ async executeIterative(module, composition, context, trace) {
994
+ const maxIterations = composition.iteration?.max_iterations ?? 10;
995
+ const continueCondition = composition.iteration?.continue_condition;
996
+ const stopCondition = composition.iteration?.stop_condition;
997
+ let currentInput = context.input;
998
+ let lastResult = null;
999
+ let iteration = 0;
1000
+ while (iteration < maxIterations) {
1001
+ if (this.isTimedOut(context)) {
1002
+ return this.timeoutError(context);
1003
+ }
1004
+ // Execute the module
1005
+ lastResult = await this.executeModule(module, currentInput, context, trace);
1006
+ context.iterationCount = iteration + 1;
1007
+ // Store result with iteration number
1008
+ context.results[`${module.name}_iteration_${iteration}`] = lastResult;
1009
+ // Check stop condition
1010
+ if (stopCondition) {
1011
+ const stopData = {
1012
+ input: context.input,
1013
+ current: lastResult,
1014
+ iteration,
1015
+ meta: lastResult && 'meta' in lastResult ? lastResult.meta : null,
1016
+ data: lastResult && 'data' in lastResult ? lastResult.data : null
1017
+ };
1018
+ if (evaluateCondition(stopCondition, stopData)) {
1019
+ break;
1020
+ }
1021
+ }
1022
+ // Check continue condition
1023
+ if (continueCondition) {
1024
+ const continueData = {
1025
+ input: context.input,
1026
+ current: lastResult,
1027
+ iteration,
1028
+ meta: lastResult && 'meta' in lastResult ? lastResult.meta : null,
1029
+ data: lastResult && 'data' in lastResult ? lastResult.data : null
1030
+ };
1031
+ if (!evaluateCondition(continueCondition, continueData)) {
1032
+ break;
1033
+ }
1034
+ }
1035
+ else {
1036
+ // No continue condition and no stop condition - only run once
1037
+ break;
1038
+ }
1039
+ // Update input for next iteration
1040
+ if (lastResult.ok && 'data' in lastResult) {
1041
+ currentInput = lastResult.data;
1042
+ }
1043
+ iteration++;
1044
+ }
1045
+ // Check if we hit iteration limit
1046
+ if (iteration >= maxIterations && continueCondition) {
1047
+ return {
1048
+ ok: false,
1049
+ meta: {
1050
+ confidence: 0.5,
1051
+ risk: 'medium',
1052
+ explain: `Iteration limit (${maxIterations}) reached`
1053
+ },
1054
+ error: {
1055
+ code: COMPOSITION_ERRORS.E4013,
1056
+ message: `Maximum iterations (${maxIterations}) exceeded`
1057
+ },
1058
+ partial_data: lastResult && 'data' in lastResult
1059
+ ? lastResult.data
1060
+ : undefined
1061
+ };
1062
+ }
1063
+ return lastResult ?? {
1064
+ ok: false,
1065
+ meta: { confidence: 0, risk: 'high', explain: 'No iteration executed' },
1066
+ error: { code: 'E4000', message: 'Iterative composition produced no result' }
1067
+ };
1068
+ }
1069
+ /**
1070
+ * Execute a single module
1071
+ */
1072
+ async executeModule(module, input, context, trace) {
1073
+ // Check depth limit
1074
+ if (context.depth >= context.maxDepth) {
1075
+ return {
1076
+ ok: false,
1077
+ meta: { confidence: 0, risk: 'high', explain: `Max depth exceeded at ${module.name}` },
1078
+ error: {
1079
+ code: COMPOSITION_ERRORS.E4005,
1080
+ message: `Maximum composition depth (${context.maxDepth}) exceeded at module: ${module.name}`
1081
+ }
1082
+ };
1083
+ }
1084
+ // Check for circular dependency
1085
+ if (context.running.has(module.name)) {
1086
+ trace.push({
1087
+ module: module.name,
1088
+ startTime: Date.now(),
1089
+ endTime: Date.now(),
1090
+ durationMs: 0,
1091
+ success: false,
1092
+ reason: 'Circular dependency detected'
1093
+ });
1094
+ return {
1095
+ ok: false,
1096
+ meta: { confidence: 0, risk: 'high', explain: `Circular dependency: ${module.name}` },
1097
+ error: {
1098
+ code: COMPOSITION_ERRORS.E4004,
1099
+ message: `Circular dependency detected: ${module.name}`
1100
+ }
1101
+ };
1102
+ }
1103
+ context.running.add(module.name);
1104
+ context.depth++; // Increment depth when entering module
1105
+ const startTime = Date.now();
1106
+ try {
1107
+ const result = await runModule(module, this.provider, {
1108
+ input,
1109
+ validateInput: true,
1110
+ validateOutput: true,
1111
+ useV22: true
1112
+ });
1113
+ const endTime = Date.now();
1114
+ trace.push({
1115
+ module: module.name,
1116
+ startTime,
1117
+ endTime,
1118
+ durationMs: endTime - startTime,
1119
+ success: result.ok
1120
+ });
1121
+ return result;
1122
+ }
1123
+ catch (error) {
1124
+ const endTime = Date.now();
1125
+ trace.push({
1126
+ module: module.name,
1127
+ startTime,
1128
+ endTime,
1129
+ durationMs: endTime - startTime,
1130
+ success: false,
1131
+ reason: error.message
1132
+ });
1133
+ return {
1134
+ ok: false,
1135
+ meta: { confidence: 0, risk: 'high', explain: error.message.slice(0, 280) },
1136
+ error: { code: 'E4000', message: error.message }
1137
+ };
1138
+ }
1139
+ finally {
1140
+ context.running.delete(module.name);
1141
+ context.depth--; // Decrement depth when exiting module
1142
+ }
1143
+ }
1144
+ /**
1145
+ * Execute module with timeout
1146
+ */
1147
+ async executeModuleWithTimeout(module, input, context, trace, timeoutMs) {
1148
+ const timeout = timeoutMs ?? context.timeoutMs;
1149
+ if (!timeout) {
1150
+ return this.executeModule(module, input, context, trace);
1151
+ }
1152
+ let timeoutId = null;
1153
+ const timeoutPromise = new Promise((_, reject) => {
1154
+ timeoutId = setTimeout(() => {
1155
+ reject(new Error(`Module ${module.name} timed out after ${timeout}ms`));
1156
+ }, timeout);
1157
+ });
1158
+ try {
1159
+ const result = await Promise.race([
1160
+ this.executeModule(module, input, context, trace),
1161
+ timeoutPromise
1162
+ ]);
1163
+ // Clear timeout on success to prevent memory leak
1164
+ if (timeoutId)
1165
+ clearTimeout(timeoutId);
1166
+ return result;
1167
+ }
1168
+ catch (error) {
1169
+ // Clear timeout on error too
1170
+ if (timeoutId)
1171
+ clearTimeout(timeoutId);
1172
+ return {
1173
+ ok: false,
1174
+ meta: { confidence: 0, risk: 'high', explain: `Timeout: ${module.name}` },
1175
+ error: {
1176
+ code: COMPOSITION_ERRORS.E4008,
1177
+ message: error.message
1178
+ }
1179
+ };
1180
+ }
1181
+ }
1182
+ /**
1183
+ * Check if composition has timed out
1184
+ */
1185
+ isTimedOut(context) {
1186
+ if (!context.timeoutMs)
1187
+ return false;
1188
+ return Date.now() - context.startTime > context.timeoutMs;
1189
+ }
1190
+ /**
1191
+ * Create timeout error response
1192
+ */
1193
+ timeoutError(context) {
1194
+ const elapsed = Date.now() - context.startTime;
1195
+ return {
1196
+ ok: false,
1197
+ meta: {
1198
+ confidence: 0,
1199
+ risk: 'high',
1200
+ explain: `Composition timed out after ${elapsed}ms`
1201
+ },
1202
+ error: {
1203
+ code: COMPOSITION_ERRORS.E4008,
1204
+ message: `Composition timeout: ${elapsed}ms exceeded ${context.timeoutMs}ms limit`
1205
+ }
1206
+ };
1207
+ }
1208
+ }
1209
+ // =============================================================================
1210
+ // Convenience Functions
1211
+ // =============================================================================
1212
+ /**
1213
+ * Execute a composed module workflow
1214
+ */
1215
+ export async function executeComposition(moduleName, input, provider, options = {}) {
1216
+ const { cwd = process.cwd(), ...execOptions } = options;
1217
+ const orchestrator = new CompositionOrchestrator(provider, cwd);
1218
+ return orchestrator.execute(moduleName, input, execOptions);
1219
+ }
1220
+ /**
1221
+ * Validate composition configuration
1222
+ */
1223
+ export function validateCompositionConfig(config) {
1224
+ const errors = [];
1225
+ // Check pattern
1226
+ const validPatterns = ['sequential', 'parallel', 'conditional', 'iterative'];
1227
+ if (!validPatterns.includes(config.pattern)) {
1228
+ errors.push(`Invalid pattern: ${config.pattern}. Must be one of: ${validPatterns.join(', ')}`);
1229
+ }
1230
+ // Check dataflow steps
1231
+ if (config.dataflow) {
1232
+ for (let i = 0; i < config.dataflow.length; i++) {
1233
+ const step = config.dataflow[i];
1234
+ if (!step.from) {
1235
+ errors.push(`Dataflow step ${i}: missing 'from' field`);
1236
+ }
1237
+ if (!step.to) {
1238
+ errors.push(`Dataflow step ${i}: missing 'to' field`);
1239
+ }
1240
+ }
1241
+ }
1242
+ // Check routing rules for conditional pattern
1243
+ if (config.pattern === 'conditional' && (!config.routing || config.routing.length === 0)) {
1244
+ errors.push('Conditional pattern requires routing rules');
1245
+ }
1246
+ // Check iteration config for iterative pattern
1247
+ if (config.pattern === 'iterative') {
1248
+ const iter = config.iteration;
1249
+ if (!iter?.continue_condition && !iter?.stop_condition) {
1250
+ errors.push('Iterative pattern requires either continue_condition or stop_condition');
1251
+ }
1252
+ }
1253
+ // Check dependencies
1254
+ if (config.requires) {
1255
+ for (const dep of config.requires) {
1256
+ if (!dep.name) {
1257
+ errors.push('Dependency missing name');
1258
+ }
1259
+ }
1260
+ }
1261
+ return {
1262
+ valid: errors.length === 0,
1263
+ errors
1264
+ };
1265
+ }