You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Library consumers need an in-process async DAG of work steps that share one TContext, where edges mean readiness dependencies only (not typed payload channels), with compile-time proof of graph legality (cycle / unknown dependency / duplicate id / self-dependency / contract mismatch) and runtime dual-path assembly (WorkGraphBuilder / IWorkGraph plus attribute-driven generation). Existing surfaces do not cover this:
Composite parallel traversal (ADR-006) parallels tree depth, not multi-predecessor DAG waves.
Step Builder proves construction step completeness, not async execution readiness.
Command Router is 1:1 dispatch, not a fork–join graph.
Watch-list Channel Pipeline and BCL TPL Dataflow are typed message networks — out of this domain's edge model.
This work is F3 admission Top-3 from Wayfinder map #244 / ROADMAP F3. Wayfinder map for this Spec: #300. Domain vocabulary lives in CONTEXT.md; API stub for reaction: docs/.Local/prototypes/work-graph-mvp-api-stub.md on branch prototype/work-graph-api-stub (#306).
Solution
Ship Fork–Join Work Graph (API prefix Work*):
Holder marker type with [WorkGraph<TContext>] names the graph and fixes TContext (prefer static class).
Step types implement IWorkStep<TContext> with ValueTask ExecuteAsync(TContext, CancellationToken) and [WorkStep(typeof(Holder), Id = "…", DependsOn = …)] (explicit holder membership).
Manual path:WorkGraphBuilder<TContext>.Add(id, step, params dependsOn).Build() → IWorkGraph<TContext>.RunAsync(ctx, ct) returning ValueTask (no trace in MVP).
Execution: async-only; topological waves; same-wave steps may run concurrently; fail-fast (cancel in-flight peers; throw). Document that overlapping unsynchronized writes to TContext are forbidden; library does not isolate/merge context.
Diagnostics (MVP matrix): see Implementation Decisions — IDs allocated in first Diagnostics PR from DP087+ (Spec does not pre-reserve numbers).
MVP Sample sketch (sibling Samples, not in this Spec's implementation): request-prep graph Auth ∥ LoadConfig → BuildPrincipal → Authorize.
As an application developer, I want a [WorkGraph<TContext>] holder separate from step classes, so that graph name and context type have a single generation anchor.
As an application developer, I want each step to be a type implementing IWorkStep<TContext>, so that steps are independently unit-testable.
As an application developer, I want [WorkStep(typeof(Holder), Id, DependsOn)] to declare membership and edges, so that multiple graphs may share the same TContext without silent aggregation.
As an application developer, I want ExecuteAsync to mutate shared TContext and return ValueTask, so that outputs do not require typed payload edges.
As an application developer, I want WorkGraphBuilder to register steps and Build() to validate the DAG at runtime, so that tests and dynamic scenarios work without the generator.
As an application developer, I want IWorkGraph.RunAsync to execute topological waves with fail-fast cancellation, so that fork–join semantics match the domain card.
As an application developer, I want generated {Name}WorkStepKeys, so that call sites and diagnostics share stable string ids (Strategy/Factory Keys precedent).
As an application developer, I want {Name}WorkGraph.Create(resolver|map) to materialize IWorkGraph from the attribute catalog, so that attribute and manual paths share execution semantics.
As an application developer, I want cycle, unknown DependsOn, duplicate id, and self-dependency to be Errors, so that illegal graphs cannot ship.
As an application developer, I want unreachable steps to be Warnings while multi-root graphs remain valid, so that Sample-style Auth ∥ LoadConfig works and dead steps are still visible.
As an application developer, I want [WorkStep] / IWorkStep / TContext mismatches to be Errors, so that attribute catalogs stay consistent with holder context.
As an application developer, I want empty graphs rejected and single-node / diamond graphs accepted, so that edge cases are unambiguous in Spec and tests.
As an application developer, I want MVP without Dop options, trace types, DI, or sync Execute, so that the first ship stays thin.
As an application developer, I do not want typed TIn/TOut edges or Channel<T> stage wiring in this domain, so that Channel Pipeline stays a watch-list boundary.
As an application developer, I do not want this domain to extend Composite TraverseParallel* or impersonate TPL Dataflow, so that Non-goals stay crisp.
As a library consumer on netstandard2.0 and net8.0, I want Core + generated code on both TFMs, so that the dual-TFM baseline holds.
As a developer, I want generation without AppDomain reflection scanning, so that AOT/trim goals stay aligned with AGENTS.md.
As a developer, I want actionable diagnostic messages (which ids / which edge), so that AGENTS.md messageFormat guidance holds.
As a developer, I want Design Doc WorkGraph.md (or agreed name) plus design index entry, so that API/diagnostics/trade-offs are recorded like other domains.
As a documentation reader, I want CHANGELOG and ROADMAP F3 Top-3 status updates when milestones land, so that backlog state stays truthful.
As a maintainer, I want DP### IDs allocated from the next free ID (DP087+) in the first Diagnostics PR without Spec pre-reservation, so that unused numbers are not burned.
As a maintainer, I want DiagnosticIds / descriptors / AnalyzerReleases / AGENTS.md updated in the same PRs that introduce rules, so that AGENTS.md process holds.
As a maintainer, I want each solution module in its own PR, so that AGENTS.md module boundaries hold.
As a maintainer, I want Runtime unit tests for builder validation + wave/fail-fast behavior and generator Verify for attribute emission/diagnostics, so that both dual-path seams stay locked.
As a contributor, I want XML docs on public APIs, file-scoped namespaces, and nullable enable, so that repo coding standards apply.
As a release manager, I want no version bump/tag/publish as part of feature PRs unless explicitly requested, so that AGENTS.md release policy holds.
As a future implementer, I want MVP not to forbid later Dop, trace/observer, sync path, aggregate failure, DI, or unregistered-step Analyzer, so that Phase 2+ stays additive.
As an application developer, I do not want an unregistered-IWorkStep Analyzer in MVP, so that the manual builder path is not forced through attributes.
Implementation Decisions
Scope and phasing
Domain name: Fork–Join Work Graph; API prefix Work*.
Samples (sibling): request-prep — separate Issue/PR in DesignPatterns.Samples.
Analyzers / CodeFixes / DI / Autofac / Package: not MVP (Package only if packing requires).
Architectural constraints (AGENTS.md)
Primitives over frameworks; Core without MSDI; dual TFM; Roslyn 4.8.0; nullable; TreatWarningsAsErrors; English for issues/PRs/commits.
No AppDomain reflection registration scans.
Diagnostic help links via existing DiagnosticHelpLinks pattern.
Single-module PRs.
Testing Decisions (seams — confirmed)
What makes a good test here: assert observable behavior — Runtime: Build validation exceptions, wave ordering, fail-fast cancellation; Generator: Verify public sources + diagnostic IDs/severities/messages — not private helpers.
Primary seams:
DesignPatterns.Tests — builder/RunAsync behavior (empty/single/diamond; cycle/dup/self/unknown at Build; fail-fast).
API stub: prototype/work-graph-api-stub / docs/.Local/prototypes/work-graph-mvp-api-stub.md.
Research notes (local branches): research/work-graph-dag-analogues, research/work-graph-nongoals-boundaries.
After this Spec: /to-tickets should split Runtime → Diagnostics → SourceGenerators+Verify → Docs → Samples (sibling) with blocking edges; each /implement in a fresh session.
Problem Statement
Library consumers need an in-process async DAG of work steps that share one
TContext, where edges mean readiness dependencies only (not typed payload channels), with compile-time proof of graph legality (cycle / unknown dependency / duplicate id / self-dependency / contract mismatch) and runtime dual-path assembly (WorkGraphBuilder/IWorkGraphplus attribute-driven generation). Existing surfaces do not cover this:This work is F3 admission Top-3 from Wayfinder map #244 / ROADMAP F3. Wayfinder map for this Spec: #300. Domain vocabulary lives in
CONTEXT.md; API stub for reaction:docs/.Local/prototypes/work-graph-mvp-api-stub.mdon branchprototype/work-graph-api-stub(#306).Solution
Ship Fork–Join Work Graph (API prefix
Work*):[WorkGraph<TContext>]names the graph and fixesTContext(preferstatic class).IWorkStep<TContext>withValueTask ExecuteAsync(TContext, CancellationToken)and[WorkStep(typeof(Holder), Id = "…", DependsOn = …)](explicit holder membership).WorkGraphBuilder<TContext>.Add(id, step, params dependsOn).Build()→IWorkGraph<TContext>.RunAsync(ctx, ct)returningValueTask(no trace in MVP).{Name}WorkStepKeys(public const stringids) +{Name}WorkGraph.Create(resolver | dictionary)filling the builder.TContextare forbidden; library does not isolate/merge context.Auth∥LoadConfig→BuildPrincipal→Authorize.DesignPatterns.Behavioral.User Stories
[WorkGraph<TContext>]holder separate from step classes, so that graph name and context type have a single generation anchor.IWorkStep<TContext>, so that steps are independently unit-testable.[WorkStep(typeof(Holder), Id, DependsOn)]to declare membership and edges, so that multiple graphs may share the sameTContextwithout silent aggregation.ExecuteAsyncto mutate sharedTContextand returnValueTask, so that outputs do not require typed payload edges.WorkGraphBuilderto register steps andBuild()to validate the DAG at runtime, so that tests and dynamic scenarios work without the generator.IWorkGraph.RunAsyncto execute topological waves with fail-fast cancellation, so that fork–join semantics match the domain card.{Name}WorkStepKeys, so that call sites and diagnostics share stable string ids (Strategy/Factory Keys precedent).{Name}WorkGraph.Create(resolver|map)to materializeIWorkGraphfrom the attribute catalog, so that attribute and manual paths share execution semantics.DependsOn, duplicate id, and self-dependency to be Errors, so that illegal graphs cannot ship.Auth∥LoadConfigworks and dead steps are still visible.[WorkStep]/IWorkStep/TContextmismatches to be Errors, so that attribute catalogs stay consistent with holder context.Execute, so that the first ship stays thin.TIn/TOutedges orChannel<T>stage wiring in this domain, so that Channel Pipeline stays a watch-list boundary.TraverseParallel*or impersonate TPL Dataflow, so that Non-goals stay crisp.WorkGraph.md(or agreed name) plus design index entry, so that API/diagnostics/trade-offs are recorded like other domains.DP###IDs allocated from the next free ID (DP087+) in the first Diagnostics PR without Spec pre-reservation, so that unused numbers are not burned.IWorkStepAnalyzer in MVP, so that the manual builder path is not forced through attributes.Implementation Decisions
Scope and phasing
Work*.IWorkStep/IWorkGraph/WorkGraphBuilder+ attributes[WorkGraph<T>]/[WorkStep]; generator Keys +Createfacade; diagnostic matrix below; Design Doc + ROADMAP/AGENTS/CHANGELOG; Runtime tests + generator Verify; dual TFM.MaxDegreeOfParallelism/ run options;RunAsynctrace/observer; sync path; aggregate/continue failure; DI/Autofac registration; unregistered-step Analyzer.Locked surface (grill + prototype)
From #303 / #306:
Diagnostic matrix (MVP)
From #304:
Build)DependsOnBuild)Build)Build)IWorkStepAnalyzerDP###: not reserved in this Spec; first Diagnostics PR assigns from DP087+.Edge-case acceptance (Spec-required)
From #305:
Build/CreateError).RunAsyncexecutes it.A→C,B→C) → legal;Cafter both predecessors.Boundary vs siblings
From #302 / #301:
Channel<T>wiring — name in Out of Scope; do not admit/specify that domain.CompositeTraverser.Task/ValueTask/WhenAll.Modules (one PR each)
IWorkStep,IWorkGraph,WorkGraphBuilder, execution/fail-fast.CONTEXT.mdif needed.Architectural constraints (AGENTS.md)
Testing Decisions (seams — confirmed)
What makes a good test here: assert observable behavior — Runtime:
Buildvalidation exceptions, wave ordering, fail-fast cancellation; Generator: Verify public sources + diagnostic IDs/severities/messages — not private helpers.Primary seams:
DesignPatterns.Tests— builder/RunAsyncbehavior (empty/single/diamond; cycle/dup/self/unknown atBuild; fail-fast).DesignPatterns.SourceGenerators.Tests— Verify Keys/facade emission + diagnostic matrix cases.Secondary: Docs module; Samples sibling after API exists.
Modules under test: Runtime + SourceGenerators (both MVP); then Docs; Samples later.
Out of Scope
Channel<T>stage wiring.TraverseParallel*as Work Graph.Execute; MVP aggregate/continue failure; MVP DI/Autofac; MVP unregistered-step Analyzer.TContextisolate/merge framework.DP###number reservation.Further Notes
prototype/work-graph-api-stub/docs/.Local/prototypes/work-graph-mvp-api-stub.md.research/work-graph-dag-analogues,research/work-graph-nongoals-boundaries./to-ticketsshould split Runtime → Diagnostics → SourceGenerators+Verify → Docs → Samples (sibling) with blocking edges; each/implementin a fresh session.