diff --git a/backend/app/adapters/schedule/__init__.py b/backend/app/adapters/schedule/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/app/infra/schedule/run_sql.py b/backend/app/adapters/schedule/scheduled_run_repository.py similarity index 95% rename from backend/app/infra/schedule/run_sql.py rename to backend/app/adapters/schedule/scheduled_run_repository.py index c2736cd8b..834870c99 100644 --- a/backend/app/infra/schedule/run_sql.py +++ b/backend/app/adapters/schedule/scheduled_run_repository.py @@ -1,7 +1,8 @@ -"""SQL adapter for the schedule context's execution-record repository. +"""Secondary adapter (owned persistence) -- the scheduled_task_runs table in SQL. -Secondary adapter implementing `ScheduledRunRepository`. Queries are migrated -unchanged from the legacy repository. +Implements `ScheduledRunRepository` from `deerflow.domain.schedule.ports`. This +context owns the `scheduled_task_runs` table and writes its own queries. +Queries are migrated unchanged from the legacy repository. The load-bearing piece is the `IntegrityError` translation in `add`: the partial unique index `uq_scheduled_task_run_active` is the atomic arbiter of @@ -29,7 +30,7 @@ from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.ports import ScheduledRunRepository # Transitional: the ORM row stays in the harness until engine, models, and -# migrations move into app/infra together. +# migrations move into app/adapters together. from deerflow.persistence.scheduled_task_runs.model import ScheduledTaskRunRow _ACTIVE_STATUS_VALUES = tuple(str(status) for status in ACTIVE_RUN_STATUSES) diff --git a/backend/app/infra/schedule/task_sql.py b/backend/app/adapters/schedule/scheduled_task_repository.py similarity index 95% rename from backend/app/infra/schedule/task_sql.py rename to backend/app/adapters/schedule/scheduled_task_repository.py index c2f0d029d..c0e277e58 100644 --- a/backend/app/infra/schedule/task_sql.py +++ b/backend/app/adapters/schedule/scheduled_task_repository.py @@ -1,10 +1,12 @@ -"""SQL adapter for the schedule context's task repository. +"""Secondary adapter (owned persistence) -- the scheduled_tasks table in SQL. -Sibling of `run_sql.py`; both consume `spec_mapping` for the stored JSON spec. +Implements `ScheduledTaskRepository` from `deerflow.domain.schedule.ports`. +This context owns the `scheduled_tasks` table and writes its own queries, so +SQL/ORM vocabulary stops at this file: methods exchange domain objects and +normalize SQLite's tz-naive reads. -Secondary adapter implementing `ScheduledTaskRepository`. SQL/ORM vocabulary -stops at this file: methods exchange domain objects and normalize SQLite's -tz-naive reads. +Sibling of `scheduled_run_repository.py`; both consume `spec_mapping` for the +stored JSON spec. **The queries are migrated unchanged from the legacy repository.** The claim statement's `FOR UPDATE SKIP LOCKED` and the `protect_terminal` conditional @@ -22,7 +24,7 @@ from datetime import UTC, datetime, timedelta from sqlalchemy import and_, or_, select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker -from app.infra.schedule.spec_mapping import spec_to_domain, spec_to_wire +from app.adapters.schedule.spec_mapping import spec_to_domain, spec_to_wire from deerflow.domain.schedule.model import ( TERMINAL_TASK_STATUSES, ContextMode, @@ -33,7 +35,7 @@ from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.ports import ScheduledTaskRepository # Transitional: the ORM row stays in the harness until engine, models, and -# migrations move into app/infra together. +# migrations move into app/adapters together. from deerflow.persistence.scheduled_tasks.model import ScheduledTaskRow logger = logging.getLogger(__name__) diff --git a/backend/app/infra/schedule/spec_mapping.py b/backend/app/adapters/schedule/spec_mapping.py similarity index 83% rename from backend/app/infra/schedule/spec_mapping.py rename to backend/app/adapters/schedule/spec_mapping.py index 36d83d231..81eb7a36e 100644 --- a/backend/app/infra/schedule/spec_mapping.py +++ b/backend/app/adapters/schedule/spec_mapping.py @@ -1,9 +1,11 @@ -"""Wire/storage <-> ScheduleSpec mapping. +"""Boundary mapping (not a port implementation) -- wire/storage <-> ScheduleSpec. -`schedule_spec` is both an HTTP request field and a JSON column -- one shape, -two boundaries -- so the mapping lives here once and both the router and the -SQL adapter import it, rather than the domain growing a `Mapping[str, Any]` in -its signatures. +Unlike its siblings in this package, this module implements no port: it is the +shared translation both of them need. `schedule_spec` is both an HTTP request +field and a JSON column -- one shape, two boundaries -- so the mapping lives +here once and both callers import it (`scheduled_task_repository` for the +stored column, the scheduled-task router for the request body), rather than the +domain growing a `Mapping[str, Any]` in its signatures. The split is deliberate: **structural** checks (is the key present? is it a str?) belong to this boundary, **value** rules (5-field cron, resolvable diff --git a/backend/app/infra/schedule/__init__.py b/backend/app/infra/schedule/__init__.py deleted file mode 100644 index 792b35005..000000000 --- a/backend/app/infra/schedule/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Secondary adapters for the schedule bounded context. - -Grouped by context rather than by technology, mirroring `domain/schedule/`: -everything the outer ring provides to this one context lives here, whether it -talks to SQL (`task_sql`, `run_sql`), converts a wire/storage shape -(`spec_mapping`), or wraps a runtime. - -`app/infra/persistence/feedback.py` still follows the older technology-first -layout because it belongs to a separate migration; the two will converge. -""" diff --git a/backend/docs/SCHEDULE_DESIGN_zh.md b/backend/docs/SCHEDULE_DESIGN_zh.md index 8d5c89f39..eb2e5e32d 100644 --- a/backend/docs/SCHEDULE_DESIGN_zh.md +++ b/backend/docs/SCHEDULE_DESIGN_zh.md @@ -61,7 +61,7 @@ flowchart LR C["deerflow/scheduler/schedules.py
时区 / cron 计算"] end subgraph TODO["🚧 待建 · 外圈"] - AD["app/infra/persistence/
app/infra/schedule/
app/scheduler/poller.py"] + AD["app/adapters/schedule/
app/scheduler/poller.py"] end R -.->|尚未调用| SV @@ -231,12 +231,12 @@ America/New_York 的 "0 9 * * *" {"cron": "0 9 * * *"} ←──→ ScheduleSpec(CRON, "Asia/Shanghai", cron="0 9 * * *") (JSON 列 / HTTP 字段) (值对象) ↑ - app/infra/schedule/spec_mapping.py + app/adapters/schedule/spec_mapping.py ``` 这不是洁癖,是一条可执行的判据:**一旦领域方法的签名里出现 `Mapping[str, Any]`,就说明领域在处理持久化/传输格式了**。解析这件事天然可以切成两半——结构校验(键在不在?值是不是字符串?)属于边界,值校验(cron 是不是 5 段、时区认不认识、`run_at` 有没有)属于 `__post_init__`。切开之后领域完全不需要看见 dict,签名全部强类型。 -同样的形状在 Feedback 上下文里也成立:`Feedback` 聚合对 ORM 行一无所知,转换全在 `app/infra/persistence/feedback.py`。 +同样的形状在 Feedback 上下文里也成立:`Feedback` 聚合对 ORM 行一无所知,转换全在 `app/adapters/feedback/feedback_repository.py`。 --- diff --git a/backend/packages/harness/deerflow/domain/__init__.py b/backend/packages/harness/deerflow/domain/__init__.py index 75edaa1c9..da003d217 100644 --- a/backend/packages/harness/deerflow/domain/__init__.py +++ b/backend/packages/harness/deerflow/domain/__init__.py @@ -5,5 +5,5 @@ Every package under this namespace is one bounded context laid out as dependencies -- no sqlalchemy/fastapi/pydantic, no `app.*`, and no harness infrastructure modules (enforced in CI by `tests/test_harness_domain_purity.py`). Adapters implementing the ports -live in `app/infra/`; wiring happens only in `app/gateway/deps.py`. +live in `app/adapters/`; wiring happens only in `app/gateway/deps.py`. """ diff --git a/backend/packages/harness/deerflow/persistence/feedback/__init__.py b/backend/packages/harness/deerflow/persistence/feedback/__init__.py index 6a1ca9f3f..3d87ac10c 100644 --- a/backend/packages/harness/deerflow/persistence/feedback/__init__.py +++ b/backend/packages/harness/deerflow/persistence/feedback/__init__.py @@ -2,8 +2,8 @@ The legacy SQL repository was replaced by the hexagonal slice: port `deerflow.domain.feedback.ports.FeedbackRepository`, adapter -`app.infra.persistence.feedback.SqlFeedbackRepository`. The ORM row stays -here until PR-N moves engine/models/migrations into app/infra. +`app.adapters.feedback.feedback_repository.SqlFeedbackRepository`. The ORM +row stays here until PR-N moves engine/models/migrations into app/adapters. """ from deerflow.persistence.feedback.model import FeedbackRow diff --git a/backend/tests/test_schedule_fakes.py b/backend/tests/test_schedule_fakes.py index 004c55a4f..9e5b534a8 100644 --- a/backend/tests/test_schedule_fakes.py +++ b/backend/tests/test_schedule_fakes.py @@ -28,8 +28,8 @@ from schedule_fakes import ( InMemoryScheduledTaskRepository, ) -from app.infra.schedule.run_sql import SqlScheduledRunRepository -from app.infra.schedule.task_sql import SqlScheduledTaskRepository +from app.adapters.schedule.scheduled_run_repository import SqlScheduledRunRepository +from app.adapters.schedule.scheduled_task_repository import SqlScheduledTaskRepository from deerflow.config.database_config import DatabaseConfig from deerflow.domain.schedule.model import ( ACTIVE_RUN_STATUSES, diff --git a/backend/tests/test_schedule_spec_mapping.py b/backend/tests/test_schedule_spec_mapping.py index b56514453..11015ed18 100644 --- a/backend/tests/test_schedule_spec_mapping.py +++ b/backend/tests/test_schedule_spec_mapping.py @@ -13,7 +13,7 @@ from datetime import UTC, datetime import pytest -from app.infra.schedule.spec_mapping import spec_to_domain, spec_to_wire +from app.adapters.schedule.spec_mapping import spec_to_domain, spec_to_wire from deerflow.domain.schedule.model import InvalidScheduleError, ScheduleSpec, ScheduleType