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.
- package/dist/cli.js +65 -12
- package/dist/commands/compose.d.ts +31 -0
- package/dist/commands/compose.js +148 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/modules/composition.d.ts +251 -0
- package/dist/modules/composition.js +1265 -0
- package/dist/modules/composition.test.d.ts +11 -0
- package/dist/modules/composition.test.js +450 -0
- package/dist/modules/index.d.ts +2 -0
- package/dist/modules/index.js +2 -0
- package/dist/modules/loader.d.ts +22 -2
- package/dist/modules/loader.js +167 -4
- package/dist/modules/policy.test.d.ts +10 -0
- package/dist/modules/policy.test.js +369 -0
- package/dist/modules/runner.d.ts +357 -1
- package/dist/modules/runner.js +1221 -64
- package/dist/modules/subagent.js +2 -0
- package/dist/modules/validator.d.ts +28 -0
- package/dist/modules/validator.js +629 -0
- package/dist/types.d.ts +92 -8
- package/package.json +2 -1
- package/src/cli.ts +73 -12
- package/src/commands/compose.ts +185 -0
- package/src/commands/index.ts +1 -0
- package/src/index.ts +35 -0
- package/src/modules/composition.test.ts +558 -0
- package/src/modules/composition.ts +1674 -0
- package/src/modules/index.ts +2 -0
- package/src/modules/loader.ts +196 -6
- package/src/modules/policy.test.ts +455 -0
- package/src/modules/runner.ts +1562 -74
- package/src/modules/subagent.ts +2 -0
- package/src/modules/validator.ts +700 -0
- package/src/types.ts +112 -8
- package/tsconfig.json +1 -1
|
@@ -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
|
+
}
|