feat(domain): add domain layer foundation -- enums, value objects, TechniqueEntity, repository ports

This commit is contained in:
2026-02-18 19:10:31 +01:00
parent e651ef8a8c
commit 5c55e7c17f
14 changed files with 761 additions and 28 deletions

View File

View File

@@ -0,0 +1,4 @@
from app.domain.ports.repositories.technique_repository import TechniqueRepository
from app.domain.ports.repositories.test_repository import TestRepository
__all__ = ["TechniqueRepository", "TestRepository"]

View File

@@ -0,0 +1,57 @@
"""Port defining how the application accesses technique data.
This is a domain contract — implementations live in infrastructure/.
The domain layer NEVER imports the implementation.
"""
from __future__ import annotations
import uuid
from typing import NamedTuple, Protocol, runtime_checkable
from app.domain.entities.technique import TechniqueEntity
from app.domain.enums import TechniqueStatus
class TechniqueWithCounts(NamedTuple):
"""Pre-aggregated technique data for heatmap/scoring."""
entity: TechniqueEntity
test_count: int
validated_test_count: int
detection_rule_count: int
@runtime_checkable
class TechniqueRepository(Protocol):
"""Data access contract for techniques (one per aggregate root)."""
# -- Single-entity access ----------------------------------------------
def find_by_id(self, technique_id: uuid.UUID) -> TechniqueEntity | None: ...
def find_by_mitre_id(self, mitre_id: str) -> TechniqueEntity | None: ...
# -- List access -------------------------------------------------------
def list_all(
self,
*,
tactic: str | None = None,
status: TechniqueStatus | None = None,
review_required: bool | None = None,
) -> list[TechniqueEntity]: ...
def list_by_ids(self, ids: list[uuid.UUID]) -> list[TechniqueEntity]: ...
# -- Batch queries (scoring/heatmap performance) -----------------------
def count_by_status(self) -> dict[TechniqueStatus, int]: ...
def find_all_with_test_counts(self) -> list[TechniqueWithCounts]: ...
# -- Mutations ---------------------------------------------------------
def save(self, technique: TechniqueEntity) -> TechniqueEntity: ...
def exists_by_mitre_id(self, mitre_id: str) -> bool: ...

View File

@@ -0,0 +1,52 @@
"""Port defining how the application accesses test data.
This is a domain contract — implementations live in infrastructure/.
"""
from __future__ import annotations
import uuid
from typing import Protocol, runtime_checkable
from app.domain.enums import TestState
class TestRepository(Protocol):
"""Data access contract for tests."""
# -- Single-entity access ----------------------------------------------
def find_by_id(self, test_id: uuid.UUID) -> object | None:
"""Return a Test ORM model by primary key, or None.
Returns the ORM model directly (not a domain entity) because
the TestEntity is constructed at the service layer via
``TestEntity.from_orm()``.
"""
...
# -- List access -------------------------------------------------------
def list_by_technique(self, technique_id: uuid.UUID) -> list[object]: ...
def list_by_state(self, state: TestState) -> list[object]: ...
def count_by_technique_and_state(
self,
technique_id: uuid.UUID,
) -> dict[TestState, int]:
"""Return test counts grouped by state for a single technique."""
...
# -- Batch queries -----------------------------------------------------
def get_states_and_results_for_technique(
self,
technique_id: uuid.UUID,
) -> list[tuple[str, str | None]]:
"""Return (state, detection_result) pairs for all tests of a technique.
Used by TechniqueEntity.recalculate_status() without loading full
test models.
"""
...