Follow-up to the operator's note that an unchecked insert is fine "as
long as the builder enforces all invariants". It didn't, so this makes
the claim true rather than assumed.
check_job_shape now decides everything Graph::insert can reject for a
builder-produced node:
- UnknownParent / UnknownDep were already impossible -- a handle only
exists if this job declared it, and the ids are minted during the
insert itself.
- DepOutsideParent was not. The grouping rule is now re-derived from the
job's own parent chains: a depender parented at `q` may only name a
proper descendant of `q` (never `q` itself, which would deadlock), and
a depender that declared no parent inherits root_parent -- so with a
container every job node qualifies, and without one the target must
also be top-level. Mirrors Graph::is_descendant, which starts at the
target's parent and so never treats a node as its own ancestor.
It also rejects an empty DepWhen, which the graph only catches when
validating a deserialized graph (Graph::validate, not insert). Such a
node inserts cleanly today and then never becomes runnable -- a silent
hang. Refusing it at declaration closes that on the way past.
Graph::insert stays the sink. The atomicity comes from the pre-pass
being complete, not from bypassing validation, and keeping the graph's
own checks means any future drift between the two copies of the
grouping rule surfaces as a loud BuildError::Graph instead of silently
corrupting the graph -- one branch per node for a backstop.
graph_rejection_surfaces_as_is asserted that the graph's rejection
surfaced through the builder. That case no longer reaches the graph, so
it now pins the stronger property: the error is DepOutsideGroup *and*
nothing was inserted. Same for the new empty-edge test.