| 25 | } |
| 26 | |
| 27 | function validatePattern(p, index) { |
| 28 | const prefix = `patterns[${index}]`; |
| 29 | const required = ['id', 'name', 'file', 'goal', 'cadence', 'risk', 'tools', 'skills', 'state', 'phases', 'human_gates']; |
| 30 | for (const key of required) { |
| 31 | if (!(key in p)) fail(`${prefix} missing required field: ${key}`); |
| 32 | } |
| 33 | if (!ID_RE.test(p.id)) fail(`${prefix}.id invalid: ${p.id}`); |
| 34 | if (!FILE_RE.test(p.file)) fail(`${prefix}.file invalid: ${p.file}`); |
| 35 | if (!CADENCE_RE.test(p.cadence)) fail(`${prefix}.cadence invalid: ${p.cadence}`); |
| 36 | if (!VALID_RISK.has(p.risk)) fail(`${prefix}.risk invalid: ${p.risk}`); |
| 37 | if (!Array.isArray(p.tools) || p.tools.length === 0) fail(`${prefix}.tools must be non-empty array`); |
| 38 | for (const t of p.tools) { |
| 39 | if (!VALID_TOOLS.has(t)) fail(`${prefix}.tools unknown tool: ${t}`); |
| 40 | } |
| 41 | if (!Array.isArray(p.skills) || p.skills.length === 0) fail(`${prefix}.skills must be non-empty array`); |
| 42 | if (!FILE_RE.test(p.state)) fail(`${prefix}.state invalid: ${p.state}`); |
| 43 | if (!Array.isArray(p.phases) || p.phases.length < 2) fail(`${prefix}.phases must have ≥2 entries`); |
| 44 | if (!Array.isArray(p.human_gates) || p.human_gates.length === 0) fail(`${prefix}.human_gates must be non-empty`); |
| 45 | if (p.week_one_mode && !VALID_MODES.has(p.week_one_mode)) fail(`${prefix}.week_one_mode invalid`); |
| 46 | if (p.token_cost && !VALID_COST.has(p.token_cost)) fail(`${prefix}.token_cost invalid`); |
| 47 | if (!p.cost) fail(`${prefix} missing required field: cost`); |
| 48 | const costKeys = ['tokens_noop', 'tokens_report', 'tokens_action', 'suggested_daily_cap', 'early_exit_required']; |
| 49 | for (const key of costKeys) { |
| 50 | if (!(key in p.cost)) fail(`${prefix}.cost missing field: ${key}`); |
| 51 | } |
| 52 | for (const key of ['tokens_noop', 'tokens_report', 'tokens_action', 'suggested_daily_cap']) { |
| 53 | if (typeof p.cost[key] !== 'number' || p.cost[key] < 1000) { |
| 54 | fail(`${prefix}.cost.${key} must be a positive integer`); |
| 55 | } |
| 56 | } |
| 57 | if (typeof p.cost.early_exit_required !== 'boolean') { |
| 58 | fail(`${prefix}.cost.early_exit_required must be boolean`); |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | async function main() { |
| 63 | const registryPath = path.join(ROOT, 'patterns', 'registry.yaml'); |