provisioning/schemas/lib/formula.ncl

131 lines
4.4 KiB
Text
Raw Normal View History

# schemas/lib/formula.ncl — Workspace Formula DAG
#
# A Formula is a typed DAG that is simultaneously:
# - A validatable declaration (Nickel typecheck + referential integrity)
# - An executable pipeline (Orchestrator consumes the DAG via nickel export)
# - A governable artifact (on+re tracks state, gates, and audit)
#
# Usage:
# let f = import "schemas/lib/formula.ncl" in
# f.make_formula { id = "...", nodes = [...], ... }
let ts = import "contracts.ncl" in
let _dep_kind = [| 'Always, 'OnSuccess, 'OnFailure |] in
let _on_error = [| 'Stop, 'Continue, 'Retry |] in
# Dependency from one FormulaNode to another (by node id)
let _FormulaDep = {
node_id | String,
kind | _dep_kind | default = 'OnSuccess,
} in
# A node in the formula DAG.
# Exactly one of `taskserv` or `component` must be present.
# - taskserv: L2 nodes — legacy field, existing formulas unchanged
# - component: L3+ nodes — unified model, orchestrator uses component.mode to resolve
let _FormulaNode = std.contract.custom (fun label value =>
let base = value | {
id | String,
taskserv | ts.TaskServDef | optional,
component | ts.ComponentDef | optional,
depends_on | Array _FormulaDep | default = [],
parallel | Bool | default = false,
on_error | _on_error | default = 'Stop,
max_retries | Number | default = 0,
} in
let has_taskserv = std.record.has_field "taskserv" base in
let has_component = std.record.has_field "component" base in
if has_taskserv && has_component then
std.contract.blame_with_message
"FormulaNode '%{base.id}': exactly one of 'taskserv' or 'component' must be present, not both"
label
else if (!has_taskserv) && (!has_component) then
std.contract.blame_with_message
"FormulaNode '%{base.id}': exactly one of 'taskserv' or 'component' must be present"
label
else
'Ok base
) in
# An explicit edge declaration (alternative to depends_on inside nodes)
let _FormulaEdge = {
from | String,
to | String,
kind | _dep_kind | default = 'OnSuccess,
} in
# Base structure without cross-field validation
let _FormulaBase = {
id | String,
description | String,
provider | String,
server | String,
nodes | Array _FormulaNode,
edges | Array _FormulaEdge | default = [],
max_parallel | Number | default = 4,
} in
# Contract: all node_id values in depends_on must reference an existing node id.
# Also validates edge endpoints.
let _Formula = std.contract.custom (fun label value =>
let base = value | _FormulaBase in
let node_ids = base.nodes |> std.array.map (fun n => n.id) in
# Check: duplicate node ids
let dup_ids = node_ids |> std.array.fold_left (fun acc id =>
if std.record.has_field id acc.seen then
{ seen = acc.seen, dups = acc.dups @ [id] }
else
{ seen = acc.seen & { "%{id}" = true }, dups = acc.dups }
) { seen = {}, dups = [] } in
if std.array.length dup_ids.dups > 0 then
std.contract.blame_with_message
"Formula '%{base.id}': duplicate node ids: %{std.string.join ", " dup_ids.dups}"
label
else
# Check: depends_on referential integrity
let bad_deps = base.nodes |> std.array.flat_map (fun node =>
node.depends_on
|> std.array.filter (fun dep =>
!(node_ids |> std.array.any (fun id => id == dep.node_id))
)
|> std.array.map (fun dep =>
"node '%{node.id}' depends_on unknown '%{dep.node_id}'"
)
) in
if std.array.length bad_deps > 0 then
std.contract.blame_with_message
"Formula '%{base.id}' has invalid depends_on: %{std.string.join ", " bad_deps}"
label
else
# Check: edge referential integrity
let bad_edges = base.edges |> std.array.filter (fun e =>
!(node_ids |> std.array.any (fun id => id == e.from))
|| !(node_ids |> std.array.any (fun id => id == e.to))
) |> std.array.map (fun e => "'%{e.from}' -> '%{e.to}'") in
if std.array.length bad_edges > 0 then
std.contract.blame_with_message
"Formula '%{base.id}' has invalid edge endpoints: %{std.string.join ", " bad_edges}"
label
else
'Ok base
) in
{
FormulaDep = _FormulaDep,
FormulaNode = _FormulaNode,
FormulaEdge = _FormulaEdge,
Formula = _Formula,
make_dep = fun data => _FormulaDep & data,
make_node = fun data => data | _FormulaNode,
make_edge = fun data => _FormulaEdge & data,
make_formula = fun data => data | _Formula,
}