Skip to content

Commit a238615

Browse files
committed
Feat: Add PolicyComposition example for AllOf AnyOf and priority
Introduce a new PolicyComposition example demonstrating reusable composed policies (AllOf, AnyOf, priority) and explicit conflict handling. Added example README, ReleaseGateState model, and four scenario programs (AllOf, AnyOf, Priority, Conflict). Also updated Examples/README.md to include build/run instructions for the new example.
1 parent 5e6814a commit a238615

7 files changed

Lines changed: 351 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# PolicyComposition
2+
3+
This example shows how to compose multiple governance policies into reusable sets.
4+
5+
It is the sample to read when you want to see `AllOf`, `AnyOf`, and priority-based composition
6+
without pushing the rules into one large handwritten policy class.
7+
8+
## Domain
9+
10+
The sample uses a simple release gate model:
11+
12+
- a release has a name, stage, and owner
13+
- policy metadata carries approvals, environment, and emergency flags
14+
- composed policies decide whether the release stays blocked, becomes approved, or gets an emergency path
15+
16+
## What this example demonstrates
17+
18+
- reusable composed policy sets
19+
- explicit merge rules for severity, requirements, side effects, and metadata
20+
- `AllOf`, `AnyOf`, and priority-based composition
21+
- deterministic conflict handling when two policies modify the same value
22+
23+
## Project structure
24+
25+
- [`Program.cs`](Program.cs)
26+
- [`PolicyComposition.csproj`](PolicyComposition.csproj)
27+
- [`State/ReleaseGateState.cs`](State/ReleaseGateState.cs)
28+
- [`Mutations/SubmitReleaseMutation.cs`](Mutations/SubmitReleaseMutation.cs)
29+
- [`Policies/ReleaseGovernancePolicies.cs`](Policies/ReleaseGovernancePolicies.cs)
30+
- [`Policies/Approval/RequireApprovalsPolicy.cs`](Policies/Approval/RequireApprovalsPolicy.cs)
31+
- [`Policies/Approval/AddAuditTrailPolicy.cs`](Policies/Approval/AddAuditTrailPolicy.cs)
32+
- [`Policies/Emergency/EmergencyOverridePolicy.cs`](Policies/Emergency/EmergencyOverridePolicy.cs)
33+
- [`Policies/Emergency/ApprovalFallbackPolicy.cs`](Policies/Emergency/ApprovalFallbackPolicy.cs)
34+
- [`Policies/Deployment/ProductionGuardPolicy.cs`](Policies/Deployment/ProductionGuardPolicy.cs)
35+
- [`Policies/Deployment/DefaultDeploymentPolicy.cs`](Policies/Deployment/DefaultDeploymentPolicy.cs)
36+
- [`Policies/Shared/SetOwnerPolicy.cs`](Policies/Shared/SetOwnerPolicy.cs)
37+
- [`Scenarios/AllOfScenario.cs`](Scenarios/AllOfScenario.cs)
38+
- [`Scenarios/AnyOfScenario.cs`](Scenarios/AnyOfScenario.cs)
39+
- [`Scenarios/PriorityScenario.cs`](Scenarios/PriorityScenario.cs)
40+
- [`Scenarios/ConflictScenario.cs`](Scenarios/ConflictScenario.cs)
41+
42+
## Run
43+
44+
```bash
45+
dotnet run --project Examples/Core/PolicyComposition/PolicyComposition.csproj
46+
```
47+
48+
## Expected output
49+
50+
You should see:
51+
52+
- an `AllOf` release gate that merges state updates, metadata, and audit side effects
53+
- an `AnyOf` gate that selects the first allowed branch
54+
- a priority-based gate that prefers the first decisive policy
55+
- a conflict example that raises `PolicyCompositionConflictException`
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using ModularityKit.Mutator.Abstractions.Policies;
2+
using PolicyComposition.Mutations;
3+
using PolicyComposition.Policies;
4+
using PolicyComposition.State;
5+
6+
namespace PolicyComposition.Scenarios;
7+
8+
/// <summary>
9+
/// Demonstrates the <c>AllOf</c> composition mode for reusable governance gates.
10+
/// </summary>
11+
/// <remarks>
12+
/// This scenario is intentionally small: it creates a release state, builds a
13+
/// release submission mutation, evaluates the approval gate, and prints the
14+
/// composed result. The goal is to show how the merged decision contains the
15+
/// final state, audit side effects, and composition metadata.
16+
/// </remarks>
17+
internal static class AllOfScenario
18+
{
19+
/// <summary>
20+
/// Evaluates the approval gate against a sample release submission.
21+
/// </summary>
22+
/// <remarks>
23+
/// The mutation carries approval count, emergency flag, and environment data
24+
/// through its execution context so the composed policies can make their own
25+
/// decisions without needing extra helper state.
26+
/// </remarks>
27+
public static async Task Run()
28+
{
29+
var state = new ReleaseGateState("release-42", "Draft", "platform");
30+
var mutation = new SubmitReleaseMutation(
31+
releaseName: state.ReleaseName,
32+
approvals: 2,
33+
emergency: false,
34+
environment: "staging");
35+
36+
var decision = await ReleaseGovernancePolicies.ApprovalGate().EvaluateAsync(mutation, state);
37+
38+
WriteDecision(decision);
39+
}
40+
41+
/// <summary>
42+
/// Writes the important parts of the composed decision to the console.
43+
/// </summary>
44+
/// <param name="decision">The composed policy decision returned by the gate.</param>
45+
private static void WriteDecision(PolicyDecision decision)
46+
{
47+
Console.WriteLine("[AnyOf] Reusable override gate");
48+
49+
Console.WriteLine($" allowed: {decision.IsAllowed}");
50+
Console.WriteLine($" reason: {decision.Reason}");
51+
Console.WriteLine($" mode: {decision.Metadata!["PolicyComposition.Mode"]}");
52+
53+
if (decision.Modifications is not null &&
54+
decision.Modifications.TryGetValue("State", out var value) &&
55+
value is ReleaseGateState state)
56+
{
57+
Console.WriteLine($" stage: {state.Stage}");
58+
Console.WriteLine($" owner: {state.Owner}");
59+
}
60+
61+
if (decision.Modifications is not null &&
62+
decision.Modifications.TryGetValue("SideEffects", out var sideEffects) &&
63+
sideEffects is IEnumerable<object> effects)
64+
{
65+
Console.WriteLine($" sideEffects: {effects.Count()}");
66+
}
67+
68+
Console.WriteLine();
69+
}
70+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using ModularityKit.Mutator.Abstractions.Policies;
2+
using PolicyComposition.Mutations;
3+
using PolicyComposition.Policies;
4+
using PolicyComposition.State;
5+
6+
namespace PolicyComposition.Scenarios;
7+
8+
/// <summary>
9+
/// Demonstrates the <c>AnyOf</c> composition mode for alternate governance branches.
10+
/// </summary>
11+
/// <remarks>
12+
/// This scenario evaluates the emergency gate against a sample release submission.
13+
/// The setup is deliberately asymmetric: the emergency flag is enabled, so the
14+
/// composed gate can show how one allowed branch wins while the blocked branch is
15+
/// still visible in the output metadata.
16+
/// </remarks>
17+
internal static class AnyOfScenario
18+
{
19+
/// <summary>
20+
/// Evaluates the emergency gate and prints the winning and blocking branches.
21+
/// </summary>
22+
/// <remarks>
23+
/// The sample mutation carries governance metadata through the execution
24+
/// context. That allows the emergency override policy to detect the explicit
25+
/// override flag while the fallback branch remains available as a reusable
26+
/// alternative path.
27+
/// </remarks>
28+
public static async Task Run()
29+
{
30+
var state = new ReleaseGateState("release-42", "Draft", "platform");
31+
var mutation = new SubmitReleaseMutation(
32+
releaseName: state.ReleaseName,
33+
approvals: 0,
34+
emergency: true,
35+
environment: "staging");
36+
37+
var decision = await ReleaseGovernancePolicies.EmergencyGate().EvaluateAsync(mutation, state);
38+
39+
WriteDecision(decision);
40+
}
41+
42+
/// <summary>
43+
/// Writes the merged result of the emergency gate to the console.
44+
/// </summary>
45+
/// <param name="decision">The composed policy decision returned by the gate.</param>
46+
private static void WriteDecision(PolicyDecision decision)
47+
{
48+
Console.WriteLine("[AnyOf] Emergency override gate");
49+
Console.WriteLine($" allowed: {decision.IsAllowed}");
50+
Console.WriteLine($" reason: {decision.Reason}");
51+
Console.WriteLine($" winning: {string.Join(", ", (string[])decision.Metadata!["PolicyComposition.WinningPolicies"])}");
52+
Console.WriteLine($" blocking: {string.Join(", ", (string[])decision.Metadata!["PolicyComposition.BlockingPolicies"])}");
53+
54+
if (decision.Modifications is not null && decision.Modifications.TryGetValue("State", out var value) && value is ReleaseGateState state)
55+
{
56+
Console.WriteLine($" stage: {state.Stage}");
57+
}
58+
59+
Console.WriteLine();
60+
}
61+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using ModularityKit.Mutator.Abstractions.Exceptions;
2+
using PolicyComposition.Mutations;
3+
using PolicyComposition.Policies;
4+
using PolicyComposition.State;
5+
6+
namespace PolicyComposition.Scenarios;
7+
8+
/// <summary>
9+
/// Demonstrates explicit conflict detection during composition.
10+
/// </summary>
11+
/// <remarks>
12+
/// This scenario intentionally composes two policies that both write the owner
13+
/// field with different values. The example is useful because it shows that the
14+
/// composition layer does not silently pick one result. Instead, it fails fast
15+
/// with a conflict exception that names the field and the policies involved.
16+
/// </remarks>
17+
internal static class ConflictScenario
18+
{
19+
/// <summary>
20+
/// Evaluates a conflicting policy set and prints the exception details.
21+
/// </summary>
22+
/// <remarks>
23+
/// The exception is caught locally so the example can show the conflict
24+
/// diagnostics without stopping the rest of the console run.
25+
/// </remarks>
26+
public static async Task Run()
27+
{
28+
var state = new ReleaseGateState("release-42", "Draft", "platform");
29+
var mutation = new SubmitReleaseMutation(
30+
releaseName: state.ReleaseName,
31+
approvals: 2,
32+
emergency: false,
33+
environment: "staging");
34+
35+
try
36+
{
37+
await ReleaseGovernancePolicies.ConflictingOwnerGate().EvaluateAsync(mutation, state);
38+
}
39+
catch (PolicyCompositionConflictException exception)
40+
{
41+
Console.WriteLine($" conflict key: {exception.ConflictKey}");
42+
Console.WriteLine($" policies: {string.Join(", ", exception.PolicyNames)}");
43+
Console.WriteLine($" message: {exception.Message}");
44+
Console.WriteLine();
45+
}
46+
}
47+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using ModularityKit.Mutator.Abstractions.Policies;
2+
using PolicyComposition.Mutations;
3+
using PolicyComposition.Policies;
4+
using PolicyComposition.State;
5+
6+
namespace PolicyComposition.Scenarios;
7+
8+
/// <summary>
9+
/// Demonstrates the priority-based composition mode for deterministic policy selection.
10+
/// </summary>
11+
/// <remarks>
12+
/// This scenario evaluates the deployment gate twice: once for a staging release
13+
/// and once for a production release. The first path shows the fallback branch
14+
/// being selected, while the second path shows the guard policy short-circuiting
15+
/// the composition with a decisive denial.
16+
/// </remarks>
17+
internal static class PriorityScenario
18+
{
19+
/// <summary>
20+
/// Evaluates the deployment gate for staging and production inputs.
21+
/// </summary>
22+
/// <remarks>
23+
/// The two inputs only differ by environment, which keeps the example focused
24+
/// on priority ordering rather than on unrelated mutation state.
25+
/// </remarks>
26+
public static async Task Run()
27+
{
28+
var state = new ReleaseGateState("release-42", "Draft", "platform");
29+
30+
var stagingMutation = new SubmitReleaseMutation(
31+
releaseName: state.ReleaseName,
32+
approvals: 1,
33+
emergency: false,
34+
environment: "staging");
35+
36+
var productionMutation = new SubmitReleaseMutation(
37+
releaseName: state.ReleaseName,
38+
approvals: 1,
39+
emergency: false,
40+
environment: "production");
41+
42+
Console.WriteLine(" staging path:");
43+
WriteDecision(await ReleaseGovernancePolicies.DeploymentGate().EvaluateAsync(stagingMutation, state));
44+
45+
Console.WriteLine(" production path:");
46+
WriteDecision(await ReleaseGovernancePolicies.DeploymentGate().EvaluateAsync(productionMutation, state));
47+
}
48+
49+
/// <summary>
50+
/// Writes the outcome of the priority-based composition.
51+
/// </summary>
52+
/// <param name="decision">The composed policy decision returned by the gate.</param>
53+
private static void WriteDecision(PolicyDecision decision)
54+
{
55+
Console.WriteLine($" allowed: {decision.IsAllowed}");
56+
Console.WriteLine($" reason: {decision.Reason}");
57+
Console.WriteLine($" selected: {string.Join(", ", (string[])decision.Metadata!["PolicyComposition.WinningPolicies"])}");
58+
59+
if (decision.Modifications is not null && decision.Modifications.TryGetValue("State", out var value) && value is ReleaseGateState state)
60+
{
61+
Console.WriteLine($" stage: {state.Stage}");
62+
}
63+
}
64+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace PolicyComposition.State;
2+
3+
/// <summary>
4+
/// Minimal release state used to demonstrate governance policy composition.
5+
/// </summary>
6+
/// <remarks>
7+
/// The example keeps the state intentionally small so the composition behavior
8+
/// is easy to follow. Each field maps directly to a visible part of the policy
9+
/// output:
10+
/// <list type="bullet">
11+
/// <item><description><see cref="ReleaseName"/> identifies the release flow.</description></item>
12+
/// <item><description><see cref="Stage"/> shows which policy branch updated the release.</description></item>
13+
/// <item><description><see cref="Owner"/> is used by the conflict example.</description></item>
14+
/// </list>
15+
/// </remarks>
16+
public sealed record ReleaseGateState
17+
{
18+
/// <summary>
19+
/// Creates a new release state.
20+
/// </summary>
21+
/// <param name="releaseName">The release identifier.</param>
22+
/// <param name="stage">The initial release stage.</param>
23+
/// <param name="owner">The initial release owner.</param>
24+
public ReleaseGateState(string releaseName, string stage, string owner)
25+
{
26+
ReleaseName = releaseName;
27+
Stage = stage;
28+
Owner = owner;
29+
}
30+
31+
/// <summary>
32+
/// The release identifier used to correlate the example flow.
33+
/// </summary>
34+
public string ReleaseName { get; init; }
35+
36+
/// <summary>
37+
/// The current release stage.
38+
/// </summary>
39+
public string Stage { get; init; }
40+
41+
/// <summary>
42+
/// The current release owner.
43+
/// </summary>
44+
public string Owner { get; init; }
45+
}

Examples/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The projects are intentionally small and focused. Each one demonstrates a differ
1616
| `BillingQuotas` | quota changes, validation, and policy limits | [`Examples/Core/BillingQuotas/README.md`](Core/BillingQuotas/README.md) |
1717
| `FeatureFlags` | feature toggles, audit/history, and batch execution | [`Examples/Core/FeatureFlags/README.md`](Core/FeatureFlags/README.md) |
1818
| `IamRoles` | role changes, approval rules, and batch migration | [`Examples/Core/IamRoles/README.md`](Core/IamRoles/README.md) |
19+
| `PolicyComposition` | reusable policy sets, merge rules, and conflict handling | [`Examples/Core/PolicyComposition/README.md`](Core/PolicyComposition/README.md) |
1920
| `WorkflowApprovals` | ordered workflow state transitions and approvals | [`Examples/Core/WorkflowApprovals/README.md`](Core/WorkflowApprovals/README.md) |
2021

2122
## Governance examples
@@ -55,6 +56,7 @@ You can also build just one example:
5556
dotnet build Examples/Core/BillingQuotas/BillingQuotas.csproj -c Release
5657
dotnet build Examples/Core/FeatureFlags/FeatureFlags.csproj -c Release
5758
dotnet build Examples/Core/IamRoles/IamRoles.csproj -c Release
59+
dotnet build Examples/Core/PolicyComposition/PolicyComposition.csproj -c Release
5860
dotnet build Examples/Core/WorkflowApprovals/WorkflowApprovals.csproj -c Release
5961
dotnet build Examples/Governance/RequestLifecycle/RequestLifecycle.csproj -c Release
6062
dotnet build Examples/Governance/GovernedExecution/GovernedExecution.csproj -c Release
@@ -75,6 +77,7 @@ From the repository root:
7577
dotnet run --project Examples/Core/BillingQuotas/BillingQuotas.csproj
7678
dotnet run --project Examples/Core/FeatureFlags/FeatureFlags.csproj
7779
dotnet run --project Examples/Core/IamRoles/IamRoles.csproj
80+
dotnet run --project Examples/Core/PolicyComposition/PolicyComposition.csproj
7881
dotnet run --project Examples/Core/WorkflowApprovals/WorkflowApprovals.csproj
7982
dotnet run --project Examples/Governance/RequestLifecycle/RequestLifecycle.csproj
8083
dotnet run --project Examples/Governance/GovernedExecution/GovernedExecution.csproj
@@ -134,6 +137,12 @@ Shows role grant and revoke workflows with approval-style rules. This is the exa
134137

135138
See [`Core/IamRoles/README.md`](Core/IamRoles/README.md).
136139

140+
### PolicyComposition
141+
142+
Shows how to reuse governance policy sets with deterministic merge rules and explicit conflict handling. This is the example to read if you want `AllOf`, `AnyOf`, and priority-based composition without writing one large bespoke policy class.
143+
144+
See [`Core/PolicyComposition/README.md`](Core/PolicyComposition/README.md).
145+
137146
### WorkflowApprovals
138147

139148
Shows a multi-step approval process with ordered execution and rejection handling. This example is the best fit if you want to study state transitions that must happen in a strict sequence.

0 commit comments

Comments
 (0)