Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 56 additions & 12 deletions .github/workflows/publish-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ on:
description: "Optional package version, usually the release tag without the leading v."
required: false
type: string
core_version:
description: "Optional manual version for ModularityKit.Mutator."
required: false
type: string
governance_version:
description: "Optional manual version for ModularityKit.Mutator.Governance."
required: false
type: string
redis_version:
description: "Optional manual version for ModularityKit.Mutator.Governance.Redis."
required: false
type: string

permissions:
contents: read
Expand All @@ -28,37 +40,69 @@ jobs:
- name: Restore
run: dotnet restore ModularityKit.Mutator.slnx

- name: Resolve package version
- name: Resolve package versions
id: version
env:
PACKAGE_VERSION: ${{ inputs.package_version }}
CORE_VERSION: ${{ inputs.core_version }}
GOVERNANCE_VERSION: ${{ inputs.governance_version }}
REDIS_VERSION: ${{ inputs.redis_version }}
REF_NAME: ${{ github.ref_name }}
run: |
version="$PACKAGE_VERSION"
if [ -z "$version" ]; then
version="$REF_NAME"
fi
version="${version#v}"
if ! printf '%s' "$version" | grep -Eq '^[0-9]+(\.[0-9]+){1,2}([-+][0-9A-Za-z.-]+)?$'; then
version="0.1.0"
fi
echo "package_version=$version" >> "$GITHUB_OUTPUT"
resolve_version() {
local value="$1"
local fallback="$2"

if [ -z "$value" ]; then
value="$fallback"
fi

if [ -z "$value" ]; then
value="$REF_NAME"
fi

value="${value#v}"

if ! printf '%s' "$value" | grep -Eq '^[0-9]+(\.[0-9]+){1,2}([-+][0-9A-Za-z.-]+)?$'; then
value="0.1.0"
fi

printf '%s' "$value"
}

shared_version="$(resolve_version "$PACKAGE_VERSION" "")"
core_version="$(resolve_version "$CORE_VERSION" "$shared_version")"
governance_version="$(resolve_version "$GOVERNANCE_VERSION" "$shared_version")"
redis_version="$(resolve_version "$REDIS_VERSION" "$shared_version")"

echo "package_version=$shared_version" >> "$GITHUB_OUTPUT"
echo "core_version=$core_version" >> "$GITHUB_OUTPUT"
echo "governance_version=$governance_version" >> "$GITHUB_OUTPUT"
echo "redis_version=$redis_version" >> "$GITHUB_OUTPUT"

- name: Pack core package
run: >
dotnet pack src/ModularityKit.Mutator.csproj
-c Release
--no-restore
-o nupkg
-p:PackageVersion=${{ steps.version.outputs.package_version }}
-p:PackageVersion=${{ steps.version.outputs.core_version }}

- name: Pack governance package
run: >
dotnet pack src/ModularityKit.Mutator.Governance.csproj
-c Release
--no-restore
-o nupkg
-p:PackageVersion=${{ steps.version.outputs.package_version }}
-p:PackageVersion=${{ steps.version.outputs.governance_version }}

- name: Pack Redis governance package
run: >
dotnet pack src/Redis/ModularityKit.Mutator.Governance.Redis.csproj
-c Release
--no-restore
-o nupkg
-p:PackageVersion=${{ steps.version.outputs.redis_version }}

- name: Upload packages
uses: actions/upload-artifact@v4
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ on:
description: Release version without the leading "v"
required: false
type: string
core_version:
description: Optional manual version for ModularityKit.Mutator
required: false
type: string
governance_version:
description: Optional manual version for ModularityKit.Mutator.Governance
required: false
type: string
redis_version:
description: Optional manual version for ModularityKit.Mutator.Governance.Redis
required: false
type: string
publish_nuget:
description: Publish to NuGet.org
required: false
Expand All @@ -23,6 +35,18 @@ on:
description: Release version without the leading "v"
required: false
type: string
core_version:
description: Optional manual version for ModularityKit.Mutator
required: false
type: string
governance_version:
description: Optional manual version for ModularityKit.Mutator.Governance
required: false
type: string
redis_version:
description: Optional manual version for ModularityKit.Mutator.Governance.Redis
required: false
type: string
publish_nuget:
description: Publish to NuGet.org
required: false
Expand All @@ -41,6 +65,9 @@ jobs:
uses: ./.github/workflows/publish-artifacts.yml
with:
package_version: ${{ inputs.version }}
core_version: ${{ inputs.core_version }}
governance_version: ${{ inputs.governance_version }}
redis_version: ${{ inputs.redis_version }}

publish-nuget:
name: Publish to NuGet.org
Expand Down
80 changes: 80 additions & 0 deletions Docs/Decision/Adr/ADR_029_Governance_Redis_Provider_Package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ADR-029: Governance Redis Provider Package

## Tag
#adr_029

## Status
Accepted

## Date
2026-06-25

## Scope
ModularityKit.Mutator.Governance.Redis

## Context

The governance package already defines:

- durable mutation requests
- optimistic concurrency around request revisions
- request, approval, and decision query contracts
- an in-memory implementation suitable for tests and examples

What remained open was a first persistence provider that can move governance state beyond in-memory storage without coupling `ModularityKit.Mutator.Governance` to a database-specific implementation.

The first provider needs to support:

- durable request document storage
- optimistic concurrency for request updates
- query-oriented reads over governed request data
- simple application integration through DI

At the same time, this should remain separate from the governance abstractions package so provider-specific concerns do not leak into the base runtime API.

## Decision

Introduce a dedicated package:

- `ModularityKit.Mutator.Governance.Redis`

The provider should:

- implement `IMutationRequestStore`
- implement `IMutationRequestQueryStore`
- register through dedicated DI extensions
- keep all Redis-specific logic inside the provider package
- reuse governance query contracts instead of inventing a Redis-specific query surface

The provider should remain additive:

- `ModularityKit.Mutator.Governance` owns contracts and runtime semantics
- `ModularityKit.Mutator.Governance.Redis` owns Redis persistence and Redis-specific internal read/write mechanics

## Design Rationale

- Governance contracts should stay provider-neutral.
- Redis is a pragmatic first persistence backend for request-oriented workflows with simple document state and indexable queue views.
- Package separation allows future providers such as EF Core or PostgreSQL without changing governance abstractions.
- DI registration gives applications a low-friction way to switch from in-memory storage to Redis-backed storage.

## Consequences

### Positive

- Governance gets a real persistence provider beyond in-memory storage.
- Redis-backed request and query implementations can evolve independently from governance contracts.
- Future providers now have a clear packaging precedent.
- Examples and tests can exercise a provider-backed read side without changing core governance APIs.

### Negative

- Provider package maintenance adds another surface to version and test.
- Redis-specific internal design decisions must now be documented and kept coherent over time.
- Query behavior remains contract-compatible but may have different performance characteristics than future relational providers.

## Related ADRs

- ADR-019: Governance Package Separation
- ADR-022: Governance Request Decisions and Storage
- ADR-026: Governance Request Query API
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# ADR-030: Governance Redis Request Storage and Query Strategy

## Tag
#adr_030

## Status
Accepted

## Date
2026-06-25

## Scope
ModularityKit.Mutator.Governance.Redis

## Context

Once the Redis provider package exists, it still needs a concrete storage and read strategy.

Governed request data has two competing needs:

- writes should stay simple and durable
- queue-oriented operational reads should avoid a full scan of every request whenever possible

The provider also needs to preserve governance runtime semantics such as:

- optimistic concurrency by request revision
- storage-agnostic request query filtering
- approval and decision projections built from parent request state

Without an explicit strategy, the Redis provider could drift into ad hoc key naming, inconsistent indexing, or duplicated query behavior that no longer matches the governance abstractions.

## Decision

The Redis provider stores one serialized request document per request and maintains a small set of Redis secondary indexes for common request-oriented reads.

Storage shape:

- one request JSON document per `MutationRequest`
- one revision key per request for optimistic concurrency
- set indexes for:
- all request ids
- requests by `StateId`
- requests by `MutationRequestStatus`
- all pending requests
- pending requests by `PendingMutationReason`

Query shape:

- Redis index selection happens first through candidate-planning internals
- matching request documents are then loaded in bulk
- final filtering is applied through governance query evaluators, not Redis-specific ad hoc logic
- approval views and decision views are projected from loaded parent requests after candidate selection

Internal provider structure should remain decomposed:

- candidate planning and execution
- document key creation and payload loading
- document materialization
- read-side query orchestration

## Design Rationale

- Document-per-request storage maps naturally to the governance request model.
- Separate revision keys give a simple optimistic concurrency mechanism in Redis transactions.
- A small set of explicit secondary indexes improves the common queue and status reads without forcing the provider into a large custom indexing subsystem.
- Reusing governance evaluators keeps provider behavior aligned with in-memory and future providers.
- Internal decomposition makes Redis-specific read mechanics easier to evolve without turning one class into the entire provider.

## Consequences

### Positive

- Request writes stay simple and explicit.
- Common operational views such as pending queues and state/status slices can be narrowed through Redis sets.
- Query semantics remain aligned with governance abstractions because the final filter pass is evaluator-driven.
- Internal provider responsibilities are easier to test and evolve independently.

### Negative

- Broad ad hoc queries still fall back to loading candidate request documents and filtering in memory.
- Index maintenance increases write-path complexity compared to pure document storage.
- Additional indexes may be needed later for higher-volume provider scenarios.

## Related ADRs

- ADR-022: Governance Request Decisions and Storage
- ADR-026: Governance Request Query API
- ADR-029: Governance Redis Provider Package
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# ADR-031: Governance Redis Serialization and Document Compatibility

## Tag
#adr_031

## Status
Accepted

## Date
2026-06-25

## Scope
ModularityKit.Mutator.Governance.Redis

## Context

The Redis provider persists governed mutation requests as serialized documents.

That creates a compatibility boundary:

- request data must round-trip without losing governance semantics
- read models must tolerate heterogeneous metadata values
- provider internals must avoid a Redis-specific domain model fork
- future package versions should evolve the serialized shape deliberately, not accidentally

The governance request model already contains nested structures such as:

- mutation intent
- request metadata
- approval requirements
- decision history
- version resolution state

Without an explicit serialization decision, the provider could drift into:

- inconsistent JSON shapes across components
- fragile metadata handling for object-valued entries
- hidden coupling between runtime classes and Redis payload format

## Decision

The Redis provider serializes the existing governance request model directly as JSON documents and treats that JSON shape as the persisted document contract for the provider.

The provider should:

- serialize full `MutationRequest` documents rather than introduce a parallel storage DTO graph
- keep provider serialization centralized in dedicated Redis serialization components
- support heterogeneous metadata values through explicit converter handling
- keep document materialization inside provider internals, not spread across query code paths
- evolve document shape through deliberate package changes backed by ADRs when compatibility semantics change

## Design Rationale

- Reusing the governance model avoids translation layers that would duplicate request, approval, and decision semantics.
- Centralized serialization keeps Redis persistence mechanics consistent across reads and writes.
- Explicit converter support is necessary because governance metadata is intentionally flexible and may contain inferred object values.
- A single persisted document contract makes provider behavior easier to reason about in tests, examples, and future migrations.

## Consequences

### Positive

- Redis persistence stays aligned with governance runtime semantics.
- Serialization behavior is easier to test because all provider paths use the same document contract.
- Metadata and nested governance structures can round-trip without ad hoc per-query parsing.
- Future compatibility changes now have an explicit decision boundary.

### Negative

- The provider is coupled to the serialized shape of the governance request model.
- Backward-compatible evolution requires discipline when changing serialized request members.
- JSON document size grows with request history and approval detail.

## Related ADRs

- ADR-022: Governance Request Decisions and Storage
- ADR-029: Governance Redis Provider Package
- ADR-030: Governance Redis Request Storage and Query Strategy
Loading
Loading