refactor(#2808): the wire state enum is the scheduler's own

`hive_host_sock::jobs::State` was a hand-maintained copy of
`hive_jobq::State` — five variants spelled the same in both, kept in sync
by whoever remembered. Adding `Skipped` last week meant adding it twice.
The wire crate now re-exports the scheduler's enum and `to_wire_state` is
gone.

Two states that were hidden now reach clients. `to_wire_state` renamed
`Pending` to `Queued` and folded `Finishing` into `Running`, so the
dashboard could not distinguish a node waiting on its dependencies from
one whose own work is done while its sub-nodes still run. Both are now
visible, and consumers say which they mean.

Every consumer had to move with it, and only the Rust ones said so: the
exhaustive matches in `hivectl` and `DagView::rollup_state` failed to
compile, while the dashboard's fourteen string comparisons would have
gone quietly wrong — a `finishing` node no longer counting as running,
a `pending` node no longer as queued.

The frontend also builds CSS class names out of the state string
(`rqe-` + state, `rqe-node-` + state) and keys its glyph map on it, all
lowercase. Those go through a `stateSlug` helper now; comparisons use the
wire spelling, presentation lowercases. Without that split every queue
entry and node chip would have silently lost its styling.

Dropping the `State as JobState` alias in hive-c0re falls out of this:
the alias only existed to tell two `State` types apart, and there is one
now.
This commit is contained in:
atlas 2026-07-27 21:50:24 +02:00
commit b9aab7e923
8 changed files with 72 additions and 93 deletions

View file

@ -40,36 +40,7 @@ impl Source {
}
}
/// Lifecycle state of a node — and, rolled up, of a DAG.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum State {
Queued,
Running,
Done,
Failed,
Cancelled,
/// The node's own dependency edges ruled it out: an outcome branch that
/// wasn't taken. Distinct from `Cancelled`, which is work actively
/// dropped. A skipped node is an expected part of a healthy run — every
/// approval DAG has two not-taken tails and every rebuild has one — so
/// consumers must not read it as a failure or cancellation signal.
Skipped,
}
impl State {
/// Whether the node will never change state again. `Skipped` counts:
/// a branch that was ruled out is as final as one that ran, and the
/// wait loops (`hivectl`'s progress display, the daemon's
/// dag-settled check) hang forever if it doesn't.
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(
self,
State::Done | State::Failed | State::Cancelled | State::Skipped
)
}
}
pub use hive_jobq::State;
/// Kind-specific payload for `Template::PermChange` DAGs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -187,17 +158,20 @@ impl DagView {
/// Rust consumer (hivectl, the wait loops, tests) uses so the dashboard's
/// JS render and the host agree: `Failed` if any node failed, else
/// **`Cancelled` if any cancelled**, else `Running` if any running, else
/// `Queued` if any queued, else `Done`. `Done` nodes are excluded
/// `Pending` if any pending, else `Done`. `Done` nodes are excluded
/// from the wire, so a DAG that is *entirely* done isn't sent at all —
/// its absence from the snapshot is what signals completion.
///
/// `Cancelled` outranks both `Running` and `Queued` because a cancelled DAG
/// `Cancelled` outranks both `Running` and `Pending` because a cancelled DAG
/// still has its weak-edged tail node to run (it reports the cancellation),
/// so `Queued`-then-`Running` would flicker back at the operator who just
/// so `Pending`-then-`Running` would flicker back at the operator who just
/// cancelled it and read as "the cancel didn't take". Outside that window
/// the states barely co-occur: a cancel *cascade* originates at a `Failed`
/// node, which returns early above.
///
/// `Finishing` counts as running: the node's own work is done but its
/// sub-nodes are still going, so the DAG is still in flight.
///
/// `Skipped` contributes nothing: a not-taken branch is an expected part of
/// a healthy run, so counting it would make every successful DAG roll up
/// non-`Done`.
@ -208,13 +182,13 @@ impl DagView {
#[must_use]
pub fn rollup_state(&self) -> State {
let mut any_running = false;
let mut any_queued = false;
let mut any_pending = false;
let mut any_cancelled = false;
for n in &self.nodes {
match n.state {
State::Failed => return State::Failed,
State::Running => any_running = true,
State::Queued => any_queued = true,
State::Running | State::Finishing => any_running = true,
State::Pending => any_pending = true,
State::Cancelled => any_cancelled = true,
State::Done | State::Skipped => {}
}
@ -223,8 +197,8 @@ impl DagView {
State::Cancelled
} else if any_running {
State::Running
} else if any_queued {
State::Queued
} else if any_pending {
State::Pending
} else {
State::Done
}