feat: implement workflow schema validator with Zod
Implement the Zod schema for workflow.yaml files and the CLI validator tool, completing the GREEN phase of TDD. - tools/schema/workflow.js: Zod schema with 5 required fields, template polymorphism (string|false), input_file_patterns with load_strategy enum, execution_hints, and instructions extension validation. Passthrough for varying optional string fields. - tools/validate-workflow-schema.js: CLI tool mirroring validate-agent-schema.js with --strict flag and GitHub Actions ::warning annotations. - package.json: Wire test:schemas and validate:schemas to include workflow schema validation. 27/27 tests passing. 12/13 real workflow files pass validation (qa/automate missing standalone — pre-existing upstream defect). Ref: MSSCI-12749
This commit is contained in:
parent
0b83185a64
commit
8119c5d30f
|
|
@ -48,9 +48,9 @@
|
||||||
"test": "npm run test:schemas && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
|
"test": "npm run test:schemas && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
|
||||||
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
|
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
|
||||||
"test:install": "node test/test-installation-components.js",
|
"test:install": "node test/test-installation-components.js",
|
||||||
"test:schemas": "node test/test-agent-schema.js",
|
"test:schemas": "node test/test-agent-schema.js && node test/test-workflow-schema.js",
|
||||||
"validate:refs": "node tools/validate-file-refs.js",
|
"validate:refs": "node tools/validate-file-refs.js",
|
||||||
"validate:schemas": "node tools/validate-agent-schema.js"
|
"validate:schemas": "node tools/validate-agent-schema.js && node tools/validate-workflow-schema.js"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,cjs,mjs}": [
|
"*.{js,cjs,mjs}": [
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,63 @@
|
||||||
// Zod schema definition for workflow.yaml files
|
// Zod schema definition for workflow.yaml files
|
||||||
// STUB — implementation pending. All validations return failure.
|
const { z } = require('zod');
|
||||||
// See: sprint/reference/mssci-12749-discovery.md for field inventory and validation rules.
|
|
||||||
|
// Load strategy enum — the three recognized values across all existing workflow files
|
||||||
|
const loadStrategyEnum = z.enum(['FULL_LOAD', 'SELECTIVE_LOAD', 'INDEX_GUIDED']);
|
||||||
|
|
||||||
|
// Schema for individual input_file_patterns entries
|
||||||
|
const inputFilePatternEntrySchema = z
|
||||||
|
.object({
|
||||||
|
load_strategy: loadStrategyEnum,
|
||||||
|
description: z.string().optional(),
|
||||||
|
whole: z.string().optional(),
|
||||||
|
sharded: z.string().optional(),
|
||||||
|
sharded_index: z.string().optional(),
|
||||||
|
sharded_single: z.string().optional(),
|
||||||
|
pattern: z.string().optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
// Schema for execution_hints
|
||||||
|
const executionHintsSchema = z
|
||||||
|
.object({
|
||||||
|
interactive: z.boolean().optional(),
|
||||||
|
autonomous: z.boolean().optional(),
|
||||||
|
iterative: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
// Main workflow schema
|
||||||
|
const workflowSchema = z
|
||||||
|
.object({
|
||||||
|
// Required fields (all 13 workflow.yaml files have these)
|
||||||
|
name: z.string().min(1),
|
||||||
|
description: z.string().min(1),
|
||||||
|
author: z.string().min(1),
|
||||||
|
standalone: z.boolean(),
|
||||||
|
web_bundle: z.boolean(),
|
||||||
|
|
||||||
|
// Structured optional fields
|
||||||
|
template: z.union([z.string(), z.literal(false)]).optional(),
|
||||||
|
input_file_patterns: z.record(z.string(), inputFilePatternEntrySchema).optional(),
|
||||||
|
execution_hints: executionHintsSchema.optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
required_tools: z.array(z.string()).optional(),
|
||||||
|
variables: z.record(z.string(), z.string()).optional(),
|
||||||
|
instructions: z.string().optional(),
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (typeof data.instructions === 'string') {
|
||||||
|
return data.instructions.endsWith('.md') || data.instructions.endsWith('.xml');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Instructions file must end with .md or .xml',
|
||||||
|
path: ['instructions'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a workflow YAML payload against the schema.
|
* Validate a workflow YAML payload against the schema.
|
||||||
|
|
@ -11,20 +68,7 @@
|
||||||
* @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
|
* @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
|
||||||
*/
|
*/
|
||||||
function validateWorkflowFile(filePath, workflowYaml) {
|
function validateWorkflowFile(filePath, workflowYaml) {
|
||||||
// TODO: Implement Zod schema validation
|
return workflowSchema.safeParse(workflowYaml);
|
||||||
// This stub returns failure so tests are in RED state.
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: {
|
|
||||||
issues: [
|
|
||||||
{
|
|
||||||
code: 'custom',
|
|
||||||
message: 'Schema not yet implemented',
|
|
||||||
path: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { validateWorkflowFile };
|
module.exports = { validateWorkflowFile };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* Workflow Schema Validator CLI
|
||||||
|
*
|
||||||
|
* Scans all workflow.yaml files in src/{core,bmm}/workflows/
|
||||||
|
* and validates them against the Zod schema.
|
||||||
|
*
|
||||||
|
* Usage: node tools/validate-workflow-schema.js [--strict] [project_root]
|
||||||
|
* Exit codes:
|
||||||
|
* 0 = success (or warnings-only in default mode)
|
||||||
|
* 1 = validation failures in --strict mode, or no files found
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* --strict Exit 1 when validation errors exist (for CI enforcement)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { glob } = require('glob');
|
||||||
|
const yaml = require('yaml');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { validateWorkflowFile } = require('./schema/workflow.js');
|
||||||
|
|
||||||
|
const isCI = !!process.env.GITHUB_ACTIONS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main validation routine
|
||||||
|
* @param {object} options
|
||||||
|
* @param {boolean} options.strict - Exit 1 on validation errors
|
||||||
|
* @param {string} [options.projectRoot] - Optional project root to scan
|
||||||
|
*/
|
||||||
|
async function main({ strict, projectRoot }) {
|
||||||
|
console.log('🔍 Scanning for workflow files...\n');
|
||||||
|
|
||||||
|
const root = projectRoot || path.join(__dirname, '..');
|
||||||
|
|
||||||
|
const workflowFiles = await glob('src/{core,bmm}/workflows/**/workflow.yaml', {
|
||||||
|
cwd: root,
|
||||||
|
absolute: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workflowFiles.length === 0) {
|
||||||
|
console.log('❌ No workflow files found. This likely indicates a configuration error.');
|
||||||
|
console.log(' Expected to find workflow.yaml files in src/{core,bmm}/workflows/');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${workflowFiles.length} workflow file(s)\n`);
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const filePath of workflowFiles.sort()) {
|
||||||
|
const relativePath = path.relative(process.cwd(), filePath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const workflowData = yaml.parse(fileContent);
|
||||||
|
|
||||||
|
const result = validateWorkflowFile(relativePath, workflowData);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ ${relativePath}`);
|
||||||
|
} else {
|
||||||
|
errors.push({
|
||||||
|
file: relativePath,
|
||||||
|
issues: result.error.issues,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errors.push({
|
||||||
|
file: relativePath,
|
||||||
|
issues: [
|
||||||
|
{
|
||||||
|
code: 'parse_error',
|
||||||
|
message: `Failed to parse YAML: ${error.message}`,
|
||||||
|
path: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.log(`\n⚠️ Validation issues in ${errors.length} file(s):\n`);
|
||||||
|
|
||||||
|
for (const { file, issues } of errors) {
|
||||||
|
console.log(`📄 ${file}`);
|
||||||
|
for (const issue of issues) {
|
||||||
|
const pathString = issue.path.length > 0 ? issue.path.join('.') : '(root)';
|
||||||
|
console.log(` Path: ${pathString}`);
|
||||||
|
console.log(` Error: ${issue.message}`);
|
||||||
|
|
||||||
|
if (isCI) {
|
||||||
|
console.log(`::warning file=${file},title=Workflow Schema::${pathString}: ${issue.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strict) {
|
||||||
|
console.log(`💥 ${errors.length} file(s) failed validation (--strict mode)\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`⚠️ ${errors.length} file(s) have warnings (non-blocking)\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✨ All ${workflowFiles.length} workflow file(s) passed validation!\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CLI arguments
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const strict = args.includes('--strict');
|
||||||
|
const projectRoot = args.find((arg) => !arg.startsWith('--'));
|
||||||
|
|
||||||
|
main({ strict, projectRoot }).catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue