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
package/dist/modules/subagent.js
CHANGED
|
@@ -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
|
+
}
|