135 lines
6.7 KiB
Text
135 lines
6.7 KiB
Text
|
|
let s = import "reflection/schema.ncl" in
|
||
|
|
|
||
|
|
# Mode: provisioning-validate-formula
|
||
|
|
# Cross-validates a workspace Formula DAG against:
|
||
|
|
# 1. Nickel typecheck (schema + referential integrity via formula.ncl contracts)
|
||
|
|
# 2. Taskserv existence (each node.taskserv.name must exist in catalog/taskservs/)
|
||
|
|
# 3. Metadata dependency cross-check (metadata.ncl.dependencies vs formula depends_on)
|
||
|
|
# 4. Conflict detection (ConflictsWith relationships in metadata.ncl)
|
||
|
|
# 5. Cycle detection (topological sort must succeed)
|
||
|
|
#
|
||
|
|
# Required params:
|
||
|
|
# {formula_file} — path to the .ncl file containing the formula (relative to provisioning root)
|
||
|
|
# {workspace_dir} — absolute path to workspace root
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "provisioning-validate-formula",
|
||
|
|
trigger = "Validate a workspace Formula DAG for correctness, completeness, and metadata coherence",
|
||
|
|
strategy = 'Override,
|
||
|
|
|
||
|
|
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 = [
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "typecheck_formula",
|
||
|
|
action = "nickel_typecheck_formula",
|
||
|
|
actor = 'Agent,
|
||
|
|
cmd = "nickel typecheck {formula_file}",
|
||
|
|
depends_on = [],
|
||
|
|
on_error = { strategy = 'Stop },
|
||
|
|
note = "Schema validation + referential integrity via formula.ncl contracts. Catches: unknown node_id in depends_on, edge endpoints, duplicate node ids.",
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "export_formula_json",
|
||
|
|
action = "export_formula_to_json",
|
||
|
|
actor = 'Agent,
|
||
|
|
cmd = "nickel export --format json {formula_file}",
|
||
|
|
depends_on = [
|
||
|
|
{ step = "typecheck_formula", kind = 'OnSuccess },
|
||
|
|
],
|
||
|
|
on_error = { strategy = 'Stop },
|
||
|
|
note = "Export formula as JSON for jq-based cross-validation steps.",
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "check_taskservs_exist",
|
||
|
|
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 = [
|
||
|
|
{ step = "export_formula_json", kind = 'OnSuccess },
|
||
|
|
],
|
||
|
|
on_error = { strategy = 'Continue },
|
||
|
|
note = "Verify each taskserv referenced by formula nodes exists in catalog/taskservs/.",
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "read_metadata_dependencies",
|
||
|
|
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 = [
|
||
|
|
{ step = "check_taskservs_exist", kind = 'Always },
|
||
|
|
],
|
||
|
|
on_error = { strategy = 'Continue },
|
||
|
|
note = "For each taskserv in the formula, read its metadata.ncl dependencies array.",
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "cross_check_dependencies",
|
||
|
|
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 = [
|
||
|
|
{ step = "read_metadata_dependencies", kind = 'Always },
|
||
|
|
],
|
||
|
|
on_error = { strategy = 'Continue },
|
||
|
|
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.",
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "detect_conflicts",
|
||
|
|
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 = [
|
||
|
|
{ step = "read_metadata_dependencies", kind = 'Always },
|
||
|
|
],
|
||
|
|
on_error = { strategy = 'Continue },
|
||
|
|
note = "Detect ConflictsWith pairs: if metadata.ncl declares a conflict and both taskservs are in the formula, it's a hard error.",
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "validate_dag_acyclic",
|
||
|
|
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 = [
|
||
|
|
{ step = "export_formula_json", kind = 'OnSuccess },
|
||
|
|
],
|
||
|
|
on_error = { strategy = 'Stop },
|
||
|
|
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.",
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id = "generate_validation_report",
|
||
|
|
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 = [
|
||
|
|
{ step = "validate_dag_acyclic", kind = 'Always },
|
||
|
|
{ step = "detect_conflicts", kind = 'Always },
|
||
|
|
{ step = "cross_check_dependencies", kind = 'Always },
|
||
|
|
],
|
||
|
|
on_error = { strategy = 'Stop },
|
||
|
|
note = "Produce the validation summary: formula metadata, node count, DAG adjacency list.",
|
||
|
|
},
|
||
|
|
|
||
|
|
],
|
||
|
|
|
||
|
|
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",
|
||
|
|
],
|
||
|
|
} | (s.Mode String)
|