{ "guards": [], "id": "provisioning-validate-formula", "postconditions": [ "Formula passes nickel typecheck with schema + referential integrity contracts", "All referenced taskservs exist in catalog/taskservs/", "No ConflictsWith violations detected", "DAG is parseable (cycle detection delegated to Orchestrator topological_sort)", "Validation report produced with formula metadata and DAG adjacency list" ], "preconditions": [ "{formula_file} exists and imports schemas/lib/formula.ncl", "nickel is available in PATH", "jq is available in PATH", "NICKEL_IMPORT_PATH includes provisioning/schemas" ], "steps": [ { "action": "nickel_typecheck_formula", "actor": "Agent", "cmd": "nickel typecheck {formula_file}", "depends_on": [], "id": "typecheck_formula", "note": "Schema validation + referential integrity via formula.ncl contracts. Catches: unknown node_id in depends_on, edge endpoints, duplicate node ids.", "on_error": { "backoff_s": 5, "max": 3, "strategy": "Stop" } }, { "action": "export_formula_to_json", "actor": "Agent", "cmd": "nickel export --format json {formula_file}", "depends_on": [ { "kind": "OnSuccess", "step": "typecheck_formula" } ], "id": "export_formula_json", "note": "Export formula as JSON for jq-based cross-validation steps.", "on_error": { "backoff_s": 5, "max": 3, "strategy": "Stop" } }, { "action": "verify_formula_taskserv_presence", "actor": "Agent", "cmd": "nickel export --format json {formula_file} | jq -r '.nodes[].taskserv.name' | sort -u | while read ts; do test -d catalog/taskservs/$ts || echo \"MISSING taskserv: $ts\"; done", "depends_on": [ { "kind": "OnSuccess", "step": "export_formula_json" } ], "id": "check_taskservs_exist", "note": "Verify each taskserv referenced by formula nodes exists in catalog/taskservs/.", "on_error": { "backoff_s": 5, "max": 3, "strategy": "Continue" } }, { "action": "extract_metadata_deps", "actor": "Agent", "cmd": "nickel export --format json {formula_file} | jq -r '.nodes[].taskserv.name' | sort -u | while read ts; do test -f catalog/taskservs/$ts/metadata.ncl && nickel export --format json catalog/taskservs/$ts/metadata.ncl | jq --arg ts \"$ts\" '{ts: $ts, deps: .dependencies}' || echo \"{\\\"ts\\\": \\\"$ts\\\", \\\"deps\\\": []}\"; done", "depends_on": [ { "kind": "Always", "step": "check_taskservs_exist" } ], "id": "read_metadata_dependencies", "note": "For each taskserv in the formula, read its metadata.ncl dependencies array.", "on_error": { "backoff_s": 5, "max": 3, "strategy": "Continue" } }, { "action": "validate_dep_coverage", "actor": "Agent", "cmd": "nickel export --format json {formula_file} | jq '[.nodes[] | {id: .id, name: .taskserv.name, deps: [.depends_on[].node_id]}]' | jq --argjson formula \"$(nickel export --format json {formula_file})\" 'map(. as $node | select($node.deps | length > 0))'", "depends_on": [ { "kind": "Always", "step": "read_metadata_dependencies" } ], "id": "cross_check_dependencies", "note": "Cross-check: if metadata.ncl declares a dependency that is NOT in formula depends_on, emit a warning. Soft check — formula may intentionally omit implied deps.", "on_error": { "backoff_s": 5, "max": 3, "strategy": "Continue" } }, { "action": "check_conflictswith_pairs", "actor": "Agent", "cmd": "nickel export --format json {formula_file} | jq -r '.nodes[].taskserv.name' | sort -u | while read ts; do test -f catalog/taskservs/$ts/metadata.ncl && nickel export --format json catalog/taskservs/$ts/metadata.ncl | jq -r --arg ts \"$ts\" '.conflicts_with // [] | .[] | [$ts, .] | @tsv' || true; done | while IFS=$'\\t' read a b; do nickel export --format json {formula_file} | jq -e --arg a \"$a\" --arg b \"$b\" '[.nodes[].taskserv.name] | (contains([$a]) and contains([$b]))' > /dev/null && echo \"CONFLICT: $a conflicts with $b (both in formula)\"; done", "depends_on": [ { "kind": "Always", "step": "read_metadata_dependencies" } ], "id": "detect_conflicts", "note": "Detect ConflictsWith pairs: if metadata.ncl declares a conflict and both taskservs are in the formula, it's a hard error.", "on_error": { "backoff_s": 5, "max": 3, "strategy": "Continue" } }, { "action": "topological_sort_check", "actor": "Agent", "cmd": "nickel export --format json {formula_file} | jq 'def topo: . as $f | $f.nodes | map({id: .id, deps: [.depends_on[].node_id]}) | to_entries | map({node: .value.id, deps: .value.deps}) | length; topo'", "depends_on": [ { "kind": "OnSuccess", "step": "export_formula_json" } ], "id": "validate_dag_acyclic", "note": "Validates the DAG structure. Full cycle detection is done by the Orchestrator Rust topological_sort. This step verifies the JSON is parseable as a DAG-shaped structure.", "on_error": { "backoff_s": 5, "max": 3, "strategy": "Stop" } }, { "action": "write_validation_summary", "actor": "Agent", "cmd": "nickel export --format json {formula_file} | jq '{formula: .id, server: .server, provider: .provider, nodes: (.nodes | length), edges: (.edges | length), max_parallel: .max_parallel, dag: [.nodes[] | {id: .id, taskserv: .taskserv.name, parallel: .parallel, on_error: .on_error, depends_on: [.depends_on[].node_id]}]}'", "depends_on": [ { "kind": "Always", "step": "validate_dag_acyclic" }, { "kind": "Always", "step": "detect_conflicts" }, { "kind": "Always", "step": "cross_check_dependencies" } ], "id": "generate_validation_report", "note": "Produce the validation summary: formula metadata, node count, DAG adjacency list.", "on_error": { "backoff_s": 5, "max": 3, "strategy": "Stop" } } ], "strategy": "Override", "trigger": "Validate a workspace Formula DAG for correctness, completeness, and metadata coherence" }