cognitive-modules-cli 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for Composition Engine
3
+ *
4
+ * Tests all COMPOSITION.md specified functionality:
5
+ * - JSONPath-like expression evaluation
6
+ * - Condition expression evaluation
7
+ * - Aggregation strategies
8
+ * - Version matching
9
+ * - Sequential, Parallel, Conditional, Iterative patterns
10
+ */
11
+ export {};
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Tests for Composition Engine
3
+ *
4
+ * Tests all COMPOSITION.md specified functionality:
5
+ * - JSONPath-like expression evaluation
6
+ * - Condition expression evaluation
7
+ * - Aggregation strategies
8
+ * - Version matching
9
+ * - Sequential, Parallel, Conditional, Iterative patterns
10
+ */
11
+ import { describe, it, expect } from 'vitest';
12
+ import { evaluateJsonPath, evaluateCondition, applyMapping, aggregateResults, versionMatches, validateCompositionConfig, } from './composition.js';
13
+ // =============================================================================
14
+ // JSONPath Expression Tests
15
+ // =============================================================================
16
+ describe('evaluateJsonPath', () => {
17
+ const testData = {
18
+ name: 'test',
19
+ nested: {
20
+ value: 42,
21
+ deep: {
22
+ array: [1, 2, 3]
23
+ }
24
+ },
25
+ items: [
26
+ { id: 1, name: 'a' },
27
+ { id: 2, name: 'b' },
28
+ { id: 3, name: 'c' }
29
+ ],
30
+ meta: {
31
+ confidence: 0.95,
32
+ risk: 'low'
33
+ },
34
+ // Test hyphenated field names (Bug fix)
35
+ 'quick-check': {
36
+ meta: {
37
+ confidence: 0.85
38
+ },
39
+ data: {
40
+ result: 'success'
41
+ }
42
+ }
43
+ };
44
+ it('should return entire object for $', () => {
45
+ expect(evaluateJsonPath('$', testData)).toEqual(testData);
46
+ });
47
+ it('should access root field with $.field', () => {
48
+ expect(evaluateJsonPath('$.name', testData)).toBe('test');
49
+ });
50
+ it('should access nested field with $.nested.field', () => {
51
+ expect(evaluateJsonPath('$.nested.value', testData)).toBe(42);
52
+ expect(evaluateJsonPath('$.nested.deep.array', testData)).toEqual([1, 2, 3]);
53
+ });
54
+ it('should access array index with $.array[0]', () => {
55
+ expect(evaluateJsonPath('$.items[0]', testData)).toEqual({ id: 1, name: 'a' });
56
+ expect(evaluateJsonPath('$.items[1].name', testData)).toBe('b');
57
+ expect(evaluateJsonPath('$.nested.deep.array[2]', testData)).toBe(3);
58
+ });
59
+ it('should map over array with $.array[*].field', () => {
60
+ expect(evaluateJsonPath('$.items[*].name', testData)).toEqual(['a', 'b', 'c']);
61
+ expect(evaluateJsonPath('$.items[*].id', testData)).toEqual([1, 2, 3]);
62
+ });
63
+ it('should return undefined for non-existent paths', () => {
64
+ expect(evaluateJsonPath('$.nonexistent', testData)).toBeUndefined();
65
+ expect(evaluateJsonPath('$.nested.nonexistent', testData)).toBeUndefined();
66
+ expect(evaluateJsonPath('$.items[99]', testData)).toBeUndefined();
67
+ });
68
+ it('should return literal values for non-JSONPath strings', () => {
69
+ expect(evaluateJsonPath('literal', testData)).toBe('literal');
70
+ expect(evaluateJsonPath('123', testData)).toBe('123');
71
+ });
72
+ it('should handle hyphenated field names', () => {
73
+ expect(evaluateJsonPath('$.quick-check.meta.confidence', testData)).toBe(0.85);
74
+ expect(evaluateJsonPath('$.quick-check.data.result', testData)).toBe('success');
75
+ });
76
+ });
77
+ // =============================================================================
78
+ // Condition Expression Tests
79
+ // =============================================================================
80
+ describe('evaluateCondition', () => {
81
+ const testData = {
82
+ meta: {
83
+ confidence: 0.85,
84
+ risk: 'low'
85
+ },
86
+ data: {
87
+ count: 5,
88
+ items: [1, 2, 3],
89
+ name: 'test module'
90
+ }
91
+ };
92
+ describe('comparison operators', () => {
93
+ it('should evaluate > operator', () => {
94
+ expect(evaluateCondition('$.meta.confidence > 0.7', testData)).toBe(true);
95
+ expect(evaluateCondition('$.meta.confidence > 0.9', testData)).toBe(false);
96
+ });
97
+ it('should evaluate < operator', () => {
98
+ expect(evaluateCondition('$.meta.confidence < 0.9', testData)).toBe(true);
99
+ expect(evaluateCondition('$.meta.confidence < 0.5', testData)).toBe(false);
100
+ });
101
+ it('should evaluate >= operator', () => {
102
+ expect(evaluateCondition('$.meta.confidence >= 0.85', testData)).toBe(true);
103
+ expect(evaluateCondition('$.meta.confidence >= 0.9', testData)).toBe(false);
104
+ });
105
+ it('should evaluate <= operator', () => {
106
+ expect(evaluateCondition('$.meta.confidence <= 0.85', testData)).toBe(true);
107
+ expect(evaluateCondition('$.meta.confidence <= 0.5', testData)).toBe(false);
108
+ });
109
+ it('should evaluate == operator', () => {
110
+ expect(evaluateCondition('$.meta.risk == "low"', testData)).toBe(true);
111
+ expect(evaluateCondition('$.meta.risk == "high"', testData)).toBe(false);
112
+ expect(evaluateCondition('$.data.count == 5', testData)).toBe(true);
113
+ });
114
+ it('should evaluate != operator', () => {
115
+ expect(evaluateCondition('$.meta.risk != "high"', testData)).toBe(true);
116
+ expect(evaluateCondition('$.meta.risk != "low"', testData)).toBe(false);
117
+ });
118
+ });
119
+ describe('logical operators', () => {
120
+ it('should evaluate && operator', () => {
121
+ expect(evaluateCondition('$.meta.confidence > 0.7 && $.meta.risk == "low"', testData)).toBe(true);
122
+ expect(evaluateCondition('$.meta.confidence > 0.9 && $.meta.risk == "low"', testData)).toBe(false);
123
+ });
124
+ it('should evaluate || operator', () => {
125
+ expect(evaluateCondition('$.meta.confidence > 0.9 || $.meta.risk == "low"', testData)).toBe(true);
126
+ expect(evaluateCondition('$.meta.confidence > 0.9 || $.meta.risk == "high"', testData)).toBe(false);
127
+ });
128
+ it('should evaluate ! operator', () => {
129
+ expect(evaluateCondition('!false', {})).toBe(true);
130
+ expect(evaluateCondition('!true', {})).toBe(false);
131
+ });
132
+ });
133
+ describe('special functions', () => {
134
+ it('should evaluate exists() function', () => {
135
+ expect(evaluateCondition('exists($.meta.confidence)', testData)).toBe(true);
136
+ expect(evaluateCondition('exists($.meta.nonexistent)', testData)).toBe(false);
137
+ });
138
+ it('should evaluate .length property', () => {
139
+ expect(evaluateCondition('$.data.items.length > 0', testData)).toBe(true);
140
+ expect(evaluateCondition('$.data.items.length == 3', testData)).toBe(true);
141
+ });
142
+ it('should evaluate contains() for strings', () => {
143
+ expect(evaluateCondition('contains($.data.name, "test")', testData)).toBe(true);
144
+ expect(evaluateCondition('contains($.data.name, "xyz")', testData)).toBe(false);
145
+ });
146
+ it('should evaluate contains() for arrays', () => {
147
+ const dataWithArray = {
148
+ tags: ['javascript', 'typescript', 'node']
149
+ };
150
+ expect(evaluateCondition('contains($.tags, "typescript")', dataWithArray)).toBe(true);
151
+ expect(evaluateCondition('contains($.tags, "python")', dataWithArray)).toBe(false);
152
+ });
153
+ });
154
+ describe('hyphenated field names', () => {
155
+ const hyphenData = {
156
+ 'quick-check': {
157
+ meta: { confidence: 0.85 },
158
+ data: { result: 'success' }
159
+ }
160
+ };
161
+ it('should handle hyphenated field names in conditions', () => {
162
+ expect(evaluateCondition('$.quick-check.meta.confidence > 0.8', hyphenData)).toBe(true);
163
+ expect(evaluateCondition('$.quick-check.meta.confidence > 0.9', hyphenData)).toBe(false);
164
+ });
165
+ it('should handle hyphenated fields with string comparison', () => {
166
+ expect(evaluateCondition('$.quick-check.data.result == "success"', hyphenData)).toBe(true);
167
+ });
168
+ });
169
+ });
170
+ // =============================================================================
171
+ // Dataflow Mapping Tests
172
+ // =============================================================================
173
+ describe('applyMapping', () => {
174
+ const sourceData = {
175
+ code: 'function test() {}',
176
+ language: 'javascript',
177
+ nested: {
178
+ value: 42
179
+ }
180
+ };
181
+ it('should map simple fields', () => {
182
+ const mapping = {
183
+ source_code: '$.code',
184
+ lang: '$.language'
185
+ };
186
+ const result = applyMapping(mapping, sourceData);
187
+ expect(result.source_code).toBe('function test() {}');
188
+ expect(result.lang).toBe('javascript');
189
+ });
190
+ it('should map nested fields', () => {
191
+ const mapping = {
192
+ extracted_value: '$.nested.value'
193
+ };
194
+ const result = applyMapping(mapping, sourceData);
195
+ expect(result.extracted_value).toBe(42);
196
+ });
197
+ it('should handle missing fields', () => {
198
+ const mapping = {
199
+ missing: '$.nonexistent'
200
+ };
201
+ const result = applyMapping(mapping, sourceData);
202
+ expect(result.missing).toBeUndefined();
203
+ });
204
+ it('should pass through entire object with $', () => {
205
+ const mapping = {
206
+ all: '$'
207
+ };
208
+ const result = applyMapping(mapping, sourceData);
209
+ expect(result.all).toEqual(sourceData);
210
+ });
211
+ });
212
+ // =============================================================================
213
+ // Aggregation Strategy Tests
214
+ // =============================================================================
215
+ describe('aggregateResults', () => {
216
+ const createResult = (data, confidence, risk) => ({
217
+ ok: true,
218
+ meta: {
219
+ confidence,
220
+ risk: risk,
221
+ explain: 'Test result'
222
+ },
223
+ data: {
224
+ ...data,
225
+ rationale: 'Test rationale'
226
+ }
227
+ });
228
+ const results = [
229
+ createResult({ field1: 'value1', common: 'first' }, 0.8, 'low'),
230
+ createResult({ field2: 'value2', common: 'second' }, 0.9, 'medium'),
231
+ createResult({ field3: 'value3', common: 'third' }, 0.7, 'none')
232
+ ];
233
+ describe('merge strategy', () => {
234
+ it('should deep merge all results (later wins)', () => {
235
+ const merged = aggregateResults(results, 'merge');
236
+ expect(merged.ok).toBe(true);
237
+ expect(merged.data.field1).toBe('value1');
238
+ expect(merged.data.field2).toBe('value2');
239
+ expect(merged.data.field3).toBe('value3');
240
+ expect(merged.data.common).toBe('third'); // Last wins
241
+ });
242
+ it('should aggregate meta values', () => {
243
+ const merged = aggregateResults(results, 'merge');
244
+ // Average confidence
245
+ expect(merged.meta.confidence).toBeCloseTo(0.8, 1);
246
+ // Max risk
247
+ expect(merged.meta.risk).toBe('medium');
248
+ });
249
+ });
250
+ describe('array strategy', () => {
251
+ it('should collect all results into array', () => {
252
+ const collected = aggregateResults(results, 'array');
253
+ expect(collected.ok).toBe(true);
254
+ const data = collected.data;
255
+ expect(data.results).toHaveLength(3);
256
+ });
257
+ });
258
+ describe('first strategy', () => {
259
+ it('should return first successful result', () => {
260
+ const first = aggregateResults(results, 'first');
261
+ expect(first.ok).toBe(true);
262
+ expect(first.data.field1).toBe('value1');
263
+ });
264
+ it('should skip failed results', () => {
265
+ const mixedResults = [
266
+ {
267
+ ok: false,
268
+ meta: { confidence: 0, risk: 'high', explain: 'Failed' },
269
+ error: { code: 'E1000', message: 'Error' }
270
+ },
271
+ createResult({ success: true }, 0.9, 'low')
272
+ ];
273
+ const first = aggregateResults(mixedResults, 'first');
274
+ expect(first.ok).toBe(true);
275
+ expect(first.data.success).toBe(true);
276
+ });
277
+ });
278
+ describe('edge cases', () => {
279
+ it('should return error for empty results', () => {
280
+ const empty = aggregateResults([], 'merge');
281
+ expect(empty.ok).toBe(false);
282
+ });
283
+ it('should return single result unchanged', () => {
284
+ const single = aggregateResults([results[0]], 'merge');
285
+ expect(single).toEqual(results[0]);
286
+ });
287
+ });
288
+ });
289
+ // =============================================================================
290
+ // Version Matching Tests
291
+ // =============================================================================
292
+ describe('versionMatches', () => {
293
+ describe('exact version', () => {
294
+ it('should match exact version', () => {
295
+ expect(versionMatches('1.0.0', '1.0.0')).toBe(true);
296
+ expect(versionMatches('1.0.0', '1.0.1')).toBe(false);
297
+ expect(versionMatches('2.0.0', '1.0.0')).toBe(false);
298
+ });
299
+ });
300
+ describe('wildcard (*)', () => {
301
+ it('should match any version with *', () => {
302
+ expect(versionMatches('1.0.0', '*')).toBe(true);
303
+ expect(versionMatches('99.99.99', '*')).toBe(true);
304
+ });
305
+ it('should match any version with empty pattern', () => {
306
+ expect(versionMatches('1.0.0', '')).toBe(true);
307
+ });
308
+ });
309
+ describe('>= operator', () => {
310
+ it('should match versions greater than or equal', () => {
311
+ expect(versionMatches('1.0.0', '>=1.0.0')).toBe(true);
312
+ expect(versionMatches('1.1.0', '>=1.0.0')).toBe(true);
313
+ expect(versionMatches('2.0.0', '>=1.0.0')).toBe(true);
314
+ expect(versionMatches('0.9.0', '>=1.0.0')).toBe(false);
315
+ });
316
+ });
317
+ describe('> operator', () => {
318
+ it('should match versions strictly greater', () => {
319
+ expect(versionMatches('1.0.1', '>1.0.0')).toBe(true);
320
+ expect(versionMatches('1.1.0', '>1.0.0')).toBe(true);
321
+ expect(versionMatches('1.0.0', '>1.0.0')).toBe(false);
322
+ });
323
+ });
324
+ describe('^ (caret) operator', () => {
325
+ it('should match same major version', () => {
326
+ expect(versionMatches('1.2.3', '^1.0.0')).toBe(true);
327
+ expect(versionMatches('1.0.0', '^1.0.0')).toBe(true);
328
+ expect(versionMatches('2.0.0', '^1.0.0')).toBe(false);
329
+ expect(versionMatches('0.9.0', '^1.0.0')).toBe(false);
330
+ });
331
+ });
332
+ describe('~ (tilde) operator', () => {
333
+ it('should match same major.minor version', () => {
334
+ expect(versionMatches('1.0.5', '~1.0.0')).toBe(true);
335
+ expect(versionMatches('1.0.0', '~1.0.0')).toBe(true);
336
+ expect(versionMatches('1.1.0', '~1.0.0')).toBe(false);
337
+ expect(versionMatches('2.0.0', '~1.0.0')).toBe(false);
338
+ });
339
+ });
340
+ });
341
+ // =============================================================================
342
+ // Composition Config Validation Tests
343
+ // =============================================================================
344
+ describe('validateCompositionConfig', () => {
345
+ it('should validate correct sequential config', () => {
346
+ const config = {
347
+ pattern: 'sequential',
348
+ requires: [{ name: 'module-a' }],
349
+ dataflow: [
350
+ { from: 'input', to: 'module-a' },
351
+ { from: 'module-a.output', to: 'output' }
352
+ ]
353
+ };
354
+ const result = validateCompositionConfig(config);
355
+ expect(result.valid).toBe(true);
356
+ expect(result.errors).toHaveLength(0);
357
+ });
358
+ it('should validate correct parallel config', () => {
359
+ const config = {
360
+ pattern: 'parallel',
361
+ requires: [
362
+ { name: 'module-a' },
363
+ { name: 'module-b' }
364
+ ],
365
+ dataflow: [
366
+ { from: 'input', to: ['module-a', 'module-b'] },
367
+ { from: ['module-a.output', 'module-b.output'], to: 'output', aggregate: 'merge' }
368
+ ]
369
+ };
370
+ const result = validateCompositionConfig(config);
371
+ expect(result.valid).toBe(true);
372
+ });
373
+ it('should require routing rules for conditional pattern', () => {
374
+ const config = {
375
+ pattern: 'conditional',
376
+ dataflow: [{ from: 'input', to: 'module-a' }]
377
+ };
378
+ const result = validateCompositionConfig(config);
379
+ expect(result.valid).toBe(false);
380
+ expect(result.errors.some(e => e.includes('routing'))).toBe(true);
381
+ });
382
+ it('should require iteration config for iterative pattern', () => {
383
+ const config = {
384
+ pattern: 'iterative',
385
+ dataflow: [{ from: 'input', to: 'module-a' }]
386
+ };
387
+ const result = validateCompositionConfig(config);
388
+ expect(result.valid).toBe(false);
389
+ expect(result.errors.some(e => e.includes('continue_condition') || e.includes('stop_condition'))).toBe(true);
390
+ });
391
+ it('should validate correct iterative config', () => {
392
+ const config = {
393
+ pattern: 'iterative',
394
+ iteration: {
395
+ max_iterations: 10,
396
+ stop_condition: '$.meta.confidence > 0.9'
397
+ }
398
+ };
399
+ const result = validateCompositionConfig(config);
400
+ expect(result.valid).toBe(true);
401
+ });
402
+ it('should detect invalid pattern', () => {
403
+ const config = {
404
+ pattern: 'invalid',
405
+ dataflow: []
406
+ };
407
+ const result = validateCompositionConfig(config);
408
+ expect(result.valid).toBe(false);
409
+ expect(result.errors.some(e => e.includes('Invalid pattern'))).toBe(true);
410
+ });
411
+ it('should detect missing dataflow fields', () => {
412
+ const config = {
413
+ pattern: 'sequential',
414
+ dataflow: [
415
+ { from: 'input' },
416
+ ]
417
+ };
418
+ const result = validateCompositionConfig(config);
419
+ expect(result.valid).toBe(false);
420
+ expect(result.errors.some(e => e.includes("missing 'to'"))).toBe(true);
421
+ });
422
+ });
423
+ // =============================================================================
424
+ // Integration Test Placeholders
425
+ // =============================================================================
426
+ describe('CompositionOrchestrator', () => {
427
+ // Note: Full integration tests require mocking Provider and modules
428
+ // These are placeholder tests that should be expanded with proper mocks
429
+ it.skip('should execute sequential composition', async () => {
430
+ // TODO: Add integration test with mocked provider
431
+ });
432
+ it.skip('should execute parallel composition', async () => {
433
+ // TODO: Add integration test with mocked provider
434
+ });
435
+ it.skip('should execute conditional composition', async () => {
436
+ // TODO: Add integration test with mocked provider
437
+ });
438
+ it.skip('should execute iterative composition', async () => {
439
+ // TODO: Add integration test with mocked provider
440
+ });
441
+ it.skip('should handle timeouts', async () => {
442
+ // TODO: Add timeout test
443
+ });
444
+ it.skip('should detect circular dependencies', async () => {
445
+ // TODO: Add circular dependency test
446
+ });
447
+ it.skip('should use fallback modules', async () => {
448
+ // TODO: Add fallback test
449
+ });
450
+ });
@@ -4,3 +4,5 @@
4
4
  export * from './loader.js';
5
5
  export * from './runner.js';
6
6
  export * from './subagent.js';
7
+ export * from './validator.js';
8
+ export * from './composition.js';
@@ -4,3 +4,5 @@
4
4
  export * from './loader.js';
5
5
  export * from './runner.js';
6
6
  export * from './subagent.js';
7
+ export * from './validator.js';
8
+ export * from './composition.js';
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Module Loader - Load and parse Cognitive Modules
3
- * Supports both v1 (MODULE.md) and v2 (module.yaml + prompt.md) formats
3
+ * Supports v0 (6-file), v1 (MODULE.md) and v2 (module.yaml + prompt.md) formats
4
4
  */
5
- import type { CognitiveModule } from '../types.js';
5
+ import type { CognitiveModule, ModuleTier, SchemaStrictness } from '../types.js';
6
6
  /**
7
7
  * Load a Cognitive Module (auto-detects format)
8
8
  */
@@ -10,3 +10,23 @@ export declare function loadModule(modulePath: string): Promise<CognitiveModule>
10
10
  export declare function findModule(name: string, searchPaths: string[]): Promise<CognitiveModule | null>;
11
11
  export declare function listModules(searchPaths: string[]): Promise<CognitiveModule[]>;
12
12
  export declare function getDefaultSearchPaths(cwd: string): string[];
13
+ /**
14
+ * Get module tier (exec, decision, exploration).
15
+ */
16
+ export declare function getModuleTier(module: CognitiveModule): ModuleTier | undefined;
17
+ /**
18
+ * Get schema strictness level.
19
+ */
20
+ export declare function getSchemaStrictness(module: CognitiveModule): SchemaStrictness;
21
+ /**
22
+ * Check if overflow (extensions.insights) is enabled.
23
+ */
24
+ export declare function isOverflowEnabled(module: CognitiveModule): boolean;
25
+ /**
26
+ * Get enum extension strategy.
27
+ */
28
+ export declare function getEnumStrategy(module: CognitiveModule): 'strict' | 'extensible';
29
+ /**
30
+ * Check if runtime should auto-wrap v2.1 to v2.2.
31
+ */
32
+ export declare function shouldAutoWrap(module: CognitiveModule): boolean;