Skip to content

Commit 32e6468

Browse files
committed
Refine governance request abstractions
1 parent 22d8ea2 commit 32e6468

7 files changed

Lines changed: 289 additions & 22 deletions

‎src/Governance/Abstractions/Requests/Decisions/MutationRequestDecision.cs‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
44

55
/// <summary>
6-
/// Captures a single decision or lifecycle transition applied to a mutation request.
6+
/// Captures single decision or lifecycle transition applied to mutation request.
77
/// </summary>
88
public sealed record MutationRequestDecision
99
{
@@ -33,8 +33,13 @@ public sealed record MutationRequestDecision
3333
public IReadOnlyDictionary<string, object> Metadata { get; init; } = new Dictionary<string, object>();
3434

3535
/// <summary>
36-
/// Creates a lifecycle decision entry.
36+
/// Creates lifecycle decision entry.
3737
/// </summary>
38+
/// <param name="type">Lifecycle decision type.</param>
39+
/// <param name="context">Actor or system context that records the decision.</param>
40+
/// <param name="reason">Optional human-readable explanation for the decision.</param>
41+
/// <param name="metadata">Optional governance metadata attached to the decision.</param>
42+
/// <returns>A lifecycle decision entry.</returns>
3843
public static MutationRequestDecision Lifecycle(
3944
MutationRequestLifecycleDecisionType type,
4045
MutationContext context,
@@ -49,6 +54,11 @@ public static MutationRequestDecision Lifecycle(
4954
/// <summary>
5055
/// Creates an approval decision entry.
5156
/// </summary>
57+
/// <param name="type">Approval decision type.</param>
58+
/// <param name="context">Actor or system context that records the decision.</param>
59+
/// <param name="reason">Optional human-readable explanation for the decision.</param>
60+
/// <param name="metadata">Optional governance metadata attached to the decision.</param>
61+
/// <returns>An approval decision entry.</returns>
5262
public static MutationRequestDecision Approval(
5363
MutationRequestApprovalDecisionType type,
5464
MutationContext context,
@@ -61,8 +71,13 @@ public static MutationRequestDecision Approval(
6171
metadata);
6272

6373
/// <summary>
64-
/// Creates a version-resolution decision entry.
74+
/// Creates version resolution decision entry.
6575
/// </summary>
76+
/// <param name="type">Version-resolution decision type.</param>
77+
/// <param name="context">Actor or system context that records the decision.</param>
78+
/// <param name="reason">Optional human-readable explanation for the decision.</param>
79+
/// <param name="metadata">Optional governance metadata attached to the decision.</param>
80+
/// <returns>A version-resolution decision entry.</returns>
6681
public static MutationRequestDecision VersionResolution(
6782
MutationRequestVersionResolutionDecisionType type,
6883
MutationContext context,
@@ -75,8 +90,13 @@ public static MutationRequestDecision VersionResolution(
7590
metadata);
7691

7792
/// <summary>
78-
/// Creates a new request decision entry.
93+
/// Creates new request decision entry.
7994
/// </summary>
95+
/// <param name="type">Decision type wrapper including category and stable code.</param>
96+
/// <param name="context">Actor or system context that records the decision.</param>
97+
/// <param name="reason">Optional human-readable explanation for the decision.</param>
98+
/// <param name="metadata">Optional governance metadata attached to the decision.</param>
99+
/// <returns>A new request decision entry.</returns>
80100
public static MutationRequestDecision Create(
81101
MutationRequestDecisionType type,
82102
MutationContext context,

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
22

33
/// <summary>
4-
/// Represents high-level lifecycle decisions taken against a mutation request.
4+
/// Represents lifecycle decisions taken against mutation request.
55
/// </summary>
66
public enum MutationRequestLifecycleDecisionType
77
{
@@ -36,5 +36,10 @@ public enum MutationRequestLifecycleDecisionType
3636
/// <summary>
3737
/// The request executed successfully.
3838
/// </summary>
39-
Executed = 7
39+
Executed = 7,
40+
41+
/// <summary>
42+
/// A successful compensation execution was recorded against this request.
43+
/// </summary>
44+
Compensated = 8
4045
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using ModularityKit.Mutator.Abstractions.Context;
2+
using ModularityKit.Mutator.Abstractions.Engine;
3+
using ModularityKit.Mutator.Abstractions.Intent;
4+
using ModularityKit.Mutator.Governance.Abstractions.Execution.Model;
5+
using ModularityKit.Mutator.Governance.Abstractions.Execution.Model.Compensation;
6+
using ModularityKit.Mutator.Governance.Abstractions.Execution.Model.Links;
7+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
8+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
9+
10+
namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Factory;
11+
12+
/// <summary>
13+
/// Creates governed mutation requests for compensation flows.
14+
/// </summary>
15+
public static class CompensationMutationRequestFactory
16+
{
17+
/// <summary>
18+
/// Creates an immediately approved compensation request using type inference for the target state and mutation.
19+
/// </summary>
20+
/// <typeparam name="TState">The target state type.</typeparam>
21+
/// <typeparam name="TMutation">The compensation mutation type.</typeparam>
22+
/// <param name="stateId">Stable identifier of the target state.</param>
23+
/// <param name="intent">Intent associated with the compensating mutation.</param>
24+
/// <param name="context">Request context describing who initiated the compensation and why.</param>
25+
/// <param name="compensation">Compensation plan describing the original execution and recovery semantics.</param>
26+
/// <param name="expectedStateVersion">Optional expected state version captured before compensation execution.</param>
27+
/// <param name="metadata">Optional governance metadata carried by the request.</param>
28+
/// <returns>An approved governed compensation request.</returns>
29+
public static MutationRequest Approved<TState, TMutation>(
30+
string stateId,
31+
MutationIntent intent,
32+
MutationContext context,
33+
GovernedCompensationPlan compensation,
34+
string? expectedStateVersion = null,
35+
IReadOnlyDictionary<string, object>? metadata = null)
36+
where TMutation : IMutation<TState>
37+
=> Approved(
38+
stateId,
39+
typeof(TState).Name,
40+
typeof(TMutation).Name,
41+
intent,
42+
context,
43+
compensation,
44+
expectedStateVersion,
45+
metadata);
46+
47+
/// <summary>
48+
/// Creates an immediately approved compensation request.
49+
/// </summary>
50+
/// <param name="stateId">Stable identifier of the target state.</param>
51+
/// <param name="stateType">Logical state type name.</param>
52+
/// <param name="mutationType">Compensation mutation type name.</param>
53+
/// <param name="intent">Intent associated with the compensating mutation.</param>
54+
/// <param name="context">Request context describing who initiated the compensation and why.</param>
55+
/// <param name="compensation">Compensation plan describing the original execution and recovery semantics.</param>
56+
/// <param name="expectedStateVersion">Optional expected state version captured before compensation execution.</param>
57+
/// <param name="metadata">Optional governance metadata carried by the request.</param>
58+
/// <returns>An approved governed compensation request.</returns>
59+
public static MutationRequest Approved(
60+
string stateId,
61+
string stateType,
62+
string mutationType,
63+
MutationIntent intent,
64+
MutationContext context,
65+
GovernedCompensationPlan compensation,
66+
string? expectedStateVersion = null,
67+
IReadOnlyDictionary<string, object>? metadata = null)
68+
{
69+
ArgumentNullException.ThrowIfNull(compensation);
70+
compensation.EnsureValid();
71+
72+
var request = MutationRequestFactory.Approved(
73+
stateId,
74+
stateType,
75+
mutationType,
76+
intent,
77+
context,
78+
expectedStateVersion,
79+
metadata);
80+
81+
return request with
82+
{
83+
Execution = new GovernedExecutionDetails
84+
{
85+
Kind = GovernedExecutionKind.Compensation,
86+
Compensation = compensation,
87+
RelatedExecutions =
88+
[
89+
new GovernedExecutionLink
90+
{
91+
RequestId = compensation.OriginalRequestId,
92+
Type = GovernedExecutionLinkType.Compensates,
93+
ExecutionKind = GovernedExecutionKind.Standard,
94+
CompensationKind = compensation.Kind,
95+
Trigger = compensation.Trigger,
96+
BatchId = compensation.BatchId
97+
}
98+
]
99+
},
100+
Decisions =
101+
[
102+
.. request.Decisions.Take(request.Decisions.Count - 1),
103+
MutationRequestDecision.Lifecycle(
104+
MutationRequestLifecycleDecisionType.Approved,
105+
context,
106+
reason: $"Compensation request approved at submission time for original request '{compensation.OriginalRequestId}'.",
107+
metadata: CreateCompensationMetadata(compensation))
108+
]
109+
};
110+
}
111+
112+
private static IReadOnlyDictionary<string, object> CreateCompensationMetadata(GovernedCompensationPlan compensation)
113+
{
114+
var metadata = new Dictionary<string, object>
115+
{
116+
["OriginalRequestId"] = compensation.OriginalRequestId,
117+
["CompensationKind"] = compensation.Kind.ToString(),
118+
["CompensationTrigger"] = compensation.Trigger.ToString()
119+
};
120+
121+
if (!string.IsNullOrWhiteSpace(compensation.BatchId))
122+
metadata["BatchId"] = compensation.BatchId;
123+
124+
if (compensation.RelatedRequestIds.Count > 0)
125+
metadata["RelatedRequestIds"] = compensation.RelatedRequestIds;
126+
127+
if (!string.IsNullOrWhiteSpace(compensation.Reason))
128+
metadata["CompensationReason"] = compensation.Reason;
129+
130+
return metadata;
131+
}
132+
}

‎src/Governance/Abstractions/Requests/Factory/MutationRequestFactory.cs‎

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ public static class MutationRequestFactory
1717
/// <summary>
1818
/// Creates a request that should enter the pending lifecycle using type inference for the target state and mutation.
1919
/// </summary>
20+
/// <typeparam name="TState">The target state type.</typeparam>
21+
/// <typeparam name="TMutation">The mutation type.</typeparam>
22+
/// <param name="stateId">Stable identifier of the target state.</param>
23+
/// <param name="intent">Intent associated with the requested mutation.</param>
24+
/// <param name="context">Request context describing who submitted the mutation and why.</param>
25+
/// <param name="pendingReason">Lifecycle reason that keeps the request pending.</param>
26+
/// <param name="requirements">Optional policy requirements attached to the request.</param>
27+
/// <param name="expectedStateVersion">Optional expected state version captured at submission time.</param>
28+
/// <param name="expiresAt">Optional expiration time for the pending request.</param>
29+
/// <param name="metadata">Optional governance metadata carried by the request.</param>
30+
/// <returns>A pending governed mutation request.</returns>
2031
public static MutationRequest Pending<TState, TMutation>(
2132
string stateId,
2233
MutationIntent intent,
@@ -42,6 +53,17 @@ public static MutationRequest Pending<TState, TMutation>(
4253
/// <summary>
4354
/// Creates a request that should enter the pending lifecycle.
4455
/// </summary>
56+
/// <param name="stateId">Stable identifier of the target state.</param>
57+
/// <param name="stateType">Logical state type name.</param>
58+
/// <param name="mutationType">Mutation type name.</param>
59+
/// <param name="intent">Intent associated with the requested mutation.</param>
60+
/// <param name="context">Request context describing who submitted the mutation and why.</param>
61+
/// <param name="pendingReason">Lifecycle reason that keeps the request pending.</param>
62+
/// <param name="requirements">Optional policy requirements attached to the request.</param>
63+
/// <param name="expectedStateVersion">Optional expected state version captured at submission time.</param>
64+
/// <param name="expiresAt">Optional expiration time for the pending request.</param>
65+
/// <param name="metadata">Optional governance metadata carried by the request.</param>
66+
/// <returns>A pending governed mutation request.</returns>
4567
public static MutationRequest Pending(
4668
string stateId,
4769
string stateType,
@@ -64,7 +86,10 @@ public static MutationRequest Pending(
6486
Status = MutationRequestStatus.Pending,
6587
PendingReason = pendingReason,
6688
Requirements = requirements ?? [],
67-
ExpectedStateVersion = expectedStateVersion,
89+
Versioning = new MutationRequestVersioningDetails
90+
{
91+
ExpectedStateVersion = expectedStateVersion
92+
},
6893
ExpiresAt = expiresAt,
6994
Metadata = metadata ?? new Dictionary<string, object>(),
7095
Decisions =
@@ -84,6 +109,16 @@ public static MutationRequest Pending(
84109
/// <summary>
85110
/// Creates a request that enters pending approval using type inference for the target state and mutation.
86111
/// </summary>
112+
/// <typeparam name="TState">The target state type.</typeparam>
113+
/// <typeparam name="TMutation">The mutation type.</typeparam>
114+
/// <param name="stateId">Stable identifier of the target state.</param>
115+
/// <param name="intent">Intent associated with the requested mutation.</param>
116+
/// <param name="context">Request context describing who submitted the mutation and why.</param>
117+
/// <param name="requirements">Policy requirements that will be translated into approval requirements.</param>
118+
/// <param name="expectedStateVersion">Optional expected state version captured at submission time.</param>
119+
/// <param name="expiresAt">Optional expiration time for the pending request.</param>
120+
/// <param name="metadata">Optional governance metadata carried by the request.</param>
121+
/// <returns>A governed mutation request pending approval.</returns>
87122
public static MutationRequest PendingApproval<TState, TMutation>(
88123
string stateId,
89124
MutationIntent intent,
@@ -107,6 +142,16 @@ public static MutationRequest PendingApproval<TState, TMutation>(
107142
/// <summary>
108143
/// Creates a request that enters pending approval with concrete request-level approval requirements.
109144
/// </summary>
145+
/// <param name="stateId">Stable identifier of the target state.</param>
146+
/// <param name="stateType">Logical state type name.</param>
147+
/// <param name="mutationType">Mutation type name.</param>
148+
/// <param name="intent">Intent associated with the requested mutation.</param>
149+
/// <param name="context">Request context describing who submitted the mutation and why.</param>
150+
/// <param name="requirements">Policy requirements that will be translated into approval requirements.</param>
151+
/// <param name="expectedStateVersion">Optional expected state version captured at submission time.</param>
152+
/// <param name="expiresAt">Optional expiration time for the pending request.</param>
153+
/// <param name="metadata">Optional governance metadata carried by the request.</param>
154+
/// <returns>A governed mutation request pending approval.</returns>
110155
public static MutationRequest PendingApproval(
111156
string stateId,
112157
string stateType,
@@ -135,7 +180,10 @@ public static MutationRequest PendingApproval(
135180
PendingReason = PendingMutationReason.Approval,
136181
Requirements = requirements,
137182
ApprovalRequirements = approvalRequirements,
138-
ExpectedStateVersion = expectedStateVersion,
183+
Versioning = new MutationRequestVersioningDetails
184+
{
185+
ExpectedStateVersion = expectedStateVersion
186+
},
139187
ExpiresAt = expiresAt,
140188
Metadata = metadata ?? new Dictionary<string, object>(),
141189
Decisions =
@@ -163,6 +211,14 @@ public static MutationRequest PendingApproval(
163211
/// <summary>
164212
/// Creates a request that is immediately approved for execution using type inference for the target state and mutation.
165213
/// </summary>
214+
/// <typeparam name="TState">The target state type.</typeparam>
215+
/// <typeparam name="TMutation">The mutation type.</typeparam>
216+
/// <param name="stateId">Stable identifier of the target state.</param>
217+
/// <param name="intent">Intent associated with the requested mutation.</param>
218+
/// <param name="context">Request context describing who submitted the mutation and why.</param>
219+
/// <param name="expectedStateVersion">Optional expected state version captured at submission time.</param>
220+
/// <param name="metadata">Optional governance metadata carried by the request.</param>
221+
/// <returns>An approved governed mutation request.</returns>
166222
public static MutationRequest Approved<TState, TMutation>(
167223
string stateId,
168224
MutationIntent intent,
@@ -182,6 +238,14 @@ public static MutationRequest Approved<TState, TMutation>(
182238
/// <summary>
183239
/// Creates a request that is immediately approved for execution.
184240
/// </summary>
241+
/// <param name="stateId">Stable identifier of the target state.</param>
242+
/// <param name="stateType">Logical state type name.</param>
243+
/// <param name="mutationType">Mutation type name.</param>
244+
/// <param name="intent">Intent associated with the requested mutation.</param>
245+
/// <param name="context">Request context describing who submitted the mutation and why.</param>
246+
/// <param name="expectedStateVersion">Optional expected state version captured at submission time.</param>
247+
/// <param name="metadata">Optional governance metadata carried by the request.</param>
248+
/// <returns>An approved governed mutation request.</returns>
185249
public static MutationRequest Approved(
186250
string stateId,
187251
string stateType,
@@ -199,7 +263,10 @@ public static MutationRequest Approved(
199263
Intent = intent,
200264
Context = context,
201265
Status = MutationRequestStatus.Approved,
202-
ExpectedStateVersion = expectedStateVersion,
266+
Versioning = new MutationRequestVersioningDetails
267+
{
268+
ExpectedStateVersion = expectedStateVersion
269+
},
203270
Metadata = metadata ?? new Dictionary<string, object>(),
204271
Decisions =
205272
[
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using ModularityKit.Mutator.Governance.Abstractions.Execution.Model;
2+
using ModularityKit.Mutator.Governance.Abstractions.Execution.Model.Compensation;
3+
using ModularityKit.Mutator.Governance.Abstractions.Execution.Model.Links;
4+
5+
namespace ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
6+
7+
/// <summary>
8+
/// Groups governed execution-specific details carried by mutation request.
9+
/// </summary>
10+
public sealed record GovernedExecutionDetails
11+
{
12+
/// <summary>
13+
/// Classifies this request as standard governed execution or compensating execution.
14+
/// </summary>
15+
public GovernedExecutionKind Kind { get; init; } = GovernedExecutionKind.Standard;
16+
17+
/// <summary>
18+
/// Compensation plan carried by this request when it compensates for prior execution.
19+
/// </summary>
20+
public GovernedCompensationPlan? Compensation { get; init; }
21+
22+
/// <summary>
23+
/// Explicit links to related governed execution records.
24+
/// </summary>
25+
public IReadOnlyList<GovernedExecutionLink> RelatedExecutions { get; init; } = [];
26+
}

0 commit comments

Comments
 (0)