Files
2026-05-27 23:29:53 +01:00

7.2 KiB

Enterprise migration: @suaveplan/workflow

Package: @suaveplan/workflow Status: PROPOSED Type: new-package (migration from upstream) Date: 2026-05-24 Design: ./design.md (deferred — covered inline) Spec delta: ./specs/workflow/spec.md

Why

Upstream ../suaveplan/packages/universal/workflow/ ships a tree-shakeable, zero-runtime-dep workflow / pipeline orchestration core with sequential + parallel step execution, conditional branching, saga compensation, retry strategies, timeouts, lifecycle hooks, and a fluent builder. It is consumed by higher-level packages (rules-engine, form-builder, multi-step business processes) and must land on genesis before any of those can migrate. The 43-file source has 100% test parity in upstream and we preserve that surface verbatim while bolting on the standard genesis additions: stable error codes via defineCodes, Zod schemas at the public boundary, OTel instruments, injectable Clock for deterministic timing.

What changes

New package @suaveplan/workflow at packages/foundation/flow/workflow/ (sibling to fsm, job-scheduler, streams). The public surface mirrors upstream exactly:

  • ADDED: createWorkflow<TData>(name, id?) and WorkflowBuilder<TData> — fluent builder with addStep, on, onError, withTimeout, buildConfig, build, run.
  • ADDED: StepBuilder<TData> — withCompensation, withGuard, withRetry, withTimeout, build.
  • ADDED: WorkflowEngine<TData> and createWorkflowEngine — the orchestrator with run, cancel, hooks accessor.
  • ADDED: Step<TCtx, TResult> and createStep — base unit with guard / retry / timeout / compensate.
  • ADDED: ParallelStep<TCtx> / createParallelStep with failFast + maxConcurrency.
  • ADDED: ConditionalStep<TCtx> / createConditionalStep with onTrue / onFalse branches.
  • ADDED: Saga<TCtx> / createSaga — sequential steps with automatic reverse-order compensation.
  • ADDED: Compensator<TCtx> / createCompensator — explicit compensation registry + compensateAll / compensateSteps.
  • ADDED: HookRegistry<TCtx> / createHookRegistry + buildStepPayload — lifecycle-event dispatch.
  • ADDED: withRetry, createRetryWrapper, resolveRetryConfig, RetryPresets — middleware-style retry.
  • ADDED: WorkflowContext<TData> / createContext + ContextSnapshot<TData>.
  • ADDED: utilities — generateId, delay, withTimeout, calculateRetryDelay, now.
  • ADDED: constants — DEFAULT_* retry / timeout values.
  • ADDED: types — WorkflowStatus, StepStatus, StepExecutor, CompensationFn, GuardFn, RetryStrategy, RetryConfig, StepResult, StepDefinition, WorkflowResult, HookEvent, HookPayload, HookListener, WorkflowConfig, plus WorkflowTransitionData / WorkflowTransitionHookMap / WorkflowTransitioningHandler for the data-pipeline hook.
  • ADDED: WORKFLOW_CODES + WorkflowError via @suaveplan/error's defineCodes — codes WORKFLOW.STEP_FAILED, WORKFLOW.TIMEOUT, WORKFLOW.CANCELLED, WORKFLOW.COMPENSATION_FAILED, WORKFLOW.GUARD_REJECTED, WORKFLOW.RETRY_EXHAUSTED.

Genesis-only additions on top of upstream:

  • WorkflowError extends SuaveplanError<WorkflowCode> (upstream throws plain Error).
  • Zod-validated WorkflowConfig schema at buildConfig() boundary.
  • OTel counter workflow.step.executed_total{outcome,step_id}, counter workflow.step.compensated_total{outcome,step_id}, histogram workflow.step.duration_ms{step_id} — emitted from Step.run and Compensator.compensateAll.
  • Injectable Clock (defaults to Date.now) on Step, WorkflowEngine, Saga, Compensator, HookRegistry — makes now()-driven timing deterministic in tests.

Parity Checklist

Capability Upstream Migrated Notes
Fluent builder (createWorkflow, WorkflowBuilder, StepBuilder) ✅ ✅
WorkflowEngine.run with onError: stop/compensate/continue ✅ ✅
WorkflowEngine.cancel mid-run ✅ ✅
Step with guard / retry / timeout / compensate ✅ ✅
ParallelStep with failFast + maxConcurrency batching ✅ ✅
ConditionalStep with onTrue / onFalse + skip when no false branch ✅ ✅
Saga orchestrator with reverse-order compensation ✅ ✅
Compensator with compensateAll + compensateSteps + continueOnError ✅ ✅
HookRegistry (on, once, off, emit, listenerCount) ✅ ✅
buildStepPayload helper ✅ ✅
withRetry, createRetryWrapper, resolveRetryConfig, RetryPresets ✅ ✅
WorkflowContext (updateData, setData, addStepResult, setMeta/getMeta, snapshot, reset) ✅ ✅
calculateRetryDelay fixed/linear/exponential ✅ ✅
generateId, delay, withTimeout, now utilities ✅ ✅
Constants (DEFAULT_*) ✅ ✅
Transition data-pipeline hook (workflow:transitioning) ✅ ✅
Subpath exports per feature module ✅ ✅
Plain Error throws ✅ ⬆️ Replaced by WorkflowError + stable codes per genesis rule.
Zod schema validation at config boundary 🚫 ⬆️ Genesis addition — WorkflowConfig is Zod-validated in buildConfig.
OTel counters + histogram 🚫 ⬆️ Genesis addition — step execution + compensation instrumented.
Injectable Clock 🚫 ⬆️ Genesis addition — defaults to Date.now, takes Clock from @suaveplan/types.

Out of scope

  • Durable replay / persistence — upstream does not ship this; out of scope for the first cut and would be a separate sibling package (workflow-store) per the genesis modular pattern.
  • Distributed execution — upstream is single-process; genesis preserves that boundary.

Source mapping

This change derives from ../suaveplan/packages/universal/workflow/src/ (43 files). One genesis feature dir per upstream feature dir; file names preserved.

Requirement Source path
Step run / compensate / guard src/steps/step/step.ts
Parallel step src/steps/parallel-step/parallel-step.ts
Conditional step src/steps/conditional-step/conditional-step.ts
Compensator src/compensation/compensator/compensator.ts
Saga src/compensation/saga/saga.ts
Workflow context src/core/context/context.ts
Workflow builder src/core/workflow-builder/workflow-builder.ts
Workflow engine src/core/workflow-engine/workflow-engine.ts
Hook registry src/middleware/hook/hook.ts
Retry middleware src/middleware/retry/retry.ts
Utils + constants src/utils/utils.ts, src/constants/index.ts
Common + hook types src/types/common.ts, src/types/workflow-hooks.ts

Verification approach

Co-located .test.ts per feature directory under src/. Every requirement in specs/workflow/spec.md has a **Verified by:** line pointing at a concrete it(...) title in one of these files. 100% coverage gate via bunx turbo test:coverage --filter=@suaveplan/workflow.

Impact

Affected dependents

No genesis-side dependents yet (bunx turbo ls --filter=...@suaveplan/workflow returns empty — additive only). The package is a prerequisite for future migrations of rules-engine and form-builder.