provisioning/schemas/lib/dag/contracts.ncl

122 lines
4.4 KiB
Text

# schemas/lib/dag/contracts.ncl — DAG domain type contracts
#
# Two distinct DAG layers:
# 1. Capability layer — ExtensionCapability/ExtensionDependency (extension metadata)
# 2. Composition layer — WorkspaceComposition (inter-formula ordering)
# 3. Resolution layer — ResolutionPolicy (capability → extension mapping)
#
# Pattern: separate let bindings with _ prefix, same as formula.ncl.
# No self-references, no let rec — each binding is in scope for subsequent ones.
# ---------------------------------------------------------------------------
# Capability layer
# ---------------------------------------------------------------------------
let _capability_kind = [| 'Required, 'Optional, 'ConflictsWith |] in
let _ExtensionCapability = {
id | String,
version | String,
interface | String,
} in
let _ExtensionDependency = {
capability | String,
kind | _capability_kind,
min_version | String | optional,
} in
# ---------------------------------------------------------------------------
# Composition layer — inter-formula DAG
# Distinct from the intra-formula DAG in formula.ncl (per-server task ordering).
# WorkspaceComposition declares execution ordering between formulas.
# ---------------------------------------------------------------------------
let _composition_condition = [| 'Completed, 'Healthy, 'Running |] in
let _FormulaDep = {
formula_id | String,
condition | _composition_condition,
} in
let _HealthGate = {
check_cmd | String,
expect | String,
timeout_ms | Number,
retries | Number,
check_server | String | optional,
} in
let _FormulaCompositionEntry = {
formula_id | String,
depends_on | Array _FormulaDep | default = [],
parallel | Bool | default = false,
health_gate | _HealthGate | optional,
} in
# Base shape — used as first step inside the custom contract (same pattern as _FormulaBase
# in formula.ncl) so missing-field errors surface before cross-field validation runs.
let _WorkspaceCompositionBase = {
formulas | Array _FormulaCompositionEntry,
} in
# Custom contract: validates referential integrity across formula entries.
# - At least one formula must have depends_on = [] (root node)
# - All depends_on[].formula_id must reference a declared formula_id
let _WorkspaceComposition = std.contract.custom (fun label value =>
let base = value | _WorkspaceCompositionBase in
let ids = base.formulas |> std.array.map (fun e => e.formula_id) in
let has_root = base.formulas |> std.array.any (fun e => e.depends_on == []) in
let bad_deps = base.formulas |> std.array.flat_map (fun e =>
e.depends_on
|> std.array.filter (fun d =>
!(ids |> std.array.any (fun id => id == d.formula_id))
)
|> std.array.map (fun d =>
"formula '%{e.formula_id}' depends_on unknown '%{d.formula_id}'"
)
) in
if !has_root then
std.contract.blame_with_message
"WorkspaceComposition: at least one formula must have depends_on = []"
label
else if (std.array.length bad_deps) > 0 then
std.contract.blame_with_message
"WorkspaceComposition: invalid depends_on references: %{std.string.join ", " bad_deps}"
label
else
'Ok base
) in
# ---------------------------------------------------------------------------
# Resolution layer — capability → concrete extension mapping
# ---------------------------------------------------------------------------
let _resolution_strategy = [| 'Strict, 'BestEffort |] in
let _ResolutionEntry = {
capability_id | String,
extension_name | String,
} in
let _ResolutionPolicy = {
strategy | _resolution_strategy,
overrides | Array _ResolutionEntry | default = [],
allow_optional_gaps | Bool,
} in
# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------
{
CapabilityKind = _capability_kind,
ExtensionCapability = _ExtensionCapability,
ExtensionDependency = _ExtensionDependency,
CompositionCondition = _composition_condition,
FormulaDep = _FormulaDep,
HealthGate = _HealthGate,
FormulaCompositionEntry = _FormulaCompositionEntry,
WorkspaceComposition = _WorkspaceComposition,
ResolutionStrategy = _resolution_strategy,
ResolutionEntry = _ResolutionEntry,
ResolutionPolicy = _ResolutionPolicy,
}