Skip to content

Commit c67063d

Browse files
committed
Feat: Extend PolicyComposition example with governance policies
1 parent ef1b875 commit c67063d

4 files changed

Lines changed: 287 additions & 0 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using ModularityKit.Mutator.Abstractions.Effects;
2+
using ModularityKit.Mutator.Abstractions.Engine;
3+
using ModularityKit.Mutator.Abstractions.Policies;
4+
using PolicyComposition.State;
5+
6+
namespace PolicyComposition.Policies.Emergency;
7+
8+
/// <summary>
9+
/// Supplies the approval-based fallback branch for the emergency gate.
10+
/// </summary>
11+
/// <remarks>
12+
/// This policy is only intended to make the emergency composition reusable when
13+
/// the explicit override is unavailable. It checks the same approval count used by
14+
/// the standard approval policy, but it maps the successful outcome to a different
15+
/// release stage so the example can show branch selection in the merged decision.
16+
/// </remarks>
17+
internal sealed class ApprovalFallbackPolicy : IMutationPolicy<ReleaseGateState>
18+
{
19+
/// <summary>
20+
/// Policy identifier used in composed diagnostics.
21+
/// </summary>
22+
public string Name => "ApprovalFallback";
23+
24+
/// <summary>
25+
/// Lower than the override branch but high enough to participate in emergency gating.
26+
/// </summary>
27+
public int Priority => 200;
28+
29+
/// <summary>
30+
/// Describes the approval-based fallback path.
31+
/// </summary>
32+
public string Description => "Fallback branch that allows the release once approvals exist.";
33+
34+
/// <summary>
35+
/// Uses the approval count to decide whether the emergency fallback can proceed.
36+
/// </summary>
37+
/// <param name="mutation">The mutation being evaluated.</param>
38+
/// <param name="state">The current release state.</param>
39+
/// <returns>
40+
/// An allowed decision that advances the release through the fallback stage
41+
/// or a blocking decision when approvals are missing.
42+
/// </returns>
43+
public PolicyDecision Evaluate(IMutation<ReleaseGateState> mutation, ReleaseGateState state)
44+
{
45+
var approvals = GetInt32(mutation.Context.Metadata, "approvals");
46+
47+
return approvals >= 2
48+
? new PolicyDecision
49+
{
50+
IsAllowed = true,
51+
PolicyName = Name,
52+
Modifications = new Dictionary<string, object>
53+
{
54+
["State"] = state with { Stage = "ApprovedViaFallback" },
55+
["SideEffect"] = SideEffect.Create("audit", "Fallback approval branch selected.")
56+
}
57+
}
58+
: PolicyDecision.Deny($"Fallback approval branch rejected the release; found {approvals} approvals.", Name);
59+
}
60+
61+
private static int GetInt32(IReadOnlyDictionary<string, object> metadata, string key)
62+
=> metadata.TryGetValue(key, out var value) && value is int number ? number : 0;
63+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using ModularityKit.Mutator.Abstractions.Effects;
2+
using ModularityKit.Mutator.Abstractions.Engine;
3+
using ModularityKit.Mutator.Abstractions.Policies;
4+
using PolicyComposition.State;
5+
6+
namespace PolicyComposition.Policies.Emergency;
7+
8+
/// <summary>
9+
/// Enables a hard emergency override when the request explicitly asks for it.
10+
/// </summary>
11+
/// <remarks>
12+
/// This policy demonstrates a decisive allow-or-deny branch inside an AnyOf
13+
/// composition. It reads the emergency flag from mutation metadata, and when the
14+
/// flag is present, it promotes the release to an emergency-approved stage and
15+
/// adds a critical side effect for audit visibility.
16+
/// </remarks>
17+
internal sealed class EmergencyOverridePolicy : IMutationPolicy<ReleaseGateState>
18+
{
19+
/// <summary>
20+
/// Policy identifier used in metadata and logs.
21+
/// </summary>
22+
public string Name => "EmergencyOverride";
23+
24+
/// <summary>
25+
/// Higher than the fallback branch, so the explicit override wins first.
26+
/// </summary>
27+
public int Priority => 400;
28+
29+
/// <summary>
30+
/// Describes the emergency override behavior.
31+
/// </summary>
32+
public string Description => "Allows an emergency release override when requested explicitly.";
33+
34+
/// <summary>
35+
/// Reads the emergency flag from metadata and either approves the override or denies it.
36+
/// </summary>
37+
/// <param name="mutation">The mutation being evaluated.</param>
38+
/// <param name="state">The current release state.</param>
39+
/// <returns>
40+
/// An allowed override decision when the emergency flag is set, otherwise a
41+
/// standard denial that allows the AnyOf composition to keep searching.
42+
/// </returns>
43+
public PolicyDecision Evaluate(IMutation<ReleaseGateState> mutation, ReleaseGateState state)
44+
{
45+
var emergency = GetBool(mutation.Context.Metadata, "emergency");
46+
47+
return emergency
48+
? new PolicyDecision
49+
{
50+
IsAllowed = true,
51+
PolicyName = Name,
52+
Modifications = new Dictionary<string, object>
53+
{
54+
["State"] = state with { Stage = "EmergencyApproved" },
55+
["SideEffect"] = SideEffect.Critical("release.override", "Emergency override applied.")
56+
},
57+
Metadata = new Dictionary<string, object>
58+
{
59+
["override"] = true
60+
}
61+
}
62+
: PolicyDecision.Deny("Emergency override not requested.", Name);
63+
}
64+
65+
private static bool GetBool(IReadOnlyDictionary<string, object> metadata, string key)
66+
=> metadata.TryGetValue(key, out var value) && value is true;
67+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using ModularityKit.Mutator.Abstractions.Policies;
2+
using PolicyComposition.Policies.Approval;
3+
using PolicyComposition.Policies.Deployment;
4+
using PolicyComposition.Policies.Emergency;
5+
using PolicyComposition.Policies.Shared;
6+
using PolicyComposition.State;
7+
8+
namespace PolicyComposition.Policies;
9+
10+
/// <summary>
11+
/// Named composed policy sets used by the release governance example.
12+
/// </summary>
13+
/// <remarks>
14+
/// This class acts as the composition root for the example. It keeps the concrete
15+
/// child policies isolated from the scenarios and exposes reusable policy sets that
16+
/// demonstrate the three composition modes:
17+
/// <list type="bullet">
18+
/// <item><description><c>AllOf</c> for mandatory approval gates.</description></item>
19+
/// <item><description><c>AnyOf</c> for emergency fallback flows.</description></item>
20+
/// <item><description><c>Priority</c> for deterministic ordered selection.</description></item>
21+
/// </list>
22+
/// </remarks>
23+
internal static class ReleaseGovernancePolicies
24+
{
25+
/// <summary>
26+
/// Builds the standard approval gate used for release promotion.
27+
/// </summary>
28+
/// <remarks>
29+
/// The gate combines:
30+
/// <list type="bullet">
31+
/// <item><description><see cref="RequireApprovalsPolicy"/> to enforce the approval threshold.</description></item>
32+
/// <item><description><see cref="AddAuditTrailPolicy"/> to add traceability once the gate succeeds.</description></item>
33+
/// </list>
34+
/// The composed policy only succeeds when both child policies can contribute
35+
/// without a conflict and the approval requirement is satisfied.
36+
/// </remarks>
37+
public static IMutationPolicy<ReleaseGateState> ApprovalGate() =>
38+
ModularityKit.Mutator.Abstractions.Policies.PolicyComposition.AllOf(
39+
"ReleaseApprovalGate",
40+
[
41+
new RequireApprovalsPolicy(),
42+
new AddAuditTrailPolicy()
43+
],
44+
priority: 500,
45+
description: "Requires two approvals and adds audit metadata.");
46+
47+
/// <summary>
48+
/// Builds the emergency gate that prefers an explicit override and falls back to approvals.
49+
/// </summary>
50+
/// <remarks>
51+
/// The gate combines:
52+
/// <list type="bullet">
53+
/// <item><description><see cref="EmergencyOverridePolicy"/> for explicit emergency approval.</description></item>
54+
/// <item><description><see cref="ApprovalFallbackPolicy"/> for the approval-based fallback path.</description></item>
55+
/// </list>
56+
/// The first allowed branch wins, which makes the result deterministic while
57+
/// still allowing a reusable emergency path to be expressed in one place.
58+
/// </remarks>
59+
public static IMutationPolicy<ReleaseGateState> EmergencyGate() =>
60+
ModularityKit.Mutator.Abstractions.Policies.PolicyComposition.AnyOf(
61+
"ReleaseEmergencyGate",
62+
[
63+
new EmergencyOverridePolicy(),
64+
new ApprovalFallbackPolicy()
65+
],
66+
priority: 400,
67+
description: "Chooses the emergency override branch when available.");
68+
69+
/// <summary>
70+
/// Builds the deployment gate that evaluates production checks before the default path.
71+
/// </summary>
72+
/// <remarks>
73+
/// The gate combines:
74+
/// <list type="bullet">
75+
/// <item><description><see cref="ProductionGuardPolicy"/> to block production releases early.</description></item>
76+
/// <item><description><see cref="DefaultDeploymentPolicy"/> as the fallback branch for non-production releases.</description></item>
77+
/// </list>
78+
/// Because this is a priority composition, the first decisive policy short-circuits
79+
/// the rest of the chain.
80+
/// </remarks>
81+
public static IMutationPolicy<ReleaseGateState> DeploymentGate() =>
82+
ModularityKit.Mutator.Abstractions.Policies.PolicyComposition.Priority(
83+
"ReleaseDeploymentGate",
84+
[
85+
new ProductionGuardPolicy(),
86+
new DefaultDeploymentPolicy()
87+
],
88+
priority: 300,
89+
description: "Uses a production guard first, then falls back to the default deployment path.");
90+
91+
/// <summary>
92+
/// Builds a composed gate that intentionally conflicts on the owner field.
93+
/// </summary>
94+
/// <remarks>
95+
/// The gate composes two <see cref="SetOwnerPolicy"/> instances that target the
96+
/// same state field with different values. The example uses this to show that
97+
/// the composition layer detects conflicting mutation results explicitly instead
98+
/// of silently picking one branch.
99+
/// </remarks>
100+
public static IMutationPolicy<ReleaseGateState> ConflictingOwnerGate() =>
101+
ModularityKit.Mutator.Abstractions.Policies.PolicyComposition.AllOf(
102+
"ReleaseOwnerConflictGate",
103+
[
104+
new SetOwnerPolicy("platform"),
105+
new SetOwnerPolicy("security")
106+
],
107+
priority: 200,
108+
description: "Demonstrates explicit conflict handling when two policies set the same field differently.");
109+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using ModularityKit.Mutator.Abstractions.Engine;
2+
using ModularityKit.Mutator.Abstractions.Policies;
3+
using PolicyComposition.State;
4+
5+
namespace PolicyComposition.Policies.Shared;
6+
7+
/// <summary>
8+
/// Forces the release owner to fixed value, so conflict handling is easy to observe.
9+
/// </summary>
10+
/// <remarks>
11+
/// The policy only touches one field. That makes it useful for the conflict
12+
/// example because two instances of the same class can be composed and produce a
13+
/// deterministic clash when they disagree on the owner.
14+
/// </remarks>
15+
internal sealed class SetOwnerPolicy(string owner) : IMutationPolicy<ReleaseGateState>
16+
{
17+
/// <summary>
18+
/// Policy identifier that includes the target owner value.
19+
/// </summary>
20+
public string Name => $"SetOwner[{owner}]";
21+
22+
/// <summary>
23+
/// Middle priority because the policy is only used as a composed leaf rule.
24+
/// </summary>
25+
public int Priority => 150;
26+
27+
/// <summary>
28+
/// Describes the owner assignment performed by the policy.
29+
/// </summary>
30+
public string Description => "Sets the release owner.";
31+
32+
/// <summary>
33+
/// Updates only the owner field and leaves the rest of the state unchanged.
34+
/// </summary>
35+
/// <param name="mutation">The mutation being evaluated.</param>
36+
/// <param name="state">The current release state.</param>
37+
/// <returns>An allowed decision that changes only the owner field.</returns>
38+
public PolicyDecision Evaluate(IMutation<ReleaseGateState> mutation, ReleaseGateState state)
39+
=> new()
40+
{
41+
IsAllowed = true,
42+
PolicyName = Name,
43+
Modifications = new Dictionary<string, object>
44+
{
45+
["State"] = state with { Owner = owner }
46+
}
47+
};
48+
}

0 commit comments

Comments
 (0)