Skip to content

Commit 826cca2

Browse files
authored
Feat: Clarify governance revalidation semantics and enum docs (#29)
2 parents db9a817 + bf0b2d2 commit 826cca2

13 files changed

Lines changed: 225 additions & 3 deletions

File tree

Examples/Governance/VersionedResolution/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ It is the direct runnable example for the semantics introduced around `ExpectedS
1212
- resolving stale requests with `RevalidateOnLatestState`
1313
- persisting a resolved outcome through `MutationRequestVersionResolutionManager`
1414
- inspecting the resulting lifecycle state and appended decision history
15+
- observing that revalidation is represented as `Pending` with `PendingMutationReason.Revalidation`
1516

1617
## Key files
1718

@@ -67,6 +68,7 @@ The sample prints one block per resolution strategy and one persisted-resolution
6768
- selected outcome
6869
- whether the request was stale
6970
- resulting request status
71+
- the revalidation pending reason when the latest-state branch is selected
7072
- updated expected version
7173
- last decision recorded during resolution
7274
- persisted request revision for the runtime path

Tests/ModularityKit.Mutator.Governance.Tests/Execution/GovernanceExecutionManagerTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,48 @@ public async Task ExecuteApproved_requires_renewed_approval_before_execution_whe
143143
Assert.Equal(MutationRequestVersionResolutionOutcome.RequiresRenewedApproval, result.Resolution.Outcome);
144144
}
145145

146+
[Fact]
147+
public async Task ExecuteApproved_revalidates_and_executes_against_latest_state_when_strategy_demands_it()
148+
{
149+
var services = new ServiceCollection();
150+
services.AddMutators(MutationEngineOptions.Strict);
151+
await using var provider = services.BuildServiceProvider();
152+
153+
var engine = provider.GetRequiredService<IMutationEngine>();
154+
var requestStore = new InMemoryMutationRequestStore();
155+
var resolutionManager = new MutationRequestVersionResolutionManager(requestStore, new MutationRequestVersionResolver());
156+
var executionManager = new GovernanceExecutionManager(requestStore, resolutionManager, engine);
157+
158+
var request = await requestStore.Create(MutationRequestTestFactory.CreateApprovedSecurityRequest("v10"));
159+
var mutation = new PromoteRoleMutation(
160+
MutationContext.User("operator-1", "Operator One", "Execute approved role promotion"),
161+
nextVersion: "v16");
162+
var state = RoleState.Create("tenant-42:roles", role: "Reader", version: "v15");
163+
164+
var result = await executionManager.ExecuteApproved(
165+
request.RequestId,
166+
mutation,
167+
state,
168+
currentStateVersion: state.Version,
169+
resultingStateVersionProvider: updated => updated.Version,
170+
governanceContext: MutationContext.Service("governance-runtime", "Revalidate and execute"),
171+
strategy: VersionedRequestResolutionStrategy.RevalidateOnLatestState);
172+
173+
Assert.True(result.WasExecuted);
174+
Assert.NotNull(result.MutationResult);
175+
Assert.Equal(MutationRequestVersionResolutionOutcome.RevalidateOnLatestState, result.Resolution.Outcome);
176+
Assert.Equal(MutationRequestStatus.Executed, result.Request.Status);
177+
Assert.Equal("v16", result.ResultingStateVersion);
178+
Assert.Equal("v16", result.Request.ResultingStateVersion);
179+
Assert.Equal("v16", result.Request.ExpectedStateVersion);
180+
Assert.Equal(
181+
MutationRequestDecisionType.Lifecycle(MutationRequestLifecycleDecisionType.Executed),
182+
result.Request.Decisions[^1].Type);
183+
Assert.Contains(
184+
result.Request.Decisions,
185+
decision => decision.Type == MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.RevalidationRequired));
186+
}
187+
146188
private sealed record RoleState(string StateId, string Role, string Version)
147189
{
148190
public static RoleState Create(string stateId, string role, string version) => new(stateId, role, version);

Tests/ModularityKit.Mutator.Governance.Tests/Resolution/MutationRequestVersionResolutionPersistenceTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
44
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
55
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
6+
using ModularityKit.Mutator.Governance.Abstractions.Resolution.Model;
67
using ModularityKit.Mutator.Governance.Abstractions.Resolution.Strategies;
78
using ModularityKit.Mutator.Governance.Runtime.Resolution.Execution;
89
using ModularityKit.Mutator.Governance.Runtime.Storage;
@@ -63,6 +64,35 @@ public async Task ResolveAndStore_persists_decision_history_and_state()
6364
Assert.Equal(loaded, resolution.Request);
6465
}
6566

67+
[Fact]
68+
public async Task ResolveAndStore_revalidation_marks_request_pending_for_revalidation()
69+
{
70+
var store = new InMemoryMutationRequestStore();
71+
var resolver = new MutationRequestVersionResolver();
72+
var manager = new MutationRequestVersionResolutionManager(store, resolver);
73+
var request = await store.Create(MutationRequestTestFactory.CreateApprovedSecurityRequest("v10"));
74+
75+
var resolution = await manager.ResolveAndStore(
76+
request.RequestId,
77+
currentStateVersion: "v15",
78+
resolutionContext: MutationContext.User("approver", "Approver", "Revalidate request"),
79+
strategy: VersionedRequestResolutionStrategy.RevalidateOnLatestState);
80+
81+
var loaded = await store.Get(request.RequestId);
82+
83+
Assert.NotNull(loaded);
84+
Assert.Equal(3, loaded.Decisions.Count);
85+
Assert.Equal(
86+
MutationRequestDecisionType.VersionResolution(MutationRequestVersionResolutionDecisionType.RevalidationRequired),
87+
loaded.Decisions[^1].Type);
88+
Assert.Equal(MutationRequestStatus.Pending, loaded.Status);
89+
Assert.Equal(PendingMutationReason.Revalidation, loaded.PendingReason);
90+
Assert.Equal("v15", loaded.ExpectedStateVersion);
91+
Assert.Equal(1, loaded.Revision);
92+
Assert.Equal(MutationRequestVersionResolutionOutcome.RevalidateOnLatestState, resolution.Outcome);
93+
Assert.Equal(loaded, resolution.Request);
94+
}
95+
6696
[Fact]
6797
public async Task ResolveAndStore_throws_not_found_for_missing_request()
6898
{

src/Governance/Abstractions/Approval/Model/MutationApprovalRequirementStatus.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Approval.Model;
55
/// </summary>
66
public enum MutationApprovalRequirementStatus
77
{
8+
/// <summary>
9+
/// The approval requirement is still waiting for a decision.
10+
/// </summary>
811
Pending = 0,
12+
/// <summary>
13+
/// The approval requirement has been approved.
14+
/// </summary>
915
Approved = 1,
16+
/// <summary>
17+
/// The approval requirement has been rejected.
18+
/// </summary>
1019
Rejected = 2,
20+
/// <summary>
21+
/// The approval requirement has been satisfied by quorum or equivalent policy.
22+
/// </summary>
1123
Satisfied = 3,
24+
/// <summary>
25+
/// The approval requirement expired before it was resolved.
26+
/// </summary>
1227
Expired = 4
1328
}

src/Governance/Abstractions/Lifecycle/Model/MutationRequestStatus.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,36 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
55
/// </summary>
66
public enum MutationRequestStatus
77
{
8+
/// <summary>
9+
/// The request has been created but not yet processed.
10+
/// </summary>
811
Created = 0,
12+
/// <summary>
13+
/// The request is waiting for an external governance condition.
14+
/// </summary>
915
Pending = 1,
16+
/// <summary>
17+
/// The request has been approved and may proceed to execution.
18+
/// </summary>
1019
Approved = 2,
20+
/// <summary>
21+
/// The request has been rejected and will not proceed.
22+
/// </summary>
1123
Rejected = 3,
24+
/// <summary>
25+
/// The request has been canceled by an explicit action.
26+
/// </summary>
1227
Canceled = 4,
28+
/// <summary>
29+
/// The request expired before it could be completed.
30+
/// </summary>
1331
Expired = 5,
32+
/// <summary>
33+
/// The request has been superseded by another request.
34+
/// </summary>
1435
Superseded = 6,
36+
/// <summary>
37+
/// The request has been executed successfully.
38+
/// </summary>
1539
Executed = 7
1640
}

src/Governance/Abstractions/Lifecycle/Model/PendingMutationReason.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,38 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
55
/// </summary>
66
public enum PendingMutationReason
77
{
8+
/// <summary>
9+
/// The request is waiting for approval.
10+
/// </summary>
811
Approval = 0,
12+
13+
/// <summary>
14+
/// The request is waiting for an external check or integration response.
15+
/// </summary>
916
ExternalCheck = 1,
17+
18+
/// <summary>
19+
/// The request is waiting for a scheduled execution window.
20+
/// </summary>
1021
Schedule = 2,
22+
23+
/// <summary>
24+
/// The request is waiting for a dependency to become ready.
25+
/// </summary>
1126
Dependency = 3,
27+
28+
/// <summary>
29+
/// The request is waiting because of quota constraints.
30+
/// </summary>
1231
Quota = 4,
13-
ManualReview = 5
32+
33+
/// <summary>
34+
/// The request is waiting for manual review.
35+
/// </summary>
36+
ManualReview = 5,
37+
38+
/// <summary>
39+
/// The request is pending because it must be revalidated against the latest state before execution.
40+
/// </summary>
41+
Revalidation = 6
1442
}

src/Governance/Abstractions/Requests/Decisions/MutationRequestApprovalDecisionType.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
55
/// </summary>
66
public enum MutationRequestApprovalDecisionType
77
{
8+
/// <summary>
9+
/// The approval requirement was requested.
10+
/// </summary>
811
Requested = 0,
12+
/// <summary>
13+
/// The approval requirement was granted.
14+
/// </summary>
915
Granted = 1,
16+
/// <summary>
17+
/// The approval requirement was rejected.
18+
/// </summary>
1019
Rejected = 2,
20+
/// <summary>
21+
/// The approval quorum for a group was satisfied.
22+
/// </summary>
1123
QuorumSatisfied = 3,
24+
/// <summary>
25+
/// The approval requirement expired.
26+
/// </summary>
1227
Expired = 4
1328
}

src/Governance/Abstractions/Requests/Decisions/MutationRequestDecisionCategory.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
55
/// </summary>
66
public enum MutationRequestDecisionCategory
77
{
8+
/// <summary>
9+
/// Lifecycle decisions describe request state transitions.
10+
/// </summary>
811
Lifecycle = 0,
12+
/// <summary>
13+
/// Approval decisions describe request-level approval processing.
14+
/// </summary>
915
Approval = 1,
16+
/// <summary>
17+
/// Version-resolution decisions describe stale and version-aware handling.
18+
/// </summary>
1019
VersionResolution = 2
1120
}

src/Governance/Abstractions/Requests/Decisions/MutationRequestLifecycleDecisionType.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,36 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
55
/// </summary>
66
public enum MutationRequestLifecycleDecisionType
77
{
8+
/// <summary>
9+
/// The request was submitted into the governance system.
10+
/// </summary>
811
Submitted = 0,
12+
/// <summary>
13+
/// The request entered a pending lifecycle state.
14+
/// </summary>
915
Pending = 1,
16+
/// <summary>
17+
/// The request was approved.
18+
/// </summary>
1019
Approved = 2,
20+
/// <summary>
21+
/// The request was rejected.
22+
/// </summary>
1123
Rejected = 3,
24+
/// <summary>
25+
/// The request was canceled.
26+
/// </summary>
1227
Canceled = 4,
28+
/// <summary>
29+
/// The request expired before completion.
30+
/// </summary>
1331
Expired = 5,
32+
/// <summary>
33+
/// The request was superseded by another request.
34+
/// </summary>
1435
Superseded = 6,
36+
/// <summary>
37+
/// The request executed successfully.
38+
/// </summary>
1539
Executed = 7
1640
}

src/Governance/Abstractions/Requests/Decisions/MutationRequestVersionResolutionDecisionType.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,20 @@ namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
55
/// </summary>
66
public enum MutationRequestVersionResolutionDecisionType
77
{
8+
/// <summary>
9+
/// The request version matched the current state version.
10+
/// </summary>
811
Validated = 0,
12+
/// <summary>
13+
/// The request must be revalidated against the latest state.
14+
/// </summary>
915
RevalidationRequired = 1,
16+
/// <summary>
17+
/// The request must obtain renewed approval before proceeding.
18+
/// </summary>
1019
RenewedApprovalRequired = 2,
20+
/// <summary>
21+
/// The request was rejected because it was stale.
22+
/// </summary>
1123
RejectedAsStale = 3
1224
}

0 commit comments

Comments
 (0)