diff --git a/Docs/Decision/Adr/ADR_019_Governance_Package_Separation.md b/Docs/Decision/Adr/ADR_019_Governance_Package_Separation.md new file mode 100644 index 0000000..8593d7b --- /dev/null +++ b/Docs/Decision/Adr/ADR_019_Governance_Package_Separation.md @@ -0,0 +1,96 @@ +# ADR-019: Governance Package Separation + +## Tag +#adr_019 + +## Status +Accepted + +## Date +2026-06-21 + +## Scope +ModularityKit.Mutator +ModularityKit.Mutator.Governance + +## Context + +`ModularityKit.Mutator` started as focused mutation runtime: + +- mutation execution +- policy evaluation +- audit and history basics +- side effects +- metrics and interception + +At the same time, the roadmap and emerging API gaps point toward broader governance model: + +- mutation requests +- pending execution lifecycle +- approval workflows +- version aware deferred execution +- governance-specific persistence and query capabilities +- compensation and resolution flows + +Those concerns are related to mutation execution, but they are not the same layer. + +If they are added directly into the core package, the main risk is that `ModularityKit.Mutator` turns from lightweight mutation runtime into heavy workflow and governance framework. That would make the core package harder to understand, harder to keep small, and harder to adopt for users who only need deterministic mutation execution. + +## Decision + +- Keep `ModularityKit.Mutator` as the core execution package. +- Introduce `ModularityKit.Mutator.Governance` as separate package in the same repository. +- Place governance specific abstractions and runtime components under the governance package rather than in the core runtime. +- Keep both packages in one repository for now, with separate project files and separate package boundaries. + +Current package split: + +### Core: `ModularityKit.Mutator` + +Responsible for: + +- mutation execution +- policy evaluation +- audit and history basics +- side effects +- metrics +- interceptor pipeline + +### Extension: `ModularityKit.Mutator.Governance` + +Responsible for: + +- `MutationRequest` +- pending mutation lifecycle +- request decisions and resolution +- approval-oriented contracts +- governance request storage contracts + +## Design Rationale + +- Preserves small and execution focused core package. +- Allows governance to evolve at different pace than the core runtime. +- Makes governance an opt in capability instead of default weight for all users. +- Keeps repo level development simple by avoiding an early split into multiple repositories. +- Provides a clean place for future packages such as persistence providers without polluting the core runtime surface. + +## Consequences + +### Positive + +- The mutation engine stays focused on direct execution concerns. +- Governance can grow into a richer model without forcing all consumers to adopt it. +- Future packages such as `EntityFrameworkCore` or `PostgreSql` governance providers have natural home. +- The repository structure now reflects the architectural boundary explicitly. + +### Negative + +- Some concepts will span package boundaries and require careful API ownership. +- Documentation must explain clearly when a feature belongs to core versus governance. +- Cross-package evolution will need discipline to avoid circular design pressure. + +## Follow up + +- Keep new governance runtime features out of `ModularityKit.Mutator` unless they are fundamental to direct execution. +- Grow pending lifecycle, approval flow, and request storage inside `ModularityKit.Mutator.Governance`. +- Revisit repo/package boundaries only if governance becomes large enough to justify separate repository in the future. diff --git a/Docs/Decision/Adr/ADR_020_Governance_MutationRequest_Model.md b/Docs/Decision/Adr/ADR_020_Governance_MutationRequest_Model.md new file mode 100644 index 0000000..c8298aa --- /dev/null +++ b/Docs/Decision/Adr/ADR_020_Governance_MutationRequest_Model.md @@ -0,0 +1,76 @@ +# ADR-020: Governance MutationRequest Model + +## Tag +#adr_020 + +## Status +Accepted + +## Date +2026-06-21 + +## Scope +ModularityKit.Mutator.Governance.Abstractions + +## Context + +The governance layer needs primary unit that can represent more than direct mutation execution. + +Core runtime mutations are immediate execution objects. Governance needs model that can survive over time and carry: + +- request identity +- state targeting +- mutation classification +- request context +- pending requirements +- request decisions +- version expectations + +Without such a model, deferred execution would have to be represented through ad hoc flags on mutation results, which is too weak for approval, expiration, cancellation, and re-resolution scenarios. + +## Decision + +- Introduce `MutationRequest` as the primary governance aggregate. +- `MutationRequest` carries: + - `RequestId` + - `StateId` + - `StateType` + - `MutationType` + - `Intent` + - `Context` + - `Status` + - `PendingReason` + - `Requirements` + - `Decisions` + - `ExpectedStateVersion` + - `ExpiresAt` + - `CreatedAt` + - `UpdatedAt` + - request-level `Metadata` +- Provide factory methods for: + - `Pending(...)` + - `Approved(...)` + +## Design Rationale + +- Gives governance durable model that is distinct from direct mutation execution. +- Keeps request lifecycle concerns out of the core `MutationResult`. +- Preserves enough context to support future approval, persistence, and re-execution flows. +- Makes version-aware deferred execution possible without leaking governance concerns into the core package. + +## Consequences + +### Positive + +- Governance now has clear primary aggregate. +- Future approval and pending lifecycle features have stable model to build on. +- Request level persistence and query APIs have a natural root entity. + +### Negative + +- Some mutation data is now represented in both core and governance layers, with different purposes. +- Governance logic will need discipline to avoid turning `MutationRequest` into an unbounded bag of state. + +## Related ADRs + +- ADR-019: Governance Package Separation diff --git a/Docs/Decision/Adr/ADR_021_Governance_Pending_Mutation_Lifecycle.md b/Docs/Decision/Adr/ADR_021_Governance_Pending_Mutation_Lifecycle.md new file mode 100644 index 0000000..f6c8b23 --- /dev/null +++ b/Docs/Decision/Adr/ADR_021_Governance_Pending_Mutation_Lifecycle.md @@ -0,0 +1,73 @@ +# ADR-021: Governance Pending Mutation Lifecycle + +## Tag +#adr_021 + +## Status +Accepted + +## Date +2026-06-21 + +## Scope +ModularityKit.Mutator.Governance.Abstractions + +## Context + +Not every governed mutation should execute immediately. + +Governance needs an explicit lifecycle for requests that are: + +- waiting for approval +- blocked on external checks +- delayed by scheduling +- waiting on dependencies +- held for quota or manual review + +If pending execution is represented only as denial or boolean flag, the runtime cannot express expiration, cancellation, superseding, or eventual execution. + +## Decision + +- Introduce `MutationRequestStatus` as the lifecycle status enum for governed requests. +- Supported statuses: + - `Created` + - `Pending` + - `Approved` + - `Rejected` + - `Canceled` + - `Expired` + - `Superseded` + - `Executed` +- Introduce `PendingMutationReason` to explain why a request is pending. +- Supported pending reasons: + - `Approval` + - `ExternalCheck` + - `Schedule` + - `Dependency` + - `Quota` + - `ManualReview` + +## Design Rationale + +- Separates lifecycle state from lifecycle cause. +- Makes pending execution first class governance concept instead of terminal failure state. +- Supports future query APIs such as “all pending approvals” or “all expired requests”. +- Allows approval flow to be one specialization of pending execution, rather than the only pending model. + +## Consequences + +### Positive + +- Governance can represent deferred execution explicitly. +- Approval workflows, external checks, and scheduling can share one lifecycle model. +- Future persistence and query layers have stable status dimensions to index. + +### Negative + +- Lifecycle semantics now need to be enforced consistently in runtime logic that does not exist yet. +- Some statuses, especially `Superseded` and `Executed`, will require careful definition once version-aware execution is implemented. + +## Related ADRs + +- ADR-019: Governance Package Separation +- ADR-020: Governance MutationRequest Model diff --git a/Docs/Decision/Adr/ADR_022_Governance_Request_Decisions_and_Storage.md b/Docs/Decision/Adr/ADR_022_Governance_Request_Decisions_and_Storage.md new file mode 100644 index 0000000..a41a462 --- /dev/null +++ b/Docs/Decision/Adr/ADR_022_Governance_Request_Decisions_and_Storage.md @@ -0,0 +1,68 @@ +# ADR-022: Governance Request Decisions and Storage + +## Tag +#adr_022 + +## Status +Accepted + +## Date +2026-06-21 + +## Scope +ModularityKit.Mutator.Governance.Abstractions +ModularityKit.Mutator.Governance.Runtime + +## Context + +Once mutation requests become durable governance objects, they need: + +- a history of lifecycle decisions +- a storage contract +- a minimal runtime implementation for local development and tests + +Without decisions, the request only shows current state and loses governance history. +Without a store contract, governance cannot grow into persistence or query features. + +## Decision + +- Introduce `MutationRequestDecision` as the record of lifecycle transition or governance action. +- Introduce `MutationRequestDecisionType` with: + - `Submitted` + - `Approved` + - `Rejected` + - `Canceled` + - `Expired` + - `Superseded` + - `Executed` +- Introduce `IMutationRequestStore` with operations for: + - storing request + - retrieving by request id + - retrieving by state id + - retrieving pending requests +- Provide `InMemoryMutationRequestStore` as the first runtime implementation. + +## Design Rationale + +- Keeps current request state and decision history together at the model level. +- Establishes a storage seam before adding provider specific persistence packages. +- Mirrors the existing core approach of starting with in-memory runtime implementations before introducing durable adapters. + +## Consequences + +### Positive + +- Governance now has durable decision log model. +- The storage contract is in place for future provider packages. +- Examples, tests, and local runtime flows can use `InMemoryMutationRequestStore` immediately. + +### Negative + +- Decision history and current status can drift if future runtime transitions are not applied carefully. +- Query capabilities are still minimal and intentionally incomplete at this stage. + +## Related ADRs + +- ADR-019: Governance Package Separation +- ADR-020: Governance MutationRequest Model +- ADR-021: Governance Pending Mutation Lifecycle diff --git a/Docs/Decision/Adr/ADR_023_Governance_Versioned_Request_Resolution.md b/Docs/Decision/Adr/ADR_023_Governance_Versioned_Request_Resolution.md new file mode 100644 index 0000000..46d6565 --- /dev/null +++ b/Docs/Decision/Adr/ADR_023_Governance_Versioned_Request_Resolution.md @@ -0,0 +1,71 @@ +# ADR-023: Governance Versioned Request Resolution + +## Tag +#adr_023 + +## Status +Proposed + +## Date +2026-06-21 + +## Scope +ModularityKit.Mutator.Governance + +## Context + +Pending mutation requests may be resolved long after they were created. + +Example: + +- request created against state version `v10` +- request enters `PendingApproval` +- approval happens after the state has advanced to `v15` + +At that point, governance must define what approval means: + +- execute against the latest state +- reject as stale +- require re-approval +- attempt re-validation and branch from there + +This is a governance concern, not just a core mutation concern, because it only appears once requests can survive beyond immediate execution. + +## Decision + +The governance package should adopt explicit version-aware request resolution semantics. + +Expected direction: + +- `MutationRequest` keeps an `ExpectedStateVersion` +- request resolution must compare current state version with expected version +- stale requests must not silently execute without an explicit rule +- runtime resolution should choose among: + - re-validate and execute against latest state + - reject as stale + - require renewed approval + +The exact resolution policy is intentionally left open for the first runtime implementation. + +## Design Rationale + +- Approval and deferred execution are not safe without explicit state version semantics. +- Silent execution on drifted state would weaken governance guarantees. +- The model already contains the right seam through `ExpectedStateVersion`. + +## Consequences + +### Positive + +- Governance runtime will have explicit semantics for stale approvals. +- Deferred execution becomes safer and more auditable. + +### Negative + +- This introduces additional policy and runtime complexity. +- Different domains may want different stale resolution strategies. + +## Related ADRs + +- ADR-020: Governance MutationRequest Model +- ADR-021: Governance Pending Mutation Lifecycle diff --git a/Docs/Decision/listadr.md b/Docs/Decision/listadr.md index 444be19..31564ae 100644 --- a/Docs/Decision/listadr.md +++ b/Docs/Decision/listadr.md @@ -3,6 +3,10 @@ This document lists all architectural decisions (ADRs) for **ModularityKit.Mutators**. See each ADR for full rationale, context, and decision details. +## Core + +These ADRs describe the base `ModularityKit.Mutator` runtime and its execution model. + | ADR | Title | Link | | ------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | | ADR-001 | StateChange and ChangeSet Model | [ADR-001](Adr/ADR_001_StateChange_ChangeSet_Model.md) | @@ -24,4 +28,16 @@ See each ADR for full rationale, context, and decision details. | ADR-017 | Mutation PolicyRegistry | [ADR-017](Adr/ADR_017_Mutation_PolicyRegistry.md) | | ADR-018 | Mutators DI Registration | [ADR-018](Adr/ADR_018_Mutators_DI_Registration.md) | +## Governance + +These ADRs describe the `ModularityKit.Mutator.Governance` extension layer and its request-based governance model. + +| ADR | Title | Link | +| ------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| ADR-019 | Governance Package Separation | [ADR-019](Adr/ADR_019_Governance_Package_Separation.md) | +| ADR-020 | Governance MutationRequest Model | [ADR-020](Adr/ADR_020_Governance_MutationRequest_Model.md) | +| ADR-021 | Governance Pending Mutation Lifecycle | [ADR-021](Adr/ADR_021_Governance_Pending_Mutation_Lifecycle.md) | +| ADR-022 | Governance Request Decisions and Storage | [ADR-022](Adr/ADR_022_Governance_Request_Decisions_and_Storage.md) | +| ADR-023 | Governance Versioned Request Resolution | [ADR-023](Adr/ADR_023_Governance_Versioned_Request_Resolution.md) | + > See individual ADRs for detailed context, decision rationale, and consequences. diff --git a/Docs/ExecutionModel.md b/Docs/ExecutionModel.md new file mode 100644 index 0000000..6fe96f9 --- /dev/null +++ b/Docs/ExecutionModel.md @@ -0,0 +1,140 @@ +# Execution Model + +This document describes the execution model that `ModularityKit.Mutator` is moving toward as governance features become first-class runtime capabilities. + +It complements the roadmap by explaining the lifecycle of a mutation once the engine supports pending execution, approvals, versioning, and compensation. + +## Current Model + +Today, the engine is centered around direct mutation execution: + +1. Receive a mutation +2. Evaluate policies +3. Validate +4. Execute or block +5. Audit +6. Persist history + +This model is strong for immediate execution flows, but it is intentionally narrow: + +- blocked mutations are terminal outcomes +- approval requirements are modeled but not yet lifecycle-driven +- concurrency is not yet part of a first-class execution contract +- compensation and re-execution are not yet modeled as governed transitions + +## Target Model + +As governance features expand, execution becomes a lifecycle rather than a single pass. + +The target shape is: + +1. Create a mutation request +2. Evaluate policies and requirements +3. Enter pending state when execution is deferred +4. Resolve requirements through approvals or external checks +5. Re-validate against the current state version +6. Execute or reject +7. Emit side effects +8. Audit and persist history +9. Optionally compensate or reverse + +This is the key conceptual shift in the project: + +- from direct mutation execution +- to governed mutation request execution + +## Core Runtime Concepts + +### Mutation Request + +Represents a request to execute a mutation under governance. + +Expected responsibilities: + +- carry the original mutation intent and context +- keep a stable request identifier +- track creation time, current status, and owning state +- record required approvals or checks +- retain the state version or snapshot contract used for re-evaluation + +### Pending Mutation + +Represents a mutation request that cannot execute immediately. + +Possible pending reasons: + +- `PendingApproval` +- `PendingExternalCheck` +- `PendingSchedule` +- `PendingDependency` +- `PendingQuota` + +Pending state should be treated as a first-class runtime state, not just a flag in `MutationResult`. + +### Resolution + +A pending mutation must eventually transition through an explicit resolution path. + +Possible outcomes: + +- approved and executed +- rejected +- canceled +- expired +- superseded by a newer request or state version + +### Versioned Execution + +Approval and deferred execution require explicit version handling. + +When a request is approved against a later state than the one originally evaluated, the runtime must define one of the following behaviors: + +- re-execute against the latest state after re-validation +- reject as stale +- require re-approval + +This behavior must be explicit and consistent across all pending mutation types. + +### Compensation + +Once the engine supports governed execution over time, compensation becomes part of the execution model rather than a simple utility. + +Compensation should describe: + +- what original execution it is linked to +- whether it is automatic or operator-triggered +- whether it restores prior state or applies a forward corrective mutation + +## Why This Needs Its Own Document + +The roadmap explains priorities and release grouping. + +The execution model explains semantics. + +That distinction matters because several features are tightly coupled: + +- pending mutation lifecycle +- approval workflow +- versioned execution +- concurrency control +- undo and compensation + +Without an explicit execution model, these features risk being implemented as isolated additions instead of one coherent runtime contract. + +## Design Pressure Points + +These are the architectural questions that should stay visible as implementation starts: + +- Is the primary unit of governance a mutation or a mutation request? +- What state version contract is required for deferred execution? +- When does a pending mutation become stale? +- Can approvals survive state drift, or must they be renewed? +- Are side effects emitted on request creation, on execution, or both? +- How are compensation flows represented in audit and history? + +## Relationship to the Roadmap + +- `v1.1 Governance Runtime` introduces pending mutation lifecycle, approval workflow, versioned execution, and concurrency control. +- `v1.2 Governance Data` adds persistence, queryability, metadata handling, and typed side effects around that runtime model. +- `v1.3 Integration` expands the model to async policy evaluation and external governance dependencies. +- `v2.0 Governance Platform` extends the lifecycle with compensation and richer policy composition. diff --git a/Docs/Roadmap.md b/Docs/Roadmap.md new file mode 100644 index 0000000..083d332 --- /dev/null +++ b/Docs/Roadmap.md @@ -0,0 +1,230 @@ +# Roadmap + +This document captures the most valuable next steps for `ModularityKit.Mutator` based on the current state of the API, runtime, and examples. + +The goal is not to add features for their own sake. The goal is to close gaps between the public model and the runtime behavior, then extend the engine into a more complete governance platform for state mutations. + +## Principles + +- Prefer features that complete existing abstractions over adding parallel APIs. +- Prioritize runtime behavior that the public API already implies. +- Keep new capabilities observable through audit, history, and examples. +- Add focused tests alongside each roadmap item before broadening the surface further. + +## Current Gaps + +These areas already exist in the model but are only partially realized in the runtime: + +- `PolicyRequirement` exists, but there is no approval lifecycle around it. +- `MutationIntent.IsReversible` exists, but there is no undo or compensation mechanism. +- `MutationEngineOptions.MaxConcurrentMutations` exists, but concurrency control is not enforced in runtime execution. +- `MutationIntent.EstimatedBlastRadius`, `Tags`, and `Metadata` exist, but the engine does not yet use them for governance or query workflows. +- The project has examples and benchmarks, but no dedicated test project yet. + +## v1.1 Governance Runtime + +Focus: establish a first-class governance runtime centered around mutation requests, pending execution, and version-aware decision making. + +### 1. Pending Mutation Lifecycle + +Add a first-class lifecycle around deferred mutation requests. + +Scope: + +- Introduce a `PendingMutation` or `MutationRequest` model for governed execution. +- Support pending reasons beyond approval, such as external checks, scheduling, or dependencies. +- Support approval expiration and explicit cancellation. +- Persist approval decisions and approval history. +- Define re-execution rules when a pending mutation is resolved against a newer state version. +- Support listing and resolving pending mutation records. + +Why this matters: + +- Approval workflow is only one specialization of pending execution. +- A single `PendingApproval` status in `MutationResult` is not enough once the engine owns a real governance process. + +### 2. Approval Workflow for `PolicyRequirement` + +Add first-class support for approval-based policy outcomes. + +Scope: + +- Introduce a pending approval result state for blocked mutations that require action rather than hard denial. +- Persist approval requirements to audit and history. +- Add follow-up mutations such as `ApproveRequirement` and `RejectRequirement`. +- Support multi-step approvals such as two-man approval and role-based approval chains. + +Why this matters: + +- The policy model already supports `RequireApproval`. +- This turns the engine from allow/deny enforcement into an actual governance workflow engine. + +### 3. Versioned Execution and Concurrency Control + +Add explicit optimistic concurrency handling around mutation execution. + +Scope: + +- Introduce version-aware mutation execution based on `stateId` and expected version. +- Use `ConcurrencyException` as a real runtime outcome instead of a dormant abstraction. +- Add optional lock-per-state execution for high-contention scenarios. +- Define how batch execution behaves when a mid-batch concurrency conflict occurs. + +Why this matters: + +- The library claims deterministic and async-safe behavior. +- Without explicit concurrency semantics, that promise is incomplete for shared state workloads. + +## v1.2 Governance Data + +Focus: make governed execution queryable, persistent, and classification-aware. + +### 1. Persistent History and Audit Stores + +Add production-ready adapters beyond in-memory implementations. + +Scope: + +- Entity Framework Core store for audit and history. +- PostgreSQL-oriented store or provider package. +- Optional Redis-backed recent-history cache for hot paths. + +Why this matters: + +- In-memory implementations are suitable for examples, tests, and development only. +- Production integration is the next natural adoption step. + +### 2. Query API for Audit and History + +Expose richer retrieval primitives than state-id-only history lookup. + +Scope: + +- Query by `actorId`, `category`, `riskLevel`, `sideEffectSeverity`, and time range. +- Query by `tags`, governance metadata, and estimated blast radius. +- Support recent activity queries and filtered timelines. +- Add approval-oriented queries such as pending approvals and recent approval decisions. +- Support risk-oriented filtering and reporting views. +- Define storage-agnostic query contracts first, then implement provider-specific adapters. + +Why this matters: + +- Audit and history become much more useful once users can answer operational questions, not just replay a single state stream. +- Persistence without queryability is only a storage layer, not an operational feature. + +### 3. Governance Metadata + +Turn intent classification fields into runtime-usable governance data. + +Scope: + +- Persist `Tags`, `Metadata`, and `EstimatedBlastRadius` into audit and history records. +- Expose them through query APIs. +- Enable policy decisions and reporting based on governance metadata. + +Why this matters: + +- These fields already exist in the public model. +- The roadmap should explicitly close the gap instead of only calling it out in `Current Gaps`. + +### 4. Typed Side Effects + +Evolve side effects beyond `object? Data`. + +Scope: + +- Add a typed side effect variant such as `SideEffect`, or +- Add serialization and registration contracts for side effect payloads. + +Why this matters: + +- Persistence and queryability make side effect payload contracts much more important. +- This is the point where side effects stop being just runtime output and become integration data. + +## v1.3 Integration + +Focus: connect governance runtime behaviors to external systems and asynchronous policy sources. + +### 1. Async Policies + +Support policy evaluation that depends on external systems. + +Scope: + +- Introduce `EvaluateAsync(...)` policy support. +- Preserve a sync path for lightweight policies. +- Define ordering and timeout semantics when multiple async policies are involved. +- Allow approval and governance checks to rely on external identity, ticketing, or compliance systems. + +Why this matters: + +- Real approval and compliance checks often depend on external identity, ticketing, quota, or feature control systems. + +## v2.0 Governance Platform + +Focus: make the engine a complete platform for governed mutations. + +### 1. Undo and Compensation + +Add explicit reversal and compensation support for reversible mutations. + +Scope: + +- Introduce a reversible mutation contract or compensation contract. +- Record links between original execution and compensating execution. +- Support compensation plans for failed batches and operator-driven rollback flows. + +Why this matters: + +- `MutationIntent.IsReversible` already exists. +- This is one of the biggest jumps in real operational value for change governance. + +### 2. Governance-Aware Policy Composition + +Add composition primitives for complex policy sets. + +Scope: + +- `AllOf`, `AnyOf`, and priority-based composition. +- Merging rules for severity, requirements, side effects, and metadata. +- Clear conflict rules when multiple policies modify the same mutation result. + +Why this matters: + +- Today, policy complexity is pushed into handwritten policy classes. +- Composition will make complex governance easier to express and reuse. + +## Cross-Cutting Work + +These tasks should accompany every milestone: + +- Improve README accuracy where examples still drift from the current package and namespace layout. +- Keep `Examples/` aligned with newly added runtime features. +- Extend `Benchmarks/` when new runtime behavior could affect hot paths. +- Maintain comprehensive test coverage for all runtime behaviors. +- Add regression tests for every bug fix. +- Require tests for all new governance features. +- Add ADRs for every public or behavioral change that alters execution semantics. + +## Execution Model + +The longer-term lifecycle model behind these milestones is documented in [`Docs/ExecutionModel.md`](ExecutionModel.md). + +## Recommended Build Order + +1. Add pending mutation lifecycle support. +2. Implement approval workflow support for `PolicyRequirement`. +3. Add versioned execution and concurrency handling to the runtime. +4. Add persistent audit/history providers. +5. Add query APIs over persisted governance data. +6. Persist and expose governance metadata. +7. Add typed side effects once persistence and query contracts are stable. +8. Add async policy support, unless external approval integrations force it earlier. +9. Add undo/compensation support once approval and persistence semantics are stable. + +## Not Recommended Yet + +These ideas may be useful later, but they are not the best next investment: + +- Distributed execution features before concurrency semantics are explicit. +- More examples before the runtime contract around approvals and concurrency is complete. diff --git a/ModularityKit.Mutator.slnx b/ModularityKit.Mutator.slnx index 61e8d8b..140b420 100644 --- a/ModularityKit.Mutator.slnx +++ b/ModularityKit.Mutator.slnx @@ -1,5 +1,6 @@ + diff --git a/README.md b/README.md index 5a833d0..9093646 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,11 @@ Controls which mutations are allowed. - `MutationResult` – Wraps the new state and change set - `ChangeSet` – Captures state modifications +## Package Layout + +- `ModularityKit.Mutator` - core mutation runtime, policies, audit, history, and side effects +- [`ModularityKit.Mutator.Governance`](src/Governance/README.md) - governed mutation request lifecycle, pending execution, and approval-oriented contracts + --- ## Metrics & Logging @@ -142,3 +147,7 @@ Key architectural decisions for **ModularityKit.Mutators** are tracked as ADRs. | ADR-005 | Mutation Audit Abstractions | Structured, immutable audit entries capturing intent, context, changes, and policy decisions | See full ADR documentation in [`Docs/Decision/Adr`](Docs/Decision/listadr) for details on each architectural decision. + +## Roadmap + +The planned evolution of the engine is documented in [`Docs/Roadmap.md`](Docs/Roadmap.md). diff --git a/src/Governance/Abstractions/Lifecycle/MutationRequestStatus.cs b/src/Governance/Abstractions/Lifecycle/MutationRequestStatus.cs new file mode 100644 index 0000000..6d352c6 --- /dev/null +++ b/src/Governance/Abstractions/Lifecycle/MutationRequestStatus.cs @@ -0,0 +1,16 @@ +namespace ModularityKit.Mutator.Governance; + +/// +/// Represents the lifecycle status of governed mutation request. +/// +public enum MutationRequestStatus +{ + Created = 0, + Pending = 1, + Approved = 2, + Rejected = 3, + Canceled = 4, + Expired = 5, + Superseded = 6, + Executed = 7 +} diff --git a/src/Governance/Abstractions/Lifecycle/PendingMutationReason.cs b/src/Governance/Abstractions/Lifecycle/PendingMutationReason.cs new file mode 100644 index 0000000..dfc5270 --- /dev/null +++ b/src/Governance/Abstractions/Lifecycle/PendingMutationReason.cs @@ -0,0 +1,14 @@ +namespace ModularityKit.Mutator.Governance; + +/// +/// Describes why a mutation request cannot execute immediately. +/// +public enum PendingMutationReason +{ + Approval = 0, + ExternalCheck = 1, + Schedule = 2, + Dependency = 3, + Quota = 4, + ManualReview = 5 +} diff --git a/src/Governance/Abstractions/Requests/MutationRequest.cs b/src/Governance/Abstractions/Requests/MutationRequest.cs new file mode 100644 index 0000000..94dbeb7 --- /dev/null +++ b/src/Governance/Abstractions/Requests/MutationRequest.cs @@ -0,0 +1,160 @@ +using ModularityKit.Mutator.Abstractions.Context; +using ModularityKit.Mutator.Abstractions.Intent; +using ModularityKit.Mutator.Abstractions.Policies; + +namespace ModularityKit.Mutator.Governance; + +/// +/// Represents a governed mutation request that may execute immediately or enter a pending lifecycle. +/// +public sealed record MutationRequest +{ + /// + /// Stable identifier for the mutation request. + /// + public string RequestId { get; init; } = Guid.NewGuid().ToString(); + + /// + /// Identifier of the state targeted by this request. + /// + public string StateId { get; init; } = string.Empty; + + /// + /// Logical state type targeted by the request. + /// + public string StateType { get; init; } = string.Empty; + + /// + /// CLR type name of the underlying mutation. + /// + public string MutationType { get; init; } = string.Empty; + + /// + /// Intent associated with the requested mutation. + /// + public MutationIntent Intent { get; init; } = null!; + + /// + /// Request context describing who requested the mutation and why. + /// + public MutationContext Context { get; init; } = null!; + + /// + /// Current lifecycle status of the request. + /// + public MutationRequestStatus Status { get; init; } = MutationRequestStatus.Created; + + /// + /// Reason why the request is pending, if it has not executed yet. + /// + public PendingMutationReason? PendingReason { get; init; } + + /// + /// Requirements that must be fulfilled before execution may proceed. + /// + public IReadOnlyList Requirements { get; init; } = []; + + /// + /// Governance decisions recorded against this request over time. + /// + public IReadOnlyList Decisions { get; init; } = []; + + /// + /// Expected version or concurrency token for the target state. + /// + public string? ExpectedStateVersion { get; init; } + + /// + /// Optional expiration time for pending requests. + /// + public DateTimeOffset? ExpiresAt { get; init; } + + /// + /// Timestamp when the request was first created. + /// + public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow; + + /// + /// Timestamp of the last lifecycle update applied to the request. + /// + public DateTimeOffset UpdatedAt { get; init; } = DateTimeOffset.UtcNow; + + /// + /// Additional governance metadata carried by the request. + /// + public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); + + /// + /// Creates a request that should enter the pending lifecycle. + /// + public static MutationRequest Pending( + string stateId, + string stateType, + string mutationType, + MutationIntent intent, + MutationContext context, + PendingMutationReason pendingReason, + IReadOnlyList? requirements = null, + string? expectedStateVersion = null, + DateTimeOffset? expiresAt = null, + IReadOnlyDictionary? metadata = null) + { + return new MutationRequest + { + StateId = stateId, + StateType = stateType, + MutationType = mutationType, + Intent = intent, + Context = context, + Status = MutationRequestStatus.Pending, + PendingReason = pendingReason, + Requirements = requirements ?? [], + ExpectedStateVersion = expectedStateVersion, + ExpiresAt = expiresAt, + Metadata = metadata ?? new Dictionary(), + Decisions = + [ + MutationRequestDecision.Create( + MutationRequestDecisionType.Submitted, + context, + reason: context.Reason) + ] + }; + } + + /// + /// Creates a request that is immediately approved for execution. + /// + public static MutationRequest Approved( + string stateId, + string stateType, + string mutationType, + MutationIntent intent, + MutationContext context, + string? expectedStateVersion = null, + IReadOnlyDictionary? metadata = null) + { + return new MutationRequest + { + StateId = stateId, + StateType = stateType, + MutationType = mutationType, + Intent = intent, + Context = context, + Status = MutationRequestStatus.Approved, + ExpectedStateVersion = expectedStateVersion, + Metadata = metadata ?? new Dictionary(), + Decisions = + [ + MutationRequestDecision.Create( + MutationRequestDecisionType.Submitted, + context, + reason: context.Reason), + MutationRequestDecision.Create( + MutationRequestDecisionType.Approved, + context, + reason: "Approved at submission time") + ] + }; + } +} diff --git a/src/Governance/Abstractions/Requests/MutationRequestDecision.cs b/src/Governance/Abstractions/Requests/MutationRequestDecision.cs new file mode 100644 index 0000000..64bb447 --- /dev/null +++ b/src/Governance/Abstractions/Requests/MutationRequestDecision.cs @@ -0,0 +1,50 @@ +using ModularityKit.Mutator.Abstractions.Context; + +namespace ModularityKit.Mutator.Governance; + +/// +/// Captures a single decision or lifecycle transition applied to a mutation request. +/// +public sealed record MutationRequestDecision +{ + /// + /// Type of the decision that was taken. + /// + public MutationRequestDecisionType Type { get; init; } + + /// + /// Context of the actor or system that recorded the decision. + /// + public MutationContext Context { get; init; } = null!; + + /// + /// Optional human-readable reason for the decision. + /// + public string? Reason { get; init; } + + /// + /// Timestamp at which the decision was recorded. + /// + public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow; + + /// + /// Optional metadata for governance integrations or diagnostics. + /// + public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); + + /// + /// Creates a new request decision entry. + /// + public static MutationRequestDecision Create( + MutationRequestDecisionType type, + MutationContext context, + string? reason = null, + IReadOnlyDictionary? metadata = null) + => new() + { + Type = type, + Context = context, + Reason = reason, + Metadata = metadata ?? new Dictionary() + }; +} diff --git a/src/Governance/Abstractions/Requests/MutationRequestDecisionType.cs b/src/Governance/Abstractions/Requests/MutationRequestDecisionType.cs new file mode 100644 index 0000000..ebe7eda --- /dev/null +++ b/src/Governance/Abstractions/Requests/MutationRequestDecisionType.cs @@ -0,0 +1,15 @@ +namespace ModularityKit.Mutator.Governance; + +/// +/// Represents a governance decision taken against a mutation request. +/// +public enum MutationRequestDecisionType +{ + Submitted = 0, + Approved = 1, + Rejected = 2, + Canceled = 3, + Expired = 4, + Superseded = 5, + Executed = 6 +} diff --git a/src/Governance/Abstractions/Storage/IMutationRequestStore.cs b/src/Governance/Abstractions/Storage/IMutationRequestStore.cs new file mode 100644 index 0000000..f3beffd --- /dev/null +++ b/src/Governance/Abstractions/Storage/IMutationRequestStore.cs @@ -0,0 +1,35 @@ +namespace ModularityKit.Mutator.Governance; + +/// +/// Stores and retrieves governed mutation requests. +/// +public interface IMutationRequestStore +{ + /// + /// Stores or updates a mutation request. + /// + Task StoreAsync( + MutationRequest request, + CancellationToken cancellationToken = default); + + /// + /// Retrieves a single mutation request by its stable identifier. + /// + Task GetAsync( + string requestId, + CancellationToken cancellationToken = default); + + /// + /// Retrieves all requests for a given state. + /// + Task> GetByStateIdAsync( + string stateId, + CancellationToken cancellationToken = default); + + /// + /// Retrieves pending requests, optionally filtered by pending reason. + /// + Task> GetPendingAsync( + PendingMutationReason? reason = null, + CancellationToken cancellationToken = default); +} diff --git a/src/Governance/README.md b/src/Governance/README.md new file mode 100644 index 0000000..d187f07 --- /dev/null +++ b/src/Governance/README.md @@ -0,0 +1,76 @@ +# ModularityKit.Mutator.Governance + +`ModularityKit.Mutator.Governance` is the governance focused extension layer for `ModularityKit.Mutator`. + +The core package stays responsible for direct mutation execution. Governance builds on top of that runtime with request based lifecycle concepts such as deferred execution, approvals, and request storage. + +## Features + +- **Mutation Requests** - model governed mutation submission as a durable request +- **Pending Lifecycle** - represent requests that cannot execute immediately +- **Decision History** - record approvals, rejections, cancellations, and other lifecycle transitions +- **Request Storage Contracts** - define a persistence seam for governance-oriented stores +- **In-Memory Runtime Support** - provide a lightweight request store for development and tests + +## Current Structure + +### Abstractions + +The package defines governance-first abstractions under: + +- `Abstractions/Requests` +- `Abstractions/Lifecycle` +- `Abstractions/Storage` + +Key types: + +- `MutationRequest` +- `MutationRequestDecision` +- `MutationRequestDecisionType` +- `MutationRequestStatus` +- `PendingMutationReason` +- `IMutationRequestStore` + +### Runtime + +The initial runtime layer currently provides: + +- `InMemoryMutationRequestStore` + +This keeps the first version small while leaving room for later persistence providers such as Entity Framework Core or PostgreSQL-backed governance stores. + +## Relationship to Core + +### `ModularityKit.Mutator` + +Responsible for: + +- mutation execution +- policy evaluation +- audit and history basics +- side effects +- metrics and interception + +### `ModularityKit.Mutator.Governance` + +Responsible for: + +- mutation request lifecycle +- pending execution modeling +- approval oriented governance contracts +- request decision history +- governance specific storage and future query seams + +## Direction + +This package is intentionally the place where broader governance behavior should grow. + +That includes future work such as: + +- pending mutation resolution +- approval workflow execution +- version aware deferred execution +- governance persistence providers +- governance query APIs + +The goal is to keep the core runtime small and execution focused while letting governance evolve as an opt-in extension. diff --git a/src/Governance/Runtime/InMemoryMutationRequestStore.cs b/src/Governance/Runtime/InMemoryMutationRequestStore.cs new file mode 100644 index 0000000..761293b --- /dev/null +++ b/src/Governance/Runtime/InMemoryMutationRequestStore.cs @@ -0,0 +1,70 @@ +using ModularityKit.Mutator.Governance; + +namespace ModularityKit.Mutator.Governance.Runtime; + +/// +/// In-memory store for governance mutation requests. +/// Suitable for examples, tests, and local development. +/// +public sealed class InMemoryMutationRequestStore : IMutationRequestStore +{ + private readonly Dictionary _requests = new(); + private readonly Lock _lock = new(); + + public Task StoreAsync( + MutationRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + + lock (_lock) + { + _requests[request.RequestId] = request; + } + + return Task.CompletedTask; + } + + public Task GetAsync( + string requestId, + CancellationToken cancellationToken = default) + { + lock (_lock) + { + _requests.TryGetValue(requestId, out var request); + return Task.FromResult(request); + } + } + + public Task> GetByStateIdAsync( + string stateId, + CancellationToken cancellationToken = default) + { + lock (_lock) + { + var requests = _requests.Values + .Where(request => request.StateId == stateId) + .OrderBy(request => request.CreatedAt) + .ToList(); + + return Task.FromResult>(requests); + } + } + + public Task> GetPendingAsync( + PendingMutationReason? reason = null, + CancellationToken cancellationToken = default) + { + lock (_lock) + { + var requests = _requests.Values + .Where(request => + request.Status == MutationRequestStatus.Pending && + (reason is null || request.PendingReason == reason)) + .OrderBy(request => request.CreatedAt) + .ToList(); + + return Task.FromResult>(requests); + } + } +} diff --git a/src/ModularityKit.Mutator.Governance.csproj b/src/ModularityKit.Mutator.Governance.csproj new file mode 100644 index 0000000..a8b79c0 --- /dev/null +++ b/src/ModularityKit.Mutator.Governance.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + diff --git a/src/ModularityKit.Mutator.csproj b/src/ModularityKit.Mutator.csproj index d9f719d..c17a51c 100644 --- a/src/ModularityKit.Mutator.csproj +++ b/src/ModularityKit.Mutator.csproj @@ -6,6 +6,10 @@ enable + + + +