Skip to content

Commit 0f14e8d

Browse files
committed
feat(examples/polygon): add workflow approvals showcase
- Added WorkflowApprovals example to Polygon solution - Demonstrates approval workflow modeling using Mutators: - Immutable workflow state with steps and initiator - High and critical risk mutations (start, approve, reject) - Showcases policy-driven governance: - Sequential step enforcement - Manager-only approval constraints - Includes end-to-end execution scenarios: - Happy path with ordered approvals - Rejection scenarios with policy diagnostics - Highlights core Mutators capabilities: - System vs user mutation contexts - Policy evaluation and decision reporting - Deterministic state evolution - Metrics and execution statistics output - Positions Polygon as a real-world governance showcase: - Workflow approvals - Quotas - IAM-style decision flows
1 parent b80e1fc commit 0f14e8d

14 files changed

Lines changed: 524 additions & 0 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rejector/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

ModularityKit.Mutator.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<Project Path="Polygon/BillingQuotas/BillingQuotas.csproj" />
44
<Project Path="Polygon/FeatureFlags/FeatureFlags.csproj" />
55
<Project Path="Polygon/IamRoles/IamRoles.csproj" />
6+
<Project Path="Polygon/WorkflowApprovals/WorkflowApprovals.csproj" />
67
</Folder>
78
<Project Path="ModularityKit.Mutator/ModularityKit.Mutator.csproj" />
89
</Solution>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using ModularityKit.Mutator.Abstractions.Changes;
2+
using ModularityKit.Mutator.Abstractions.Context;
3+
using ModularityKit.Mutator.Abstractions.Engine;
4+
using ModularityKit.Mutator.Abstractions.Intent;
5+
using ModularityKit.Mutator.Abstractions.Results;
6+
using WorkflowApprovals.State;
7+
8+
namespace WorkflowApprovals.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that approves specific step in an <see cref="ApprovalWorkflowState"/>.
12+
/// </summary>
13+
internal sealed record ApproveStepMutation(
14+
int StepIndex,
15+
string Approver,
16+
MutationContext Context
17+
) : IMutation<ApprovalWorkflowState>
18+
{
19+
public MutationIntent Intent { get; } = new()
20+
{
21+
OperationName = "ApproveStep",
22+
Category = "Workflow",
23+
RiskLevel = MutationRiskLevel.High,
24+
Description = "Approve a workflow step"
25+
};
26+
27+
public ValidationResult Validate(ApprovalWorkflowState state)
28+
{
29+
var result = new ValidationResult();
30+
if (StepIndex < 0 || StepIndex >= state.Steps.Count)
31+
result.AddError("StepIndex", "Invalid step index");
32+
if (string.IsNullOrEmpty(Approver))
33+
result.AddError("Approver", "Approver cannot be empty");
34+
return result;
35+
}
36+
37+
public MutationResult<ApprovalWorkflowState> Apply(ApprovalWorkflowState state)
38+
{
39+
var steps = state.Steps.ToList();
40+
var oldStep = steps[StepIndex];
41+
var newStep = oldStep with
42+
{
43+
Status = StepStatus.Approved,
44+
ApprovedBy = Approver
45+
};
46+
steps[StepIndex] = newStep;
47+
48+
var newState = state with { Steps = steps };
49+
50+
var changes = ChangeSet.Single(
51+
StateChange.Modified($"Steps[{StepIndex}]", oldStep.Status, newStep.Status)
52+
);
53+
54+
return MutationResult<ApprovalWorkflowState>.Success(newState, changes);
55+
}
56+
57+
public MutationResult<ApprovalWorkflowState> Simulate(ApprovalWorkflowState state) => Apply(state);
58+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using ModularityKit.Mutator.Abstractions.Changes;
2+
using ModularityKit.Mutator.Abstractions.Context;
3+
using ModularityKit.Mutator.Abstractions.Engine;
4+
using ModularityKit.Mutator.Abstractions.Intent;
5+
using ModularityKit.Mutator.Abstractions.Results;
6+
using WorkflowApprovals.State;
7+
8+
namespace WorkflowApprovals.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that rejects the entire workflow in an <see cref="ApprovalWorkflowState"/>.
12+
/// </summary>
13+
internal sealed record RejectWorkflowMutation(
14+
string Rejector,
15+
MutationContext Context
16+
) : IMutation<ApprovalWorkflowState>
17+
{
18+
public MutationIntent Intent { get; } = new()
19+
{
20+
OperationName = "RejectWorkflow",
21+
Category = "Workflow",
22+
RiskLevel = MutationRiskLevel.Critical,
23+
Description = "Rejects the entire workflow"
24+
};
25+
26+
public ValidationResult Validate(ApprovalWorkflowState state)
27+
{
28+
var result = new ValidationResult();
29+
if (string.IsNullOrEmpty(Rejector))
30+
result.AddError("Reject", "Reject cannot be empty");
31+
return result;
32+
}
33+
34+
public MutationResult<ApprovalWorkflowState> Apply(ApprovalWorkflowState state)
35+
{
36+
var steps = state.Steps.Select(s => s with
37+
{
38+
Status = StepStatus.Rejected,
39+
RejectedBy = Rejector
40+
}).ToList();
41+
42+
var newState = state with { Steps = steps };
43+
var changes = ChangeSet.Single(StateChange.Modified("Workflow", null, "Rejected"));
44+
return MutationResult<ApprovalWorkflowState>.Success(newState, changes);
45+
}
46+
47+
public MutationResult<ApprovalWorkflowState> Simulate(ApprovalWorkflowState state) => Apply(state);
48+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using ModularityKit.Mutator.Abstractions.Changes;
2+
using ModularityKit.Mutator.Abstractions.Context;
3+
using ModularityKit.Mutator.Abstractions.Engine;
4+
using ModularityKit.Mutator.Abstractions.Intent;
5+
using ModularityKit.Mutator.Abstractions.Results;
6+
using WorkflowApprovals.State;
7+
8+
namespace WorkflowApprovals.Mutations;
9+
10+
/// <summary>
11+
/// Mutation that starts new approval workflow in an <see cref="ApprovalWorkflowState"/>.
12+
/// </summary>
13+
internal sealed record StartApprovalMutation(
14+
string Initiator,
15+
string[] StepNames,
16+
MutationContext Context
17+
) : IMutation<ApprovalWorkflowState>
18+
{
19+
public MutationIntent Intent { get; } = new()
20+
{
21+
OperationName = "StartWorkflow",
22+
Category = "Workflow",
23+
RiskLevel = MutationRiskLevel.Medium,
24+
Description = "Starts a new approval workflow"
25+
};
26+
27+
public ValidationResult Validate(ApprovalWorkflowState state)
28+
{
29+
var result = new ValidationResult();
30+
if (string.IsNullOrEmpty(Initiator))
31+
result.AddError("Initiator", "Initiator cannot be empty");
32+
if (StepNames.Length == 0)
33+
result.AddError("Steps", "Workflow must have at least one step");
34+
return result;
35+
}
36+
37+
public MutationResult<ApprovalWorkflowState> Apply(ApprovalWorkflowState state)
38+
{
39+
var steps = StepNames.Select(name => new WorkflowStep(name)).ToList();
40+
var newState = state with
41+
{
42+
WorkflowId = Guid.NewGuid().ToString(),
43+
Steps = steps,
44+
Initiator = Initiator
45+
};
46+
47+
var changes = ChangeSet.Single(StateChange.Added("Steps", steps));
48+
return MutationResult<ApprovalWorkflowState>.Success(newState, changes);
49+
}
50+
51+
public MutationResult<ApprovalWorkflowState> Simulate(ApprovalWorkflowState state) => Apply(state);
52+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using ModularityKit.Mutator.Abstractions.Engine;
2+
using ModularityKit.Mutator.Abstractions.Policies;
3+
using WorkflowApprovals.Mutations;
4+
using WorkflowApprovals.State;
5+
6+
namespace WorkflowApprovals.Policies;
7+
8+
/// <summary>
9+
/// Policy that enforces sequential approval of workflow steps in an <see cref="ApprovalWorkflowState"/>.
10+
/// </summary>
11+
internal sealed class EnforceOrderPolicy : IMutationPolicy<ApprovalWorkflowState>
12+
{
13+
public string Name => "EnforceOrderPolicy";
14+
public int Priority => 100;
15+
public string Description => "Ensures steps are approved in order.";
16+
17+
/// <summary>
18+
/// Evaluates mutation against the policy.
19+
/// Step can only be approved if the previous step is already approved.
20+
/// </summary>
21+
/// <param name="mutation">The mutation being evaluated.</param>
22+
/// <param name="state">Current workflow state.</param>
23+
/// <returns>A <see cref="PolicyDecision"/> indicating whether the mutation is allowed or denied.</returns>
24+
public PolicyDecision Evaluate(IMutation<ApprovalWorkflowState> mutation, ApprovalWorkflowState state)
25+
{
26+
if (mutation is not ApproveStepMutation { StepIndex: > 0 } stepMut)
27+
return PolicyDecision.Allow();
28+
29+
if (stepMut.StepIndex - 1 >= state.Steps.Count)
30+
{
31+
return PolicyDecision.Deny(
32+
$"Step index {stepMut.StepIndex} is out of range for the current workflow.");
33+
}
34+
35+
var previous = state.Steps[stepMut.StepIndex - 1];
36+
if (previous.Status != StepStatus.Approved)
37+
{
38+
return PolicyDecision.Deny(
39+
$"Cannot approve step {stepMut.StepIndex} before previous step is approved");
40+
}
41+
return PolicyDecision.Allow();
42+
}
43+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using ModularityKit.Mutator.Abstractions.Engine;
2+
using ModularityKit.Mutator.Abstractions.Policies;
3+
using WorkflowApprovals.Mutations;
4+
using WorkflowApprovals.State;
5+
6+
namespace WorkflowApprovals.Policies;
7+
8+
/// <summary>
9+
/// Policy that requires workflow steps to be approved by designated manager in <see cref="ApprovalWorkflowState"/>.
10+
/// </summary>
11+
internal sealed class RequireManagerApprovalPolicy : IMutationPolicy<ApprovalWorkflowState>
12+
{
13+
private readonly HashSet<string> _managers = ["alice", "bob"];
14+
15+
public string Name => "RequireManagerApproval";
16+
public int Priority => 100;
17+
public string Description => "Ensures only managers can approve workflow steps.";
18+
19+
/// <summary>
20+
/// Evaluates a mutation against the policy.
21+
/// Only designated managers in <see cref="_managers"/> can approve steps.
22+
/// </summary>
23+
/// <param name="mutation">The mutation being evaluated.</param>
24+
/// <param name="state">Current workflow state.</param>
25+
/// <returns>A <see cref="PolicyDecision"/> indicating whether the mutation is allowed or denied.</returns>
26+
public PolicyDecision Evaluate(IMutation<ApprovalWorkflowState> mutation, ApprovalWorkflowState state)
27+
{
28+
if (mutation is not ApproveStepMutation stepMut) return PolicyDecision.Allow();
29+
if (!_managers.Contains(stepMut.Approver))
30+
{
31+
return PolicyDecision.Deny($"Approver '{stepMut.Approver}' is not manager");
32+
}
33+
return PolicyDecision.Allow();
34+
}
35+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModularityKit.Mutator.Abstractions;
3+
using ModularityKit.Mutator.Abstractions.Engine;
4+
using ModularityKit.Mutator.Runtime;
5+
using WorkflowApprovals.Policies;
6+
7+
namespace WorkflowApprovals;
8+
9+
internal static class Program
10+
{
11+
private static async Task Main()
12+
{
13+
var services = new ServiceCollection();
14+
services.AddMutators(MutationEngineOptions.Strict, addDefaultLoggingInterceptor: true);
15+
16+
var provider = services.BuildServiceProvider();
17+
var engine = provider.GetRequiredService<IMutationEngine>();
18+
19+
engine.RegisterPolicy(new EnforceOrderPolicy());
20+
engine.RegisterPolicy(new RequireManagerApprovalPolicy());
21+
22+
Console.WriteLine("=== ModularityKit.Mutators - Complete Example ===\n");
23+
24+
await Scenarios.HappyPathScenario.Run(engine);
25+
await Scenarios.RejectedScenario.Run(engine);
26+
27+
28+
Console.WriteLine("\n METRICS & STATISTICS");
29+
30+
var stats = await engine.GetStatisticsAsync();
31+
32+
Console.WriteLine($"\n Mutation Statistics:");
33+
Console.WriteLine($" Total executed: {stats.TotalExecuted}");
34+
35+
Console.WriteLine($"\n Performance Metrics:");
36+
Console.WriteLine($" Average execution time: {stats.AverageExecutionTime.TotalMilliseconds:F2} ms");
37+
Console.WriteLine($" Median execution time: {stats.MedianExecutionTime.TotalMilliseconds:F2} ms");
38+
Console.WriteLine($" P95 execution time: {stats.P95ExecutionTime.TotalMilliseconds:F2} ms");
39+
}
40+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Abstractions.Engine;
3+
using WorkflowApprovals.Mutations;
4+
using WorkflowApprovals.State;
5+
6+
namespace WorkflowApprovals.Scenarios;
7+
8+
/// <summary>
9+
/// HappyPathScenario
10+
///
11+
/// Demonstrates a "happy path" execution of an approval workflow using <see cref="IMutation{TState}"/>
12+
/// and <see cref="StartApprovalMutation"/> where all steps are approved successfully.
13+
///
14+
/// Scenario Details:
15+
/// - Starts a new approval workflow using <see cref="ApproveStepMutation"/>.
16+
/// - Sequentially approves each step in the workflow using <see cref="MutationContext.System"/>.
17+
/// - Uses <see cref="MutationContext"/> to provide system metadata and correlation ID for audit purposes.
18+
/// - Logs success and failure for each mutation.
19+
/// - Works with <see cref="ApprovalWorkflowState"/> which tracks workflow steps and their approval status.
20+
///
21+
/// Key Steps:
22+
/// 1. Initialize <see cref="StartApprovalMutation"/> as empty workflow state.
23+
/// 2. Start workflow with multiple steps via <see cref="IMutationEngine"/>.
24+
/// 3. Sequentially approve each step using predefined approvers.
25+
/// 4. Update workflow state after each mutation.
26+
/// 5. Print final state of all steps, including approved or rejected information.
27+
///
28+
/// Example Use Case:
29+
/// - Simulating normal workflow progression for testing or documentation purposes.
30+
/// - Verifying that policies allow proper step approvals in sequence.
31+
/// - Demonstrating auditability and traceability with correlation IDs and system metadata.
32+
///
33+
/// Notes:
34+
/// - If a mutation is blocked due to a policy decision, the scenario logs the blocking reason.
35+
/// - This scenario assumes all mutations succeed under normal conditions.
36+
/// - Can be extended with rejection scenarios or conditional approvals for more complex workflows.
37+
/// </summary>
38+
internal static class HappyPathScenario
39+
{
40+
internal static async Task Run(IMutationEngine engine)
41+
{
42+
Console.WriteLine("\n=== Happy Path Scenario ===");
43+
44+
var state = new ApprovalWorkflowState();
45+
46+
var ctx = MutationContext.System("Start workflow", correlationId: "workflow-123");
47+
48+
var start = new StartApprovalMutation("initiator", ["Step1", "Step2", "Step3"], ctx);
49+
var result = await engine.ExecuteAsync(start, state);
50+
state = result.NewState!;
51+
52+
var approvers = new[] { "alice", "bob", "alice" };
53+
54+
for (var i = 0; i < state.Steps.Count; i++)
55+
{
56+
var approve = new ApproveStepMutation(i, approvers[i], ctx);
57+
var res = await engine.ExecuteAsync(approve, state);
58+
if (res.IsSuccess)
59+
state = res.NewState!;
60+
else
61+
{
62+
Console.WriteLine($"✗ Step {i} blocked:");
63+
foreach (var dec in res.PolicyDecisions)
64+
Console.WriteLine($" {dec.PolicyName}{dec.Reason}");
65+
}
66+
}
67+
68+
Console.WriteLine("Workflow final state:");
69+
for (var i = 0; i < state.Steps.Count; i++)
70+
{
71+
var s = state.Steps[i];
72+
Console.WriteLine($" Step{i}: {s.Status} by {(s.ApprovedBy ?? s.RejectedBy ?? "-")}");
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)