jobq: make the builder the only way nodes enter a graph

`Graph::insert` was public and validating, and the builder's insert loop
called it with `?`. That made job-level atomicity an accident: the loop
mutates as it goes, so a rejection at node `i` left `0..i` already in the
graph — the error fired loudly *after* the corruption, not instead of it.
It held only because `check_job_shape` happens to be exhaustive, with
nothing in the types saying so.

Make the guarantee structural instead. `Graph::insert` becomes
`pub(crate)`; the builder drains into a new infallible
`insert_unchecked` (via `Scheduler::append_unchecked`), so
`insert_with`'s sink returns a bare `NodeId` and a half-built job is no
longer expressible. Re-checking at the sink cannot add safety anyway — it
can only report after the mutation it was meant to prevent.

Drop `BuildError::Graph`: nothing in the builder path can produce a
`GraphError` any more. Clippy could not see this (an unreachable variant
of a `pub` enum is still constructible from outside the crate).

`Scheduler::append` stays public and validating — hive-c0re inserts a
DAG's container node through it. Folding that away means removing the
container/DagView indirection, which is out of scope here.
This commit is contained in:
atlas 2026-08-02 15:45:27 +02:00
commit a2edad715f
3 changed files with 54 additions and 13 deletions

View file

@ -29,7 +29,7 @@
use std::cell::RefCell;
use std::collections::HashMap;
use crate::{Dep, DepWhen, GraphError, NodeId, TerminalState};
use crate::{Dep, DepWhen, NodeId, TerminalState};
/// An opaque identity for a node **within the job being built**.
///
@ -108,10 +108,6 @@ pub enum BuildError {
/// The target of the unsatisfiable edge.
dep: NodeGuid,
},
/// The graph rejected an otherwise well-formed node — an out-of-group edge,
/// an unsatisfiable [`DepWhen`], and so on.
#[error(transparent)]
Graph(#[from] GraphError),
}
/// Reject a job whose own declarations don't hold up — **before anything is
@ -310,14 +306,15 @@ impl<N, R> JobBuilder<N, R> {
///
/// # Errors
///
/// [`BuildError::ForwardEdge`] / [`BuildError::ForwardParent`] if a node
/// references one declared after it, or [`BuildError::Graph`] if the graph
/// rejects a node (see [`crate::Graph::insert`]).
/// A [`BuildError`] if the job's own declarations don't hold up — see
/// [`check_job_shape`], which decides every one of them **before** the
/// first insert. The sink itself is infallible: by the time it runs, the
/// job is known-good, so no node can be rejected half-way through.
pub(crate) fn insert_with(
self,
root_parent: Option<NodeId>,
wanted: &[NodeGuid],
mut insert: impl FnMut(N, Vec<Dep<R>>, Option<NodeId>) -> Result<NodeId, GraphError>,
mut insert: impl FnMut(N, Vec<Dep<R>>, Option<NodeId>) -> NodeId,
) -> Result<Vec<NodeId>, BuildError> {
let pending = self.nodes.into_inner();
check_job_shape(&pending, wanted, root_parent)?;
@ -337,7 +334,10 @@ impl<N, R> JobBuilder<N, R> {
.into_iter()
.map(|(name, count)| Dep::Resource { name, count }),
);
let id = insert(node.payload, deps, parent)?;
// Infallible by construction: `check_job_shape` above decided every
// rejection the graph could raise, so there is nothing left here to
// abandon a half-inserted job on.
let id = insert(node.payload, deps, parent);
ids.insert(node.guid, id);
}
Ok(wanted.iter().map(|g| ids[g]).collect())