rayhpeng 44cb9fd375 feat(harness): add feedback domain slice (model, ports, service)
Hexagonal inner ring for the feedback bounded context: frozen aggregate
with construct-time invariants (rating, reason-tag slugs), technology-
neutral ports (typing.Protocol), and the application service with
explicit user_id. Guarded by an AST purity test that keeps domain/ free
of infrastructure imports.
2026-07-23 15:30:33 +08:00

70 lines
2.7 KiB
Python

from typing import Protocol, runtime_checkable
from deerflow.domain.feedback.model import Feedback
@runtime_checkable
class FeedbackRepository(Protocol):
"""Output port for feedback persistence.
Implementations exchange domain objects only and translate storage
failures into domain errors -- no storage vocabulary (SQL, tables,
files) may leak through this contract.
"""
async def save(self, feedback: Feedback) -> Feedback:
"""Store the user's current rating for a run (idempotent upsert).
Creates the entry if absent; otherwise replaces rating/comment and
refreshes created_at while keeping the aggregate identity
(thread_id, run_id, user_id). Returns the stored state.
"""
...
async def latest_per_run_in_thread(self, thread_id: str, *, user_id: str | None) -> dict[str, Feedback]:
"""Return each run's current feedback across a whole thread, keyed by run_id.
Single bulk read used to badge the message list -- avoids one
query per run. Ownership filter: a non-None user_id restricts results to the
user's entries; None means no filtering (no-auth mode).
"""
...
async def latest_for_runs(self, thread_id: str, run_ids: set[str], *, user_id: str | None) -> dict[str, Feedback]:
"""Return current feedback for only the selected runs of a thread.
Paged variant of latest_per_run_in_thread: the message-list page
endpoint only needs badges for the runs on the current page.
Returns an empty mapping when run_ids is empty. Same ownership
filter as find_for_run.
"""
...
async def remove_for_run(self, thread_id: str, run_id: str, *, user_id: str | None) -> bool:
"""Retract the user's feedback for a run.
Returns True if an entry was removed, False if none existed.
Ownership filter: a non-None user_id restricts results to the
user's entries; None means no filtering (no-auth mode).
"""
...
class RunLookup(Protocol):
"""Narrow output port: the only question the feedback context asks
about runs -- which thread does a run belong to.
Deliberately not the full run store: depending on this one-method
contract keeps the feedback context decoupled from the execution
context's wide repository interface (interface segregation).
"""
async def thread_of(self, run_id: str) -> str | None:
"""Return the thread_id owning the given run, or None if the run
does not exist.
Used by the service to verify run ownership before writing
feedback (rejects cross-thread run ids as RunNotFoundError).
"""
...