cognitive-modules-cli 2.2.0 → 2.2.5

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +572 -28
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +222 -13
  7. package/dist/commands/compose.d.ts +31 -0
  8. package/dist/commands/compose.js +185 -0
  9. package/dist/commands/index.d.ts +5 -0
  10. package/dist/commands/index.js +5 -0
  11. package/dist/commands/init.js +23 -1
  12. package/dist/commands/migrate.d.ts +30 -0
  13. package/dist/commands/migrate.js +650 -0
  14. package/dist/commands/pipe.d.ts +1 -0
  15. package/dist/commands/pipe.js +31 -11
  16. package/dist/commands/remove.js +33 -2
  17. package/dist/commands/run.d.ts +1 -0
  18. package/dist/commands/run.js +37 -27
  19. package/dist/commands/search.d.ts +28 -0
  20. package/dist/commands/search.js +143 -0
  21. package/dist/commands/test.d.ts +65 -0
  22. package/dist/commands/test.js +454 -0
  23. package/dist/commands/update.d.ts +1 -0
  24. package/dist/commands/update.js +106 -14
  25. package/dist/commands/validate.d.ts +36 -0
  26. package/dist/commands/validate.js +97 -0
  27. package/dist/errors/index.d.ts +218 -0
  28. package/dist/errors/index.js +412 -0
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +5 -1
  31. package/dist/mcp/server.js +84 -79
  32. package/dist/modules/composition.d.ts +251 -0
  33. package/dist/modules/composition.js +1330 -0
  34. package/dist/modules/index.d.ts +2 -0
  35. package/dist/modules/index.js +2 -0
  36. package/dist/modules/loader.d.ts +22 -2
  37. package/dist/modules/loader.js +171 -6
  38. package/dist/modules/runner.d.ts +422 -1
  39. package/dist/modules/runner.js +1472 -71
  40. package/dist/modules/subagent.d.ts +6 -1
  41. package/dist/modules/subagent.js +20 -13
  42. package/dist/modules/validator.d.ts +28 -0
  43. package/dist/modules/validator.js +637 -0
  44. package/dist/providers/anthropic.d.ts +15 -0
  45. package/dist/providers/anthropic.js +147 -5
  46. package/dist/providers/base.d.ts +11 -0
  47. package/dist/providers/base.js +18 -0
  48. package/dist/providers/gemini.d.ts +15 -0
  49. package/dist/providers/gemini.js +122 -5
  50. package/dist/providers/ollama.d.ts +15 -0
  51. package/dist/providers/ollama.js +111 -3
  52. package/dist/providers/openai.d.ts +11 -0
  53. package/dist/providers/openai.js +133 -0
  54. package/dist/registry/client.d.ts +204 -0
  55. package/dist/registry/client.js +356 -0
  56. package/dist/registry/index.d.ts +4 -0
  57. package/dist/registry/index.js +4 -0
  58. package/dist/server/http.js +173 -42
  59. package/dist/types.d.ts +123 -8
  60. package/dist/types.js +4 -1
  61. package/dist/version.d.ts +1 -0
  62. package/dist/version.js +4 -0
  63. package/package.json +32 -7
  64. package/src/cli.ts +0 -410
  65. package/src/commands/add.ts +0 -315
  66. package/src/commands/index.ts +0 -12
  67. package/src/commands/init.ts +0 -94
  68. package/src/commands/list.ts +0 -33
  69. package/src/commands/pipe.ts +0 -76
  70. package/src/commands/remove.ts +0 -57
  71. package/src/commands/run.ts +0 -80
  72. package/src/commands/update.ts +0 -130
  73. package/src/commands/versions.ts +0 -79
  74. package/src/index.ts +0 -55
  75. package/src/mcp/index.ts +0 -5
  76. package/src/mcp/server.ts +0 -403
  77. package/src/modules/index.ts +0 -7
  78. package/src/modules/loader.ts +0 -318
  79. package/src/modules/runner.ts +0 -495
  80. package/src/modules/subagent.ts +0 -275
  81. package/src/providers/anthropic.ts +0 -89
  82. package/src/providers/base.ts +0 -29
  83. package/src/providers/deepseek.ts +0 -83
  84. package/src/providers/gemini.ts +0 -117
  85. package/src/providers/index.ts +0 -78
  86. package/src/providers/minimax.ts +0 -81
  87. package/src/providers/moonshot.ts +0 -82
  88. package/src/providers/ollama.ts +0 -83
  89. package/src/providers/openai.ts +0 -84
  90. package/src/providers/qwen.ts +0 -82
  91. package/src/server/http.ts +0 -316
  92. package/src/server/index.ts +0 -6
  93. package/src/types.ts +0 -495
  94. package/tsconfig.json +0 -17
@@ -0,0 +1,1330 @@
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 riskLevels = { none: 0, low: 1, medium: 2, high: 3 };
405
+ const riskNames = ['none', 'low', 'medium', 'high'];
406
+ const maxRiskLevel = allMeta.length > 0
407
+ ? Math.max(...allMeta.map(m => riskLevels[m.risk] ?? 2))
408
+ : 2;
409
+ const maxRisk = riskNames[maxRiskLevel];
410
+ return {
411
+ ok: true,
412
+ meta: {
413
+ confidence: avgConfidence,
414
+ risk: maxRisk,
415
+ explain: `Aggregated ${allData.length} results`
416
+ },
417
+ data: {
418
+ results: allData,
419
+ rationale: `Combined ${allData.length} module outputs into array`
420
+ }
421
+ };
422
+ }
423
+ case 'merge':
424
+ default: {
425
+ // Deep merge all results (later wins)
426
+ let mergedData = {};
427
+ let mergedMeta = {
428
+ confidence: 0.5,
429
+ risk: 'medium',
430
+ explain: ''
431
+ };
432
+ const explains = [];
433
+ let totalConfidence = 0;
434
+ let maxRiskLevel = 0;
435
+ const riskLevels = { none: 0, low: 1, medium: 2, high: 3 };
436
+ const riskNames = ['none', 'low', 'medium', 'high'];
437
+ for (const result of results) {
438
+ if (result.ok && 'data' in result) {
439
+ mergedData = deepMerge(mergedData, result.data);
440
+ }
441
+ if ('meta' in result) {
442
+ const meta = result.meta;
443
+ totalConfidence += meta.confidence;
444
+ maxRiskLevel = Math.max(maxRiskLevel, riskLevels[meta.risk] ?? 2);
445
+ if (meta.explain) {
446
+ explains.push(meta.explain);
447
+ }
448
+ }
449
+ }
450
+ mergedMeta = {
451
+ confidence: totalConfidence / results.length,
452
+ risk: riskNames[maxRiskLevel],
453
+ explain: explains.join('; ').slice(0, 280)
454
+ };
455
+ return {
456
+ ok: true,
457
+ meta: mergedMeta,
458
+ data: {
459
+ ...mergedData,
460
+ rationale: `Merged ${results.length} module outputs`
461
+ }
462
+ };
463
+ }
464
+ }
465
+ }
466
+ // =============================================================================
467
+ // Dependency Resolution
468
+ // =============================================================================
469
+ /**
470
+ * Check if version matches pattern (simplified semver)
471
+ */
472
+ export function versionMatches(version, pattern) {
473
+ if (!pattern || pattern === '*') {
474
+ return true;
475
+ }
476
+ // Parse version into parts
477
+ const parseVersion = (v) => {
478
+ return v.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
479
+ };
480
+ const vParts = parseVersion(version);
481
+ // Exact match
482
+ if (!pattern.startsWith('^') && !pattern.startsWith('~') && !pattern.startsWith('>') && !pattern.startsWith('<')) {
483
+ const pParts = parseVersion(pattern);
484
+ return vParts[0] === pParts[0] && vParts[1] === pParts[1] && vParts[2] === pParts[2];
485
+ }
486
+ // >= match
487
+ if (pattern.startsWith('>=')) {
488
+ const pParts = parseVersion(pattern.slice(2));
489
+ for (let i = 0; i < 3; i++) {
490
+ if (vParts[i] > pParts[i])
491
+ return true;
492
+ if (vParts[i] < pParts[i])
493
+ return false;
494
+ }
495
+ return true;
496
+ }
497
+ // > match
498
+ if (pattern.startsWith('>') && !pattern.startsWith('>=')) {
499
+ const pParts = parseVersion(pattern.slice(1));
500
+ for (let i = 0; i < 3; i++) {
501
+ if (vParts[i] > pParts[i])
502
+ return true;
503
+ if (vParts[i] < pParts[i])
504
+ return false;
505
+ }
506
+ return false;
507
+ }
508
+ // <= match
509
+ if (pattern.startsWith('<=')) {
510
+ const pParts = parseVersion(pattern.slice(2));
511
+ for (let i = 0; i < 3; i++) {
512
+ if (vParts[i] < pParts[i])
513
+ return true;
514
+ if (vParts[i] > pParts[i])
515
+ return false;
516
+ }
517
+ return true;
518
+ }
519
+ // < match
520
+ if (pattern.startsWith('<') && !pattern.startsWith('<=')) {
521
+ const pParts = parseVersion(pattern.slice(1));
522
+ for (let i = 0; i < 3; i++) {
523
+ if (vParts[i] < pParts[i])
524
+ return true;
525
+ if (vParts[i] > pParts[i])
526
+ return false;
527
+ }
528
+ return false;
529
+ }
530
+ // ^ (compatible) - same major
531
+ if (pattern.startsWith('^')) {
532
+ const pParts = parseVersion(pattern.slice(1));
533
+ return vParts[0] === pParts[0] &&
534
+ (vParts[1] > pParts[1] || (vParts[1] === pParts[1] && vParts[2] >= pParts[2]));
535
+ }
536
+ // ~ (patch only) - same major.minor
537
+ if (pattern.startsWith('~')) {
538
+ const pParts = parseVersion(pattern.slice(1));
539
+ return vParts[0] === pParts[0] && vParts[1] === pParts[1] && vParts[2] >= pParts[2];
540
+ }
541
+ return true;
542
+ }
543
+ /**
544
+ * Resolve a dependency, checking version and trying fallbacks
545
+ */
546
+ export async function resolveDependency(dep, searchPaths) {
547
+ // Try primary module
548
+ const module = await findModule(dep.name, searchPaths);
549
+ if (module) {
550
+ // Check version if specified
551
+ if (dep.version && !versionMatches(module.version, dep.version)) {
552
+ console.warn(`Module ${dep.name} version ${module.version} does not match ${dep.version}`);
553
+ if (!dep.optional) {
554
+ // Try fallback
555
+ if (dep.fallback) {
556
+ return findModule(dep.fallback, searchPaths);
557
+ }
558
+ return null;
559
+ }
560
+ }
561
+ return module;
562
+ }
563
+ // Try fallback
564
+ if (dep.fallback) {
565
+ return findModule(dep.fallback, searchPaths);
566
+ }
567
+ // Optional dependency not found
568
+ if (dep.optional) {
569
+ return null;
570
+ }
571
+ throw new Error(`Required dependency not found: ${dep.name}`);
572
+ }
573
+ // =============================================================================
574
+ // Composition Orchestrator
575
+ // =============================================================================
576
+ export class CompositionOrchestrator {
577
+ provider;
578
+ cwd;
579
+ searchPaths;
580
+ constructor(provider, cwd = process.cwd()) {
581
+ this.provider = provider;
582
+ this.cwd = cwd;
583
+ this.searchPaths = getDefaultSearchPaths(cwd);
584
+ }
585
+ /**
586
+ * Execute a composed module workflow
587
+ */
588
+ async execute(moduleName, input, options = {}) {
589
+ const startTime = Date.now();
590
+ const trace = [];
591
+ const moduleResults = {};
592
+ // Create context
593
+ const context = {
594
+ depth: 0,
595
+ maxDepth: options.maxDepth ?? 5,
596
+ results: {},
597
+ input,
598
+ running: new Set(),
599
+ startTime,
600
+ timeoutMs: options.timeoutMs,
601
+ iterationCount: 0
602
+ };
603
+ try {
604
+ // Load the main module
605
+ const module = await findModule(moduleName, this.searchPaths);
606
+ if (!module) {
607
+ return {
608
+ ok: false,
609
+ moduleResults: {},
610
+ trace: [],
611
+ totalTimeMs: Date.now() - startTime,
612
+ error: {
613
+ code: COMPOSITION_ERRORS.E4009,
614
+ message: `Module not found: ${moduleName}`
615
+ }
616
+ };
617
+ }
618
+ // Check if module has composition config
619
+ const composition = this.getCompositionConfig(module);
620
+ let result;
621
+ if (composition) {
622
+ // Execute composition workflow
623
+ result = await this.executeComposition(module, composition, context, trace);
624
+ }
625
+ else {
626
+ // Simple execution (no composition)
627
+ result = await this.executeModule(module, input, context, trace);
628
+ }
629
+ // Collect all results
630
+ for (const [name, res] of Object.entries(context.results)) {
631
+ moduleResults[name] = res;
632
+ }
633
+ moduleResults[moduleName] = result;
634
+ return {
635
+ ok: result.ok,
636
+ result,
637
+ moduleResults,
638
+ trace,
639
+ totalTimeMs: Date.now() - startTime
640
+ };
641
+ }
642
+ catch (error) {
643
+ return {
644
+ ok: false,
645
+ moduleResults,
646
+ trace,
647
+ totalTimeMs: Date.now() - startTime,
648
+ error: {
649
+ code: 'E4000',
650
+ message: error.message
651
+ }
652
+ };
653
+ }
654
+ }
655
+ /**
656
+ * Get composition config from module (if exists)
657
+ */
658
+ getCompositionConfig(module) {
659
+ // Check if module has composition in its metadata
660
+ const raw = module.composition;
661
+ return raw ?? null;
662
+ }
663
+ /**
664
+ * Execute composition based on pattern
665
+ */
666
+ async executeComposition(module, composition, context, trace) {
667
+ // Check timeout
668
+ if (this.isTimedOut(context)) {
669
+ return this.timeoutError(context);
670
+ }
671
+ // Check depth
672
+ if (context.depth > context.maxDepth) {
673
+ return {
674
+ ok: false,
675
+ meta: { confidence: 0, risk: 'high', explain: 'Max composition depth exceeded' },
676
+ error: { code: COMPOSITION_ERRORS.E4005, message: `Maximum composition depth (${context.maxDepth}) exceeded` }
677
+ };
678
+ }
679
+ // Resolve dependencies first
680
+ if (composition.requires) {
681
+ for (const dep of composition.requires) {
682
+ const resolved = await resolveDependency(dep, this.searchPaths);
683
+ if (!resolved && !dep.optional) {
684
+ return {
685
+ ok: false,
686
+ meta: { confidence: 0, risk: 'high', explain: `Dependency not found: ${dep.name}` },
687
+ error: { code: COMPOSITION_ERRORS.E4009, message: `Required dependency not found: ${dep.name}` }
688
+ };
689
+ }
690
+ }
691
+ }
692
+ // Execute based on pattern
693
+ switch (composition.pattern) {
694
+ case 'sequential':
695
+ return this.executeSequential(module, composition, context, trace);
696
+ case 'parallel':
697
+ return this.executeParallel(module, composition, context, trace);
698
+ case 'conditional':
699
+ return this.executeConditional(module, composition, context, trace);
700
+ case 'iterative':
701
+ return this.executeIterative(module, composition, context, trace);
702
+ default:
703
+ // Default to sequential
704
+ return this.executeSequential(module, composition, context, trace);
705
+ }
706
+ }
707
+ /**
708
+ * Execute sequential composition: A → B → C
709
+ */
710
+ async executeSequential(module, composition, context, trace) {
711
+ const dataflow = composition.dataflow ?? [];
712
+ let currentData = context.input;
713
+ let lastResult = null;
714
+ for (const step of dataflow) {
715
+ // Check timeout
716
+ if (this.isTimedOut(context)) {
717
+ return this.timeoutError(context);
718
+ }
719
+ // Check condition
720
+ if (step.condition) {
721
+ const conditionData = {
722
+ input: context.input,
723
+ ...context.results,
724
+ current: currentData
725
+ };
726
+ if (!evaluateCondition(step.condition, conditionData)) {
727
+ trace.push({
728
+ module: Array.isArray(step.to) ? step.to.join(',') : step.to,
729
+ startTime: Date.now(),
730
+ endTime: Date.now(),
731
+ durationMs: 0,
732
+ success: true,
733
+ skipped: true,
734
+ reason: `Condition not met: ${step.condition}`
735
+ });
736
+ continue;
737
+ }
738
+ }
739
+ // Get source data
740
+ const sources = Array.isArray(step.from) ? step.from : [step.from];
741
+ const sourceDataArray = [];
742
+ for (const source of sources) {
743
+ if (source === 'input') {
744
+ sourceDataArray.push(context.input);
745
+ }
746
+ else if (source.endsWith('.output')) {
747
+ const moduleName = source.replace('.output', '');
748
+ const moduleResult = context.results[moduleName];
749
+ if (moduleResult && 'data' in moduleResult) {
750
+ sourceDataArray.push(moduleResult.data);
751
+ }
752
+ }
753
+ else {
754
+ // Assume it's a module name
755
+ const moduleResult = context.results[source];
756
+ if (moduleResult && 'data' in moduleResult) {
757
+ sourceDataArray.push(moduleResult.data);
758
+ }
759
+ }
760
+ }
761
+ // Aggregate sources if multiple
762
+ let sourceData;
763
+ if (sourceDataArray.length === 1) {
764
+ sourceData = sourceDataArray[0];
765
+ }
766
+ else if (sourceDataArray.length > 1) {
767
+ sourceData = { sources: sourceDataArray };
768
+ }
769
+ else {
770
+ sourceData = currentData;
771
+ }
772
+ // Apply mapping
773
+ if (step.mapping) {
774
+ currentData = applyMapping(step.mapping, sourceData);
775
+ }
776
+ else {
777
+ currentData = sourceData;
778
+ }
779
+ // Execute target(s)
780
+ const targets = Array.isArray(step.to) ? step.to : [step.to];
781
+ if (targets.length === 1 && targets[0] === 'output') {
782
+ // Final output, no module to execute
783
+ continue;
784
+ }
785
+ for (const target of targets) {
786
+ if (target === 'output')
787
+ continue;
788
+ // Find and execute target module
789
+ const targetModule = await findModule(target, this.searchPaths);
790
+ if (!targetModule) {
791
+ return {
792
+ ok: false,
793
+ meta: { confidence: 0, risk: 'high', explain: `Target module not found: ${target}` },
794
+ error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${target}` }
795
+ };
796
+ }
797
+ // Get timeout for this dependency
798
+ const depConfig = composition.requires?.find(d => d.name === target);
799
+ const depTimeout = depConfig?.timeout_ms;
800
+ // Execute with timeout
801
+ lastResult = await this.executeModuleWithTimeout(targetModule, currentData, context, trace, depTimeout);
802
+ // Store result
803
+ context.results[target] = lastResult;
804
+ if (!lastResult.ok) {
805
+ // Check if we should use fallback
806
+ if (depConfig?.fallback) {
807
+ const fallbackModule = await findModule(depConfig.fallback, this.searchPaths);
808
+ if (fallbackModule) {
809
+ lastResult = await this.executeModuleWithTimeout(fallbackModule, currentData, context, trace, depTimeout);
810
+ context.results[depConfig.fallback] = lastResult;
811
+ }
812
+ }
813
+ if (!lastResult.ok && !depConfig?.optional) {
814
+ return lastResult;
815
+ }
816
+ }
817
+ // Update current data with result
818
+ if (lastResult.ok && 'data' in lastResult) {
819
+ currentData = lastResult.data;
820
+ }
821
+ }
822
+ }
823
+ // Return last result or execute main module
824
+ if (lastResult) {
825
+ return lastResult;
826
+ }
827
+ // Execute the main module with composed input
828
+ return this.executeModule(module, currentData, context, trace);
829
+ }
830
+ /**
831
+ * Execute parallel composition: A → [B, C, D] → Aggregator
832
+ */
833
+ async executeParallel(module, composition, context, trace) {
834
+ const dataflow = composition.dataflow ?? [];
835
+ // Find parallel execution steps (where 'to' is an array)
836
+ for (const step of dataflow) {
837
+ if (this.isTimedOut(context)) {
838
+ return this.timeoutError(context);
839
+ }
840
+ // Get source data
841
+ let sourceData = context.input;
842
+ if (step.from) {
843
+ const sources = Array.isArray(step.from) ? step.from : [step.from];
844
+ const sourceDataArray = [];
845
+ for (const source of sources) {
846
+ if (source === 'input') {
847
+ sourceDataArray.push(context.input);
848
+ }
849
+ else {
850
+ const moduleName = source.replace('.output', '');
851
+ const moduleResult = context.results[moduleName];
852
+ if (moduleResult && 'data' in moduleResult) {
853
+ sourceDataArray.push(moduleResult.data);
854
+ }
855
+ }
856
+ }
857
+ if (sourceDataArray.length === 1) {
858
+ sourceData = sourceDataArray[0];
859
+ }
860
+ else if (sourceDataArray.length > 1) {
861
+ sourceData = { sources: sourceDataArray };
862
+ }
863
+ else {
864
+ sourceData = context.input;
865
+ }
866
+ }
867
+ // Apply mapping
868
+ if (step.mapping) {
869
+ sourceData = applyMapping(step.mapping, sourceData);
870
+ }
871
+ // Get targets
872
+ const targets = Array.isArray(step.to) ? step.to : [step.to];
873
+ if (targets.every(t => t === 'output')) {
874
+ continue;
875
+ }
876
+ // Execute targets in parallel
877
+ const parallelPromises = [];
878
+ const parallelModules = [];
879
+ for (const target of targets) {
880
+ if (target === 'output')
881
+ continue;
882
+ const targetModule = await findModule(target, this.searchPaths);
883
+ if (!targetModule) {
884
+ if (!composition.requires?.find(d => d.name === target)?.optional) {
885
+ return {
886
+ ok: false,
887
+ meta: { confidence: 0, risk: 'high', explain: `Module not found: ${target}` },
888
+ error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${target}` }
889
+ };
890
+ }
891
+ continue;
892
+ }
893
+ const depConfig = composition.requires?.find(d => d.name === target);
894
+ const depTimeout = depConfig?.timeout_ms;
895
+ parallelModules.push(target);
896
+ parallelPromises.push(this.executeModuleWithTimeout(targetModule, sourceData, { ...context, running: new Set(context.running) }, trace, depTimeout));
897
+ }
898
+ // Wait for all parallel executions
899
+ const parallelResults = await Promise.all(parallelPromises);
900
+ // Store results
901
+ for (let i = 0; i < parallelModules.length; i++) {
902
+ context.results[parallelModules[i]] = parallelResults[i];
903
+ }
904
+ // Attempt fallbacks for failed modules
905
+ for (let i = 0; i < parallelModules.length; i++) {
906
+ const result = parallelResults[i];
907
+ if (result.ok)
908
+ continue;
909
+ const name = parallelModules[i];
910
+ const depConfig = composition.requires?.find(d => d.name === name);
911
+ const fallbackName = depConfig?.fallback;
912
+ if (!fallbackName)
913
+ continue;
914
+ const fallbackModule = await findModule(fallbackName, this.searchPaths);
915
+ if (!fallbackModule)
916
+ continue;
917
+ const depTimeout = depConfig?.timeout_ms;
918
+ const fallbackResult = await this.executeModuleWithTimeout(fallbackModule, sourceData, context, trace, depTimeout);
919
+ parallelResults[i] = fallbackResult;
920
+ context.results[fallbackName] = fallbackResult;
921
+ context.results[name] = fallbackResult;
922
+ }
923
+ // Check for failures
924
+ const failures = parallelResults.filter(r => !r.ok);
925
+ if (failures.length > 0) {
926
+ // Check if all failed modules are optional
927
+ const allOptional = parallelModules.every((name, i) => {
928
+ if (parallelResults[i].ok)
929
+ return true;
930
+ return composition.requires?.find(d => d.name === name)?.optional;
931
+ });
932
+ if (!allOptional) {
933
+ return failures[0];
934
+ }
935
+ }
936
+ }
937
+ // Aggregate results
938
+ const aggregateStep = dataflow.find(s => {
939
+ const targets = Array.isArray(s.to) ? s.to : [s.to];
940
+ return targets.includes('output');
941
+ });
942
+ if (aggregateStep && Array.isArray(aggregateStep.from)) {
943
+ const resultsToAggregate = [];
944
+ for (const source of aggregateStep.from) {
945
+ const moduleName = source.replace('.output', '');
946
+ const result = context.results[moduleName];
947
+ if (result) {
948
+ resultsToAggregate.push(result);
949
+ }
950
+ }
951
+ const strategy = aggregateStep.aggregate ?? 'merge';
952
+ return aggregateResults(resultsToAggregate, strategy);
953
+ }
954
+ // Execute main module with all results
955
+ return this.executeModule(module, context.input, context, trace);
956
+ }
957
+ /**
958
+ * Execute conditional composition: A → (condition) → B or C
959
+ */
960
+ async executeConditional(module, composition, context, trace) {
961
+ const routing = composition.routing ?? [];
962
+ // First, execute the initial module to get data for conditions
963
+ const dataflow = composition.dataflow ?? [];
964
+ let conditionData = context.input;
965
+ // Execute initial steps
966
+ for (const step of dataflow) {
967
+ if (this.isTimedOut(context)) {
968
+ return this.timeoutError(context);
969
+ }
970
+ const targets = Array.isArray(step.to) ? step.to : [step.to];
971
+ // Execute non-routing targets
972
+ for (const target of targets) {
973
+ if (target === 'output')
974
+ continue;
975
+ // Check if this target is involved in routing
976
+ const isRoutingTarget = routing.some(r => r.next === target);
977
+ if (isRoutingTarget)
978
+ continue;
979
+ const targetModule = await findModule(target, this.searchPaths);
980
+ if (!targetModule)
981
+ continue;
982
+ // Get source data
983
+ let sourceData = context.input;
984
+ if (step.from) {
985
+ const source = Array.isArray(step.from) ? step.from[0] : step.from;
986
+ if (source !== 'input') {
987
+ const moduleName = source.replace('.output', '');
988
+ const result = context.results[moduleName];
989
+ if (result && 'data' in result) {
990
+ sourceData = result.data;
991
+ }
992
+ }
993
+ }
994
+ if (step.mapping) {
995
+ sourceData = applyMapping(step.mapping, sourceData);
996
+ }
997
+ const result = await this.executeModule(targetModule, sourceData, context, trace);
998
+ context.results[target] = result;
999
+ // Update condition data - include full result (meta + data) for routing conditions
1000
+ conditionData = {
1001
+ input: context.input,
1002
+ ...Object.fromEntries(Object.entries(context.results).map(([k, v]) => [
1003
+ k,
1004
+ v // Keep full result including meta and data
1005
+ ]))
1006
+ };
1007
+ }
1008
+ }
1009
+ // Evaluate routing conditions
1010
+ // Build condition data with proper structure for accessing $.module-name.meta.confidence
1011
+ const routingConditionData = {
1012
+ input: context.input,
1013
+ ...Object.fromEntries(Object.entries(context.results).map(([k, v]) => [k, v]))
1014
+ };
1015
+ for (const rule of routing) {
1016
+ const matches = evaluateCondition(rule.condition, routingConditionData);
1017
+ if (matches) {
1018
+ if (rule.next === null) {
1019
+ // Use current result directly
1020
+ const lastResult = Object.values(context.results).pop();
1021
+ return lastResult ?? this.executeModule(module, context.input, context, trace);
1022
+ }
1023
+ // Execute the next module
1024
+ const nextModule = await findModule(rule.next, this.searchPaths);
1025
+ if (!nextModule) {
1026
+ return {
1027
+ ok: false,
1028
+ meta: { confidence: 0, risk: 'high', explain: `Routing target not found: ${rule.next}` },
1029
+ error: { code: COMPOSITION_ERRORS.E4009, message: `Module not found: ${rule.next}` }
1030
+ };
1031
+ }
1032
+ // Pass through the data
1033
+ let nextInput = context.input;
1034
+ const lastDataflowStep = dataflow[dataflow.length - 1];
1035
+ if (lastDataflowStep?.mapping) {
1036
+ nextInput = applyMapping(lastDataflowStep.mapping, conditionData);
1037
+ }
1038
+ return this.executeModule(nextModule, nextInput, context, trace);
1039
+ }
1040
+ }
1041
+ // No routing matched, execute main module
1042
+ return this.executeModule(module, context.input, context, trace);
1043
+ }
1044
+ /**
1045
+ * Execute iterative composition: A → (check) → A → ... → Done
1046
+ */
1047
+ async executeIterative(module, composition, context, trace) {
1048
+ const maxIterations = composition.iteration?.max_iterations ?? 10;
1049
+ const continueCondition = composition.iteration?.continue_condition;
1050
+ const stopCondition = composition.iteration?.stop_condition;
1051
+ let currentInput = context.input;
1052
+ let lastResult = null;
1053
+ let iteration = 0;
1054
+ while (iteration < maxIterations) {
1055
+ if (this.isTimedOut(context)) {
1056
+ return this.timeoutError(context);
1057
+ }
1058
+ // Execute the module
1059
+ lastResult = await this.executeModule(module, currentInput, context, trace);
1060
+ context.iterationCount = iteration + 1;
1061
+ // Store result with iteration number
1062
+ context.results[`${module.name}_iteration_${iteration}`] = lastResult;
1063
+ // Check stop condition
1064
+ if (stopCondition) {
1065
+ const stopData = {
1066
+ input: context.input,
1067
+ current: lastResult,
1068
+ iteration,
1069
+ meta: lastResult && 'meta' in lastResult ? lastResult.meta : null,
1070
+ data: lastResult && 'data' in lastResult ? lastResult.data : null
1071
+ };
1072
+ if (evaluateCondition(stopCondition, stopData)) {
1073
+ break;
1074
+ }
1075
+ }
1076
+ // Check continue condition
1077
+ if (continueCondition) {
1078
+ const continueData = {
1079
+ input: context.input,
1080
+ current: lastResult,
1081
+ iteration,
1082
+ meta: lastResult && 'meta' in lastResult ? lastResult.meta : null,
1083
+ data: lastResult && 'data' in lastResult ? lastResult.data : null
1084
+ };
1085
+ if (!evaluateCondition(continueCondition, continueData)) {
1086
+ break;
1087
+ }
1088
+ }
1089
+ else {
1090
+ // No continue condition and no stop condition - only run once
1091
+ break;
1092
+ }
1093
+ // Update input for next iteration
1094
+ if (lastResult.ok && 'data' in lastResult) {
1095
+ currentInput = lastResult.data;
1096
+ }
1097
+ iteration++;
1098
+ }
1099
+ // Check if we hit iteration limit
1100
+ if (iteration >= maxIterations && continueCondition) {
1101
+ return {
1102
+ ok: false,
1103
+ meta: {
1104
+ confidence: 0.5,
1105
+ risk: 'medium',
1106
+ explain: `Iteration limit (${maxIterations}) reached`
1107
+ },
1108
+ error: {
1109
+ code: COMPOSITION_ERRORS.E4013,
1110
+ message: `Maximum iterations (${maxIterations}) exceeded`
1111
+ },
1112
+ partial_data: lastResult && 'data' in lastResult
1113
+ ? lastResult.data
1114
+ : undefined
1115
+ };
1116
+ }
1117
+ return lastResult ?? {
1118
+ ok: false,
1119
+ meta: { confidence: 0, risk: 'high', explain: 'No iteration executed' },
1120
+ error: { code: 'E4000', message: 'Iterative composition produced no result' }
1121
+ };
1122
+ }
1123
+ /**
1124
+ * Execute a single module
1125
+ */
1126
+ async executeModule(module, input, context, trace) {
1127
+ // Check depth limit
1128
+ if (context.depth >= context.maxDepth) {
1129
+ return {
1130
+ ok: false,
1131
+ meta: { confidence: 0, risk: 'high', explain: `Max depth exceeded at ${module.name}` },
1132
+ error: {
1133
+ code: COMPOSITION_ERRORS.E4005,
1134
+ message: `Maximum composition depth (${context.maxDepth}) exceeded at module: ${module.name}`
1135
+ }
1136
+ };
1137
+ }
1138
+ // Check for circular dependency
1139
+ if (context.running.has(module.name)) {
1140
+ trace.push({
1141
+ module: module.name,
1142
+ startTime: Date.now(),
1143
+ endTime: Date.now(),
1144
+ durationMs: 0,
1145
+ success: false,
1146
+ reason: 'Circular dependency detected'
1147
+ });
1148
+ return {
1149
+ ok: false,
1150
+ meta: { confidence: 0, risk: 'high', explain: `Circular dependency: ${module.name}` },
1151
+ error: {
1152
+ code: COMPOSITION_ERRORS.E4004,
1153
+ message: `Circular dependency detected: ${module.name}`
1154
+ }
1155
+ };
1156
+ }
1157
+ context.running.add(module.name);
1158
+ context.depth++; // Increment depth when entering module
1159
+ const startTime = Date.now();
1160
+ try {
1161
+ const result = await runModule(module, this.provider, {
1162
+ input,
1163
+ validateInput: true,
1164
+ validateOutput: true,
1165
+ useV22: true
1166
+ });
1167
+ const endTime = Date.now();
1168
+ trace.push({
1169
+ module: module.name,
1170
+ startTime,
1171
+ endTime,
1172
+ durationMs: endTime - startTime,
1173
+ success: result.ok
1174
+ });
1175
+ return result;
1176
+ }
1177
+ catch (error) {
1178
+ const endTime = Date.now();
1179
+ trace.push({
1180
+ module: module.name,
1181
+ startTime,
1182
+ endTime,
1183
+ durationMs: endTime - startTime,
1184
+ success: false,
1185
+ reason: error.message
1186
+ });
1187
+ return {
1188
+ ok: false,
1189
+ meta: { confidence: 0, risk: 'high', explain: error.message.slice(0, 280) },
1190
+ error: { code: 'E4000', message: error.message }
1191
+ };
1192
+ }
1193
+ finally {
1194
+ context.running.delete(module.name);
1195
+ context.depth--; // Decrement depth when exiting module
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Execute module with timeout
1200
+ */
1201
+ async executeModuleWithTimeout(module, input, context, trace, timeoutMs) {
1202
+ const timeout = timeoutMs ?? context.timeoutMs;
1203
+ const localTrace = [];
1204
+ const localContext = {
1205
+ ...context,
1206
+ running: new Set(context.running),
1207
+ results: { ...context.results },
1208
+ };
1209
+ const startTime = Date.now();
1210
+ const execPromise = this.executeModule(module, input, localContext, localTrace);
1211
+ if (!timeout) {
1212
+ const result = await execPromise;
1213
+ trace.push(...localTrace);
1214
+ return result;
1215
+ }
1216
+ let timeoutId = null;
1217
+ const timeoutPromise = new Promise((resolve) => {
1218
+ timeoutId = setTimeout(() => resolve({ type: 'timeout' }), timeout);
1219
+ });
1220
+ const raceResult = await Promise.race([
1221
+ execPromise.then(result => ({ type: 'result', result })),
1222
+ timeoutPromise
1223
+ ]);
1224
+ if (timeoutId)
1225
+ clearTimeout(timeoutId);
1226
+ if ('result' in raceResult) {
1227
+ trace.push(...localTrace);
1228
+ return raceResult.result;
1229
+ }
1230
+ trace.push({
1231
+ module: module.name,
1232
+ startTime,
1233
+ endTime: Date.now(),
1234
+ durationMs: Date.now() - startTime,
1235
+ success: false,
1236
+ reason: `Timeout after ${timeout}ms`
1237
+ });
1238
+ return {
1239
+ ok: false,
1240
+ meta: { confidence: 0, risk: 'high', explain: `Timeout: ${module.name}` },
1241
+ error: {
1242
+ code: COMPOSITION_ERRORS.E4008,
1243
+ message: `Module ${module.name} timed out after ${timeout}ms`
1244
+ }
1245
+ };
1246
+ }
1247
+ /**
1248
+ * Check if composition has timed out
1249
+ */
1250
+ isTimedOut(context) {
1251
+ if (!context.timeoutMs)
1252
+ return false;
1253
+ return Date.now() - context.startTime > context.timeoutMs;
1254
+ }
1255
+ /**
1256
+ * Create timeout error response
1257
+ */
1258
+ timeoutError(context) {
1259
+ const elapsed = Date.now() - context.startTime;
1260
+ return {
1261
+ ok: false,
1262
+ meta: {
1263
+ confidence: 0,
1264
+ risk: 'high',
1265
+ explain: `Composition timed out after ${elapsed}ms`
1266
+ },
1267
+ error: {
1268
+ code: COMPOSITION_ERRORS.E4008,
1269
+ message: `Composition timeout: ${elapsed}ms exceeded ${context.timeoutMs}ms limit`
1270
+ }
1271
+ };
1272
+ }
1273
+ }
1274
+ // =============================================================================
1275
+ // Convenience Functions
1276
+ // =============================================================================
1277
+ /**
1278
+ * Execute a composed module workflow
1279
+ */
1280
+ export async function executeComposition(moduleName, input, provider, options = {}) {
1281
+ const { cwd = process.cwd(), ...execOptions } = options;
1282
+ const orchestrator = new CompositionOrchestrator(provider, cwd);
1283
+ return orchestrator.execute(moduleName, input, execOptions);
1284
+ }
1285
+ /**
1286
+ * Validate composition configuration
1287
+ */
1288
+ export function validateCompositionConfig(config) {
1289
+ const errors = [];
1290
+ // Check pattern
1291
+ const validPatterns = ['sequential', 'parallel', 'conditional', 'iterative'];
1292
+ if (!validPatterns.includes(config.pattern)) {
1293
+ errors.push(`Invalid pattern: ${config.pattern}. Must be one of: ${validPatterns.join(', ')}`);
1294
+ }
1295
+ // Check dataflow steps
1296
+ if (config.dataflow) {
1297
+ for (let i = 0; i < config.dataflow.length; i++) {
1298
+ const step = config.dataflow[i];
1299
+ if (!step.from) {
1300
+ errors.push(`Dataflow step ${i}: missing 'from' field`);
1301
+ }
1302
+ if (!step.to) {
1303
+ errors.push(`Dataflow step ${i}: missing 'to' field`);
1304
+ }
1305
+ }
1306
+ }
1307
+ // Check routing rules for conditional pattern
1308
+ if (config.pattern === 'conditional' && (!config.routing || config.routing.length === 0)) {
1309
+ errors.push('Conditional pattern requires routing rules');
1310
+ }
1311
+ // Check iteration config for iterative pattern
1312
+ if (config.pattern === 'iterative') {
1313
+ const iter = config.iteration;
1314
+ if (!iter?.continue_condition && !iter?.stop_condition) {
1315
+ errors.push('Iterative pattern requires either continue_condition or stop_condition');
1316
+ }
1317
+ }
1318
+ // Check dependencies
1319
+ if (config.requires) {
1320
+ for (const dep of config.requires) {
1321
+ if (!dep.name) {
1322
+ errors.push('Dependency missing name');
1323
+ }
1324
+ }
1325
+ }
1326
+ return {
1327
+ valid: errors.length === 0,
1328
+ errors
1329
+ };
1330
+ }