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