use ../lib_provisioning/workspace * use ../lib_provisioning/workspace/notation.nu * use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval, ncl-eval-soft] # Resolve the provisioning root for --import-path resolution. def provisioning-root [] : nothing -> string { $env.PROVISIONING? | default "/usr/local/provisioning" } # Export a Nickel file as parsed JSON, or error with stderr context. def nickel-export [path: string] : nothing -> record { ncl-eval $path [(provisioning-root)] } # Show the workspace DAG composition for a given infra. # # Renders each formula_id with its depends_on edges, conditions, health gates, # and parallel flag. Marks root and terminal nodes. export def "main dag show" [ --workspace (-w): string # Workspace name (default: active) --infra (-i): string = "wuji" # Infra name ] : nothing -> nothing { let ws_name = if ($workspace | is-not-empty) { $workspace } else { let details = (get-active-workspace-details) if ($details == null) { error make { msg: "No active workspace. Pass --workspace or activate one first." } } $details.name } let ws_root = (get-workspace-path $ws_name) if ($ws_root | is-empty) { error make { msg: $"Workspace '($ws_name)' not found in registry." } } let dag_path = ($ws_root | path join "infra" $infra "dag.ncl") if not ($dag_path | path exists) { error make { msg: $"dag.ncl not found at ($dag_path)" } } let dag = (nickel-export $dag_path) let formulas = $dag.composition.formulas # Determine roots (no depends_on) and terminals (not depended upon by others). let all_dep_targets = ($formulas | each { |e| $e.depends_on | each { |d| $d.formula_id } } | flatten) let roots = ($formulas | where ($it.depends_on | length) == 0 | each { |e| $e.formula_id }) let terminals = ($formulas | where { |e| not ($all_dep_targets | any { |d| $d == $e.formula_id }) } | each { |e| $e.formula_id }) print $"DAG: ($dag.workspace) / ($dag.infra)" print "" for entry in $formulas { let is_root = ($roots | any { |r| $r == $entry.formula_id }) let is_terminal = ($terminals | any { |t| $t == $entry.formula_id }) let tags = ([ (if $is_root { "[root]" } else { "" }) (if $is_terminal { "[terminal]" } else { "" }) (if $entry.parallel { "[parallel]" } else { "" }) ] | where ($it | is-not-empty) | str join " ") print $" ($entry.formula_id) ($tags)" if ($entry.depends_on | length) > 0 { for dep in $entry.depends_on { print $" └─ depends_on: ($dep.formula_id) [($dep.condition)]" } } if "health_gate" in $entry and ($entry.health_gate != null) { let g = $entry.health_gate print $" └─ health_gate: ($g.check_cmd) | expect=($g.expect) timeout=($g.timeout_ms)ms retries=($g.retries)" } print "" } } # Validate dag.ncl against its Nickel schema and cross-check formula_ids against servers.ncl. export def "main dag validate" [ --workspace (-w): string # Workspace name (default: active) --infra (-i): string = "wuji" # Infra name ] : nothing -> nothing { let ws_name = if ($workspace | is-not-empty) { $workspace } else { let details = (get-active-workspace-details) if ($details == null) { error make { msg: "No active workspace. Pass --workspace or activate one first." } } $details.name } let ws_root = (get-workspace-path $ws_name) if ($ws_root | is-empty) { error make { msg: $"Workspace '($ws_name)' not found in registry." } } let dag_path = ($ws_root | path join "infra" $infra "dag.ncl") let servers_path = ($ws_root | path join "infra" $infra "servers.ncl") let prov_root = (provisioning-root) mut passed = true # Step 1: schema + contract validation via nickel export print " [1/3] Nickel schema + WorkspaceComposition contract ..." let dag_data = (ncl-eval-soft $dag_path [$prov_root] null) if ($dag_data | is-not-empty) { print " PASS" } else { print " FAIL: nickel export failed or empty" $passed = false } # Step 2: load servers.ncl formula IDs print " [2/3] Cross-check formula_ids against servers.ncl ..." let servers_data = (ncl-eval-soft $servers_path [$prov_root] null) if ($servers_data | is-empty) { print " SKIP (servers.ncl export failed)" } else if ($dag_data | is-not-empty) { let dag_ids = ($dag_data | get composition.formulas | each { |e| $e.formula_id }) let server_ids = ($servers_data | get formulas | each { |f| $f.id }) let dangling = ($dag_ids | where { |id| not ($server_ids | any { |sid| $sid == $id }) }) if ($dangling | length) == 0 { print " PASS" } else { print $" FAIL: dag.ncl references unknown formula_ids: ($dangling | str join ', ')" $passed = false } } # Step 3: check all formulas in servers.ncl are covered by dag.ncl print " [3/3] Coverage — all servers.ncl formulas present in dag.ncl ..." if ($servers_data | is-not-empty) and ($dag_data | is-not-empty) { let dag_ids = ($dag_data | get composition.formulas | each { |e| $e.formula_id }) let server_ids = ($servers_data | get formulas | each { |f| $f.id }) let uncovered = ($server_ids | where { |id| not ($dag_ids | any { |did| $did == $id }) }) if ($uncovered | length) == 0 { print " PASS" } else { print $" WARN: servers.ncl formulas not in dag.ncl (intentional?): ($uncovered | str join ', ')" } } print "" if $passed { print "dag validate: OK" } else { print "dag validate: FAILED" exit 1 } } # Export dag.ncl in various formats. export def "main dag export" [ --workspace (-w): string # Workspace name (default: active) --infra (-i): string = "wuji" # Infra name --format (-f): string = "json" # Output format: json, dot, cytoscape-json ] : nothing -> nothing { let ws_name = if ($workspace | is-not-empty) { $workspace } else { let details = (get-active-workspace-details) if ($details == null) { error make { msg: "No active workspace. Pass --workspace or activate one first." } } $details.name } let ws_root = (get-workspace-path $ws_name) if ($ws_root | is-empty) { error make { msg: $"Workspace '($ws_name)' not found in registry." } } let dag_path = ($ws_root | path join "infra" $infra "dag.ncl") let dag = (nickel-export $dag_path) match $format { "json" => { print ($dag | to json) } "dot" => { print "digraph dag {" print " rankdir=LR;" for entry in $dag.composition.formulas { let shape = if ($entry.depends_on | length) == 0 { "shape=invhouse" } else { "shape=box" } print $" \"($entry.formula_id)\" [($shape)];" for dep in $entry.depends_on { let label = $dep.condition print $" \"($dep.formula_id)\" -> \"($entry.formula_id)\" [label=\"($label)\"];" } if "health_gate" in $entry and ($entry.health_gate != null) and (($entry.depends_on | length) > 0) { let gate_id = $"health_gate__($entry.depends_on.0.formula_id)__($entry.formula_id)" print $" \"($gate_id)\" [shape=hexagon label=\"health gate\"];" print $" \"($entry.depends_on.0.formula_id)\" -> \"($gate_id)\" [style=dashed];" print $" \"($gate_id)\" -> \"($entry.formula_id)\" [style=dashed];" } } print "}" } "cytoscape-json" => { let nodes = ($dag.composition.formulas | each { |e| { data: { id: $e.formula_id, label: $e.formula_id, shape: "rectangle", parallel: $e.parallel, } } }) let edges = ($dag.composition.formulas | each { |e| $e.depends_on | each { |dep| { data: { id: $"($dep.formula_id)__($e.formula_id)", source: $dep.formula_id, target: $e.formula_id, label: $dep.condition, } } } } | flatten) print ({ elements: { nodes: $nodes, edges: $edges } } | to json) } _ => { error make { msg: $"Unknown format '($format)'. Valid: json, dot, cytoscape-json" } } } }