Skip to content

feat/aggregate reconstitute#291

Merged
gbrennon merged 86 commits into
mainfrom
feat/aggregate-reconstitute
Jul 24, 2026
Merged

feat/aggregate reconstitute#291
gbrennon merged 86 commits into
mainfrom
feat/aggregate-reconstitute

Conversation

@gbrennon

Copy link
Copy Markdown
Contributor
  • refactor: restructure Message into message/ subpackage with abstract from_payload_fields
  • refactor: replace Serializable with MessageCodec/DictMessageCodec in infrastructure
  • feat: add reconstitute classmethod to AggregateRoot
  • refactor: rename _from_payload_fields to from_payload_fields across tests and scripts
  • test: reorganize message tests into decorators, metadata, and serialization modules
  • docs: update reference docs for message and serialization API changes

gbrennon added 6 commits July 23, 2026 00:42
…from_payload_fields

Split the monolithic message.py into message/_message.py (base Message
class with abstract from_payload_fields), message/_metadata.py
(MessageMetadata), and message/__init__.py (public re-exports).

Make from_payload_fields @AbstractMethod on Command, Event, and Query
base classes. Update @event_dataclass / @command_dataclass /
@query_dataclass decorators to auto-patch the new method name.
…infrastructure

Remove the structural Serializable protocol. Introduce MessageCodec
(abstract encode/decode) and DictMessageCodec (concrete dict-based
implementation). Update infrastructure __init__.py exports to surface
the new codec types.
Add AggregateRoot.reconstitute(id, version, events) classmethod for
rebuilding aggregates from event store history. This replaces the
removed _validate_identity helper.

Update AggregateRepository to use reconstitute() when loading
aggregates, passing the aggregate type via constructor.
…ests and scripts

Match the renamed abstract method on Message. All test fakes and inline
subclasses now implement from_payload_fields (no leading underscore).
Scripts/release command updated identically.
…zation modules

Split test_serializable.py into focused test modules: test_decorators.py
(message dataclass decorator behavior), test_metadata.py
(MessageMetadata), and new infrastructure/serialization/ tests.

Update test_aggregate_root.py for reconstitute classmethod.
Update messages.md to reflect abstract from_payload_fields and
restructured message module. Update index.md and adapters.md to
reference MessageCodec/DictMessageCodec instead of Serializable.
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://forging-blocks-org.github.io/forging-blocks/pr-preview/pr-291/

Built to branch gh-pages at 2026-07-23 23:24 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@personal-reviewer personal-reviewer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review

Verdict: Changes Requested

The PR makes significant architectural improvements with clear separation of concerns and better documentation. However, the new reconstitute method requires corresponding tests to ensure its correctness. The decorator documentation could be slightly more explicit about inheritance handling.

Issues

  1. [testability] [MINOR] src/forging_blocks/domain/aggregate_root/aggregate_root.py

The new reconstitute classmethod in AggregateRoot lacks corresponding tests. This method is critical for aggregate reconstruction and should have unit tests verifying its behavior with different event sequences.

Current:

    @classmethod
    def reconstitute(
        cls,
        aggregate_id: TId,
        events: Sequence[Event[EventPayloadType]],
    ) -> Self:
        """Reconstitute an aggregate from a sequence of stored events."""
        instance = cls(aggregate_id)
        for event in events:
            instance.replay(event)
        return instance

Suggested:

Add unit tests in test/aggregate_root_test.py that:
1. Verify reconstitute with empty event list
2. Test with single event
3. Test with multiple events in order
4. Validate version increments correctly
  1. [design] [INFO] src/forging_blocks/foundation/messages/decorators.py

The message_dataclass decorator could benefit from clearer documentation about how it handles type variance and inheritance. The current docstring could be more explicit about how it interacts with abstract base classes.

Current:

def message_dataclass(
    frozen: bool = True,
    slots: bool = True,
) -> Callable[[type[_M]], type[_M]]:
    """Decorate a class as a message dataclass."""
    ...

Suggested:

Enhance the docstring to explicitly state: 'This decorator is compatible with ABCs and will properly handle abstract method inheritance when used with @abstractmethod'

Praise

  • src/forging_blocks/domain/aggregate_root/aggregate_root.py: The reimplementation of _validate_identity with clear error conditions demonstrates good defensive programming practices and proper error handling.

  • docs/reference/foundation/messages.md: The documentation update clearly explains the new codec architecture and removes outdated references to Serializable, maintaining accurate technical documentation.

Suggestions

No suggestions to apply.


Reviewed by code-review:latest

Explicitly describe how the decorator patches abstract methods
(_payload, value, from_payload_fields) inherited from Event, Command,
and Query bases, removing them from __abstractmethods__ so decorated
subclasses are instantiable without manual stubs.
…_dataclass

Replace markdown bold **ABC interaction:** header with plain prose paragraph, matching the project docstring style shown in value_object.py.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Forging Blocks’ messaging and serialization APIs by splitting Message into a dedicated message/ subpackage, replacing the old structural Serializable protocol with an explicit codec abstraction (MessageCodec + DictMessageCodec), and adding AggregateRoot.reconstitute() so repositories can rebuild aggregates from stored events. It also aligns tests, scripts, and reference docs to the new reconstruction method (from_payload_fields) and the codec-based serialization approach.

Changes:

  • Refactors foundation messages: moves Message/MessageMetadata into foundation/messages/message/ and standardizes reconstruction on from_payload_fields.
  • Replaces Serializable with MessageCodec and adds DictMessageCodec for dict-based message encoding/decoding.
  • Adds AggregateRoot.reconstitute() and updates AggregateRepository to rehydrate aggregates from the event store when not present in snapshot storage.

Reviewed changes

Copilot reviewed 42 out of 43 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/scripts/release/infrastructure/bus/test_release_event_bus.py Updates script-level bus tests to use from_payload_fields and MessageMetadata.
tests/forging_blocks/infrastructure/unit_of_work/test_in_memory_unit_of_work.py Renames _from_payload_fields to from_payload_fields in unit-of-work tests.
tests/forging_blocks/infrastructure/test_event_bus.py Updates event bus tests to the new reconstruction method name.
tests/forging_blocks/infrastructure/serialization/test_dict_message_codec.py Adds round-trip and edge-case tests for DictMessageCodec.
tests/forging_blocks/infrastructure/serialization/init.py Test package init adjustment for serialization test module organization.
tests/forging_blocks/infrastructure/repositories/test_aggregate_repository.py Updates repository tests for reconstitution behavior and new aggregate_type parameter.
tests/forging_blocks/infrastructure/message_bus/test_message_bus_query_fetcher.py Renames _from_payload_fields to from_payload_fields in message bus tests.
tests/forging_blocks/infrastructure/message_bus/test_message_bus_event_publisher.py Renames _from_payload_fields to from_payload_fields in message bus tests.
tests/forging_blocks/infrastructure/message_bus/test_message_bus_command_sender.py Renames _from_payload_fields to from_payload_fields in message bus tests.
tests/forging_blocks/infrastructure/message_bus/test_in_memory_message_bus.py Updates in-memory message bus tests to from_payload_fields.
tests/forging_blocks/foundation/messages/test_serializable.py Re-scopes tests from Serializable serialization to decorator/payload behavior.
tests/forging_blocks/foundation/messages/test_metadata.py Adds dedicated unit tests for MessageMetadata.
tests/forging_blocks/foundation/messages/test_message.py Updates Message tests for the new base class behavior and reconstruction naming.
tests/forging_blocks/foundation/messages/test_event.py Renames event reconstruction hook to from_payload_fields.
tests/forging_blocks/foundation/messages/test_decorators.py Extracts decorator internal tests into a dedicated module.
tests/forging_blocks/foundation/messages/test_command.py Updates command tests to from_payload_fields and trims old dict-serialization expectations.
tests/forging_blocks/domain/aggregate_root/test_aggregate_root.py Adds tests for AggregateRoot.reconstitute() and refines identity validation coverage.
tests/forging_blocks/application/ports/outbound/test_query_fetcher.py Updates outbound port tests to from_payload_fields.
tests/forging_blocks/application/ports/outbound/test_event_publisher.py Updates outbound port tests to from_payload_fields.
tests/forging_blocks/application/ports/outbound/test_command_sender.py Updates outbound port tests to from_payload_fields.
tests/fixtures/simple_fake_command.py Updates fixture to implement from_payload_fields.
tests/fixtures/simple_fake_command_with_value.py Updates fixture to implement from_payload_fields.
tests/fixtures/fake_event_with_value.py Updates fixture to implement from_payload_fields.
tests/fixtures/fake_event_with_name.py Updates fixture to implement from_payload_fields.
src/forging_blocks/infrastructure/serialization/serializable.py Removes the old Serializable protocol.
src/forging_blocks/infrastructure/serialization/_message_codec.py Introduces MessageCodec ABC (encode/decode contract).
src/forging_blocks/infrastructure/serialization/_dict_message_codec.py Adds dict-based message codec implementation.
src/forging_blocks/infrastructure/serialization/init.py Updates serialization exports to codec-based API.
src/forging_blocks/infrastructure/repositories/aggregate_repository.py Adds aggregate_type and reconstitutes aggregates from event store when needed.
src/forging_blocks/infrastructure/init.py Re-exports codecs from infrastructure package and updates module docs.
src/forging_blocks/foundation/messages/query.py Updates Query payload typing to be generic (not dict-only).
src/forging_blocks/foundation/messages/message/_metadata.py Moves MessageMetadata into the new message/ subpackage.
src/forging_blocks/foundation/messages/message/_message.py Moves Message base class into the new message/ subpackage and adds abstract from_payload_fields.
src/forging_blocks/foundation/messages/message/init.py Exposes Message and MessageMetadata from the new subpackage.
src/forging_blocks/foundation/messages/message.py Removes the old monolithic message module.
src/forging_blocks/foundation/messages/event.py Updates Event payload typing to be generic (not dict-only).
src/forging_blocks/foundation/messages/decorators.py Renames decorator wiring to from_payload_fields and patches abstractness accordingly.
src/forging_blocks/foundation/messages/command.py Updates Command docs to reflect broader “system” messaging terminology.
src/forging_blocks/domain/aggregate_root/aggregate_root.py Adds AggregateRoot.reconstitute() for event-sourced rebuilds.
scripts/release/domain/commands/open_pull_request_command.py Updates release script command reconstruction to from_payload_fields.
docs/reference/infrastructure/adapters.md Updates docs from Serializable to codec-based serialization.
docs/reference/index.md Updates reference index to mention MessageCodec/DictMessageCodec.
docs/reference/foundation/messages.md Updates message docs to reflect codec-based serialization and from_payload_fields.
Comments suppressed due to low confidence (3)

src/forging_blocks/foundation/messages/decorators.py:52

  • message_dataclass’s internal typing is inconsistent with the behavior: the decorator always produces a dict payload (get_payload_fields / from_payload_fields), but _M is currently bound to Message[Any]. Also, _PatchedMessage.from_payload_fields is annotated as returning type[Self], but it returns an instance (Self). Tightening these annotations prevents incorrect usage and helps pyright/ruff catch mistakes earlier.
_M = TypeVar("_M", bound="Message[Any]")


@runtime_checkable
class _PatchedMessage(Protocol):
    """Structural type describing a message class after decorator patching.

    This protocol allows pyright to verify that the patched attributes exist
    and have the correct signatures, replacing attribute assignment suppressions
    with a proper type-safe cast boundary.
    """

    def get_payload_fields(self) -> dict[str, object]: ...

    @classmethod
    def from_payload_fields(
        cls,
        data: dict[str, object],
        metadata: MessageMetadata,
    ) -> type[Self]: ...

src/forging_blocks/foundation/messages/query.py:38

  • Query._payload is now generic (QueryPayloadType), but the docstring still says it returns a dictionary. Updating the wording avoids misleading API documentation for non-dict payload queries.
    def _payload(self) -> QueryPayloadType:
        """Retrieve the query's payload as a dictionary.

        Subclasses MUST implement this property to return the query-specific
        data.
        """

src/forging_blocks/foundation/messages/event.py:51

  • Event._payload is now generic (RawEventType), but the docstring still says it returns a dictionary. Updating the wording keeps the public API docs accurate for non-dict payload events.
    @abstractmethod
    def _payload(self) -> RawEventType:
        """Retrieve the event's payload as a dictionary.

        Subclasses MUST implement this property to return the event-specific
        data.
        """

Comment on lines +13 to +22
Contains technical-level information about messages such as:
- Unique message identifier
- When the message was created
- correlation_id is used to trace related messages across systems.
- correlation_id is used to link messages that belong to the same business process.
- causation_id is used to identify the immediate predecessor message that caused

This separation allows messages to focus on foundational data while keeping
infrastructure handling concerns in metadata without foundation understand anything about
infrastructure rules.
Comment thread src/forging_blocks/infrastructure/serialization/_dict_message_codec.py Outdated
gbrennon added 5 commits July 23, 2026 02:46
… created_at fallback

- Constrain DictMessageCodec generic from Message[Any] to
  Message[dict[str, object]] to match the actual cast on message.value
- Remove .get() fallback to datetime.min for created_at metadata field;
  use direct key access consistent with message_id, causation_id, and
  correlation_id
- Fix _PatchedMessage.from_payload_fields return annotation from
  type[Self] to Self (classmethod returns an instance, not a type)
- Deduplicate correlation_id bullets in MessageMetadata docstring, merge
  into single item covering tracing and process linking
- Complete truncated causation_id sentence: 'that caused this message'
- Fix grammar: 'without needing to understand' instead of 'without
  foundation understand anything about'
- Update Event._payload and Query._payload docstrings from stale 'as a
  dictionary' to generic 'event/query-specific payload data'
…ghtening

The Message[Any] -> Message[dict[str, object]] change made message.value
already dict[str, object], so the cast became redundant.
…thods

Remove the internal _to_data/_from_data indirection in MessageCodec.
encode and decode are now @AbstractMethod directly — subclasses
implement them instead of the private helpers. DictMessageCodec renamed
its methods accordingly. Removed unused runtime_final import.
…metaclass

Since encode/decode are now the public abstract methods and no
@runtime_final methods remain, FinalABCMeta adds no value. Using ABC
directly is the idiomatic pattern for non-port ABCs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 43 changed files in this pull request and generated 3 comments.

Comment on lines +123 to +125
aggregate = self._aggregate_type.reconstitute(id, events)

return aggregate
Comment on lines +97 to +102
"""Get the timestamp when this message was created.

Returns:
When the message was created (UTC timezone).

"""
@@ -30,7 +29,7 @@ class OrderCreated(Event[dict[str, object]]):

from forging_blocks.foundation.messages.message import Message, MessageMetadata

_M = TypeVar("_M", bound="Message[dict[str, object]]")
_M = TypeVar("_M", bound="Message[Any]")
AggregateRepository.get_by_id() now saves the reconstituted aggregate
to the in-memory store so subsequent reads don't replay the full event
stream from the event store.
gbrennon added 20 commits July 23, 2026 16:03
…move

Update documentation to reflect the move of ValueObject, Messages, and
Specifications from Foundation to Domain:

- foundation.md: remove moved sections from Foundation overview
- domain.md: update overview to list new domain modules
- domain/value-objects.md, specifications.md: update origin references
- auto-eq.md, auto-freeze.md: fix value-objects.md cross-reference links
- Guide docs: align import examples with new domain module paths
AggregateRoot and AggregateVersion are now loaded via module __getattr__
to avoid the foundation → context → domain → aggregate_root → foundation
cycle introduced by moving ValueObject to the domain layer.
Remove stale Foundation nav entries for Messages, Value Objects, and
Specifications. Add Messages to Domain nav section.
…t safety

The circular import is real: foundation/__init__ imports
foundation.context.authorization_context which imports
domain.value_object which triggers domain/__init__.py which
attempts to import aggregate_root.aggregate_root which needs
forging_blocks.foundation.FinalABCMeta -- but foundation is
still loading. __getattr__ lazy-imports AggregateRoot and
AggregateVersion to break the cycle.
AuthorizationContext, ServiceContext, and TransactionContext are too
application-specific for the reusable library. Downstream code should
define its own context types and supply them via the generic type
parameters on PermissionChecker, AuthorizationPort, and
TransactionManagerPort.
ResourcePermissionChecker and RoleBasedPermissionChecker are too
application-specific for the reusable library. Downstream code should
implement PermissionChecker directly with its own permission logic.
…rameters

Replace deleted concrete context types with domain-appropriate
type parameters: PermissionCheckContext (permission checkers),
AuthorizationCheckContext (authorization port), and
TransactionSessionContext (transaction manager port).

Docs updated to reflect generic signatures and remove stale
references to deleted concrete classes.
…ePermissionChecker

Align shipped implementation with documented convention in docs/permissions.md.
Functionally equivalent — PermissionChecker is @runtime_checkable — but explicit
inheritance improves code navigation and removes a doc/code inconsistency.

@personal-reviewer personal-reviewer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review

Verdict: Approved

The PR demonstrates excellent refactoring with clear separation of concerns, improved documentation, and proper use of SOLID principles. The introduction of auto-decorators for immutability and the restructuring of message handling show strong architectural improvements. All changes appear well-tested and documented.

Issues

No issues found.

Praise

  • docs/reference/foundation/auto-decorators.md: Excellent introduction of @auto_freeze, @auto_eq, and @auto_hash decorators. These provide lightweight immutability and equality handling without requiring inheritance from base classes, improving flexibility and reducing boilerplate.

  • docs/reference/domain/messages.md: Clear separation of message types (Command, Event, Query) with explicit documentation. The refactoring to use MessageCodec in infrastructure layer adheres to Dependency Inversion Principle by decoupling message serialization from domain logic.

  • docs/reference/domain/permissions.md: Clean redesign of permission checkers with generic type parameters (PermissionChecker[PermissionCheckContext]). This improves testability and allows application-specific context implementations without modifying base classes.

Suggestions

No suggestions to apply.


Reviewed by code-review:latest

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 158 out of 168 changed files in this pull request and generated 4 comments.

f"in docstring code example (block line {line_no})"
)

return warnings
@@ -0,0 +1,42 @@
"""Message codec ABC for encoding/decoding foundation messages."""

def decode(self, data: dict[str, object], message_type: type[M]) -> M:
"""Decode *data* back into a message of *message_type*."""
raw_metadata = cast(dict[str, object], data["metadata"])
Comment thread scripts/generate_autodoc_pages.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 158 out of 168 changed files in this pull request and generated 6 comments.

Comment on lines +179 to +182
case ast.Import(names=names):
for alias in names:
imported[alias.asname or alias.name] = n.lineno
case ast.ImportFrom(names=names):
Comment on lines 265 to 266

@pytest.mark.unit
class TestMain:
@@ -0,0 +1,48 @@
"""The `auto_hash` decorator generates `__hash__` for class instances.
@@ -0,0 +1,33 @@
"""The `auto_freeze` decorator enforces immutability after `__init__` completes.
@@ -0,0 +1,43 @@
"""The `auto_eq` decorator generates `__eq__` for class instances.
@@ -0,0 +1,129 @@
"""Tests for message decorators and frozen instance behavior."""
@gbrennon
gbrennon merged commit fc9d9a2 into main Jul 24, 2026
13 checks passed
@gbrennon
gbrennon deleted the feat/aggregate-reconstitute branch July 24, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants