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

@ -100,6 +100,21 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
self.graph.insert(payload, deps, parent)
}
/// [`Scheduler::append`] for a node the builder has already validated —
/// infallible, so the insert loop cannot abandon a half-built job.
///
/// Not public: the only caller is [`Scheduler::insert_job`], feeding nodes
/// that [`crate::builder::check_job_shape`] has already proved
/// well-formed. See [`Graph::insert_unchecked`].
pub(crate) fn append_unchecked(
&mut self,
payload: N,
deps: Vec<Dep<R>>,
parent: Option<NodeId>,
) -> NodeId {
self.graph.insert_unchecked(payload, deps, parent)
}
/// Insert a whole job under `root_parent`, returning the id each handle's
/// node was minted as.
///
@ -131,7 +146,7 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
let job = JobBuilder::new();
let wanted = declare(&job);
job.insert_with(root_parent, &wanted, |payload, deps, parent| {
self.append(payload, deps, parent)
self.append_unchecked(payload, deps, parent)
})
}