diff --git a/features/domain_repository_protocols.feature b/features/domain_repository_protocols.feature new file mode 100644 index 000000000..2a41cc87d --- /dev/null +++ b/features/domain_repository_protocols.feature @@ -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" diff --git a/features/steps/domain_repository_protocols_steps.py b/features/steps/domain_repository_protocols_steps.py new file mode 100644 index 000000000..dc264475a --- /dev/null +++ b/features/steps/domain_repository_protocols_steps.py @@ -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'" + ) diff --git a/src/cleveragents/domain/repositories/__init__.py b/src/cleveragents/domain/repositories/__init__.py new file mode 100644 index 000000000..f0bf4a371 --- /dev/null +++ b/src/cleveragents/domain/repositories/__init__.py @@ -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", +] diff --git a/src/cleveragents/domain/repositories/action_repository.py b/src/cleveragents/domain/repositories/action_repository.py new file mode 100644 index 000000000..e0440eb98 --- /dev/null +++ b/src/cleveragents/domain/repositories/action_repository.py @@ -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. + """ + ... diff --git a/src/cleveragents/domain/repositories/decision_repository.py b/src/cleveragents/domain/repositories/decision_repository.py new file mode 100644 index 000000000..14ea5955d --- /dev/null +++ b/src/cleveragents/domain/repositories/decision_repository.py @@ -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. + """ + ... diff --git a/src/cleveragents/domain/repositories/plan_repository.py b/src/cleveragents/domain/repositories/plan_repository.py new file mode 100644 index 000000000..d70d03c9b --- /dev/null +++ b/src/cleveragents/domain/repositories/plan_repository.py @@ -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. + """ + ... diff --git a/src/cleveragents/domain/repositories/project_repository.py b/src/cleveragents/domain/repositories/project_repository.py new file mode 100644 index 000000000..3daa6f69e --- /dev/null +++ b/src/cleveragents/domain/repositories/project_repository.py @@ -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. + """ + ... diff --git a/src/cleveragents/infrastructure/database/repositories.py b/src/cleveragents/infrastructure/database/repositories.py index 29b366a07..96076f902 100644 --- a/src/cleveragents/infrastructure/database/repositories.py +++ b/src/cleveragents/infrastructure/database/repositories.py @@ -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.