refactor(job-queue): a job is a recipe, not a value you carry

Follows the jobq change: a builder can no longer be constructed or
inserted outside `hive_jobq`, so `DagSpec` cannot hold one. It carries a
`Declare` — `Box<dyn FnOnce(&Job) + Send>` — and the queue runs it
against a builder jobq owns, at the moment it inserts.

`NodeOutput.append_subgraph` becomes `Vec<Declare>` for the same reason,
and this is where the shape was always heading: that field's doc already
said an executor "cannot reach the queue, so it hands the declaration
back", while its type was a `Vec<Job>` the executor had built itself.
The rejected `build_nodes -> Vec<NodeSpec>` was the first version of that
escape hatch; a recipe is the last one, because there is no job-shaped
value to hand over at all.

Templates and the power-op assemblers move their owned data into the
closure and are otherwise unchanged — `rebuild_nodes`, `node` and the
tail helpers already took `&Job` and returned handles, so only each
template's outermost frame moved.

Two `Debug` impls are hand-written: a closure has nothing to show, and
its nodes do not exist until the queue runs it. `NodeOutput` reports how
many subgraphs were emitted, `DagSpec` its source and reason.

`append_subgraph`'s `is_empty()` early-return is gone — you cannot ask a
recipe whether it will declare anything without running it. It now
inserts and returns an empty id list if nothing was declared, which
takes the queue lock in a case that previously skipped it.

The two in-DAG-growth tests build `Declare`s now, so they exercise the
shape an executor actually produces rather than one only a test could
construct. 45 job-queue tests unchanged and passing.
This commit is contained in:
atlas 2026-08-02 13:40:03 +02:00 committed by mara
commit 9c97365f8f
8 changed files with 336 additions and 284 deletions

View file

@ -5,11 +5,14 @@
//! than computing where that node landed, and there is no positional index to
//! get wrong.
//!
//! **An insertion API, not a spec factory.** [`JobBuilder::insert_into`]
//! consumes the builder and puts the nodes straight into a [`Graph`], returning
//! the ids the graph minted. Nothing job-shaped comes back out — there is no
//! intermediate node-description type to keep in sync with [`Graph::insert`]'s
//! signature.
//! **An insertion API, not a spec factory.** A builder is only ever handed to a
//! closure by an insertion entry point ([`Graph::insert_job`],
//! [`crate::scheduler::Scheduler::insert_job`]), which inserts the declared
//! nodes and returns the ids the graph minted. It cannot be constructed, held
//! or inserted from outside this crate, and there is no intermediate
//! node-description type to keep in sync with [`Graph::insert`]'s signature —
//! so a job has no representation that can be passed around instead of being
//! inserted.
//!
//! **Payload-agnostic.** Generic over the same `N` and `R` as [`Graph`]: the
//! builder knows nothing about what a node *does*, only how nodes relate.