feat(jobq): add a job builder that names nodes instead of counting them

`JobBuilder::node(payload)` hands back a `NodeRef` handle obtainable no
other way, and edges are handle -> handle. `insert_into(graph,
root_parent)` consumes the builder, inserts in declaration order, and
returns the id each handle was minted as. There is no intermediate
node-description type: the builder inserts through `Graph::insert`
directly, so nothing has to stay in sync with that signature.

`root_parent` is the group's attachment point — a node that declared no
parent hangs there. That is what makes a job a self-contained sub-DAG: a
template is written without knowing which container node it will live
under, and the same builder serves a runtime-emitted subgraph hanging
off its emitting node.

Generic over the same `N`/`R` as `Graph`, so it belongs to the library
rather than to any one caller's node kind. `.needs(name)` /
`.needs_units(name, count)` declare resource deps at the construction
site, next to the node that needs them.

`Scheduler::insert_job` is the same over a scheduler, via the shared
`insert_with` sink, so a caller building a job never reaches past the
scheduler at the graph underneath.

Declaration order is enforced rather than papered over: a node
referencing one declared later is a named `BuildError::ForwardEdge` /
`ForwardParent`. Sorting for the caller would silently accept a shape
the graph cannot express, and would put ordering logic in a second
place.

Additive — `Graph` is untouched.
This commit is contained in:
atlas 2026-08-02 12:32:13 +02:00 committed by mara
commit f161f8e40f
3 changed files with 547 additions and 0 deletions

View file

@ -26,9 +26,12 @@
//! re-entrant borrow, one branch at a time). Single-threaded — the scheduler
//! owns the resource table and mutates it directly. See [`scheduler`].
pub mod builder;
pub mod resources;
pub mod scheduler;
pub use builder::{BuildError, JobBuilder, NodeGuid, NodeRef};
use chrono::{DateTime, Utc};
/// Opaque, stable, monotonic node identifier.