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: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}": [
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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