catalogue: carry and validate a container schedule (ADR 0053)

A container may declare schedule: "<cron>", the recurring twin of
run-once. The resolver already carries a resource's keys through
untouched, so schedule reaches the rendered host declaration on its own;
what belongs here is refusing, near its author, what the host would
otherwise refuse far away.

The manifest parser refuses a schedule that is not a string, one that is
not a well-formed five-field cron (cron.go: fields, ranges, *, comma,
dash, slash), and the contradictory pair run-once + schedule -- a
container runs once and gates, or on a cadence, or stays up, never two.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-06 14:08:51 +02:00
parent dc65440e31
commit 78b8b6e256
3 changed files with 224 additions and 16 deletions
+43 -16
View File
@@ -734,23 +734,50 @@ func ParseManifest(raw []byte) (Manifest, error) {
if fmt.Sprint(r["type"]) != "container" {
continue
}
raw, present := r["run-once"]
if !present {
continue
}
once, ok := raw.(bool)
if !ok {
problems = append(problems, fmt.Sprintf(
"%s declares run-once on %v as a %T; run-once is true or false",
m.Module, r["id"], raw))
continue
}
if once {
if _, hasRestart := r["restart-on"]; hasRestart {
var runOnce bool
if raw, present := r["run-once"]; present {
once, ok := raw.(bool)
if !ok {
problems = append(problems, fmt.Sprintf(
"%s declares %v as run-once and with restart-on; a run-once step runs to "+
"completion rather than staying running to be restarted (novox/hq ADR 0052)",
m.Module, r["id"]))
"%s declares run-once on %v as a %T; run-once is true or false",
m.Module, r["id"], raw))
} else {
runOnce = once
if once {
if _, hasRestart := r["restart-on"]; hasRestart {
problems = append(problems, fmt.Sprintf(
"%s declares %v as run-once and with restart-on; a run-once step runs to "+
"completion rather than staying running to be restarted (novox/hq ADR 0052)",
m.Module, r["id"]))
}
}
}
}
// **A scheduled container runs on a recurring cadence** (novox/hq ADR 0053), the recurring
// twin of run-once. The control plane carries the field to the host unchanged (the resolver
// copies every key of a resource, so `schedule` reaches the rendered declaration on its own);
// what belongs here is refusing, near its author, a value the host would only refuse far away.
// Three things: a value that is not a string, a string that is not a valid cron, and the pair
// run-once + schedule — a container runs once, or on a cadence, or stays up, never two.
if raw, present := r["schedule"]; present {
cron, ok := raw.(string)
switch {
case !ok:
problems = append(problems, fmt.Sprintf(
"%s declares schedule on %v as a %T; a schedule is a five-field cron string",
m.Module, r["id"], raw))
default:
if err := validateCron(cron); err != nil {
problems = append(problems, fmt.Sprintf(
"%s declares schedule %q on %v, which is not a valid cron: %v",
m.Module, cron, r["id"], err))
}
if runOnce {
problems = append(problems, fmt.Sprintf(
"%s declares %v as both run-once and schedule; a container runs once and "+
"gates, or on a cadence, or stays up — never two (novox/hq ADR 0053)",
m.Module, r["id"]))
}
}
}
}