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.
| Location | Role 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.rs | The shared-corpus precedent, which this task deliberately does not follow (see below) |
.moon/workspace.yml | Carries the packages/rust/* glob, so a new crate directory registers itself |
Proposed
Section titled “Proposed”The crate
Section titled “The crate”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.
Module stubs
Section titled “Module stubs”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.
| Module | Lands stubbed here | Implemented by |
|---|---|---|
graph | Graph, NodeId, NodeAttrs, attrs, CycleError | T-JVXC-gs-dag-core |
status | NodeStatus, TriggerRule, StatusSource, StatusFn, Done | T-CLV1-gs-status-model-and-trigger-rules |
ready | get_ready, ReadyNode, ReadyOptions, WherePredicate | T-I3QP-gs-push-readiness-frontier |
schedule | Schedule, Filter, filter_fn, filter_fn_scoped, Verdict, Retry, FilterCtx, Explanation, BlockedBy | T-7OJL-gs-eligibility-filters-and-explain |
sorters | Sorter, sorter_fn, ByKey, WeightRule, by_key, priority_weight | T-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:
| Item | Shape | Why |
|---|---|---|
StatusFn | A newtype wrapping a Fn(&NodeId) -> NodeStatus | A 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 setters | A #[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_weight | Take &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.
Fixture matrix (expected outputs)
Section titled “Fixture matrix (expected outputs)”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.
Provenance legend
Section titled “Provenance legend”| Tag | Meaning |
|---|---|
[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”. |
Conventions
Section titled “Conventions”NodeStatus = Pending | Running | Success | Failed | Skipped; a node theStatusSourcehas never heard of readsPending[C-PPXU].- The id-list sugar
Done(["a"])makes those idsSuccessand every other nodePending[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
Pendingand its trigger rule is satisfied by its parents’ statuses;Runningnodes are never in the frontier[API],[EX 7]. ReadyNode { id, attrs }; fixtures assert on the ordered list of ids, and onattrswhere noted[API].- Tie-break: candidates equal under the active sorter, or with no sorter,
resolve by
add_nodeinsertion order.topo_sortbreaks ties the same way[RATIFIED].
Shared graphs
Section titled “Shared graphs”// 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 id | Input | Call | Expected | Prov. |
|---|---|---|---|---|
F-GRAPH-immutable | g1 | g1.add_node("Z", attrs()) | new graph has Z; g1 still 3 nodes | [API] |
F-GRAPH-deps | g1 | dependencies("T-2") | ["T-1"] | [EX 1] |
F-GRAPH-dependents | g1 | dependents("T-1") | ["T-2","T-3"] (insertion order) | [EX 1] |
F-GRAPH-ancestors | g_goal | ancestors("M-0011") | {T-7,T-9,T-6,T-8} (as a set) | [API] |
F-GRAPH-descendants | g_weight | descendants("X") | ["Xc"] | [API] |
F-GRAPH-topo | g1 | topo_sort() | Ok(["T-1","T-2","T-3"]) | [API]+[RATIFIED] |
F-GRAPH-hasCycle-false | g1 | has_cycle() | false | [API] |
F-CYCLE-hasCycle | g_cycle | has_cycle() | true | [API] |
F-CYCLE-construct | building g_cycle | depends_on("B","A") closing the cycle | succeeds — cycles are constructible, lazily detected | [API]+[RATIFIED] |
F-CYCLE-topo-err | g_cycle | topo_sort() | Err(CycleError) naming a node on the cycle | [API] |
F-CYCLE-members | g_cycle | cycles() | one member set, {A,B} | [API] |
F-CYCLE-getReady | g_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.
| Preset | Ready iff (over parents) | Root (empty parents) |
|---|---|---|
AllSuccess | every parent Success | true [EX 2] |
AllFailed | every parent Failed | true |
AllDone | every parent in {Success,Failed,Skipped} | true [EX 3] |
OneSuccess | at least one parent Success | false |
OneFailed | at least one parent Failed | false |
NoneFailed | all parents done and none Failed | true |
NoneSkipped | all parents done and none Skipped | true |
Always | true regardless of parents | true |
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 id | rule | (P1,P2) | X ready? | Prov. |
|---|---|---|---|---|
F-TR-allsucc-1 | AllSuccess | (su,su) | yes | [EX 2] |
F-TR-allsucc-2 | AllSuccess | (su,fa) | no | [EX 3] |
F-TR-allsucc-3 | AllSuccess | (su,sk) | no | [C-PPXU] |
F-TR-allsucc-4 | AllSuccess | (su,pe) | no | [C-PPXU] |
F-TR-allfail-1 | AllFailed | (fa,fa) | yes | [C-PPXU] |
F-TR-allfail-2 | AllFailed | (su,fa) | no | [C-PPXU] |
F-TR-alldone-1 | AllDone | (su,fa) | yes | [EX 3] |
F-TR-alldone-2 | AllDone | (fa,sk) | yes | [C-PPXU] |
F-TR-alldone-3 | AllDone | (su,pe) | no | [C-PPXU] |
F-TR-onesucc-1 | OneSuccess | (su,pe) | yes | [C-PPXU] |
F-TR-onesucc-2 | OneSuccess | (fa,fa) | no | [C-PPXU] |
F-TR-onefail-1 | OneFailed | (su,fa) | yes | [C-PPXU] |
F-TR-onefail-2 | OneFailed | (su,su) | no | [C-PPXU] |
F-TR-nonefail-1 | NoneFailed | (su,sk) | yes | [C-PPXU] |
F-TR-nonefail-2 | NoneFailed | (su,fa) | no | [C-PPXU] |
F-TR-nonefail-3 | NoneFailed | (su,pe) | no (not all done) | [C-PPXU] |
F-TR-noneskip-1 | NoneSkipped | (su,fa) | yes | [C-PPXU] |
F-TR-noneskip-2 | NoneSkipped | (su,sk) | no | [C-PPXU] |
F-TR-always-1 | Always | (pe,pe) | yes | [C-PPXU] |
F-TR-always-2 | Always | (fa,fa) | yes | [C-PPXU] |
Root fixtures assert is_satisfied_by(&[]) for each preset, transcribed from
the Root column above:
| Fixture id | rule | X ready? |
|---|---|---|
F-ROOT-allsucc | AllSuccess | yes |
F-ROOT-allfail | AllFailed | yes |
F-ROOT-alldone | AllDone | yes |
F-ROOT-onesucc | OneSuccess | no |
F-ROOT-onefail | OneFailed | no |
F-ROOT-nonefail | NoneFailed | yes |
F-ROOT-noneskip | NoneSkipped | yes |
F-ROOT-always | Always | yes |
State-representation fixtures, asserting on StatusSource over g1’s ids:
| Fixture id | Assertion | Prov. |
|---|---|---|
F-STATE-sugar | Done(["T-1"]) and a map of {T-1: Success} give identical frontiers | [EX 2] |
F-STATE-missing | an empty map treats every node as Pending, so the frontier is ["T-1"] | [C-PPXU] |
F-STATE-closure | a 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 id | Input | Call | Expected (ids) | Prov. |
|---|---|---|---|---|
F-FRONT-root | g1, Done([]) | get_ready | ["T-1"], attrs category: impl, weight: 3 | [EX 2] |
F-FRONT-unlock | g1, Done(["T-1"]) | get_ready | ["T-2","T-3"] | [EX 2] |
F-FRONT-limit | g1, Done(["T-1"]), limit: 1 | get_ready | ["T-2"] | [EX 2]+[RATIFIED] |
F-FRONT-running-excl | g1, {T-1: Running} | get_ready | [] | [EX 7] |
F-FRONT-trigger | g2, {build: Failed} | get_ready | ["cleanup"] | [EX 3] |
F-FRONT-where | g1, Done(["T-1"]), where_fn on category == "impl" | get_ready | ["T-2"] | [API] |
F-FRONT-unknown-parent | g1 plus depends_on("T-4","missing") | get_ready | T-4 absent | [API] req. C |
F-FRONT-pure | g1, any state | get_ready twice | identical 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 id | Setup | Call | Expected | Prov. |
|---|---|---|---|---|
F-FILT-cap-block | g_filter, {A: Running, B: Running}, filter cap-impl blocking when chosen_and_in_flight(category == "impl") >= 2 | explain(state, "T-2") | not eligible; blocked by cap-impl, reason impl cap (2), retry OnChange | [EX 4] |
F-FILT-appliesTo | g1, Done(["T-1"]), filter no-lease scoped to category == "impl" | ready | T-3 (docs) is not gated; only impl nodes are | [EX 4] |
F-FILT-ok | any node passing every filter | explain | eligible, blocked_by empty | [API] |
F-FILT-retry-never | filter returning Blocked { retry: Never } for category == "docs" | explain(state, "D") | blocked with retry Never | [RATIFIED] |
F-FILT-where-vs-filter | g1, Done(["T-1"]), same predicate as where_fn and as a registered filter | ready | equal frontiers | [API] |
F-FILT-explain-agrees | any blocked node | ready and explain | the 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 id | Input | Call | Expected | Prov. |
|---|---|---|---|---|
F-SORT-self | g1, Done(["T-1"]), limit: 1 | priority_weight(SelfOnly) | ["T-2"] (tie on weight, insertion order) | [EX 5]+[RATIFIED] |
F-SORT-down-self | g_weight, Done([]), limit: 1 | priority_weight(SelfOnly) | ["Y"] (2 > 1) | [API] |
F-SORT-down-crit | g_weight, Done([]), limit: 1 | priority_weight(Downstream) | ["X"] (X bubbles Xc’s 10, beating Y’s 2) | [API] |
F-SORT-key-tuple | g_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-stable | any frontier and sorter | ready twice | identical order | [RATIFIED] |
Layers 6 to 8 — deferred past cutover
Section titled “Layers 6 to 8 — deferred past cutover”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 id | Layer | Input | Call | Expected | Prov. |
|---|---|---|---|---|---|
F-GOAL-whatBlocks | 6 | g_goal, Done([]) | what_blocks(state, "M-0011") | ["T-7","T-9"] | [EX 6] |
F-GOAL-toward-rank | 6 | g_goal, Done([]), toward: ["M-0011"], limit: 1 | get_ready | ["T-6"] | [EX 6]+[RATIFIED] |
F-GOAL-toward-spill | 6 | g_goal, Done([]), toward: ["M-0011"], limit: 3 | get_ready | ["T-6","T-8","T-99"] | [API] |
F-GOAL-onlyToward | 6 | g_goal, Done([]), where_fn: only_toward("M-0011"), limit: 4 | get_ready | ["T-6","T-8"] | [API] |
F-PULL-resolve | 7 | g_goal, Done([]) | resolve(g, state, "M-0011", 4) | ["T-6","T-8"] | [EX 8] |
F-PULL-eq-toward | 7 | same graph and state | resolve vs get_ready with only_toward | equal id sets | [API] |
F-TRACK-ready | 8 | Tracker::new(g1) | ready() | ["T-1"] | [EX 7] |
F-TRACK-start | 8 | start("T-1") then ready() | — | [] | [EX 7] |
F-TRACK-complete | 8 | complete("T-1", Success) then ready() | — | ["T-2","T-3"] | [EX 7] |
F-TRACK-parity | 8 | each step | ready() vs get_ready on the snapshot | equal | [API] |
F-TRACK-resolver | 8 | Tracker::with_status(g1, source) returning T-1: Success | ready() | ["T-2","T-3"] | [EX 7] |
Ratified defaults (confirm at PR review)
Section titled “Ratified defaults (confirm at PR review)”These pin D-BPD8 questions the examples and signatures did not close outright. Each is transcribed as a fixture above.
| # | Question | Pinned answer | Basis |
|---|---|---|---|
| R-1 | Tie-break for deterministic ordering | add_node insertion order, for both frontier ties and topo_sort | Frozen by [EX 2] — limit: 1 gives ["T-2"], and T-2 was added before T-3 |
| R-2 | Cycle handling, eager or lazy | Lazy — depends_on never fails, topo_sort returns Err, get_ready returns [] for nodes on a cycle | has_cycle being queryable implies cyclic graphs are constructible |
| R-3 | WeightRule::Downstream formula | Max over self and transitive descendants, iterated to a fixed point — corrected from the scalar sum | C-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-4 | Retry::Never trigger | No built-in Never conditions in v0.1; filters opt in | [API] — the two-tier hint is filter-declared |
| R-5 | chosen_and_in_flight batch semantics | Evaluate in final sort order; “chosen” is selected-earlier-this-batch plus already-Running | [API] — the only deterministic reading |
Approach
Section titled “Approach”- Scaffold the crate:
Cargo.toml,moon.ymlcopied from intersect’s shape with crate-scoped task overrides, andsrc/lib.rs. - Author
src/graph.rs,src/status.rs,src/ready.rs,src/schedule.rsandsrc/sorters.rsas real signatures withtodo!()bodies. - Transcribe the Layer 1 to Layer 5 fixture tables into
constdata tables intests/graph.rs,tests/status.rs,tests/ready.rs,tests/schedule.rsandtests/sorters.rs, each test carrying#[ignore = "<task id> implements this"]. - Confirm
cargo test -p graph-schedulercompiles and reports every test ignored with zero failures, and thatcargo test -p graph-scheduler -- --ignoredlists the full remaining spec. - Confirm
cargo clippy -p graph-scheduler --all-targets -- -D warningsis clean over the stubs.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
packages/rust/graph-scheduler/Cargo.toml | new | Package graph-scheduler, serde + serde_json |
packages/rust/graph-scheduler/moon.yml | new | layer: library, crate-scoped tasks with merge: replace |
packages/rust/graph-scheduler/src/lib.rs | new | Module declarations, re-exports, crate docs |
packages/rust/graph-scheduler/src/graph.rs | new | Layer 1 stubs |
packages/rust/graph-scheduler/src/status.rs | new | Layer 2 stubs |
packages/rust/graph-scheduler/src/ready.rs | new | Layer 3 stubs |
packages/rust/graph-scheduler/src/schedule.rs | new | Layer 4 stubs |
packages/rust/graph-scheduler/src/sorters.rs | new | Layer 5 stubs |
packages/rust/graph-scheduler/tests/common/mod.rs | new | The shared fixture graphs, so a graph has one definition rather than five |
packages/rust/graph-scheduler/tests/graph.rs | new | Layer 1 fixtures, ignored |
packages/rust/graph-scheduler/tests/status.rs | new | Layer 2 fixtures, ignored |
packages/rust/graph-scheduler/tests/ready.rs | new | Layer 3 fixtures, ignored |
packages/rust/graph-scheduler/tests/schedule.rs | new | Layer 4 fixtures, ignored |
packages/rust/graph-scheduler/tests/sorters.rs | new | Layer 5 fixtures, ignored |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: The crate exists at
packages/rust/graph-schedulerwith Cargo package namegraph-scheduler, andmoon project graph-schedulerreports both the derived bare id and the matching Cargo alias. - AC-2:
moon run graph-scheduler:test,:lintand:fmt-checkall 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, andcargo build -p graph-schedulersucceeds. - 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
consttable 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, socargo test -p graph-schedulerreports zero failures and zero non-ignored tests. - AC-6:
cargo test -p graph-scheduler -- --ignored --listlists one entry per fixture row in Layers 1 through 5. - AC-7:
cargo clippy -p graph-scheduler --all-targets -- -D warningsis clean. - AC-8: No JSON fixture file and no
include_str!appears in the crate; fixtures are Rustconstdata.
Out of scope
Section titled “Out of scope”- 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.
Dependencies
Section titled “Dependencies”None. This task scaffolds the crate the rest of the chain builds in.