130 lines
4.4 KiB
Text
130 lines
4.4 KiB
Text
# 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,
|
|
}
|