refactor(schedule): move the secondary adapters to app/adapters

Adopts the layout feedback landed in cb49dd67: secondary adapters live
under `app/adapters/<context>/`, one file per port, the file named after
the port in snake_case with the technology carried by the class name.
`app/infra/` is now gone entirely.

The rename also separates two meanings of "run" that shared one filename
space: `run_sql.py` held `ScheduledRun` (an execution record), while the
`run_launcher.py` still to come deals in Gateway runs.

  task_sql.py     -> scheduled_task_repository.py
  run_sql.py      -> scheduled_run_repository.py
  spec_mapping.py -> spec_mapping.py  (implements no port, so no rename)

Both SQL adapters now carry the `Secondary adapter (owned persistence)`
docstring marker -- this context owns both tables and writes its own
queries. `spec_mapping` says instead that it is a boundary mapping and
names its two callers. The package `__init__.py` is empty, so imports go
through the full path and a class's home file stays unambiguous.

Pure move: every top-level symbol was compared against its pre-move
original by AST dump (docstrings excluded), plus a separate per-class
method-name comparison, since a whole-class dump reports a docstring edit
and a renamed method the same way.

Also repoints three docstrings and one diagram that still named the
deleted `app/infra/` path.
This commit is contained in:
rayhpeng 2026-07-28 18:14:46 +08:00
parent 35be955177
commit 0bae77ffc0
10 changed files with 30 additions and 35 deletions

View File

@ -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)

View File

@ -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__)

View File

@ -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

View File

@ -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.
"""

View File

@ -61,7 +61,7 @@ flowchart LR
C["deerflow/scheduler/schedules.py<br/>时区 / cron 计算"]
end
subgraph TODO["🚧 待建 · 外圈"]
AD["app/infra/persistence/<br/>app/infra/schedule/<br/>app/scheduler/poller.py"]
AD["app/adapters/schedule/<br/>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`。
---

View File

@ -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`.
"""

View File

@ -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

View File

@ -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,

View File

@ -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