# 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,
}
