Skip to content

Commit 41f747e

Browse files
authored
Feat: Decompose governance query models and align runtime integrations (#41)
## Summary Decompose the governance query model into grouped request filters plus dedicated approval and decision namespaces, then align runtime, Redis, docs, examples, and tests with that split. ## Added - grouped request query filters for scope, actor, intent, metadata, lifecycle, and time range - `MutationRequestQueries` presets for common governance request query shapes - approval and decision query models under dedicated namespaces - `MutationRequestDecision` category-specific helpers for lifecycle, approval, and version-resolution decisions - ADR-033 covering governance query model decomposition - regression coverage for grouped query filters, metadata scope separation, decision helpers, governed execution metadata propagation, and Redis serialization roundtrips ## Changed - governance request queries no longer accumulate all filters on one flat `MutationRequestQuery` surface - runtime and in-memory governance query flows now resolve the decomposed request query model and dedicated approval and decision types - Redis candidate selection, query reading, materialization, and ordering now use the decomposed query namespaces and grouped filters - governed execution now carries request and intent metadata through execution-facing audit surfaces - governance and Redis examples now use the updated query namespaces and decision helper factories - governance and Redis docs now reflect the decomposed query API and practical usage surface ## Result The governance read model is now organized by concern instead of one expanding flat query namespace. Request filters have clearer ownership, approval and decision projections are separated cleanly, and the runtime plus Redis integrations stay aligned with the same query contracts. The execution path also preserves richer governance metadata for downstream audit and history consumers. ## Testing - `dotnet test Tests/ModularityKit.Mutator.Governance.Tests/ModularityKit.Mutator.Governance.Tests.csproj` - `dotnet test Tests/ModularityKit.Mutator.Governance.Redis.Tests/ModularityKit.Mutator.Governance.Redis.Tests.csproj` - `dotnet build Examples/Governance/Queries/Queries.csproj` - `dotnet build Examples/Governance/RedisQueries/RedisQueries.csproj` ## Linked Issues - Closes #22 ## Checklist - [x] governance query abstractions are decomposed into request, approval, and decision concerns - [x] runtime and Redis query flows are aligned with the new grouped filter model - [x] docs, examples, and tests cover the updated governance query surface
2 parents bd8d878 + 1d914d3 commit 41f747e

43 files changed

Lines changed: 985 additions & 446 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# ADR-033: Governance Query Model Decomposition
2+
3+
## Tag
4+
#adr_033
5+
6+
## Status
7+
Accepted
8+
9+
## Date
10+
2026-06-30
11+
12+
## Scope
13+
ModularityKit.Mutator.Governance.Abstractions.Queries
14+
15+
## Context
16+
17+
ADR-026 established that governance needs storage agnostic query API, but it intentionally left the exact query object model open.
18+
19+
As the query surface grew, `MutationRequestQuery` started to accumulate several distinct concerns:
20+
21+
- request and state identity filters
22+
- actor filters
23+
- intent classification filters such as category, tags, metadata, and blast radius
24+
- lifecycle and decision-history filters
25+
- approval oriented and decision oriented projections adjacent to the same flat namespace
26+
27+
Without more explicit structure, the query model would keep growing as one wide record with loosely related fields. That creates several problems:
28+
29+
- the request query type becomes harder to read and evolve
30+
- approval and decision query types remain mixed into the same flat model namespace
31+
- future additions such as risk level or execution specific filters would further flatten unrelated concepts into one type
32+
33+
## Decision
34+
35+
The governance query abstractions should be decomposed into grouped models and organized by query concern.
36+
37+
Request query shape:
38+
39+
- `MutationRequestQuery` remains the root request query contract
40+
- request query filters are grouped into explicit components:
41+
- `Scope`
42+
- `Actor`
43+
- `Intent`
44+
- `Metadata`
45+
- `Lifecycle`
46+
- `TimeRange`
47+
- request query presets move into `MutationRequestQueries`
48+
49+
Namespace and folder organization:
50+
51+
- request query abstractions live under `...Queries.Model.Requests`
52+
- request query filter types live under `...Queries.Model.Requests.Filters`
53+
- approval query types live under `...Queries.Model.Approvals`
54+
- decision query types live under `...Queries.Model.Decisions`
55+
56+
Evaluation semantics:
57+
58+
- query evaluators compose groupspecific matches rather than expanding one flat request query type indefinitely
59+
- approval and decision query types continue to depend on the request query contract where needed, but remain organized as distinct concerns
60+
61+
## Design Rationale
62+
63+
- The request query contract stays explicit without becoming one ever growing bag of filters.
64+
- Filter grouping makes new additions easier to place without weakening the model.
65+
- Approval and decision queries are part of the same read side, but not the same concern as request filters, so they should not share one flat namespace.
66+
- Moving presets out of `MutationRequestQuery` keeps the model closer to a DTO and avoids mixing data shape with convenience factories.
67+
68+
## Consequences
69+
70+
### Positive
71+
72+
- The query model is easier to scan and extend.
73+
- Filter placement becomes more disciplined as the governance read side grows.
74+
- Namespace organization better reflects actual query responsibilities.
75+
- The query API stays storage agnostic while becoming more explicit internally.
76+
77+
### Negative
78+
79+
- Consumers need a small amount of additional object construction when building request queries.
80+
- Existing code that referenced the old flat model or flat namespaces must be updated.
81+
- Some changes that are semantically query related now span several small types instead of one file.
82+
83+
## Related ADRs
84+
85+
- ADR-026: Governance Request Query API
86+
- ADR-030: Governance Redis Request Storage and Query Strategy

Docs/Decision/listadr.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ These ADRs describe the `ModularityKit.Mutator.Governance` extension layer and i
4848
| ADR-030 | Governance Redis Request Storage and Query Strategy | [ADR-030](Adr/ADR_030_Governance_Redis_Request_Storage_and_Query_Strategy.md) |
4949
| ADR-031 | Governance Redis Serialization and Document Compatibility | [ADR-031](Adr/ADR_031_Governance_Redis_Serialization_and_Document_Compatibility.md) |
5050
| ADR-032 | Governance Redis Concurrency and Index Maintenance Model | [ADR-032](Adr/ADR_032_Governance_Redis_Concurrency_and_Index_Maintenance_Model.md) |
51+
| ADR-033 | Governance Query Model Decomposition | [ADR-033](Adr/ADR_033_Governance_Query_Model_Decomposition.md) |
5152

5253
> See individual ADRs for detailed context, decision rationale, and consequences.

Examples/Governance/Queries/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ It focuses on listing governed requests, approval work, and decision history wit
77
## What it demonstrates
88

99
- querying governed requests with `MutationRequestQuery`
10+
- filtering by intent tags, intent metadata, request metadata, and blast radius
1011
- listing the pending approval queue through `IMutationRequestQueryStore`
1112
- listing pending requests by `PendingMutationReason`
1213
- querying requests by `StateId` and request category
@@ -25,9 +26,9 @@ It focuses on listing governed requests, approval work, and decision history wit
2526
- [`Scenarios/ApprovalQueryScenario.cs`](Scenarios/ApprovalQueryScenario.cs)
2627
- [`Scenarios/DecisionQueryScenario.cs`](Scenarios/DecisionQueryScenario.cs)
2728
- [`src/Governance/Abstractions/Queries/Contracts/IMutationRequestQueryStore.cs`](../../../src/Governance/Abstractions/Queries/Contracts/IMutationRequestQueryStore.cs)
28-
- [`src/Governance/Abstractions/Queries/Model/MutationRequestQuery.cs`](../../../src/Governance/Abstractions/Queries/Model/MutationRequestQuery.cs)
29-
- [`src/Governance/Abstractions/Queries/Model/MutationApprovalQuery.cs`](../../../src/Governance/Abstractions/Queries/Model/MutationApprovalQuery.cs)
30-
- [`src/Governance/Abstractions/Queries/Model/MutationRequestDecisionQuery.cs`](../../../src/Governance/Abstractions/Queries/Model/MutationRequestDecisionQuery.cs)
29+
- [`src/Governance/Abstractions/Queries/Model/Requests/MutationRequestQuery.cs`](../../../src/Governance/Abstractions/Queries/Model/Requests/MutationRequestQuery.cs)
30+
- [`src/Governance/Abstractions/Queries/Model/Approvals/MutationApprovalQuery.cs`](../../../src/Governance/Abstractions/Queries/Model/Approvals/MutationApprovalQuery.cs)
31+
- [`src/Governance/Abstractions/Queries/Model/Decisions/MutationRequestDecisionQuery.cs`](../../../src/Governance/Abstractions/Queries/Model/Decisions/MutationRequestDecisionQuery.cs)
3132

3233
## Run
3334

@@ -43,6 +44,7 @@ The sample prints:
4344
- pending external-check requests
4445
- requests filtered by request category
4546
- requests filtered by state
47+
- requests filtered by governance metadata
4648
- recent approval-driven requests
4749
- approval views filtered by approver
4850
- recent version-resolution decisions

Examples/Governance/Queries/Scenarios/ApprovalQueryScenario.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using ModularityKit.Mutator.Governance.Abstractions.Queries.Contracts;
2-
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model;
2+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Approvals;
33

44
namespace Queries.Scenarios;
55

Examples/Governance/Queries/Scenarios/DecisionQueryScenario.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using ModularityKit.Mutator.Governance.Abstractions.Queries.Contracts;
2-
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model;
2+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Decisions;
33

44
namespace Queries.Scenarios;
55

Examples/Governance/Queries/Scenarios/GovernanceQueriesSampleData.cs

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using ModularityKit.Mutator.Abstractions.Policies;
44
using ModularityKit.Mutator.Governance.Abstractions.Approval.Model;
55
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
6+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Approvals;
7+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Decisions;
68
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
79
using ModularityKit.Mutator.Governance.Abstractions.Requests.Factory;
810
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
@@ -76,7 +78,7 @@ public static void PrintRequests(IReadOnlyList<MutationRequest> requests)
7678
Console.WriteLine("- none");
7779
}
7880

79-
public static void PrintApprovals(IReadOnlyList<ModularityKit.Mutator.Governance.Abstractions.Queries.Model.MutationApprovalView> approvals)
81+
public static void PrintApprovals(IReadOnlyList<MutationApprovalView> approvals)
8082
{
8183
foreach (var approval in approvals)
8284
{
@@ -88,7 +90,7 @@ public static void PrintApprovals(IReadOnlyList<ModularityKit.Mutator.Governance
8890
Console.WriteLine("- none");
8991
}
9092

91-
public static void PrintDecisions(IReadOnlyList<ModularityKit.Mutator.Governance.Abstractions.Queries.Model.MutationRequestDecisionView> decisions)
93+
public static void PrintDecisions(IReadOnlyList<MutationRequestDecisionView> decisions)
9294
{
9395
foreach (var decision in decisions)
9496
{
@@ -114,7 +116,13 @@ private static MutationRequest CreatePendingApprovalRequest(
114116
{
115117
OperationName = "ExampleOperation",
116118
Category = category,
117-
Description = $"Governed request for {category.ToLowerInvariant()} flow"
119+
Description = $"Governed request for {category.ToLowerInvariant()} flow",
120+
Tags = new HashSet<string> { category.ToLowerInvariant(), "approval" },
121+
EstimatedBlastRadius = category == "Security" ? BlastRadius.Module : BlastRadius.Single,
122+
Metadata = new Dictionary<string, object>
123+
{
124+
["risk-owner"] = category == "Security" ? "platform" : "finance"
125+
}
118126
},
119127
context: MutationContext.User("requester", "Requester", "Need governed change"),
120128
requirements:
@@ -126,6 +134,10 @@ private static MutationRequest CreatePendingApprovalRequest(
126134
RequestId = requestId,
127135
CreatedAt = createdAt,
128136
UpdatedAt = createdAt,
137+
Metadata = new Dictionary<string, object>
138+
{
139+
["ticket"] = category == "Security" ? "INC-42" : "BILL-7"
140+
},
129141
ApprovalRequirements =
130142
[
131143
new MutationApprovalRequirement
@@ -150,15 +162,25 @@ private static MutationRequest CreateExternalCheckRequest(
150162
{
151163
OperationName = "ExampleOperation",
152164
Category = category,
153-
Description = "Waiting for dependency validation"
165+
Description = "Waiting for dependency validation",
166+
Tags = new HashSet<string> { "external-check" },
167+
EstimatedBlastRadius = BlastRadius.Single,
168+
Metadata = new Dictionary<string, object>
169+
{
170+
["risk-owner"] = "release"
171+
}
154172
},
155173
context: MutationContext.Service("release-orchestrator", "Waiting for external dependency"),
156174
pendingReason: PendingMutationReason.ExternalCheck)
157175
with
158176
{
159177
RequestId = requestId,
160178
CreatedAt = createdAt,
161-
UpdatedAt = createdAt
179+
UpdatedAt = createdAt,
180+
Metadata = new Dictionary<string, object>
181+
{
182+
["ticket"] = "REL-99"
183+
}
162184
};
163185

164186
private static MutationRequest CreateRecentlyApprovedRequest(
@@ -175,7 +197,13 @@ private static MutationRequest CreateRecentlyApprovedRequest(
175197
{
176198
OperationName = "ExampleOperation",
177199
Category = category,
178-
Description = "Recently approved governed request"
200+
Description = "Recently approved governed request",
201+
Tags = new HashSet<string> { category.ToLowerInvariant(), "approved" },
202+
EstimatedBlastRadius = category == "Security" ? BlastRadius.Module : BlastRadius.Single,
203+
Metadata = new Dictionary<string, object>
204+
{
205+
["risk-owner"] = category == "Security" ? "platform" : "finance"
206+
}
179207
},
180208
context: MutationContext.User("requester", "Requester", "Need privileged change"),
181209
requirements:
@@ -189,6 +217,10 @@ private static MutationRequest CreateRecentlyApprovedRequest(
189217
PendingReason = null,
190218
CreatedAt = approvedAt.AddMinutes(-20),
191219
UpdatedAt = approvedAt,
220+
Metadata = new Dictionary<string, object>
221+
{
222+
["ticket"] = category == "Security" ? "INC-77" : "BILL-9"
223+
},
192224
ApprovalRequirements =
193225
[
194226
new MutationApprovalRequirement
@@ -252,7 +284,13 @@ private static MutationRequest CreateResolvedRequest(
252284
{
253285
OperationName = "ExampleOperation",
254286
Category = category,
255-
Description = "Resolved governed request"
287+
Description = "Resolved governed request",
288+
Tags = new HashSet<string> { category.ToLowerInvariant(), "resolution" },
289+
EstimatedBlastRadius = BlastRadius.Single,
290+
Metadata = new Dictionary<string, object>
291+
{
292+
["risk-owner"] = "governance"
293+
}
256294
},
257295
context: MutationContext.Service("governance-runtime", "Resolve stale request"))
258296
with
@@ -261,6 +299,10 @@ private static MutationRequest CreateResolvedRequest(
261299
Status = MutationRequestStatus.Approved,
262300
CreatedAt = decisionTimestamp.AddMinutes(-30),
263301
UpdatedAt = decisionTimestamp,
302+
Metadata = new Dictionary<string, object>
303+
{
304+
["ticket"] = "CFG-5"
305+
},
264306
Decisions =
265307
[
266308
MutationRequestDecision.Lifecycle(
@@ -293,7 +335,13 @@ private static MutationRequest CreateExecutedRequest(
293335
{
294336
OperationName = "ExampleOperation",
295337
Category = category,
296-
Description = "Executed governed request"
338+
Description = "Executed governed request",
339+
Tags = new HashSet<string> { category.ToLowerInvariant(), "executed" },
340+
EstimatedBlastRadius = BlastRadius.Single,
341+
Metadata = new Dictionary<string, object>
342+
{
343+
["risk-owner"] = "finance"
344+
}
297345
},
298346
context: MutationContext.Service("governance-runtime", "Execute approved request"))
299347
with
@@ -303,6 +351,10 @@ private static MutationRequest CreateExecutedRequest(
303351
CreatedAt = executedAt.AddMinutes(-15),
304352
UpdatedAt = executedAt,
305353
ExecutedAt = executedAt,
354+
Metadata = new Dictionary<string, object>
355+
{
356+
["ticket"] = "BILL-22"
357+
},
306358
Decisions =
307359
[
308360
MutationRequestDecision.Lifecycle(

Examples/Governance/Queries/Scenarios/RequestQueryScenario.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using ModularityKit.Mutator.Governance.Abstractions.Queries.Contracts;
2-
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model;
2+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Requests;
3+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Requests.Filters;
34
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
5+
using ModularityKit.Mutator.Abstractions.Intent;
46

57
namespace Queries.Scenarios;
68

@@ -14,19 +16,43 @@ public static async Task Run(IMutationRequestQueryStore queryStore)
1416
GovernanceQueriesSampleData.PrintSection("Pending External Check Requests");
1517
GovernanceQueriesSampleData.PrintRequests(await queryStore.GetPendingRequestsAsync(new MutationRequestQuery
1618
{
17-
PendingReasons = new HashSet<PendingMutationReason> { PendingMutationReason.ExternalCheck }
19+
Lifecycle = new MutationRequestLifecycleFilter
20+
{
21+
PendingReasons = new HashSet<PendingMutationReason> { PendingMutationReason.ExternalCheck }
22+
}
1823
}));
1924

2025
GovernanceQueriesSampleData.PrintSection("Billing Requests");
2126
GovernanceQueriesSampleData.PrintRequests(await queryStore.QueryAsync(new MutationRequestQuery
2227
{
23-
Categories = new HashSet<string> { "Billing" }
28+
Intent = new MutationRequestIntentFilter
29+
{
30+
Categories = new HashSet<string> { "Billing" }
31+
}
2432
}));
2533

2634
GovernanceQueriesSampleData.PrintSection("Requests For tenant-42:roles");
2735
GovernanceQueriesSampleData.PrintRequests(await queryStore.QueryAsync(new MutationRequestQuery
2836
{
29-
StateIds = new HashSet<string> { "tenant-42:roles" }
37+
Scope = new MutationRequestScopeFilter
38+
{
39+
StateIds = new HashSet<string> { "tenant-42:roles" }
40+
}
41+
}));
42+
43+
GovernanceQueriesSampleData.PrintSection("Metadata-classified Security Requests");
44+
GovernanceQueriesSampleData.PrintRequests(await queryStore.QueryAsync(new MutationRequestQuery
45+
{
46+
Intent = new MutationRequestIntentFilter
47+
{
48+
Tags = new HashSet<string> { "security" },
49+
Metadata = new Dictionary<string, object?> { ["risk-owner"] = "platform" },
50+
MinimumBlastRadiusScope = BlastRadiusScope.Module
51+
},
52+
Metadata = new MutationRequestMetadataFilter
53+
{
54+
Values = new Dictionary<string, object?> { ["ticket"] = "INC-42" }
55+
}
3056
}));
3157

3258
GovernanceQueriesSampleData.PrintSection("Recent Approval Driven Requests");

Examples/Governance/RedisQueries/Scenarios/GovernanceRedisQueriesScenario.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using ModularityKit.Mutator.Abstractions.Policies;
55
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
66
using ModularityKit.Mutator.Governance.Abstractions.Queries.Contracts;
7-
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model;
7+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Approvals;
8+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Decisions;
9+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model.Requests;
810
using ModularityKit.Mutator.Governance.Abstractions.Requests.Decisions;
911
using ModularityKit.Mutator.Governance.Abstractions.Requests.Factory;
1012
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;

Tests/ModularityKit.Mutator.Governance.Redis.Tests/Serialization/Converters/RedisMutationRequestSerializerTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public void Roundtrip_preserves_request_shape_needed_by_governance_runtime()
7373
Assert.Equal(request.Intent.Category, roundtrip.Intent.Category);
7474
Assert.Contains("security", roundtrip.Intent.Tags);
7575
Assert.Equal(BlastRadiusScope.Module, roundtrip.Intent.EstimatedBlastRadius?.Scope);
76+
Assert.Equal("platform", roundtrip.Intent.Metadata["risk-owner"]);
7677
Assert.Equal("security", roundtrip.Metadata["team"]);
7778
Assert.Single(roundtrip.Requirements);
7879
Assert.Single(roundtrip.ApprovalRequirements);

0 commit comments

Comments
 (0)