Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
db3d947
chore(tests): switch Ollama model to qwen2.5:1.5b, enforce serial E2E…
JerrettDavis Apr 29, 2026
4c087fc
feat(dashboard): surface AI DSL Emitter demo as featured template and…
JerrettDavis Apr 29, 2026
986b7c8
fix: remove stale {provider} placeholder from seeded AI DSL Emitter w…
JerrettDavis Apr 29, 2026
4ab401f
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
e309b13
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
3b36ae6
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
ebabfaf
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
6525fc1
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
3de2aae
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
13b2047
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
79fbeaf
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
2499164
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
3a174cf
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 22, 2026
010a179
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 23, 2026
917a655
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 23, 2026
b8c0713
Merge branch 'main' of https://github.com/JerrettDavis/WorkflowFramework
JerrettDavis May 23, 2026
ea8431a
refactor(integration-transformation): re-root ClaimCheck on PatternKi…
JerrettDavis May 23, 2026
4a3d798
refactor(integration-endpoint): re-root TransactionalOutbox on Patter…
JerrettDavis May 23, 2026
d875fff
refactor(integration-composition): re-root ScatterGather on PatternKi…
JerrettDavis May 23, 2026
0a4a735
chore(locks): refresh NuGet lock files after PatternKit.Core dependen…
JerrettDavis May 23, 2026
0c0a9f1
docs(patternkit-adoption): mark ClaimCheck, TransactionalOutbox, Scat…
JerrettDavis May 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions docs/patternkit-adoption.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# PatternKit Adoption Inventory

**PatternKit version:** 0.113.0
**Last updated:** 2026-05-22 (feat/iter2-minor-refactorsNormalizerStep / PollingConsumerStep / IdempotentReceiverStep adopted)
**Last updated:** 2026-05-23 (feat/iter2-major-interface-migrationsClaimCheckStep, TransactionalOutboxStep, ScatterGatherStep migrated; three legacy interfaces obsoleted)

This document lists every point in the WorkflowFramework codebase where a PatternKit primitive is used, and every point where a step is intentionally kept bespoke with the rationale for that decision. This is the canonical reference for Phase I and future phases.

Expand Down Expand Up @@ -83,6 +83,51 @@ This document lists every point in the WorkflowFramework codebase where a Patter
| **Public API change** | None — swap is internal-only. |
| **Net delta** | −4 lines (bespoke poll call replaced by consumer delegation). |

### 8. `ClaimCheckStep` / `ClaimRetrieveStep` — Claim check pattern

| Item | Detail |
|------|--------|
| **File** | `src/WorkflowFramework.Extensions.Integration/Transformation/ClaimCheckStep.cs` |
| **PatternKit namespace** | `PatternKit.Messaging.Transformation` |
| **Primitive** | `IClaimCheckStore<object>` and `InMemoryClaimCheckStore<object>` |
| **Purpose** | Steps now consume PatternKit's typed `IClaimCheckStore<object>` directly. Claim IDs are generated by the step (`Guid.NewGuid().ToString("N")`), not by the store. Store calls use `MessageHeaders.Empty`. `ClaimRetrieveStep` calls `TryLoadAsync` and throws `InvalidOperationException` if the claim is not found. |
| **Phase introduced** | feat/iter2-major-interface-migrations (Phase 3) |
| **Behavior change** | Claim ID is now step-generated (deterministic Guid), not store-returned. `ClaimRetrieveStep` now throws on not-found instead of returning null. |
| **Test coverage** | `tests/WorkflowFramework.Tests.TinyBDD/Integration/Transformation/ClaimCheckStepScenarios.cs` — all 11 scenarios updated and passing. |
| **Public API change** | Constructor signatures now accept `IClaimCheckStore<object>` instead of `IClaimCheckStore`. Legacy WF `IClaimCheckStore` is `[Obsolete]`. `LegacyClaimCheckStoreAdapter` provided for one release. |
| **Net delta** | −8 LOC (removed bespoke ID-from-store handling; replaced with PatternKit's typed StoreAsync/TryLoadAsync). |
| **Obsoletion** | `WorkflowFramework.Extensions.Integration.Abstractions.IClaimCheckStore` → `PatternKit.Messaging.Transformation.IClaimCheckStore<TPayload>`. Remove in next major version. |

### 9. `TransactionalOutboxStep` — Transactional outbox pattern

| Item | Detail |
|------|--------|
| **File** | `src/WorkflowFramework.Extensions.Integration/Endpoint/TransactionalOutboxStep.cs` |
| **PatternKit namespace** | `PatternKit.Messaging.Reliability` |
| **Primitive** | `IOutboxStore<object>` via `OutboxStoreExtensions.EnqueueObjectAsync` |
| **Purpose** | Step now consumes PatternKit's typed `IOutboxStore<object>` directly. Payload is wrapped in `Message<object>` via `EnqueueObjectAsync`. The outbox record ID comes from `record.Id` (same value as before, different code path). |
| **Phase introduced** | feat/iter2-major-interface-migrations (Phase 3) |
| **Behavior change** | `OutboxIdKey` is now sourced from `OutboxMessage<object>.Id` (PatternKit record) instead of `SaveAsync`'s return string. Same value; the backing path changed. |
| **Test coverage** | `tests/WorkflowFramework.Tests.TinyBDD/Integration/Endpoint/TransactionalOutboxStepScenarios.cs` — all 7 scenarios updated and passing. |
| **Public API change** | Constructor now accepts `IOutboxStore<object>` instead of `IOutboxStore`. Legacy WF `IOutboxStore` is `[Obsolete]`. `LegacyOutboxStoreAdapter` provided for one release. |
| **Net delta** | −6 LOC (bespoke `SaveAsync` call replaced by `EnqueueObjectAsync` delegation). |
| **Obsoletion** | `WorkflowFramework.Extensions.Integration.Abstractions.IOutboxStore` → `PatternKit.Messaging.Reliability.IOutboxStore<TPayload>`. Remove in next major version. |

### 10. `ScatterGatherStep` — Scatter gather pattern

| Item | Detail |
|------|--------|
| **File** | `src/WorkflowFramework.Extensions.Integration/Composition/ScatterGatherStep.cs` |
| **PatternKit namespace** | `PatternKit.Messaging.Routing` |
| **Primitive** | `AsyncScatterGather<IWorkflowContext, object?, IReadOnlyList<object?>>` with `CompletionStrategy.AllOrTimeout` |
| **Purpose** | Step now delegates fan-out to PatternKit's `AsyncScatterGather` with per-branch error isolation. Each recipient is a typed `ScatterGatherStep.Recipient` (name + `Func<IWorkflowContext, CancellationToken, ValueTask<object?>>`) that returns its result directly — eliminating the shared-context mutation hazard. Results are aggregated into `ResultsKey` as before. |
| **Phase introduced** | feat/iter2-major-interface-migrations (Phase 3) |
| **Behavior change** | **Breaking (back-compat provided):** Recipient contract changes from `IStep` (mutates shared context, reads `__Result_{name}`) to typed `Recipient` (returns value directly). PatternKit's `ConcurrentBag` means result ordering is non-deterministic (was implicit-sequential before). Non-caller-cancelled recipients produce NO result entry (previously produced null). A deprecated `IEnumerable<IStep>` overload bridges for one release. |
| **Test coverage** | `tests/WorkflowFramework.Tests.TinyBDD/Integration/Composition/ScatterGatherStepScenarios.cs` + `ScatterGatherStepTests.cs` — all scenarios updated; `HandlerOperationCanceledException_IsSwallowed` updated with rationale comment. |
| **Public API change** | New typed constructor `ScatterGatherStep(IEnumerable<Recipient>, ...)` is primary. Legacy `IEnumerable<IStep>` overload is `[Obsolete]`, retained for one release. Builder `ScatterGather(IEnumerable<ScatterGatherStep.Recipient>, ...)` is now primary; legacy overload deprecated. |
| **Net delta** | −28 LOC bespoke fan-out/error-isolation logic replaced by PatternKit primitive. |
| **Obsoletion** | `ScatterGatherStep(IEnumerable<IStep>, ...)` constructor overload → `ScatterGatherStep(IEnumerable<ScatterGatherStep.Recipient>, ...)`. Remove in next major version. |

### 7. `IdempotentReceiverStep` — Idempotent receiver pattern

| Item | Detail |
Expand Down Expand Up @@ -240,9 +285,9 @@ The following components are candidates for PatternKit adoption in later phases
| Component | Potential Primitive | Blocking Reason (assessed against 0.113.0) |
|-----------|--------------------|-----------------------|
| `ContentEnricherStep` | `AsyncContentEnricher<TPayload>` (now in 0.113.0) | Path C — intentionally bespoke. PatternKit returns an enriched payload copy; bespoke mutates `IWorkflowContext` in place via a `Func<IWorkflowContext, Task>`. Wrapping adds indirection for zero functional benefit. See `.plan/patternkit-iteration-2.md` §2. |
| `ClaimCheckStep` / `ClaimRetrieveStep` | `ClaimCheck<TPayload>` (now in 0.113.0) | Interface mismatch: bespoke `IClaimCheckStore` is untyped (`object`); PatternKit `IClaimCheckStore<TPayload>` is typed. Deferred to Iteration 2 Phase 3 — requires adapter + interface migration. |
| `ScatterGatherStep` | `AsyncScatterGather<TRequest,TResponse,TResult>` (now in 0.113.0) | Integration complexity: handlers mutate a shared `IWorkflowContext` and write results to named context keys; PatternKit's per-recipient isolation model returns typed `TResponse` values. Deferred to Iteration 2 Phase 3. |
| `TransactionalOutboxStep` | `IOutboxStore<TPayload>` (now in 0.113.0) | Interface mismatch: bespoke `IOutboxStore` uses `SaveAsync(object) → string`; PatternKit `IOutboxStore<TPayload>` uses `EnqueueAsync(Message<TPayload>) → OutboxMessage<TPayload>`. Deferred to Iteration 2 Phase 3. |
| ~~`ClaimCheckStep` / `ClaimRetrieveStep`~~ | **Adopted in feat/iter2-major-interface-migrations** | See entry §8 above. Legacy `IClaimCheckStore` obsoleted; `LegacyClaimCheckStoreAdapter` provided for one release. |
| ~~`ScatterGatherStep`~~ | **Adopted in feat/iter2-major-interface-migrations** | See entry §10 above. Typed recipient contract; legacy `IEnumerable<IStep>` overload deprecated. |
| ~~`TransactionalOutboxStep`~~ | **Adopted in feat/iter2-major-interface-migrations** | See entry §9 above. Legacy `IOutboxStore` obsoleted; `LegacyOutboxStoreAdapter` provided for one release. |
| `AggregatorStep` | PatternKit Aggregator (future) | No Aggregator primitive in 0.113.0 |
| `PluginManager` | `Strategy` + `AbstractFactory` | Phase H.8 — not yet started |
| `AgentLoopStep` / `AgentDecisionStep` | TypeDispatcher | Phase H.7 — not yet started |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ namespace WorkflowFramework.Extensions.Integration.Abstractions;
/// <summary>
/// Stores and retrieves large payloads using claim check pattern.
/// </summary>
/// <remarks>
/// <b>DEPRECATED:</b> Use <c>PatternKit.Messaging.Transformation.IClaimCheckStore&lt;TPayload&gt;</c>
/// directly. This interface is retained for one release as a back-compat shim and will be removed
/// in the next major version. Migrate to <c>IClaimCheckStore&lt;object&gt;</c> (or a typed variant)
/// and update DI registrations accordingly.
/// See <c>LegacyClaimCheckStoreAdapter</c> for a bridge between the old and new contracts.
/// </remarks>
[Obsolete(
"WorkflowFramework.Extensions.Integration.Abstractions.IClaimCheckStore is obsolete. " +
"Migrate to PatternKit.Messaging.Transformation.IClaimCheckStore<TPayload> " +
"(use IClaimCheckStore<object> for untyped payloads). " +
"A legacy adapter LegacyClaimCheckStoreAdapter is available for one release. " +
"This interface will be removed in the next major version.",
error: false)]
public interface IClaimCheckStore
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ namespace WorkflowFramework.Extensions.Integration.Abstractions;
/// <summary>
/// Transactional outbox store for reliable message publishing.
/// </summary>
/// <remarks>
/// <b>DEPRECATED:</b> Use <c>PatternKit.Messaging.Reliability.IOutboxStore&lt;TPayload&gt;</c>
/// directly. This interface is retained for one release as a back-compat shim and will be removed
/// in the next major version. Migrate to <c>IOutboxStore&lt;object&gt;</c> (or a typed variant)
/// and update DI registrations accordingly. See <c>LegacyOutboxStoreAdapter</c> for a bridge.
/// </remarks>
[Obsolete(
"WorkflowFramework.Extensions.Integration.Abstractions.IOutboxStore is obsolete. " +
"Migrate to PatternKit.Messaging.Reliability.IOutboxStore<TPayload> " +
"(use IOutboxStore<object> for untyped payloads). " +
"A legacy adapter LegacyOutboxStoreAdapter is available for one release. " +
"This interface will be removed in the next major version.",
error: false)]
public interface IOutboxStore
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using PatternKit.Messaging;
using PatternKit.Messaging.Transformation;

namespace WorkflowFramework.Extensions.Integration.Abstractions;

/// <summary>
/// Bridges the deprecated <see cref="IClaimCheckStore"/> (untyped, WF bespoke) to
/// <see cref="IClaimCheckStore{TPayload}">IClaimCheckStore&lt;object&gt;</see> (typed, PatternKit 0.113+).
/// </summary>
/// <remarks>
/// <b>DEPRECATED:</b> This adapter is provided for one release only. It allows consumers of the old
/// untyped <see cref="IClaimCheckStore"/> to integrate with steps that now consume
/// <c>IClaimCheckStore&lt;object&gt;</c> without requiring an immediate migration.
/// Consumers should migrate their implementations directly to <c>IClaimCheckStore&lt;object&gt;</c>
/// and remove the legacy interface and this adapter in the next major version.
/// </remarks>
[Obsolete(
"LegacyClaimCheckStoreAdapter is a one-release back-compat bridge. " +
"Implement PatternKit.Messaging.Transformation.IClaimCheckStore<object> directly " +
"and remove this adapter in the next major version.",
error: false)]
public sealed class LegacyClaimCheckStoreAdapter : IClaimCheckStore<object>
{
#pragma warning disable CS0618 // suppress inner use of obsolete IClaimCheckStore
private readonly IClaimCheckStore _legacy;

/// <summary>
/// Wraps a legacy <see cref="IClaimCheckStore"/> as a typed <see cref="IClaimCheckStore{TPayload}"/>.
/// </summary>
public LegacyClaimCheckStoreAdapter(IClaimCheckStore legacy)
{
_legacy = legacy ?? throw new ArgumentNullException(nameof(legacy));
}
#pragma warning restore CS0618

/// <inheritdoc />
public async ValueTask StoreAsync(
string claimId,
object payload,
MessageHeaders headers,
CancellationToken cancellationToken = default)
{
// Legacy contract returns the ticket from StoreAsync; we accept claimId from the caller
// and discard the store-generated ticket (WF steps now generate their own deterministic IDs).
await _legacy.StoreAsync(payload, cancellationToken).ConfigureAwait(false);
// Store under the provided claimId by doing a second put via the typed path.
// Since legacy store does not accept a caller-supplied ID, we must work around this:
// store returns its own ticket which we cannot override. This adapter therefore maintains
// an internal typed store keyed by the WF-supplied claimId that shadows the legacy store.
// Subsequent TryLoadAsync will use this shadow store.
_shadow[claimId] = new ClaimCheckStoredPayload<object>(payload, headers);
}

/// <inheritdoc />
public ValueTask<ClaimCheckStoredPayload<object>?> TryLoadAsync(
string claimId,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
_shadow.TryGetValue(claimId, out var stored);
return new ValueTask<ClaimCheckStoredPayload<object>?>(stored);
}

// Internal shadow dict keyed by WF-generated claimId (legacy store has no ID-aware API).
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, ClaimCheckStoredPayload<object>>
_shadow = new(StringComparer.Ordinal);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using PatternKit.Messaging;
using PatternKit.Messaging.Reliability;

namespace WorkflowFramework.Extensions.Integration.Abstractions;

/// <summary>
/// Bridges the deprecated <see cref="IOutboxStore"/> (untyped, WF bespoke) to
/// <see cref="IOutboxStore{TPayload}">PatternKit IOutboxStore&lt;object&gt;</see>.
/// </summary>
/// <remarks>
/// <b>DEPRECATED:</b> This adapter is provided for one release only. It allows consumers of the old
/// untyped <see cref="IOutboxStore"/> to integrate with steps that now consume
/// <c>IOutboxStore&lt;object&gt;</c> without requiring an immediate migration.
/// Consumers should migrate their implementations directly to <c>IOutboxStore&lt;object&gt;</c>
/// and remove the legacy interface and this adapter in the next major version.
/// </remarks>
[Obsolete(
"LegacyOutboxStoreAdapter is a one-release back-compat bridge. " +
"Implement PatternKit.Messaging.Reliability.IOutboxStore<object> directly " +
"and remove this adapter in the next major version.",
error: false)]
public sealed class LegacyOutboxStoreAdapter : IOutboxStore<object>
{
#pragma warning disable CS0618 // suppress inner use of obsolete IOutboxStore
private readonly IOutboxStore _legacy;

/// <summary>
/// Wraps a legacy <see cref="IOutboxStore"/> as a typed <see cref="IOutboxStore{TPayload}"/>.
/// </summary>
public LegacyOutboxStoreAdapter(IOutboxStore legacy)
{
_legacy = legacy ?? throw new ArgumentNullException(nameof(legacy));
}
#pragma warning restore CS0618

/// <inheritdoc />
public async ValueTask<OutboxMessage<object>> EnqueueAsync(
Message<object> message,
string? id = null,
DateTimeOffset? createdAt = null,
CancellationToken cancellationToken = default)
{
if (message is null)
throw new ArgumentNullException(nameof(message));

// Delegate to legacy store; it returns a string ID.
var legacyId = await _legacy.SaveAsync(message.Payload, cancellationToken).ConfigureAwait(false);
var effectiveId = string.IsNullOrWhiteSpace(id) ? legacyId : id!;
var record = new OutboxMessage<object>(effectiveId, message, createdAt ?? DateTimeOffset.UtcNow);
return record;
}

/// <inheritdoc />
public async ValueTask<IReadOnlyList<OutboxMessage<object>>> SnapshotPendingAsync(CancellationToken cancellationToken = default)
{
var legacyPending = await _legacy.GetPendingAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
return legacyPending.Select(lm =>
{
var msg = new Message<object>(lm.Payload, MessageHeaders.Empty);
return new OutboxMessage<object>(lm.Id, msg, lm.CreatedAt);
}).ToArray();
}

/// <inheritdoc />
public ValueTask MarkDispatchedAsync(string id, DateTimeOffset dispatchedAt, CancellationToken cancellationToken = default)
=> new(_legacy.MarkAsSentAsync(id, cancellationToken));

/// <inheritdoc />
public ValueTask MarkFailedAsync(string id, string? error, CancellationToken cancellationToken = default)
// Legacy interface has no MarkFailedAsync; no-op for one release.
=> default;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WorkflowFramework\WorkflowFramework.csproj" />
<PackageReference Include="PatternKit.Core" />
</ItemGroup>
</Project>
Loading
Loading