52 lines
1.6 KiB
Plaintext
52 lines
1.6 KiB
Plaintext
|
|
# 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,
|
||
|
|
}
|