Skip to content

T-ROJC-gs-test-corpus-and-disabled-suites

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

Scaffold the graph-scheduler crate and land the spec-first test corpus for the v0.1 layers as ignored Rust suites over todo!() stubs, so each later task enables its slice and makes it green. First task in the E3 chain; everything else depends on it.

LocationRole today
packages/rust/graph-scheduler/Does not exist. The former packages/ts/graph-scheduler scaffold was deleted with the TypeScript plan
packages/rust/intersect/The layout precedent — Cargo.toml, moon.yml with crate-scoped task overrides, src/, tests/
packages/rust/intersect/tests/conformance.rsThe shared-corpus precedent, which this task deliberately does not follow (see below)
.moon/workspace.ymlCarries the packages/rust/* glob, so a new crate directory registers itself

packages/rust/graph-scheduler, Cargo package graph-scheduler, lib graph_scheduler. Per S-0009-moon-project-ids the crate takes the bare derived moon id graph-scheduler and declares no id: — the Cargo alias claims the bare name regardless, so a derived bare id keeps the project’s two names in agreement. No TypeScript peer is built (D-VSLI-distributed-work-runner-architecture), so the -ts twin rule is not engaged and nothing else in the workspace claims the name.

moon.yml copies packages/rust/intersect/moon.yml: layer: 'library', stack: 'backend', language: 'rust', and crate-scoped fmt / fmt-check / lint / test task overrides each carrying merge: 'replace' — without it moon appends the local command to the inherited one. No dependsOn is needed: the crate has no cross-tier corpus edge, which is the only reason intersect declares one.

Dependencies are serde and serde_json, both { workspace = true }. See T-CLV1-gs-status-model-and-trigger-rules for why serde is not optional.

src/lib.rs declares the v0.1 modules and re-exports their surface. Each public item lands with its real signature and a todo!() body, so the suites compile — which is strictly stronger than the TypeScript plan’s type-check, and is what makes an ignored Rust suite meaningful at all.

ModuleLands stubbed hereImplemented by
graphGraph, NodeId, NodeAttrs, attrs, CycleErrorT-JVXC-gs-dag-core
statusNodeStatus, TriggerRule, StatusSource, StatusFn, DoneT-CLV1-gs-status-model-and-trigger-rules
readyget_ready, ReadyNode, ReadyOptions, WherePredicateT-I3QP-gs-push-readiness-frontier
scheduleSchedule, Filter, filter_fn, filter_fn_scoped, Verdict, Retry, FilterCtx, Explanation, BlockedByT-7OJL-gs-eligibility-filters-and-explain
sortersSorter, sorter_fn, ByKey, WeightRule, by_key, priority_weightT-BR6H-gs-priority-sorters

Schedule::sort is stubbed with the filters even though ordering is T-BR6H-gs-priority-sorters’s, so that task fills a stub rather than widening a shipped public API.

Three shapes the Rust type system fixes, which the TypeScript sketch left open:

ItemShapeWhy
StatusFnA newtype wrapping a Fn(&NodeId) -> NodeStatusA blanket impl<F: Fn(..)> StatusSource for F overlaps every other implementation as far as coherence can tell, so the closure case has to be named
ReadyOptions#[non_exhaustive] plus builder settersA #[non_exhaustive] struct admits no struct expression outside its crate, functional update included, so ..Default::default() would not compile in the suites. Setters deliver the same “adding a field is not a breaking change” guarantee
by_key / priority_weightTake &Graph, return an owning ByKey<K>WeightRule::Downstream propagates over descendants, and Sorter::compare(&self, a, b) has no graph. Resolving the propagated key map once at construction keeps the sorter owning everything, so ReadyOptions needs no lifetime parameter

Stub bodies must not trip clippy -D warnings: prefix unused parameters with _. The goal, pull and tracker modules are not scaffolded here — their tasks are deferred past cutover, and an unbuilt module carrying an ignored suite is dead weight until then.

The corpus is Rust data tables, not a JSON fixture corpus

Section titled “The corpus is Rust data tables, not a JSON fixture corpus”

The intersect shared-fixture approach does not apply. It exists because intersect has two implementations — a published TypeScript mirror and a Rust port — held in agreement by one language-neutral corpus that both tiers read, the Rust side reaching across the package boundary with include_str!. graph-scheduler has no second tier: D-VSLI settles that no TypeScript peer is built, and sdlc task next keeps its own frontier until the CLI is Rust. A conformance corpus with one implementation has nothing to conform to, and the include_str! indirection buys only ceremony.

So each layer’s fixtures land as a const slice of Rust structs beside the suite that reads them, table-driven. Two properties are kept deliberately, so that a future twin is cheap rather than pre-paid:

  • Fixtures stay data, not inline assertions, so a JSON projection is a serialization away if a TypeScript consumer ever arrives.
  • Expected values stay transcribed from the matrix below, never recomputed by the implementation under test.

describe.skip / test.todo becomes #[ignore = "<task id> implements this"]. An ignored Rust test still compiles and still type-checks its call sites, so the suite is green-because-ignored in the same sense the TypeScript plan intended, and cargo test -p graph-scheduler -- --ignored shows the whole remaining spec.

This is the executable spec, and it is the enumerated set every acceptance criterion below refers to. Transcribe these into the per-layer const tables; do not invent expected values.

TagMeaning
[EX n]Transcribed from D-BPD8/examples.md example n.
[API]Transcribed from D-BPD8/layered-api.md signature/prose.
[C-PPXU]Fixed by C-PPXU-status-model-and-trigger-rules.md (“the Airflow set”).
[RATIFIED]A pinned default resolving a D-BPD8 open question; see “Ratified defaults”.
  • NodeStatus = Pending | Running | Success | Failed | Skipped; a node the StatusSource has never heard of reads Pending [C-PPXU].
  • The id-list sugar Done(["a"]) makes those ids Success and every other node Pending [EX 2].
  • Edge direction: depends_on(node, dependency) reads “node depends on dependency”; the edge points dependent to prerequisite [API].
  • Frontier rule: a node is ready iff its own status is Pending and its trigger rule is satisfied by its parents’ statuses; Running nodes are never in the frontier [API], [EX 7].
  • ReadyNode { id, attrs }; fixtures assert on the ordered list of ids, and on attrs where noted [API].
  • Tie-break: candidates equal under the active sorter, or with no sorter, resolve by add_node insertion order. topo_sort breaks ties the same way [RATIFIED].
// G1 — the canonical example graph. [EX 1]
let g1 = Graph::new()
.add_node("T-1", attrs().category("impl").weight(3))
.add_node("T-2", attrs().category("impl"))
.add_node("T-3", attrs().category("docs"))
.depends_on("T-2", "T-1")
.depends_on("T-3", "T-1");
// G2 — trigger-rule graph. [EX 3]
let g2 = Graph::new()
.add_node("build", attrs().category("impl"))
.add_node("deploy", attrs().trigger(TriggerRule::AllSuccess)) // the default
.add_node("cleanup", attrs().trigger(TriggerRule::AllDone))
.depends_on("deploy", "build")
.depends_on("cleanup", "build");
// G_TR — two-parent probe for the trigger truth tables.
// X carries the rule under test; vary status(P1), status(P2).
fn g_tr(rule: TriggerRule) -> Graph {
Graph::new()
.add_node("P1", attrs())
.add_node("P2", attrs())
.add_node("X", attrs().trigger(rule))
.depends_on("X", "P1")
.depends_on("X", "P2")
}
// G_CYCLE — a 2-cycle probe.
let g_cycle = Graph::new()
.add_node("A", attrs())
.add_node("B", attrs())
.depends_on("A", "B")
.depends_on("B", "A");
// G_GOAL — goal-direction probe; what_blocks = ["T-7", "T-9"]. [EX 6]
let g_goal = Graph::new()
.add_node("M-0011", attrs())
.add_node("T-6", attrs())
.add_node("T-7", attrs())
.add_node("T-8", attrs())
.add_node("T-9", attrs())
.add_node("T-99", attrs().category("docs")) // non-ancestor, for only_toward
.depends_on("M-0011", "T-7")
.depends_on("M-0011", "T-9")
.depends_on("T-7", "T-6")
.depends_on("T-9", "T-8");
// G_WEIGHT — downstream-propagation probe (self vs bubbled).
let g_weight = Graph::new()
.add_node("X", attrs().weight(1))
.add_node("Y", attrs().weight(2))
.add_node("Xc", attrs().weight(10))
.depends_on("Xc", "X");
// G_FILTER — filter/explain probe reproducing "impl cap (2)". [EX 4]
let g_filter = Graph::new()
.add_node("A", attrs().category("impl"))
.add_node("B", attrs().category("impl"))
.add_node("T-2", attrs().category("impl"))
.add_node("D", attrs().category("docs"));

Layer 1 — DAG core (src/graph.rs, tests/graph.rs)

Section titled “Layer 1 — DAG core (src/graph.rs, tests/graph.rs)”
Fixture idInputCallExpectedProv.
F-GRAPH-immutableg1g1.add_node("Z", attrs())new graph has Z; g1 still 3 nodes[API]
F-GRAPH-depsg1dependencies("T-2")["T-1"][EX 1]
F-GRAPH-dependentsg1dependents("T-1")["T-2","T-3"] (insertion order)[EX 1]
F-GRAPH-ancestorsg_goalancestors("M-0011"){T-7,T-9,T-6,T-8} (as a set)[API]
F-GRAPH-descendantsg_weightdescendants("X")["Xc"][API]
F-GRAPH-topog1topo_sort()Ok(["T-1","T-2","T-3"])[API]+[RATIFIED]
F-GRAPH-hasCycle-falseg1has_cycle()false[API]
F-CYCLE-hasCycleg_cyclehas_cycle()true[API]
F-CYCLE-constructbuilding g_cycledepends_on("B","A") closing the cyclesucceeds — cycles are constructible, lazily detected[API]+[RATIFIED]
F-CYCLE-topo-errg_cycletopo_sort()Err(CycleError) naming a node on the cycle[API]
F-CYCLE-membersg_cyclecycles()one member set, {A,B}[API]
F-CYCLE-getReadyg_cycle, Done([])get_ready[], no error[RATIFIED]

Layer 2 — status + trigger rules (src/status.rs, tests/status.rs)

Section titled “Layer 2 — status + trigger rules (src/status.rs, tests/status.rs)”

Closed-form predicate over the parent statuses; X itself must be Pending to be considered. Empty-parent (root) conventions follow standard Airflow [C-PPXU]: all_* and none_* are vacuously true on roots, one_* are false.

PresetReady iff (over parents)Root (empty parents)
AllSuccessevery parent Successtrue [EX 2]
AllFailedevery parent Failedtrue
AllDoneevery parent in {Success,Failed,Skipped}true [EX 3]
OneSuccessat least one parent Successfalse
OneFailedat least one parent Failedfalse
NoneFailedall parents done and none Failedtrue
NoneSkippedall parents done and none Skippedtrue
Alwaystrue regardless of parentstrue

Truth-table fixtures use g_tr(rule) with X Pending, over (P1,P2) pairs. Abbreviations: pe pending, ru running, su success, fa failed, sk skipped. Expected is “is X in the get_ready frontier?”.

The assertions sit one layer lower, at TriggerRule::is_satisfied_by and StatusSource. T-CLV1-gs-status-model-and-trigger-rules lands before T-I3QP-gs-push-readiness-frontier builds the frontier, so a Layer 2 suite calling get_ready could not be green when its own task finishes. X is Pending in every row, so the predicate and the frontier answer the same question.

Fixture idrule(P1,P2)X ready?Prov.
F-TR-allsucc-1AllSuccess(su,su)yes[EX 2]
F-TR-allsucc-2AllSuccess(su,fa)no[EX 3]
F-TR-allsucc-3AllSuccess(su,sk)no[C-PPXU]
F-TR-allsucc-4AllSuccess(su,pe)no[C-PPXU]
F-TR-allfail-1AllFailed(fa,fa)yes[C-PPXU]
F-TR-allfail-2AllFailed(su,fa)no[C-PPXU]
F-TR-alldone-1AllDone(su,fa)yes[EX 3]
F-TR-alldone-2AllDone(fa,sk)yes[C-PPXU]
F-TR-alldone-3AllDone(su,pe)no[C-PPXU]
F-TR-onesucc-1OneSuccess(su,pe)yes[C-PPXU]
F-TR-onesucc-2OneSuccess(fa,fa)no[C-PPXU]
F-TR-onefail-1OneFailed(su,fa)yes[C-PPXU]
F-TR-onefail-2OneFailed(su,su)no[C-PPXU]
F-TR-nonefail-1NoneFailed(su,sk)yes[C-PPXU]
F-TR-nonefail-2NoneFailed(su,fa)no[C-PPXU]
F-TR-nonefail-3NoneFailed(su,pe)no (not all done)[C-PPXU]
F-TR-noneskip-1NoneSkipped(su,fa)yes[C-PPXU]
F-TR-noneskip-2NoneSkipped(su,sk)no[C-PPXU]
F-TR-always-1Always(pe,pe)yes[C-PPXU]
F-TR-always-2Always(fa,fa)yes[C-PPXU]

Root fixtures assert is_satisfied_by(&[]) for each preset, transcribed from the Root column above:

Fixture idruleX ready?
F-ROOT-allsuccAllSuccessyes
F-ROOT-allfailAllFailedyes
F-ROOT-alldoneAllDoneyes
F-ROOT-onesuccOneSuccessno
F-ROOT-onefailOneFailedno
F-ROOT-nonefailNoneFailedyes
F-ROOT-noneskipNoneSkippedyes
F-ROOT-alwaysAlwaysyes

State-representation fixtures, asserting on StatusSource over g1’s ids:

Fixture idAssertionProv.
F-STATE-sugarDone(["T-1"]) and a map of {T-1: Success} give identical frontiers[EX 2]
F-STATE-missingan empty map treats every node as Pending, so the frontier is ["T-1"][C-PPXU]
F-STATE-closurea Fn(&NodeId) -> NodeStatus source gives the same frontier as the equivalent map[API]

Layer 3 — push frontier (src/ready.rs, tests/ready.rs)

Section titled “Layer 3 — push frontier (src/ready.rs, tests/ready.rs)”
Fixture idInputCallExpected (ids)Prov.
F-FRONT-rootg1, Done([])get_ready["T-1"], attrs category: impl, weight: 3[EX 2]
F-FRONT-unlockg1, Done(["T-1"])get_ready["T-2","T-3"][EX 2]
F-FRONT-limitg1, Done(["T-1"]), limit: 1get_ready["T-2"][EX 2]+[RATIFIED]
F-FRONT-running-exclg1, {T-1: Running}get_ready[][EX 7]
F-FRONT-triggerg2, {build: Failed}get_ready["cleanup"][EX 3]
F-FRONT-whereg1, Done(["T-1"]), where_fn on category == "impl"get_ready["T-2"][API]
F-FRONT-unknown-parentg1 plus depends_on("T-4","missing")get_readyT-4 absent[API] req. C
F-FRONT-pureg1, any stateget_ready twiceidentical results[API]

Layer 4 — filters + explain (src/schedule.rs, tests/schedule.rs)

Section titled “Layer 4 — filters + explain (src/schedule.rs, tests/schedule.rs)”

Verdict::Ok / Verdict::Blocked { reason, retry }; explain returns Explanation { eligible, blocked_by: Vec<BlockedBy> } [API].

Fixture idSetupCallExpectedProv.
F-FILT-cap-blockg_filter, {A: Running, B: Running}, filter cap-impl blocking when chosen_and_in_flight(category == "impl") >= 2explain(state, "T-2")not eligible; blocked by cap-impl, reason impl cap (2), retry OnChange[EX 4]
F-FILT-appliesTog1, Done(["T-1"]), filter no-lease scoped to category == "impl"readyT-3 (docs) is not gated; only impl nodes are[EX 4]
F-FILT-okany node passing every filterexplaineligible, blocked_by empty[API]
F-FILT-retry-neverfilter returning Blocked { retry: Never } for category == "docs"explain(state, "D")blocked with retry Never[RATIFIED]
F-FILT-where-vs-filterg1, Done(["T-1"]), same predicate as where_fn and as a registered filterreadyequal frontiers[API]
F-FILT-explain-agreesany blocked nodeready and explainthe node ready withholds is the node explain calls ineligible, same rule and reason[API]

chosen_and_in_flight [RATIFIED]: candidates are evaluated in final sort order; “chosen” counts peers already selected this batch that passed every filter, plus nodes already Running.

Layer 5 — priority sorters (src/sorters.rs, tests/sorters.rs)

Section titled “Layer 5 — priority sorters (src/sorters.rs, tests/sorters.rs)”
Fixture idInputCallExpectedProv.
F-SORT-selfg1, Done(["T-1"]), limit: 1priority_weight(SelfOnly)["T-2"] (tie on weight, insertion order)[EX 5]+[RATIFIED]
F-SORT-down-selfg_weight, Done([]), limit: 1priority_weight(SelfOnly)["Y"] (2 > 1)[API]
F-SORT-down-critg_weight, Done([]), limit: 1priority_weight(Downstream)["X"] (X bubbles Xc’s 10, beating Y’s 2)[API]
F-SORT-key-tupleg_weight, Done([]), key_fn returning (weight, id)by_key(Downstream, key_fn)["X","Y"] — X inherits Xc’s (10,"Xc"), outranking Y’s own (2,"Y")[C-V44L] req. A
F-SORT-stableany frontier and sorterready twiceidentical order[RATIFIED]

Goal-direction, pull and the tracker are not part of v0.1. Their fixture rows stay recorded here as the spec their tasks will transcribe, and their suites and stubs land with those tasks rather than now.

Fixture idLayerInputCallExpectedProv.
F-GOAL-whatBlocks6g_goal, Done([])what_blocks(state, "M-0011")["T-7","T-9"][EX 6]
F-GOAL-toward-rank6g_goal, Done([]), toward: ["M-0011"], limit: 1get_ready["T-6"][EX 6]+[RATIFIED]
F-GOAL-toward-spill6g_goal, Done([]), toward: ["M-0011"], limit: 3get_ready["T-6","T-8","T-99"][API]
F-GOAL-onlyToward6g_goal, Done([]), where_fn: only_toward("M-0011"), limit: 4get_ready["T-6","T-8"][API]
F-PULL-resolve7g_goal, Done([])resolve(g, state, "M-0011", 4)["T-6","T-8"][EX 8]
F-PULL-eq-toward7same graph and stateresolve vs get_ready with only_towardequal id sets[API]
F-TRACK-ready8Tracker::new(g1)ready()["T-1"][EX 7]
F-TRACK-start8start("T-1") then ready()[][EX 7]
F-TRACK-complete8complete("T-1", Success) then ready()["T-2","T-3"][EX 7]
F-TRACK-parity8each stepready() vs get_ready on the snapshotequal[API]
F-TRACK-resolver8Tracker::with_status(g1, source) returning T-1: Successready()["T-2","T-3"][EX 7]

These pin D-BPD8 questions the examples and signatures did not close outright. Each is transcribed as a fixture above.

#QuestionPinned answerBasis
R-1Tie-break for deterministic orderingadd_node insertion order, for both frontier ties and topo_sortFrozen by [EX 2]limit: 1 gives ["T-2"], and T-2 was added before T-3
R-2Cycle handling, eager or lazyLazy — depends_on never fails, topo_sort returns Err, get_ready returns [] for nodes on a cyclehas_cycle being queryable implies cyclic graphs are constructible
R-3WeightRule::Downstream formulaMax over self and transitive descendants, iterated to a fixed point — corrected from the scalar sumC-V44L requirement A needs a comparable key, which cannot be summed. F-SORT-down-crit does not discriminate the two formulas, so the sum was pinned on evidence that could not tell them apart. Resolved in T-BR6H-gs-priority-sorters
R-4Retry::Never triggerNo built-in Never conditions in v0.1; filters opt in[API] — the two-tier hint is filter-declared
R-5chosen_and_in_flight batch semanticsEvaluate in final sort order; “chosen” is selected-earlier-this-batch plus already-Running[API] — the only deterministic reading
  1. Scaffold the crate: Cargo.toml, moon.yml copied from intersect’s shape with crate-scoped task overrides, and src/lib.rs.
  2. Author src/graph.rs, src/status.rs, src/ready.rs, src/schedule.rs and src/sorters.rs as real signatures with todo!() bodies.
  3. Transcribe the Layer 1 to Layer 5 fixture tables into const data tables in tests/graph.rs, tests/status.rs, tests/ready.rs, tests/schedule.rs and tests/sorters.rs, each test carrying #[ignore = "<task id> implements this"].
  4. Confirm cargo test -p graph-scheduler compiles and reports every test ignored with zero failures, and that cargo test -p graph-scheduler -- --ignored lists the full remaining spec.
  5. Confirm cargo clippy -p graph-scheduler --all-targets -- -D warnings is clean over the stubs.
LocationKindChange
packages/rust/graph-scheduler/Cargo.tomlnewPackage graph-scheduler, serde + serde_json
packages/rust/graph-scheduler/moon.ymlnewlayer: library, crate-scoped tasks with merge: replace
packages/rust/graph-scheduler/src/lib.rsnewModule declarations, re-exports, crate docs
packages/rust/graph-scheduler/src/graph.rsnewLayer 1 stubs
packages/rust/graph-scheduler/src/status.rsnewLayer 2 stubs
packages/rust/graph-scheduler/src/ready.rsnewLayer 3 stubs
packages/rust/graph-scheduler/src/schedule.rsnewLayer 4 stubs
packages/rust/graph-scheduler/src/sorters.rsnewLayer 5 stubs
packages/rust/graph-scheduler/tests/common/mod.rsnewThe shared fixture graphs, so a graph has one definition rather than five
packages/rust/graph-scheduler/tests/graph.rsnewLayer 1 fixtures, ignored
packages/rust/graph-scheduler/tests/status.rsnewLayer 2 fixtures, ignored
packages/rust/graph-scheduler/tests/ready.rsnewLayer 3 fixtures, ignored
packages/rust/graph-scheduler/tests/schedule.rsnewLayer 4 fixtures, ignored
packages/rust/graph-scheduler/tests/sorters.rsnewLayer 5 fixtures, ignored
  • AC-1: The crate exists at packages/rust/graph-scheduler with Cargo package name graph-scheduler, and moon project graph-scheduler reports both the derived bare id and the matching Cargo alias.
  • AC-2: moon run graph-scheduler:test, :lint and :fmt-check all resolve and run against this crate alone, not the whole cargo workspace.
  • AC-3: Every public item listed in the module-stub table above exists with its real signature and a todo!() body, and cargo build -p graph-scheduler succeeds.
  • AC-4: Every fixture in the Fixture matrix for Layers 1 through 5 — the five layers named in the module-stub table — is present as a transcribed const table entry with the expected value this document states, and no expected value is invented beyond those rows.
  • AC-5: Every test in those five suites carries #[ignore] naming the task that implements it, so cargo test -p graph-scheduler reports zero failures and zero non-ignored tests.
  • AC-6: cargo test -p graph-scheduler -- --ignored --list lists one entry per fixture row in Layers 1 through 5.
  • AC-7: cargo clippy -p graph-scheduler --all-targets -- -D warnings is clean.
  • AC-8: No JSON fixture file and no include_str! appears in the crate; fixtures are Rust const data.
  • Any real implementation — stubs only. Each later task un-ignores its own slice.
  • Layers 6 to 8 (goal-direction, pull, tracker). Their rows are recorded above as the spec; their stubs and suites land with their own deferred tasks.
  • Any change to packages/rust/foreman. The type move is T-CLV1-gs-status-model-and-trigger-rules’s.

None. This task scaffolds the crate the rest of the chain builds in.


← Back to Tasks