cognitive-modules-cli 2.2.1 → 2.2.7

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 (101) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +519 -23
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +383 -16
  7. package/dist/commands/compose.js +60 -23
  8. package/dist/commands/index.d.ts +4 -0
  9. package/dist/commands/index.js +4 -0
  10. package/dist/commands/init.js +23 -1
  11. package/dist/commands/migrate.d.ts +30 -0
  12. package/dist/commands/migrate.js +650 -0
  13. package/dist/commands/pipe.d.ts +1 -0
  14. package/dist/commands/pipe.js +31 -11
  15. package/dist/commands/remove.js +33 -2
  16. package/dist/commands/run.d.ts +2 -0
  17. package/dist/commands/run.js +61 -28
  18. package/dist/commands/search.d.ts +28 -0
  19. package/dist/commands/search.js +143 -0
  20. package/dist/commands/test.d.ts +65 -0
  21. package/dist/commands/test.js +454 -0
  22. package/dist/commands/update.d.ts +1 -0
  23. package/dist/commands/update.js +106 -14
  24. package/dist/commands/validate.d.ts +36 -0
  25. package/dist/commands/validate.js +97 -0
  26. package/dist/errors/index.d.ts +225 -0
  27. package/dist/errors/index.js +420 -0
  28. package/dist/mcp/server.js +84 -79
  29. package/dist/modules/composition.js +97 -32
  30. package/dist/modules/loader.js +4 -2
  31. package/dist/modules/runner.d.ts +72 -5
  32. package/dist/modules/runner.js +306 -59
  33. package/dist/modules/subagent.d.ts +6 -1
  34. package/dist/modules/subagent.js +18 -13
  35. package/dist/modules/validator.js +14 -6
  36. package/dist/providers/anthropic.d.ts +15 -0
  37. package/dist/providers/anthropic.js +147 -5
  38. package/dist/providers/base.d.ts +11 -0
  39. package/dist/providers/base.js +18 -0
  40. package/dist/providers/gemini.d.ts +15 -0
  41. package/dist/providers/gemini.js +122 -5
  42. package/dist/providers/ollama.d.ts +15 -0
  43. package/dist/providers/ollama.js +111 -3
  44. package/dist/providers/openai.d.ts +11 -0
  45. package/dist/providers/openai.js +133 -0
  46. package/dist/registry/client.d.ts +212 -0
  47. package/dist/registry/client.js +359 -0
  48. package/dist/registry/index.d.ts +4 -0
  49. package/dist/registry/index.js +4 -0
  50. package/dist/registry/tar.d.ts +8 -0
  51. package/dist/registry/tar.js +353 -0
  52. package/dist/server/http.js +301 -45
  53. package/dist/server/index.d.ts +2 -0
  54. package/dist/server/index.js +1 -0
  55. package/dist/server/sse.d.ts +13 -0
  56. package/dist/server/sse.js +22 -0
  57. package/dist/types.d.ts +32 -1
  58. package/dist/types.js +4 -1
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +4 -0
  61. package/package.json +31 -7
  62. package/dist/modules/composition.test.d.ts +0 -11
  63. package/dist/modules/composition.test.js +0 -450
  64. package/dist/modules/policy.test.d.ts +0 -10
  65. package/dist/modules/policy.test.js +0 -369
  66. package/src/cli.ts +0 -471
  67. package/src/commands/add.ts +0 -315
  68. package/src/commands/compose.ts +0 -185
  69. package/src/commands/index.ts +0 -13
  70. package/src/commands/init.ts +0 -94
  71. package/src/commands/list.ts +0 -33
  72. package/src/commands/pipe.ts +0 -76
  73. package/src/commands/remove.ts +0 -57
  74. package/src/commands/run.ts +0 -80
  75. package/src/commands/update.ts +0 -130
  76. package/src/commands/versions.ts +0 -79
  77. package/src/index.ts +0 -90
  78. package/src/mcp/index.ts +0 -5
  79. package/src/mcp/server.ts +0 -403
  80. package/src/modules/composition.test.ts +0 -558
  81. package/src/modules/composition.ts +0 -1674
  82. package/src/modules/index.ts +0 -9
  83. package/src/modules/loader.ts +0 -508
  84. package/src/modules/policy.test.ts +0 -455
  85. package/src/modules/runner.ts +0 -1983
  86. package/src/modules/subagent.ts +0 -277
  87. package/src/modules/validator.ts +0 -700
  88. package/src/providers/anthropic.ts +0 -89
  89. package/src/providers/base.ts +0 -29
  90. package/src/providers/deepseek.ts +0 -83
  91. package/src/providers/gemini.ts +0 -117
  92. package/src/providers/index.ts +0 -78
  93. package/src/providers/minimax.ts +0 -81
  94. package/src/providers/moonshot.ts +0 -82
  95. package/src/providers/ollama.ts +0 -83
  96. package/src/providers/openai.ts +0 -84
  97. package/src/providers/qwen.ts +0 -82
  98. package/src/server/http.ts +0 -316
  99. package/src/server/index.ts +0 -6
  100. package/src/types.ts +0 -599
  101. package/tsconfig.json +0 -17
@@ -1,700 +0,0 @@
1
- /**
2
- * Module Validator - Validate cognitive module structure and examples.
3
- * Supports v0, v1, v2.1, and v2.2 module formats.
4
- */
5
-
6
- import * as fs from 'node:fs/promises';
7
- import * as path from 'node:path';
8
- import yaml from 'js-yaml';
9
- import _Ajv from 'ajv';
10
- const Ajv = _Ajv.default || _Ajv;
11
- import type { RiskLevel } from '../types.js';
12
-
13
- const ajv = new Ajv({ allErrors: true, strict: false });
14
-
15
- // =============================================================================
16
- // Types
17
- // =============================================================================
18
-
19
- export interface ValidationResult {
20
- valid: boolean;
21
- errors: string[];
22
- warnings: string[];
23
- }
24
-
25
- // =============================================================================
26
- // Main Validation Entry Point
27
- // =============================================================================
28
-
29
- /**
30
- * Validate a cognitive module's structure and examples.
31
- * Supports all formats.
32
- *
33
- * @param nameOrPath Module name or path
34
- * @param v22 If true, validate v2.2 specific requirements
35
- * @returns Validation result with errors and warnings
36
- */
37
- export async function validateModule(
38
- modulePath: string,
39
- v22: boolean = false
40
- ): Promise<ValidationResult> {
41
- const errors: string[] = [];
42
- const warnings: string[] = [];
43
-
44
- // Check if path exists
45
- try {
46
- await fs.access(modulePath);
47
- } catch {
48
- return { valid: false, errors: [`Module not found: ${modulePath}`], warnings: [] };
49
- }
50
-
51
- // Detect format
52
- const hasModuleYaml = await fileExists(path.join(modulePath, 'module.yaml'));
53
- const hasModuleMd = await fileExists(path.join(modulePath, 'MODULE.md'));
54
- const hasOldModuleMd = await fileExists(path.join(modulePath, 'module.md'));
55
-
56
- if (hasModuleYaml) {
57
- // v2.x format
58
- if (v22) {
59
- return validateV22Format(modulePath);
60
- } else {
61
- return validateV2Format(modulePath);
62
- }
63
- } else if (hasModuleMd) {
64
- // v1 format
65
- if (v22) {
66
- errors.push("Module is v1 format. Use 'cogn migrate' to upgrade to v2.2");
67
- return { valid: false, errors, warnings };
68
- }
69
- return validateV1Format(modulePath);
70
- } else if (hasOldModuleMd) {
71
- // v0 format
72
- if (v22) {
73
- errors.push("Module is v0 format. Use 'cogn migrate' to upgrade to v2.2");
74
- return { valid: false, errors, warnings };
75
- }
76
- return validateV0Format(modulePath);
77
- } else {
78
- return { valid: false, errors: ['Missing module.yaml, MODULE.md, or module.md'], warnings: [] };
79
- }
80
- }
81
-
82
- // =============================================================================
83
- // v2.2 Validation
84
- // =============================================================================
85
-
86
- async function validateV22Format(modulePath: string): Promise<ValidationResult> {
87
- const errors: string[] = [];
88
- const warnings: string[] = [];
89
-
90
- // Check module.yaml
91
- const moduleYamlPath = path.join(modulePath, 'module.yaml');
92
- let manifest: Record<string, unknown>;
93
-
94
- try {
95
- const content = await fs.readFile(moduleYamlPath, 'utf-8');
96
- manifest = yaml.load(content) as Record<string, unknown>;
97
- } catch (e) {
98
- errors.push(`Invalid YAML in module.yaml: ${(e as Error).message}`);
99
- return { valid: false, errors, warnings };
100
- }
101
-
102
- // Check v2.2 required fields
103
- const v22RequiredFields = ['name', 'version', 'responsibility'];
104
- for (const field of v22RequiredFields) {
105
- if (!(field in manifest)) {
106
- errors.push(`module.yaml missing required field: ${field}`);
107
- }
108
- }
109
-
110
- // Check tier (v2.2 specific)
111
- const tier = manifest.tier as string | undefined;
112
- if (!tier) {
113
- warnings.push("module.yaml missing 'tier' (recommended: exec | decision | exploration)");
114
- } else if (!['exec', 'decision', 'exploration'].includes(tier)) {
115
- errors.push(`Invalid tier: ${tier}. Must be exec | decision | exploration`);
116
- }
117
-
118
- // Check schema_strictness
119
- const schemaStrictness = manifest.schema_strictness as string | undefined;
120
- if (schemaStrictness && !['high', 'medium', 'low'].includes(schemaStrictness)) {
121
- errors.push(`Invalid schema_strictness: ${schemaStrictness}. Must be high | medium | low`);
122
- }
123
-
124
- // Check overflow config
125
- const overflow = (manifest.overflow as Record<string, unknown>) ?? {};
126
- if (overflow.enabled) {
127
- if (overflow.require_suggested_mapping === undefined) {
128
- warnings.push("overflow.require_suggested_mapping not set (recommended for recoverable insights)");
129
- }
130
- }
131
-
132
- // Check enums config
133
- const enums = (manifest.enums as Record<string, unknown>) ?? {};
134
- const strategy = enums.strategy as string | undefined;
135
- if (strategy && !['strict', 'extensible'].includes(strategy)) {
136
- errors.push(`Invalid enums.strategy: ${strategy}. Must be strict | extensible`);
137
- }
138
-
139
- // Check compat config
140
- const compat = manifest.compat as Record<string, unknown> | undefined;
141
- if (!compat) {
142
- warnings.push("module.yaml missing 'compat' section (recommended for migration)");
143
- }
144
-
145
- // Check excludes
146
- const excludes = (manifest.excludes as string[]) ?? [];
147
- if (excludes.length === 0) {
148
- warnings.push("'excludes' list is empty (should list what module won't do)");
149
- }
150
-
151
- // Check prompt.md
152
- const promptPath = path.join(modulePath, 'prompt.md');
153
- if (!await fileExists(promptPath)) {
154
- errors.push("Missing prompt.md (required for v2.2)");
155
- } else {
156
- const prompt = await fs.readFile(promptPath, 'utf-8');
157
-
158
- // Check for v2.2 envelope format instructions
159
- if (!prompt.toLowerCase().includes('meta') && !prompt.toLowerCase().includes('envelope')) {
160
- warnings.push("prompt.md should mention v2.2 envelope format with meta/data separation");
161
- }
162
-
163
- if (prompt.length < 100) {
164
- warnings.push("prompt.md seems too short (< 100 chars)");
165
- }
166
- }
167
-
168
- // Check schema.json
169
- const schemaPath = path.join(modulePath, 'schema.json');
170
- if (!await fileExists(schemaPath)) {
171
- errors.push("Missing schema.json (required for v2.2)");
172
- } else {
173
- try {
174
- const schemaContent = await fs.readFile(schemaPath, 'utf-8');
175
- const schema = JSON.parse(schemaContent) as Record<string, unknown>;
176
-
177
- // Check for meta schema (v2.2 required)
178
- if (!('meta' in schema)) {
179
- errors.push("schema.json missing 'meta' schema (required for v2.2)");
180
- } else {
181
- const metaSchema = schema.meta as Record<string, unknown>;
182
- const metaRequired = (metaSchema.required as string[]) ?? [];
183
-
184
- if (!metaRequired.includes('confidence')) {
185
- errors.push("meta schema must require 'confidence'");
186
- }
187
- if (!metaRequired.includes('risk')) {
188
- errors.push("meta schema must require 'risk'");
189
- }
190
- if (!metaRequired.includes('explain')) {
191
- errors.push("meta schema must require 'explain'");
192
- }
193
-
194
- // Check explain maxLength
195
- const properties = (metaSchema.properties as Record<string, unknown>) ?? {};
196
- const explainProps = (properties.explain as Record<string, unknown>) ?? {};
197
- const maxLength = (explainProps.maxLength as number) ?? 999;
198
- if (maxLength > 280) {
199
- warnings.push("meta.explain should have maxLength <= 280");
200
- }
201
- }
202
-
203
- // Check for input schema
204
- if (!('input' in schema)) {
205
- warnings.push("schema.json missing 'input' definition");
206
- }
207
-
208
- // Check for data schema (v2.2 uses 'data' instead of 'output')
209
- if (!('data' in schema) && !('output' in schema)) {
210
- errors.push("schema.json missing 'data' (or 'output') definition");
211
- } else if ('data' in schema) {
212
- const dataSchema = schema.data as Record<string, unknown>;
213
- const dataRequired = (dataSchema.required as string[]) ?? [];
214
-
215
- if (!dataRequired.includes('rationale')) {
216
- warnings.push("data schema should require 'rationale' for audit");
217
- }
218
- }
219
-
220
- // Check for error schema
221
- if (!('error' in schema)) {
222
- warnings.push("schema.json missing 'error' definition");
223
- }
224
-
225
- // Check for $defs/extensions (v2.2 overflow)
226
- if (overflow.enabled) {
227
- const defs = (schema.$defs as Record<string, unknown>) ?? {};
228
- if (!('extensions' in defs)) {
229
- warnings.push("schema.json missing '$defs.extensions' (needed for overflow)");
230
- }
231
- }
232
-
233
- } catch (e) {
234
- errors.push(`Invalid JSON in schema.json: ${(e as Error).message}`);
235
- }
236
- }
237
-
238
- // Check tests directory
239
- const testsPath = path.join(modulePath, 'tests');
240
- if (!await fileExists(testsPath)) {
241
- warnings.push("Missing tests directory (recommended)");
242
- } else {
243
- // Check for v2.2 format in expected files
244
- try {
245
- const entries = await fs.readdir(testsPath);
246
- for (const entry of entries) {
247
- if (entry.endsWith('.expected.json')) {
248
- try {
249
- const expectedContent = await fs.readFile(path.join(testsPath, entry), 'utf-8');
250
- const expected = JSON.parse(expectedContent) as Record<string, unknown>;
251
-
252
- // Check if example uses v2.2 format
253
- const example = (expected.$example as Record<string, unknown>) ?? {};
254
- if (example.ok === true && !('meta' in example)) {
255
- warnings.push(`${entry}: $example missing 'meta' (v2.2 format)`);
256
- }
257
- } catch {
258
- // Skip invalid JSON
259
- }
260
- }
261
- }
262
- } catch {
263
- // Skip if can't read tests directory
264
- }
265
- }
266
-
267
- return { valid: errors.length === 0, errors, warnings };
268
- }
269
-
270
- // =============================================================================
271
- // v2.x (non-strict) Validation
272
- // =============================================================================
273
-
274
- async function validateV2Format(modulePath: string): Promise<ValidationResult> {
275
- const errors: string[] = [];
276
- const warnings: string[] = [];
277
-
278
- // Check module.yaml
279
- const moduleYamlPath = path.join(modulePath, 'module.yaml');
280
- let manifest: Record<string, unknown>;
281
-
282
- try {
283
- const content = await fs.readFile(moduleYamlPath, 'utf-8');
284
- manifest = yaml.load(content) as Record<string, unknown>;
285
- } catch (e) {
286
- errors.push(`Invalid YAML in module.yaml: ${(e as Error).message}`);
287
- return { valid: false, errors, warnings };
288
- }
289
-
290
- // Check required fields
291
- const requiredFields = ['name', 'version', 'responsibility'];
292
- for (const field of requiredFields) {
293
- if (!(field in manifest)) {
294
- errors.push(`module.yaml missing required field: ${field}`);
295
- }
296
- }
297
-
298
- // Check excludes
299
- const excludes = (manifest.excludes as string[]) ?? [];
300
- if (excludes.length === 0) {
301
- warnings.push("'excludes' list is empty");
302
- }
303
-
304
- // Check prompt.md or MODULE.md
305
- const promptPath = path.join(modulePath, 'prompt.md');
306
- const moduleMdPath = path.join(modulePath, 'MODULE.md');
307
-
308
- if (!await fileExists(promptPath) && !await fileExists(moduleMdPath)) {
309
- errors.push("Missing prompt.md or MODULE.md");
310
- } else if (await fileExists(promptPath)) {
311
- const prompt = await fs.readFile(promptPath, 'utf-8');
312
- if (prompt.length < 50) {
313
- warnings.push("prompt.md seems too short (< 50 chars)");
314
- }
315
- }
316
-
317
- // Check schema.json
318
- const schemaPath = path.join(modulePath, 'schema.json');
319
- if (!await fileExists(schemaPath)) {
320
- warnings.push("Missing schema.json (recommended)");
321
- } else {
322
- try {
323
- const schemaContent = await fs.readFile(schemaPath, 'utf-8');
324
- const schema = JSON.parse(schemaContent) as Record<string, unknown>;
325
-
326
- if (!('input' in schema)) {
327
- warnings.push("schema.json missing 'input' definition");
328
- }
329
-
330
- // Accept both 'data' and 'output'
331
- if (!('data' in schema) && !('output' in schema)) {
332
- warnings.push("schema.json missing 'data' or 'output' definition");
333
- }
334
-
335
- } catch (e) {
336
- errors.push(`Invalid JSON in schema.json: ${(e as Error).message}`);
337
- }
338
- }
339
-
340
- // Check for v2.2 features and suggest upgrade
341
- if (!manifest.tier) {
342
- warnings.push("Consider adding 'tier' for v2.2 (use 'cogn validate --v22' for full check)");
343
- }
344
-
345
- return { valid: errors.length === 0, errors, warnings };
346
- }
347
-
348
- // =============================================================================
349
- // v1 Format Validation (MODULE.md + schema.json)
350
- // =============================================================================
351
-
352
- async function validateV1Format(modulePath: string): Promise<ValidationResult> {
353
- const errors: string[] = [];
354
- const warnings: string[] = [];
355
-
356
- // Check MODULE.md
357
- const moduleMdPath = path.join(modulePath, 'MODULE.md');
358
-
359
- try {
360
- const content = await fs.readFile(moduleMdPath, 'utf-8');
361
-
362
- if (content.length === 0) {
363
- errors.push("MODULE.md is empty");
364
- return { valid: false, errors, warnings };
365
- }
366
-
367
- // Parse frontmatter
368
- if (!content.startsWith('---')) {
369
- errors.push("MODULE.md must start with YAML frontmatter (---)");
370
- } else {
371
- const parts = content.split('---');
372
- if (parts.length < 3) {
373
- errors.push("MODULE.md frontmatter not properly closed");
374
- } else {
375
- try {
376
- const frontmatter = yaml.load(parts[1]) as Record<string, unknown>;
377
- const body = parts.slice(2).join('---').trim();
378
-
379
- // Check required fields
380
- const requiredFields = ['name', 'version', 'responsibility', 'excludes'];
381
- for (const field of requiredFields) {
382
- if (!(field in frontmatter)) {
383
- errors.push(`MODULE.md missing required field: ${field}`);
384
- }
385
- }
386
-
387
- if ('excludes' in frontmatter) {
388
- if (!Array.isArray(frontmatter.excludes)) {
389
- errors.push("'excludes' must be a list");
390
- } else if (frontmatter.excludes.length === 0) {
391
- warnings.push("'excludes' list is empty");
392
- }
393
- }
394
-
395
- // Check body has content
396
- if (body.length < 50) {
397
- warnings.push("MODULE.md body seems too short (< 50 chars)");
398
- }
399
-
400
- } catch (e) {
401
- errors.push(`Invalid YAML in MODULE.md: ${(e as Error).message}`);
402
- }
403
- }
404
- }
405
- } catch (e) {
406
- errors.push(`Cannot read MODULE.md: ${(e as Error).message}`);
407
- return { valid: false, errors, warnings };
408
- }
409
-
410
- // Check schema.json
411
- const schemaPath = path.join(modulePath, 'schema.json');
412
- if (!await fileExists(schemaPath)) {
413
- warnings.push("Missing schema.json (recommended for validation)");
414
- } else {
415
- try {
416
- const schemaContent = await fs.readFile(schemaPath, 'utf-8');
417
- const schema = JSON.parse(schemaContent) as Record<string, unknown>;
418
-
419
- if (!('input' in schema)) {
420
- warnings.push("schema.json missing 'input' definition");
421
- }
422
- if (!('output' in schema)) {
423
- warnings.push("schema.json missing 'output' definition");
424
- }
425
-
426
- // Check output has required fields
427
- const output = (schema.output as Record<string, unknown>) ?? {};
428
- const required = (output.required as string[]) ?? [];
429
- if (!required.includes('confidence')) {
430
- warnings.push("output schema should require 'confidence'");
431
- }
432
- if (!required.includes('rationale')) {
433
- warnings.push("output schema should require 'rationale'");
434
- }
435
-
436
- } catch (e) {
437
- errors.push(`Invalid JSON in schema.json: ${(e as Error).message}`);
438
- }
439
- }
440
-
441
- // Check examples
442
- const examplesPath = path.join(modulePath, 'examples');
443
- if (!await fileExists(examplesPath)) {
444
- warnings.push("Missing examples directory (recommended)");
445
- } else {
446
- await validateExamples(examplesPath, path.join(modulePath, 'schema.json'), errors, warnings);
447
- }
448
-
449
- // Suggest v2.2 upgrade
450
- warnings.push("Consider upgrading to v2.2 format for better Control/Data separation");
451
-
452
- return { valid: errors.length === 0, errors, warnings };
453
- }
454
-
455
- // =============================================================================
456
- // v0 Format Validation (6-file format)
457
- // =============================================================================
458
-
459
- async function validateV0Format(modulePath: string): Promise<ValidationResult> {
460
- const errors: string[] = [];
461
- const warnings: string[] = [];
462
-
463
- // Check required files
464
- const requiredFiles = [
465
- 'module.md',
466
- 'input.schema.json',
467
- 'output.schema.json',
468
- 'constraints.yaml',
469
- 'prompt.txt',
470
- ];
471
-
472
- for (const filename of requiredFiles) {
473
- const filepath = path.join(modulePath, filename);
474
- if (!await fileExists(filepath)) {
475
- errors.push(`Missing required file: ${filename}`);
476
- } else {
477
- const stat = await fs.stat(filepath);
478
- if (stat.size === 0) {
479
- errors.push(`File is empty: ${filename}`);
480
- }
481
- }
482
- }
483
-
484
- // Check examples directory
485
- const examplesPath = path.join(modulePath, 'examples');
486
- if (!await fileExists(examplesPath)) {
487
- errors.push("Missing examples directory");
488
- } else {
489
- if (!await fileExists(path.join(examplesPath, 'input.json'))) {
490
- errors.push("Missing examples/input.json");
491
- }
492
- if (!await fileExists(path.join(examplesPath, 'output.json'))) {
493
- errors.push("Missing examples/output.json");
494
- }
495
- }
496
-
497
- if (errors.length > 0) {
498
- return { valid: false, errors, warnings };
499
- }
500
-
501
- // Validate module.md frontmatter
502
- try {
503
- const content = await fs.readFile(path.join(modulePath, 'module.md'), 'utf-8');
504
-
505
- if (!content.startsWith('---')) {
506
- errors.push("module.md must start with YAML frontmatter (---)");
507
- } else {
508
- const parts = content.split('---');
509
- if (parts.length < 3) {
510
- errors.push("module.md frontmatter not properly closed");
511
- } else {
512
- try {
513
- const frontmatter = yaml.load(parts[1]) as Record<string, unknown>;
514
-
515
- const requiredFields = ['name', 'version', 'responsibility', 'excludes'];
516
- for (const field of requiredFields) {
517
- if (!(field in frontmatter)) {
518
- errors.push(`module.md missing required field: ${field}`);
519
- }
520
- }
521
-
522
- if ('excludes' in frontmatter) {
523
- if (!Array.isArray(frontmatter.excludes)) {
524
- errors.push("'excludes' must be a list");
525
- } else if (frontmatter.excludes.length === 0) {
526
- warnings.push("'excludes' list is empty");
527
- }
528
- }
529
-
530
- } catch (e) {
531
- errors.push(`Invalid YAML in module.md: ${(e as Error).message}`);
532
- }
533
- }
534
- }
535
- } catch (e) {
536
- errors.push(`Cannot read module.md: ${(e as Error).message}`);
537
- }
538
-
539
- // Suggest v2.2 upgrade
540
- warnings.push("v0 format is deprecated. Consider upgrading to v2.2");
541
-
542
- return { valid: errors.length === 0, errors, warnings };
543
- }
544
-
545
- // =============================================================================
546
- // Helper Functions
547
- // =============================================================================
548
-
549
- async function fileExists(filepath: string): Promise<boolean> {
550
- try {
551
- await fs.access(filepath);
552
- return true;
553
- } catch {
554
- return false;
555
- }
556
- }
557
-
558
- async function validateExamples(
559
- examplesPath: string,
560
- schemaPath: string,
561
- errors: string[],
562
- warnings: string[]
563
- ): Promise<void> {
564
- if (!await fileExists(path.join(examplesPath, 'input.json'))) {
565
- warnings.push("Missing examples/input.json");
566
- }
567
- if (!await fileExists(path.join(examplesPath, 'output.json'))) {
568
- warnings.push("Missing examples/output.json");
569
- }
570
-
571
- // Validate examples against schema if both exist
572
- if (await fileExists(schemaPath)) {
573
- try {
574
- const schemaContent = await fs.readFile(schemaPath, 'utf-8');
575
- const schema = JSON.parse(schemaContent) as Record<string, unknown>;
576
-
577
- // Validate input example
578
- const inputExamplePath = path.join(examplesPath, 'input.json');
579
- if (await fileExists(inputExamplePath) && 'input' in schema) {
580
- try {
581
- const inputContent = await fs.readFile(inputExamplePath, 'utf-8');
582
- const inputExample = JSON.parse(inputContent);
583
-
584
- const validate = ajv.compile(schema.input as object);
585
- const valid = validate(inputExample);
586
- if (!valid && validate.errors) {
587
- errors.push(`Example input fails schema: ${validate.errors[0]?.message}`);
588
- }
589
- } catch (e) {
590
- errors.push(`Invalid JSON in examples/input.json: ${(e as Error).message}`);
591
- }
592
- }
593
-
594
- // Validate output example
595
- const outputExamplePath = path.join(examplesPath, 'output.json');
596
- const outputSchema = (schema.output || schema.data) as object | undefined;
597
- if (await fileExists(outputExamplePath) && outputSchema) {
598
- try {
599
- const outputContent = await fs.readFile(outputExamplePath, 'utf-8');
600
- const outputExample = JSON.parse(outputContent) as Record<string, unknown>;
601
-
602
- const validate = ajv.compile(outputSchema);
603
- const valid = validate(outputExample);
604
- if (!valid && validate.errors) {
605
- errors.push(`Example output fails schema: ${validate.errors[0]?.message}`);
606
- }
607
-
608
- // Check confidence
609
- if ('confidence' in outputExample) {
610
- const conf = outputExample.confidence as number;
611
- if (conf < 0 || conf > 1) {
612
- errors.push(`Confidence must be 0-1, got: ${conf}`);
613
- }
614
- }
615
- } catch (e) {
616
- errors.push(`Invalid JSON in examples/output.json: ${(e as Error).message}`);
617
- }
618
- }
619
-
620
- } catch {
621
- // Skip if schema can't be read
622
- }
623
- }
624
- }
625
-
626
- // =============================================================================
627
- // Envelope Validation
628
- // =============================================================================
629
-
630
- /**
631
- * Validate a response against v2.2 envelope format.
632
- *
633
- * @param response The response dict to validate
634
- * @returns Validation result
635
- */
636
- export function validateV22Envelope(response: Record<string, unknown>): { valid: boolean; errors: string[] } {
637
- const errors: string[] = [];
638
-
639
- // Check ok field
640
- if (!('ok' in response)) {
641
- errors.push("Missing 'ok' field");
642
- return { valid: false, errors };
643
- }
644
-
645
- // Check meta
646
- if (!('meta' in response)) {
647
- errors.push("Missing 'meta' field (required for v2.2)");
648
- } else {
649
- const meta = response.meta as Record<string, unknown>;
650
-
651
- if (!('confidence' in meta)) {
652
- errors.push("meta missing 'confidence'");
653
- } else if (typeof meta.confidence !== 'number') {
654
- errors.push("meta.confidence must be a number");
655
- } else if (meta.confidence < 0 || meta.confidence > 1) {
656
- errors.push("meta.confidence must be between 0 and 1");
657
- }
658
-
659
- if (!('risk' in meta)) {
660
- errors.push("meta missing 'risk'");
661
- } else {
662
- const validRisks: RiskLevel[] = ['none', 'low', 'medium', 'high'];
663
- if (!validRisks.includes(meta.risk as RiskLevel)) {
664
- errors.push(`meta.risk must be none|low|medium|high, got: ${meta.risk}`);
665
- }
666
- }
667
-
668
- if (!('explain' in meta)) {
669
- errors.push("meta missing 'explain'");
670
- } else {
671
- const explain = (meta.explain as string) ?? '';
672
- if (explain.length > 280) {
673
- errors.push(`meta.explain exceeds 280 chars (${explain.length} chars)`);
674
- }
675
- }
676
- }
677
-
678
- // Check data or error
679
- if (response.ok) {
680
- if (!('data' in response)) {
681
- errors.push("Success response missing 'data' field");
682
- }
683
- // Note: data.rationale is recommended but not required by v2.2 envelope spec
684
- // The data schema validation will enforce it if the module specifies it as required
685
- } else {
686
- if (!('error' in response)) {
687
- errors.push("Error response missing 'error' field");
688
- } else {
689
- const error = response.error as Record<string, unknown>;
690
- if (!('code' in error)) {
691
- errors.push("error missing 'code'");
692
- }
693
- if (!('message' in error)) {
694
- errors.push("error missing 'message'");
695
- }
696
- }
697
- }
698
-
699
- return { valid: errors.length === 0, errors };
700
- }