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