jobq: templates swallow the DagSpec layer

DagSpec described the graph the templates were about to build, one layer
below the templates themselves. Per #2972 the templates should be that
unit, so the spec type is gone and every declarer writes onto the job
builder directly.

- delete DagSpec<F> and its hand-written Debug impl
- submit(source, reason, declare: impl FnOnce(&Job)) replaces the
  pre-built-spec signature; submit_and_emit follows
- all six templates take &Job; the Source is now the caller's to pass,
  which spawn and approval_deploy previously hardcoded while the other
  four did not
- power_dag dissolves into stop_nodes/start_nodes/restart_nodes, which
  borrow their targets instead of owning them

315 tests pass unchanged.
This commit is contained in:
atlas 2026-08-03 01:23:40 +02:00 committed by mara
commit 6899f574f6
7 changed files with 347 additions and 550 deletions

View file

@ -426,44 +426,3 @@ impl NodeKind {
// - `DeployWindow` brackets a deploy without itself stopping anything.
}
}
/// Submit-time spec for a whole DAG: the group's metadata plus the declared —
/// not yet inserted — nodes. Built by `templates.rs`, inserted by
/// `JobQueue::submit`.
///
/// No DAG-level `agent` — every node carries its own (a DAG can span agents),
/// and the queue derives per-agent leasing from [`NodeKind::agent`].
/// Type-specific payloads (`PermChange`'s file payload) ride the node that
/// consumes them ([`NodeKind::WritePermFile`]), not this generic spec.
///
/// There is no separate per-node spec type, and no built job either: `declare`
/// is a *recipe* the queue runs against a builder `hive_jobq` owns, at the
/// moment it inserts. A shape that has been declared is therefore always
/// insertable — a dangling edge or a cycle cannot be expressed, so there is
/// nothing left for a submit-time validation pass to reject.
///
/// Generic over the recipe rather than boxing it: a spec goes from the template
/// that returns it straight to the `submit` that consumes it, so the closure's
/// concrete type is known the whole way and needs neither an allocation nor a
/// `Send` bound. Nothing boxes a recipe any more — a running node grows its DAG
/// by declaring straight onto the builder it was handed, so there is no recipe
/// to store and replay across a task boundary.
pub struct DagSpec<F> {
pub source: Source,
/// Free-form "why".
pub reason: String,
/// Declares the DAG's nodes — their edges, grouping and resources — onto
/// the builder the queue hands it.
pub declare: F,
}
impl<F> std::fmt::Debug for DagSpec<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// The recipe is a closure; there is nothing to show of it, and its
// nodes do not exist until the queue runs it.
f.debug_struct("DagSpec")
.field("source", &self.source)
.field("reason", &self.reason)
.finish_non_exhaustive()
}
}