# Validation contracts for reflection modes. # These are applied AFTER the schema contract — they enforce semantic invariants # that Nickel's structural typing cannot express alone. # Pattern mirrors jpl_ontology/adrs/constraints.ncl: separate file, pure contracts. let _non_empty_steps = std.contract.custom ( fun label => fun value => if std.array.length value.steps == 0 then 'Error { message = "Mode '%{value.id}': steps must not be empty — a mode with no steps is passive documentation, not an executable contract" } else 'Ok value ) in let _valid_trigger = std.contract.custom ( fun label => fun value => if std.string.length value.trigger == 0 then 'Error { message = "Mode '%{value.id}': trigger must not be empty — it identifies how this mode is invoked" } else 'Ok value ) in # Ensures every step that declares a cmd actually has a meaningful command (not whitespace-only). let _non_empty_cmds = std.contract.custom ( fun label => fun value => let bad = value.steps |> std.array.filter (fun s => std.record.has_field "cmd" s && std.string.length (std.string.trim s.cmd) == 0 ) |> std.array.map (fun s => s.id) in if std.array.length bad > 0 then 'Error { message = "Mode '%{value.id}': steps with empty cmd: %{std.string.join ", " bad}" } else 'Ok value ) in { NonEmptySteps = _non_empty_steps, ValidTrigger = _valid_trigger, NonEmptyCmds = _non_empty_cmds, }