Skip to content

T-CLV1-gs-status-model-and-trigger-rules

Status: open/ready · Impact: high · Complexity: medium

Implement the status enum and the Airflow-style trigger rules as graph_scheduler::status, and make graph-scheduler the owner of those two types — packages/rust/foreman deletes its copies and depends on the crate. On the v0.1 critical path.

This is the one task in the E3 chain that edits a shipped contract, so it is supervised rather than autonomous/pr.

LocationRole today
packages/rust/foreman/src/dag.rsDefines NodeStatus (line 85) and TriggerRule (line 108). Its own doc comment calls NodeStatus “exactly the graph-scheduler status enum, and deliberately so”
packages/rust/foreman/src/run.rsNodeState.status: NodeStatus — reaches the type from the emitted status API
packages/rust/foreman/src/protocol.rsStepOutcome::as_node_status maps the three terminal step outcomes onto NodeStatus
packages/rust/foreman/build.rsontogen gen_clients pipeline; emits generated/bindings.ts for the closure reachable from src/api/v1
packages/rust/foreman/tests/codegen_sync.rsAsserts NodeStatus and TriggerRule are present in the emitted bindings, and pins the NodeStatus union text verbatim
packages/rust/graph-scheduler/src/status.rsDoes not exist

src/status.rs in graph-scheduler owns three items.

ItemShape
NodeStatusPending (default) / Running / Success / Failed / Skipped, #[serde(rename_all = "snake_case")]
TriggerRuleThe eight Airflow presets, AllSuccess default, #[serde(rename_all = "snake_case")]
TriggerRule::is_satisfied_by(&self, parents: &[NodeStatus]) -> boolThe closed-form predicate over parent statuses

Root (empty-parent) conventions follow standard Airflow and are pinned by T-ROJC’s Layer 2 table: all_* and none_* are vacuously true, one_* are false.

D-BPD8’s State is a TypeScript union (ReadonlyMap | { done: [...] }) and its tracker separately takes a status(id) resolver. Rust collapses both into one trait, which also answers D-BPD8 first-consumer requirement D (the “derive Status from a typed domain model” adapter pattern):

trait StatusSource { fn status(&self, id: &NodeId) -> NodeStatus; }

with implementations for BTreeMap<NodeId, NodeStatus>, a Done(Vec<NodeId>) newtype giving the id-list sugar, and a StatusFn(F) newtype wrapping any Fn(&NodeId) -> NodeStatus. The closure case is wrapped rather than blanket- implemented because impl<F: Fn(&NodeId) -> NodeStatus> StatusSource for F overlaps every other implementation as far as coherence can tell. A node the source has never heard of reads Pending, which is what makes an unknown depends_on target fail safe (requirement C).

NodeStatus and TriggerRule move down. Nothing else in foreman::dag does, and the reason is a hard dependency cycle rather than a preference:

  • DagNodeDef (foreman/src/dag.rs:51) carries pub step: StepDef (line 56), imported at line 14.
  • StepDef is the work runner’s execution vocabulary — a compiled-in handler registry key, a command line, a child process speaking the step protocol, an agent driver. It belongs to foreman.
  • So moving DagNodeDef would make graph-scheduler name StepDef and depend on foreman, while foreman depends on graph-scheduler. Cargo rejects that.

DagDef, DagNodeDef, DagEdgeDef, RetryPolicy, BackoffPolicy and FailurePolicy therefore stay in foreman. They are also not duplicated in graph-scheduler: D-BPD8’s core is an in-memory Graph built by add_node / depends_on and queried, whereas DagDef is a serialized wire format carrying retry, timeout and step payloads. Only NodeStatus and TriggerRule are the same type in both designs. See the E3 section of docs/planning/d-vsli-implementation-plan.md for the recorded open question on whether the split should go further.

  • Add graph-scheduler to [dependencies] and to moon.yml’s dependsOn (moon infers no edge from { workspace = true }).
  • Delete the two enums from src/dag.rs and re-export them from the same path so foreman::dag::NodeStatus keeps resolving for every existing caller and the public API does not break: pub use graph_scheduler::status::{NodeStatus, TriggerRule};
  • Leave build.rs’s pipeline in place and add pool_extra_roots: vec!["../graph-scheduler/src".into()].

The emit stays in foreman. ontogen’s long-tail walker is a syntactic syn::Item scan of source directories, and pool_extra_roots exists for precisely this case — its in-source comment describes the roots as “workspace-sibling crates the consuming crate re-exports types from”. Relative roots are joined to CARGO_MANIFEST_DIR. Moving the emit into graph-scheduler was rejected: ontogen has no types-only generator, so graph-scheduler would need a fabricated src/api/v1 surface to root an emit it has no use for, and it would take ontogen + biome-fmt build dependencies into a crate that is meant to stay dependency-light.

tests/codegen_sync.rs is the guard that makes a silent failure impossible: it asserts the exact emitted union text, so if the pool does not reach the moved enums the test fails loudly rather than emitting incomplete bindings.

graph-scheduler carries serde and serde_json. D-BPD8’s “zero-dep” claim is amended to dependency-light, matching packages/rust/intersect, which carries thiserror. serde is deliberately not an optional feature: the derives would then sit behind #[cfg_attr(feature = …)], and ontogen’s syntactic walker reads derive and serde attributes straight from the source text, so a gated rename_all risks emitting PascalCase variants that disagree with the wire without any error.

  1. Implement src/status.rs — the two enums and is_satisfied_by — plus the StatusSource trait and its three implementations.
  2. Un-ignore the Layer 2 suite in tests/status.rs and make it green, including the twenty trigger-rule truth-table cases.
  3. Add the graph-scheduler dependency to foreman’s Cargo.toml and moon.yml; delete the two enums from foreman/src/dag.rs and re-export them from that module.
  4. Add pool_extra_roots to foreman/build.rs, run cargo check -p foreman, and confirm generated/bindings.ts carries unchanged union text (see AC-5: the declarations relocate within the file; the text is what the guards pin).
  5. Run cargo test -p foreman and moon run foreman:codegen-drift.
LocationKindChange
packages/rust/graph-scheduler/src/status.rsmodifyReplace stubs with the enums, is_satisfied_by, and StatusSource
packages/rust/graph-scheduler/src/lib.rsmodifyRe-export NodeStatus, TriggerRule, StatusSource
packages/rust/graph-scheduler/tests/status.rsmodifyRemove #[ignore] from the Layer 2 suite
packages/rust/foreman/src/dag.rsmodifyDelete both enums; re-export them from graph-scheduler
packages/rust/foreman/Cargo.tomlmodifyAdd the graph-scheduler dependency
packages/rust/foreman/moon.ymlmodifyAdd graph-scheduler to dependsOn
packages/rust/foreman/build.rsmodifyAdd pool_extra_roots for ../graph-scheduler/src
  • AC-1: graph_scheduler::status defines NodeStatus (five values, Pending default) and TriggerRule (eight presets, AllSuccess default), both serializing snake_case.
  • AC-2: TriggerRule::is_satisfied_by matches every row of T-ROJC’s Layer 2 truth table, including the root conventions.
  • AC-3: StatusSource is implemented for a status map, the Done id-list newtype, and the StatusFn closure wrapper; an unknown id reads Pending.
  • AC-4: foreman/src/dag.rs no longer defines either enum and re-exports both, so foreman::dag::NodeStatus and foreman::dag::TriggerRule still resolve.
  • AC-5: packages/rust/foreman/generated/bindings.ts carries the same NodeStatus and TriggerRule union text after the move. Amended at implementation time: the file is not byte-identical — ontogen’s walker emits declarations in discovery order, and reaching the enums through pool_extra_roots moves their position in the file. The union text is character-identical, TS type aliases are order-independent, and tests/codegen_sync.rs pins the text either way, so the reordered file is committed as the new baseline.
  • AC-6: cargo test -p foreman passes, including all six tests in tests/codegen_sync.rs, and moon run foreman:codegen-drift reports no drift.
  • AC-7: The Layer 2 suite in tests/status.rs has no #[ignore] left and cargo test -p graph-scheduler is green.
  • AC-8: DagDef, DagNodeDef, DagEdgeDef, RetryPolicy, BackoffPolicy and FailurePolicy are still defined in foreman/src/dag.rs.
  • Moving the DAG definition types, retry, backoff or failure policy. Blocked by the StepDef cycle above.
  • The frontier query itself (T-I3QP-gs-push-readiness-frontier).
  • Any change to foreman’s src/api/v1 surface or the emitted client.
  • T-JVXC-gs-dag-coreis_satisfied_by reads parent statuses resolved through the DAG core.

T-JVXC-gs-dag-core


← Back to Tasks