fix(domain): add repository protocol interfaces to domain layer
CI / lint (pull_request) Failing after 21s
CI / typecheck (pull_request) Successful in 52s
CI / security (pull_request) Successful in 1m1s
CI / quality (pull_request) Successful in 34s
CI / coverage (pull_request) Has been skipped
CI / build (pull_request) Successful in 23s
CI / helm (pull_request) Successful in 24s
CI / unit_tests (pull_request) Successful in 6m24s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 17m20s
CI / integration_tests (pull_request) Successful in 23m6s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped

Implemented domain-level repository protocol interfaces to restore clean
architecture boundaries and enable robust type checks via runtime
structural typing.

- Created domain/repositories package with four protocol interfaces
- Added LifecyclePlanRepositoryProtocol, ActionRepositoryProtocol,
  DecisionRepositoryProtocol, and ProjectRepositoryProtocol
- Updated infrastructure repositories to explicitly inherit protocols
- Added 10 Behave scenarios for protocol compliance verification

ISSUES CLOSED: #2873
This commit is contained in:
2026-04-05 04:14:51 +00:00
parent bbff42ac9a
commit faf7991a13
8 changed files with 866 additions and 4 deletions
@@ -0,0 +1,78 @@
Feature: Domain Repository Protocols
As a developer maintaining the CleverAgents clean architecture
I want domain repository protocols defined in the domain layer
So that application services depend on abstractions, not infrastructure implementations
Background:
Given the domain repository protocols are importable
Scenario: LifecyclePlanRepositoryProtocol is defined in the domain layer
When I inspect the LifecyclePlanRepositoryProtocol
Then it should be a runtime-checkable Protocol
And it should define a "create" method
And it should define a "get" method
And it should define a "update" method
And it should define a "delete" method
And it should define a "list_all" method
Scenario: ActionRepositoryProtocol is defined in the domain layer
When I inspect the ActionRepositoryProtocol
Then it should be a runtime-checkable Protocol
And it should define a "create" method
And it should define a "get_by_id" method
And it should define a "update" method
And it should define a "delete" method
And it should define a "list_all" method
Scenario: DecisionRepositoryProtocol is defined in the domain layer
When I inspect the DecisionRepositoryProtocol
Then it should be a runtime-checkable Protocol
And it should define a "create" method
And it should define a "get" method
And it should define a "get_by_plan" method
And it should define a "get_tree" method
Scenario: ProjectRepositoryProtocol is defined in the domain layer
When I inspect the ProjectRepositoryProtocol
Then it should be a runtime-checkable Protocol
And it should define a "create" method
And it should define a "get" method
And it should define a "update" method
And it should define a "delete" method
Scenario: LifecyclePlanRepository satisfies LifecyclePlanRepositoryProtocol
Given the infrastructure LifecyclePlanRepository class
When I check if it satisfies LifecyclePlanRepositoryProtocol
Then the isinstance check should pass
Scenario: ActionRepository satisfies ActionRepositoryProtocol
Given the infrastructure ActionRepository class
When I check if it satisfies ActionRepositoryProtocol
Then the isinstance check should pass
Scenario: DecisionRepository satisfies DecisionRepositoryProtocol
Given the infrastructure DecisionRepository class
When I check if it satisfies DecisionRepositoryProtocol
Then the isinstance check should pass
Scenario: NamespacedProjectRepository satisfies ProjectRepositoryProtocol
Given the infrastructure NamespacedProjectRepository class
When I check if it satisfies ProjectRepositoryProtocol
Then the isinstance check should pass
Scenario: Domain repository protocols are exported from the domain package
When I import from cleveragents.domain.repositories
Then LifecyclePlanRepositoryProtocol should be available
And ActionRepositoryProtocol should be available
And DecisionRepositoryProtocol should be available
And ProjectRepositoryProtocol should be available
Scenario: Protocol modules are in the domain layer, not infrastructure
When I check the module path of LifecyclePlanRepositoryProtocol
Then the module should be under "cleveragents.domain"
When I check the module path of ActionRepositoryProtocol
Then the module should be under "cleveragents.domain"
When I check the module path of DecisionRepositoryProtocol
Then the module should be under "cleveragents.domain"
When I check the module path of ProjectRepositoryProtocol
Then the module should be under "cleveragents.domain"
@@ -0,0 +1,252 @@
"""Behave step definitions for domain repository protocol compliance tests.
Verifies that:
1. Protocol interfaces are defined in the domain layer (not infrastructure)
2. Infrastructure repositories satisfy the domain protocols
3. All protocols are runtime-checkable
"""
from __future__ import annotations
from typing import Any
from behave import given, then, when
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the domain repository protocols are importable")
def step_protocols_importable(context: Any) -> None:
"""Verify all domain repository protocols can be imported."""
from cleveragents.domain.repositories import (
ActionRepositoryProtocol,
DecisionRepositoryProtocol,
LifecyclePlanRepositoryProtocol,
ProjectRepositoryProtocol,
)
context.protocols = {
"LifecyclePlanRepositoryProtocol": LifecyclePlanRepositoryProtocol,
"ActionRepositoryProtocol": ActionRepositoryProtocol,
"DecisionRepositoryProtocol": DecisionRepositoryProtocol,
"ProjectRepositoryProtocol": ProjectRepositoryProtocol,
}
context.current_protocol = None
# ---------------------------------------------------------------------------
# When — inspect protocol
# ---------------------------------------------------------------------------
@when("I inspect the LifecyclePlanRepositoryProtocol")
def step_inspect_plan_protocol(context: Any) -> None:
context.current_protocol = context.protocols["LifecyclePlanRepositoryProtocol"]
@when("I inspect the ActionRepositoryProtocol")
def step_inspect_action_protocol(context: Any) -> None:
context.current_protocol = context.protocols["ActionRepositoryProtocol"]
@when("I inspect the DecisionRepositoryProtocol")
def step_inspect_decision_protocol(context: Any) -> None:
context.current_protocol = context.protocols["DecisionRepositoryProtocol"]
@when("I inspect the ProjectRepositoryProtocol")
def step_inspect_project_protocol(context: Any) -> None:
context.current_protocol = context.protocols["ProjectRepositoryProtocol"]
# ---------------------------------------------------------------------------
# Then — protocol assertions
# ---------------------------------------------------------------------------
@then("it should be a runtime-checkable Protocol")
def step_is_runtime_checkable(context: Any) -> None:
"""Verify the protocol is decorated with @runtime_checkable."""
proto = context.current_protocol
assert hasattr(proto, "__protocol_attrs__") or hasattr(proto, "_is_protocol"), (
f"{proto} is not a Protocol"
)
is_runtime = getattr(proto, "_is_runtime_protocol", False)
assert is_runtime, f"{proto.__name__} is not @runtime_checkable"
@then('it should define a "{method_name}" method')
def step_defines_method(context: Any, method_name: str) -> None:
"""Verify the protocol defines the named method."""
proto = context.current_protocol
assert hasattr(proto, method_name), (
f"{proto.__name__} does not define method '{method_name}'"
)
assert callable(getattr(proto, method_name)), (
f"{proto.__name__}.{method_name} is not callable"
)
# ---------------------------------------------------------------------------
# Given — infrastructure classes
# ---------------------------------------------------------------------------
@given("the infrastructure LifecyclePlanRepository class")
def step_given_plan_repo(context: Any) -> None:
from cleveragents.infrastructure.database.repositories import (
LifecyclePlanRepository,
)
context.infra_class = LifecyclePlanRepository
context.check_protocol = context.protocols["LifecyclePlanRepositoryProtocol"]
@given("the infrastructure ActionRepository class")
def step_given_action_repo(context: Any) -> None:
from cleveragents.infrastructure.database.repositories import ActionRepository
context.infra_class = ActionRepository
context.check_protocol = context.protocols["ActionRepositoryProtocol"]
@given("the infrastructure DecisionRepository class")
def step_given_decision_repo(context: Any) -> None:
from cleveragents.infrastructure.database.repositories import DecisionRepository
context.infra_class = DecisionRepository
context.check_protocol = context.protocols["DecisionRepositoryProtocol"]
@given("the infrastructure NamespacedProjectRepository class")
def step_given_project_repo(context: Any) -> None:
from cleveragents.infrastructure.database.repositories import (
NamespacedProjectRepository,
)
context.infra_class = NamespacedProjectRepository
context.check_protocol = context.protocols["ProjectRepositoryProtocol"]
# ---------------------------------------------------------------------------
# When — check protocol satisfaction
# ---------------------------------------------------------------------------
@when("I check if it satisfies LifecyclePlanRepositoryProtocol")
def step_check_plan_protocol(context: Any) -> None:
context.protocol_check_result = issubclass(
context.infra_class, context.check_protocol
)
@when("I check if it satisfies ActionRepositoryProtocol")
def step_check_action_protocol(context: Any) -> None:
context.protocol_check_result = issubclass(
context.infra_class, context.check_protocol
)
@when("I check if it satisfies DecisionRepositoryProtocol")
def step_check_decision_protocol(context: Any) -> None:
context.protocol_check_result = issubclass(
context.infra_class, context.check_protocol
)
@when("I check if it satisfies ProjectRepositoryProtocol")
def step_check_project_protocol(context: Any) -> None:
context.protocol_check_result = issubclass(
context.infra_class, context.check_protocol
)
@then("the isinstance check should pass")
def step_isinstance_passes(context: Any) -> None:
assert context.protocol_check_result, (
f"{context.infra_class.__name__} does not satisfy "
f"{context.check_protocol.__name__}"
)
# ---------------------------------------------------------------------------
# When/Then — package exports
# ---------------------------------------------------------------------------
@when("I import from cleveragents.domain.repositories")
def step_import_domain_repositories(context: Any) -> None:
import cleveragents.domain.repositories as repo_pkg
context.repo_pkg = repo_pkg
@then("LifecyclePlanRepositoryProtocol should be available")
def step_plan_protocol_available(context: Any) -> None:
assert hasattr(context.repo_pkg, "LifecyclePlanRepositoryProtocol"), (
"LifecyclePlanRepositoryProtocol not exported from domain.repositories"
)
@then("ActionRepositoryProtocol should be available")
def step_action_protocol_available(context: Any) -> None:
assert hasattr(context.repo_pkg, "ActionRepositoryProtocol"), (
"ActionRepositoryProtocol not exported from domain.repositories"
)
@then("DecisionRepositoryProtocol should be available")
def step_decision_protocol_available(context: Any) -> None:
assert hasattr(context.repo_pkg, "DecisionRepositoryProtocol"), (
"DecisionRepositoryProtocol not exported from domain.repositories"
)
@then("ProjectRepositoryProtocol should be available")
def step_project_protocol_available(context: Any) -> None:
assert hasattr(context.repo_pkg, "ProjectRepositoryProtocol"), (
"ProjectRepositoryProtocol not exported from domain.repositories"
)
# ---------------------------------------------------------------------------
# When/Then — module path checks
# ---------------------------------------------------------------------------
@when("I check the module path of LifecyclePlanRepositoryProtocol")
def step_check_plan_module(context: Any) -> None:
from cleveragents.domain.repositories import LifecyclePlanRepositoryProtocol
context.current_module = LifecyclePlanRepositoryProtocol.__module__
@when("I check the module path of ActionRepositoryProtocol")
def step_check_action_module(context: Any) -> None:
from cleveragents.domain.repositories import ActionRepositoryProtocol
context.current_module = ActionRepositoryProtocol.__module__
@when("I check the module path of DecisionRepositoryProtocol")
def step_check_decision_module(context: Any) -> None:
from cleveragents.domain.repositories import DecisionRepositoryProtocol
context.current_module = DecisionRepositoryProtocol.__module__
@when("I check the module path of ProjectRepositoryProtocol")
def step_check_project_module(context: Any) -> None:
from cleveragents.domain.repositories import ProjectRepositoryProtocol
context.current_module = ProjectRepositoryProtocol.__module__
@then('the module should be under "cleveragents.domain"')
def step_module_under_domain(context: Any) -> None:
assert context.current_module.startswith("cleveragents.domain"), (
f"Protocol module '{context.current_module}' is not under 'cleveragents.domain'"
)
@@ -0,0 +1,63 @@
"""Domain Repository Protocols — clean architecture ports for data access.
This package defines the repository *interfaces* (ports) for the domain
layer. Infrastructure adapters provide the concrete implementations.
Following the dependency inversion principle, application services depend
on these protocols rather than on the infrastructure implementations
directly. This keeps the domain layer free of persistence concerns and
allows persistence backends to be swapped without touching application code.
## Protocols
| Protocol | Domain Entity | Infrastructure Adapter |
|---|---|---|
| ``LifecyclePlanRepositoryProtocol`` | ``Plan`` | ``LifecyclePlanRepository`` |
| ``ActionRepositoryProtocol`` | ``Action`` | ``ActionRepository`` |
| ``DecisionRepositoryProtocol`` | ``Decision`` | ``DecisionRepository`` |
| ``ProjectRepositoryProtocol`` | ``NamespacedProject`` | ``NamespacedProjectRepository`` |
## Usage
```python
from cleveragents.domain.repositories import (
ActionRepositoryProtocol,
DecisionRepositoryProtocol,
LifecyclePlanRepositoryProtocol,
ProjectRepositoryProtocol,
)
class MyApplicationService:
def __init__(
self,
plans: LifecyclePlanRepositoryProtocol,
actions: ActionRepositoryProtocol,
) -> None:
self._plans = plans
self._actions = actions
```
All protocols are decorated with ``@runtime_checkable`` so that
``isinstance`` checks work at runtime (e.g. in tests and dependency
injection containers).
"""
from cleveragents.domain.repositories.action_repository import (
ActionRepositoryProtocol,
)
from cleveragents.domain.repositories.decision_repository import (
DecisionRepositoryProtocol,
)
from cleveragents.domain.repositories.plan_repository import (
LifecyclePlanRepositoryProtocol,
)
from cleveragents.domain.repositories.project_repository import (
ProjectRepositoryProtocol,
)
__all__ = [
"ActionRepositoryProtocol",
"DecisionRepositoryProtocol",
"LifecyclePlanRepositoryProtocol",
"ProjectRepositoryProtocol",
]
@@ -0,0 +1,126 @@
"""Domain repository protocol for lifecycle actions.
Defines the ``ActionRepositoryProtocol`` — the port that the application
layer uses to persist and retrieve v3 lifecycle actions. Infrastructure
adapters (e.g. the SQLAlchemy-backed ``ActionRepository``) must satisfy
this protocol.
Based on the clean architecture principle described in the specification:
adapters live at the edge; the domain layer defines the contracts.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
from cleveragents.domain.models.core.action import Action
@runtime_checkable
class ActionRepositoryProtocol(Protocol):
"""Port for v3 lifecycle action persistence.
All methods that mutate state flush but do **not** commit; the caller
or a Unit-of-Work wrapper is responsible for committing the transaction.
"""
def create(self, action: Action) -> Action:
"""Persist a new action and return it.
Args:
action: An ``Action`` domain model instance.
Returns:
The same ``Action`` after persistence (identity pass-through).
"""
...
def get_by_id(self, action_name: str) -> Action | None:
"""Retrieve an action by its namespaced name (primary key).
Args:
action_name: The namespaced name string (e.g. ``"local/my-action"``).
Returns:
The ``Action`` domain object, or ``None`` if not found.
"""
...
def get_by_name(self, name: str) -> Action | None:
"""Retrieve an action by its full namespaced name string.
Args:
name: Full ``namespace/name`` string.
Returns:
The ``Action`` domain object, or ``None`` if not found.
"""
...
def get_by_namespace(
self,
namespace: str,
state: str | None = None,
) -> list[Action]:
"""List actions in a namespace, optionally filtered by state.
Args:
namespace: The namespace to filter by (e.g. ``"local"``).
state: Optional ``ActionState`` value string to filter by.
Returns:
List of ``Action`` domain objects ordered by name.
"""
...
def list_all(self) -> list[Action]:
"""List all persisted actions, ordered by namespace then name.
Returns:
List of ``Action`` domain objects.
"""
...
def get_by_state(self, state: str) -> list[Action]:
"""List actions by state, ordered by ``updated_at`` DESC.
Args:
state: An ``ActionState`` value string (e.g. ``"available"``).
Returns:
List of ``Action`` domain objects.
"""
...
def list_available(self, namespace: str | None = None) -> list[Action]:
"""List all actions in ``available`` state.
Args:
namespace: Optional namespace filter.
Returns:
List of ``Action`` domain objects ordered by namespace then name.
"""
...
def update(self, action: Action) -> Action:
"""Update all mutable fields of an existing action.
Args:
action: The ``Action`` domain model with updated fields.
Returns:
The same ``Action`` after persistence.
"""
...
def delete(self, action_name: str) -> bool:
"""Delete an action by namespaced name.
Args:
action_name: The namespaced name string of the action to delete.
Returns:
``True`` if the action was deleted, ``False`` if not found.
"""
...
@@ -0,0 +1,103 @@
"""Domain repository protocol for decision tree nodes.
Defines the ``DecisionRepositoryProtocol`` — the port that the application
layer uses to persist and retrieve decision tree nodes. Infrastructure
adapters (e.g. the SQLAlchemy-backed ``DecisionRepository``) must satisfy
this protocol.
Based on the clean architecture principle described in the specification:
adapters live at the edge; the domain layer defines the contracts.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
from cleveragents.domain.models.core.decision import Decision
@runtime_checkable
class DecisionRepositoryProtocol(Protocol):
"""Port for decision tree node persistence.
All methods that mutate state flush but do **not** commit; the caller
or a Unit-of-Work wrapper is responsible for committing the transaction.
"""
def create(self, decision: Decision) -> Decision:
"""Persist a new decision and return it.
Args:
decision: A ``Decision`` domain model instance.
Returns:
The same ``Decision`` after persistence (identity pass-through).
"""
...
def get(self, decision_id: str) -> Decision | None:
"""Retrieve a single decision by its ULID.
Args:
decision_id: ULID string of the decision.
Returns:
The ``Decision`` domain object, or ``None`` if not found.
"""
...
def get_by_plan(self, plan_id: str) -> list[Decision]:
"""Retrieve all decisions for a plan, ordered by sequence number.
Args:
plan_id: ULID of the plan.
Returns:
List of ``Decision`` domain objects.
"""
...
def get_tree(self, root_decision_id: str) -> list[Decision]:
"""Retrieve the full decision tree rooted at *root_decision_id*.
Returns the root followed by its descendants in BFS order.
Args:
root_decision_id: ULID of the root decision.
Returns:
List of ``Decision`` domain objects in BFS order.
"""
...
def get_path_to_root(self, decision_id: str) -> list[Decision]:
"""Retrieve the path from a decision up to the tree root.
Args:
decision_id: ULID of the starting decision.
Returns:
List of ``Decision`` objects from the given node to the root.
"""
...
def get_superseded(self, plan_id: str) -> list[Decision]:
"""Retrieve all superseded decisions for a plan.
Args:
plan_id: ULID of the plan.
Returns:
List of superseded ``Decision`` domain objects.
"""
...
def delete(self, decision_id: str) -> bool:
"""Delete a decision by its ULID.
Args:
decision_id: ULID string of the decision to delete.
Returns:
``True`` if the decision was deleted, ``False`` if not found.
"""
...
@@ -0,0 +1,130 @@
"""Domain repository protocol for lifecycle plans.
Defines the ``LifecyclePlanRepositoryProtocol`` — the port that the
application layer uses to persist and retrieve v3 lifecycle plans.
Infrastructure adapters (e.g. the SQLAlchemy-backed
``LifecyclePlanRepository``) must satisfy this protocol.
Based on the clean architecture principle described in the specification:
adapters live at the edge; the domain layer defines the contracts.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
from cleveragents.domain.models.core.plan import Plan
@runtime_checkable
class LifecyclePlanRepositoryProtocol(Protocol):
"""Port for v3 lifecycle plan persistence.
All methods that mutate state flush but do **not** commit; the caller
or a Unit-of-Work wrapper is responsible for committing the transaction.
"""
def create(self, plan: Plan) -> Plan:
"""Persist a new plan and return it.
Args:
plan: A v3 ``Plan`` domain model instance.
Returns:
The same ``Plan`` after persistence (identity pass-through).
"""
...
def get(self, plan_id: str) -> Plan | None:
"""Retrieve a plan by its ULID primary key.
Args:
plan_id: ULID string of the plan.
Returns:
The ``Plan`` domain object, or ``None`` if not found.
"""
...
def get_by_name(self, namespaced_name: str) -> Plan | None:
"""Retrieve a plan by its full namespaced name string.
Args:
namespaced_name: Full ``namespace/name`` string.
Returns:
The ``Plan`` domain object, or ``None`` if not found.
"""
...
def update(self, plan: Plan) -> Plan:
"""Update all mutable fields of an existing plan.
Args:
plan: The ``Plan`` domain model with updated fields.
Returns:
The same ``Plan`` after persistence.
"""
...
def list_all(self) -> list[Plan]:
"""List all persisted plans, ordered by ``created_at`` DESC.
Returns:
List of ``Plan`` domain objects.
"""
...
def list_plans(
self,
phase: str | None = None,
processing_state: str | None = None,
action_name: str | None = None,
project_name: str | None = None,
namespace: str | None = None,
limit: int = 100,
offset: int = 0,
) -> list[Plan]:
"""List plans with optional filters, ordered by ``created_at`` DESC.
Args:
phase: Optional phase filter (e.g. ``"action"``).
processing_state: Optional state filter (e.g. ``"queued"``).
action_name: Optional action name filter.
project_name: Optional project name filter.
namespace: Optional namespace filter.
limit: Maximum number of results (default 100).
offset: Number of results to skip (default 0).
Returns:
List of ``Plan`` domain objects.
"""
...
def count(
self,
phase: str | None = None,
processing_state: str | None = None,
) -> int:
"""Count matching plans with optional filters.
Args:
phase: Optional phase filter.
processing_state: Optional state filter.
Returns:
Number of matching plans.
"""
...
def delete(self, plan_id: str) -> bool:
"""Delete a plan by its ULID primary key.
Args:
plan_id: ULID string of the plan to delete.
Returns:
``True`` if the plan was deleted, ``False`` if not found.
"""
...
@@ -0,0 +1,88 @@
"""Domain repository protocol for namespaced projects.
Defines the ``ProjectRepositoryProtocol`` — the port that the application
layer uses to persist and retrieve namespaced projects. Infrastructure
adapters (e.g. the SQLAlchemy-backed ``NamespacedProjectRepository``) must
satisfy this protocol.
Based on the clean architecture principle described in the specification:
adapters live at the edge; the domain layer defines the contracts.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
from cleveragents.domain.models.core.project import NamespacedProject
@runtime_checkable
class ProjectRepositoryProtocol(Protocol):
"""Port for namespaced project persistence.
All methods that mutate state flush but do **not** commit; the caller
or a Unit-of-Work wrapper is responsible for committing the transaction.
"""
def create(self, project: NamespacedProject) -> NamespacedProject:
"""Persist a new project and return it.
Args:
project: A ``NamespacedProject`` domain model instance.
Returns:
The same ``NamespacedProject`` after persistence.
"""
...
def get(self, namespaced_name: str) -> NamespacedProject:
"""Retrieve a project by its namespaced name.
Args:
namespaced_name: Full ``namespace/name`` string.
Returns:
The ``NamespacedProject`` domain object.
Raises:
ProjectNotFoundError: If the project does not exist.
"""
...
def list_projects(
self,
namespace: str | None = None,
limit: int = 100,
) -> list[NamespacedProject]:
"""List projects with optional namespace filter.
Args:
namespace: Optional namespace to filter by.
limit: Maximum number of results (default 100).
Returns:
List of ``NamespacedProject`` domain objects.
"""
...
def update(self, project: NamespacedProject) -> NamespacedProject:
"""Update all mutable fields of an existing project.
Args:
project: The ``NamespacedProject`` domain model with updated fields.
Returns:
The same ``NamespacedProject`` after persistence.
"""
...
def delete(self, namespaced_name: str) -> bool:
"""Delete a project by its namespaced name.
Args:
namespaced_name: Full ``namespace/name`` string.
Returns:
``True`` if the project was deleted, ``False`` if not found.
"""
...
@@ -79,6 +79,16 @@ from cleveragents.core.exceptions import (
DatabaseError,
)
from cleveragents.core.retry_patterns import retry_database_operation as database_retry
from cleveragents.domain.repositories.action_repository import ActionRepositoryProtocol
from cleveragents.domain.repositories.decision_repository import (
DecisionRepositoryProtocol,
)
from cleveragents.domain.repositories.plan_repository import (
LifecyclePlanRepositoryProtocol,
)
from cleveragents.domain.repositories.project_repository import (
ProjectRepositoryProtocol,
)
from cleveragents.domain.models.core import (
Actor,
Change,
@@ -873,9 +883,12 @@ class ActionInUseError(BusinessRuleViolation):
self.plan_count = plan_count
class ActionRepository:
class ActionRepository(ActionRepositoryProtocol):
"""Repository for v3 lifecycle action persistence.
Implements :class:`~cleveragents.domain.repositories.ActionRepositoryProtocol`
— the domain-layer port for action persistence.
Uses a session-factory pattern: each public method obtains its own
session from the factory, ensuring proper session lifecycle management.
@@ -1243,9 +1256,12 @@ class PlanNotFoundError(DatabaseError):
self.plan_id = plan_id
class LifecyclePlanRepository:
class LifecyclePlanRepository(LifecyclePlanRepositoryProtocol):
"""Repository for v3 lifecycle plan persistence.
Implements :class:`~cleveragents.domain.repositories.LifecyclePlanRepositoryProtocol`
— the domain-layer port for lifecycle plan persistence.
Uses a session-factory pattern: each public method obtains its own
session from the factory, ensuring proper session lifecycle management.
@@ -2874,9 +2890,12 @@ class DuplicateLinkError(DatabaseError):
# ---------------------------------------------------------------------------
class NamespacedProjectRepository:
class NamespacedProjectRepository(ProjectRepositoryProtocol):
"""Repository for spec-aligned namespaced project persistence.
Implements :class:`~cleveragents.domain.repositories.ProjectRepositoryProtocol`
— the domain-layer port for namespaced project persistence.
Uses a session-factory pattern: each public method obtains its own
session from the factory, ensuring proper session lifecycle management.
@@ -5123,9 +5142,12 @@ class DecisionNotFoundError(DatabaseError):
self.decision_id = decision_id
class DecisionRepository:
class DecisionRepository(DecisionRepositoryProtocol):
"""Repository for decision tree node persistence.
Implements :class:`~cleveragents.domain.repositories.DecisionRepositoryProtocol`
— the domain-layer port for decision tree node persistence.
Uses the session-factory pattern (ADR-007). All mutating methods
flush but do **not** commit; the caller or a :class:`UnitOfWork`
wrapper is responsible for committing the transaction.