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:
Michael Pursifull 2026-02-04 13:27:57 -06:00
parent 0b83185a64
commit 8119c5d30f
No known key found for this signature in database
3 changed files with 181 additions and 18 deletions

View File

@ -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:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
"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:schemas": "node tools/validate-agent-schema.js"
"validate:schemas": "node tools/validate-agent-schema.js && node tools/validate-workflow-schema.js"
},
"lint-staged": {
"*.{js,cjs,mjs}": [

View File

@ -1,6 +1,63 @@
// Zod schema definition for workflow.yaml files
// STUB — implementation pending. All validations return failure.
// See: sprint/reference/mssci-12749-discovery.md for field inventory and validation rules.
const { z } = require('zod');
// 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.
@ -11,20 +68,7 @@
* @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
*/
function validateWorkflowFile(filePath, workflowYaml) {
// TODO: Implement Zod schema validation
// This stub returns failure so tests are in RED state.
return {
success: false,
error: {
issues: [
{
code: 'custom',
message: 'Schema not yet implemented',
path: [],
},
],
},
};
return workflowSchema.safeParse(workflowYaml);
}
module.exports = { validateWorkflowFile };

View File

@ -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);
});