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
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user