diff --git a/AGENTS.md b/AGENTS.md
index 01f810bb3..54c95e223 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -66,11 +66,13 @@ The legacy roots have no physical directories in the source tree. Current images
| `app/adapters/cache/` | Redis 与文件缓存等具体持久化实现 | 缓存协议、装饰器和进程内缓存策略 | `backends.py`, `redis.py` |
| `app/adapters/system/` | 操作系统、文件、进程、标准流、包/资源安装、显示和 Rust 加速适配 | 业务规则、进程重启决策 | `host.py`, `display/`, `stdio.py`, `package.py`, `resource.py`, `rust.py`, `fsproxy.py` |
| `app/adapters/external/` | CookieCloud、插件市场、OCR、IP 归属和 MoviePilot Server 等命名外部生态 | 通用 HTTP/DNS/文件机制或可复用领域语义 | `market.py`, `server.py`, `cookiecloud.py`, `ocr.py`, `location.py`, `wechat_crypt.py` |
-| `app/application/` | 读取配置/持久化状态的聚焦应用服务和服务族规则 | 多领域 Chain 编排、底层通用机制、通用传输协议 | `recognition.py`, `filter.py`, `filter_rules.py`, `notification.py`, `mediaserver.py`, `rss.py`, `site/sites.*` |
+| `app/application/` | 聚焦应用服务、用例命令,以及由用例拥有的持久化 Port/Protocol | SQLAlchemy、Session、Oper 等具体 DB 实现,多领域 Chain 编排、底层通用机制、通用传输协议 | `recognition.py`, `filter.py`, `outbox.py`, `subscription/write.py`, `workflow.py` |
| `app/application/messaging/` | 消息渲染/路由、交互和 Agent 到消息桥接:`interaction.py` 通用交互契约和视图工具;`router.py` 统一交互优先级和回调分发;`site.py`/`subscribe.py`/`skill.py` 对应命令的会话、输入解析和视图;`media.py` 媒体交互状态(业务工作流仍由 `MediaInteractionChain` 执行);`plugin.py` 插件输入接管和插件按钮回调;`agent.py` Agent 选择状态、回调协议和 WebAgent 消息桥接;`message.py` 通知渲染、模板和队列。不作为推荐给插件直接使用的公开 SDK | 认证策略、通用 HTTP、服务发现、仅端点使用的 Web Push 行为 | `message.py`, `interaction.py`, `router.py`, `agent.py` |
| `app/application/security/` | 认证、授权、Cookie、Passkey、OTP/二次认证、路径/URL 安全、SSRF 和签名策略 | 通用 URL 解析、进程运行策略、普通业务校验 | `access.py`, `auth.py`, `cookie.py`, `passkey.py`, `otp.py`, `twofactor.py`, `url.py` |
-| `app/chain/` | Reusable use-case orchestration across modules, services, Oper classes, events, and caches; chains reach modules only through `run_module` dispatch on method-name contracts | Transport schemas, backend-specific protocol details, generic primitives, direct imports of module internals (classes, exceptions, constants) | `media.py`, `download.py`, `subscribe.py`, `transfer.py` |
-| `app/startup/` | Composition root: inject providers/adapters, order initialization and shutdown, decide restart/lifecycle policy | Reusable business rules or adapter implementation details | `lifecycle.py`, `domain_initializer.py`, `cache_initializer.py`, `managed_resources_initializer.py`, `modules_initializer.py` |
+| `app/chain/` | Reusable use-case orchestration across modules, Application services, injected ports, events, and caches; chains reach modules only through `run_module` dispatch on method-name contracts | Transport schemas, backend-specific protocol details, generic primitives, direct Oper/DB imports, direct imports of module internals (classes, exceptions, constants) | `media.py`, `download.py`, `subscribe.py`, `transfer.py` |
+| `app/db/oper/` | 面向表和持久化值的 SQLAlchemy 数据访问;接收调用方 Session,只查询、暂存或 flush | Application 业务规则、隐式事务所有权、外部副作用 | `subscribe.py`, `site.py`, `workflow.py` |
+| `app/db/adapters/` | 实现 Application 持久化 Port,创建短生命周期 Session/UoW,并适配 Oper | 用例规则、启动顺序、进程生命周期 | `subscription.py`, `site.py`, `outbox.py`, `workflow.py` |
+| `app/startup/` | Composition root: `composition/` 构造并注入跨层依赖,`initializers/` 按领域初始化,`lifecycle/` 编排启动关闭 | Reusable business rules or adapter implementation details | `composition/context.py`, `composition/database.py`, `initializers/modules.py`, `lifecycle/components.py` |
| `app/sdk/` | Deliberately curated stable imports for new plugins | Canonical implementation logic or host-internal dependencies | `browser.py`, `cache.py`, `logging.py`, `media.py`, `network.py`, `services.py` |
| `app/runtime/compat/` | 仅依赖标准库的精确旧导入路由、资源前置扫描和 DEBUG 诊断 | 业务实现、通配猜测、目标模块的提前导入 | `manifest.py`, `imports.py`, `resource_imports.py`, `diagnostics.py` |
@@ -87,9 +89,10 @@ Use these questions in order before creating or moving a module:
5. Does it perform configured network, cache, OS/process, file, package/resource, stdio, or Rust I/O? Put it under the matching `adapters` technical boundary.
6. Does it implement a named external product/ecosystem workflow? Put it in `adapters/external`.
7. Does it own authentication, authorization, signing, SSRF, URL/path safety, OTP, passkeys, or two-factor behavior? Put it in `application/security`.
-8. Does it read persisted user configuration, coordinate one bounded capability, or normalize/match one service family? Put it in `application`.
-9. Does it coordinate several modules/services/Oper classes for one use case? Put it in `chain`.
-10. Is it public to plugins or only preserving an old path? Curate it in `sdk` or map it in `runtime/compat`; do not move implementation there.
+8. Does it define a use case or the persistence Port required by that use case? Put it in `application`; do not import concrete DB there.
+9. Does it implement an Application persistence Port with SQLAlchemy Session/UoW/Oper? Put it in `db/adapters`.
+10. Does it coordinate several modules/services/Oper classes for one use case? Put it in `chain`.
+11. Is it public to plugins or only preserving an old path? Curate it in `sdk` or map it in `runtime/compat`; do not move implementation there.
### Enforced Split Examples
@@ -97,11 +100,11 @@ These decisions are architectural constraints, not naming suggestions:
* Cache contracts, memory backends, decorators, and proxies stay in `app/runtime/cache.py`; Redis and filesystem implementations stay in `app/adapters/cache/backends.py`. Startup registers concrete factories before decorated business modules are imported. Legacy `app.core.cache` resolves to the complete `app.sdk.cache` facade.
* The complete logging runtime stays in `app/runtime/log.py`: policy, console/plugin routing, async rotating file output, and shutdown. `app.runtime.config` supplies the resolved settings and log path. `runtime/log.py` remains a dependency leaf with no `app.*` imports. Plugins use `app.sdk.logging`; legacy `app.log` resolves to that SDK facade.
-* Recognition parsing stays pure in `app/domain/meta/` and `app/domain/metainfo.py`. `app/application/recognition.py` reads `SystemConfigOper`; `app/startup/domain_initializer.py` injects rules, extension policy, source defaults, TMDB image construction, and the optional Rust accelerator.
+* Recognition parsing stays pure in `app/domain/meta/` and `app/domain/metainfo.py`. `app/application/recognition.py` consumes injected configuration; `app/startup/initializers/domain.py` injects rules, extension policy, source defaults, TMDB image construction, and the optional Rust accelerator.
* Kodi-style NFO reading and metadata document generation are one domain capability and stay together in `app/domain/scraper.py`; a separate `domain/nfo.py` must not be recreated.
* `app/application/mediaserver.py` is the single media-server service capability module. It owns configured service discovery together with Provider ID normalization and music-library matching, while reusing generic identity rules from `app/domain/media.py`.
* Configured notification-service discovery belongs in `app/application/notification.py`. Web Push subscription and manual-send HTTP behavior stays in `app/api/endpoints/message.py`; it is not a reusable messaging capability module.
-* `app/adapters/system/resource.py` detects/downloads/installs resources and returns whether installation occurred. Only `app/startup/modules_initializer.py` may decide to restart the process afterward.
+* `app/adapters/system/resource.py` detects/downloads/installs resources and returns whether installation occurred. Only `app/startup/initializers/modules.py` may decide to restart the process afterward.
* Process memory/GC policy belongs in `app/runtime/gc.py`; external IP-location APIs belong in `app/adapters/external/location.py`.
* Security implementation filenames use package-context nouns: `app/application/security/url.py` and `app/application/security/twofactor.py`. Historical `app.utils.security` and `app.helper.twofa` remain compatibility mappings only.
diff --git a/app/api/context.py b/app/api/context.py
index 247d2cf1b..72bc9f022 100644
--- a/app/api/context.py
+++ b/app/api/context.py
@@ -17,7 +17,7 @@ from app.application.subscription.mutation import (
SubscriptionHistoryMutationRepository,
SubscriptionMutationRepository,
)
-from app.startup.context import (
+from app.startup.composition.context import (
AgentChatRuntime,
HostRuntime,
SubscriptionRuntime,
diff --git a/app/api/dependencies/agent.py b/app/api/dependencies/agent.py
index 9785d9e5c..cdfec0d8c 100644
--- a/app/api/dependencies/agent.py
+++ b/app/api/dependencies/agent.py
@@ -17,7 +17,7 @@ from app.application.messaging.chat import (
AsyncUnitOfWork,
)
from app.application.messaging.message import MessageQueryService
-from app.startup.context import AgentChatRuntime, HostRuntime
+from app.startup.composition.context import AgentChatRuntime, HostRuntime
def get_agent_chat_service(
diff --git a/app/api/dependencies/auth.py b/app/api/dependencies/auth.py
index ab83a4660..48e05fcb8 100644
--- a/app/api/dependencies/auth.py
+++ b/app/api/dependencies/auth.py
@@ -21,7 +21,7 @@ from app.application.security.user import (
UserService,
)
from app.schemas.token import TokenPayload as _SchemaTokenPayload
-from app.startup.context import HostRuntime
+from app.startup.composition.context import HostRuntime
def get_user_service(
diff --git a/app/api/dependencies/history.py b/app/api/dependencies/history.py
index 3367ac8af..5e86c79f4 100644
--- a/app/api/dependencies/history.py
+++ b/app/api/dependencies/history.py
@@ -18,7 +18,7 @@ from app.chain.storage import StorageChain
from app.runtime.events import eventmanager
from app.schemas.types import EventType
from app.schemas.workflow import FileItem as _SchemaFileItem
-from app.startup.context import HostRuntime
+from app.startup.composition.context import HostRuntime
def get_mediaserver_query_service(
diff --git a/app/api/dependencies/site.py b/app/api/dependencies/site.py
index 01e383e0e..08ba4bcc0 100644
--- a/app/api/dependencies/site.py
+++ b/app/api/dependencies/site.py
@@ -14,7 +14,7 @@ from app.domain import site as site_rules
from app.foundation import url as url_tools
from app.runtime.events import eventmanager
from app.schemas.types import EventType
-from app.startup.context import HostRuntime
+from app.startup.composition.context import HostRuntime
async def _publish_site_updated(payload: dict[str, Any]) -> None:
diff --git a/app/api/dependencies/subscription.py b/app/api/dependencies/subscription.py
index c82ea88f0..065908b05 100644
--- a/app/api/dependencies/subscription.py
+++ b/app/api/dependencies/subscription.py
@@ -37,7 +37,7 @@ from app.application.subscription.search import SearchSubscriptionsCommand
from app.runtime.events import eventmanager
from app.runtime.log import logger
from app.schemas.types import EventType
-from app.startup.context import HostRuntime
+from app.startup.composition.context import HostRuntime
from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.runtime.tasks import TaskRegistry
diff --git a/app/api/dependencies/workflow.py b/app/api/dependencies/workflow.py
index ff3acbda8..f5ce6a506 100644
--- a/app/api/dependencies/workflow.py
+++ b/app/api/dependencies/workflow.py
@@ -17,7 +17,7 @@ from app.application.workflow import (
)
from app.runtime.config import global_vars
from app.workflow import WorkFlowManager
-from app.startup.context import HostRuntime
+from app.startup.composition.context import HostRuntime
def get_workflow_mutation_command(
diff --git a/app/application/agent.py b/app/application/agent.py
index efec398d1..dba3e29a8 100644
--- a/app/application/agent.py
+++ b/app/application/agent.py
@@ -13,7 +13,7 @@ from typing import Any, Callable, Optional
Provider = Callable[[], Any]
-# provider 注册表由 startup/agent_initializer 在组合根装配。
+# provider 注册表由 startup/initializers/agent.py 在组合根装配。
_agent_manager_provider: Optional[Provider] = None
_running_agent_manager_provider: Optional[Provider] = None
_prompt_manager_provider: Optional[Provider] = None
@@ -66,7 +66,7 @@ def _resolve(provider: Optional[Provider], service_name: str) -> Any:
if provider is None:
raise RuntimeError(
f"Agent 服务 {service_name} 未注册:"
- "请先导入 app.startup.agent_initializer 完成组合根装配"
+ "请先导入 app.startup.initializers.agent 完成组合根装配"
)
return provider()
diff --git a/app/application/commands.py b/app/application/commands.py
index 7aebbe229..ff6e0400a 100644
--- a/app/application/commands.py
+++ b/app/application/commands.py
@@ -11,7 +11,7 @@ Command 实现由 startup 组合根在导入期注册,避免 application 层
from typing import Any, Dict, Optional
-# Command 类:由 startup/command_initializer 在导入期注册。
+# Command 类:由 startup/initializers/command.py 在导入期注册。
_command_class: Any = None
diff --git a/app/application/messaging/skill.py b/app/application/messaging/skill.py
index 96dfc5822..da6b534ca 100644
--- a/app/application/messaging/skill.py
+++ b/app/application/messaging/skill.py
@@ -58,7 +58,7 @@ def _resolve_skill_catalog() -> SkillCatalogPort:
"""解析已注入的技能目录;缺少组合根装配时给出明确错误。"""
if _skill_catalog_provider is None:
raise RuntimeError(
- "技能目录服务未注册:请先导入 app.startup.agent_initializer "
+ "技能目录服务未注册:请先导入 app.startup.initializers.agent "
"完成组合根装配"
)
return _skill_catalog_provider()
diff --git a/app/application/scheduling.py b/app/application/scheduling.py
index 97daf070e..b3ff11d18 100644
--- a/app/application/scheduling.py
+++ b/app/application/scheduling.py
@@ -17,7 +17,7 @@ from typing import Any, Awaitable, Callable, List, Optional, cast
# Agent 自主定时任务在运行时调度器中的任务 ID 前缀。
AGENT_TASK_JOB_PREFIX = "agent-task"
-# Scheduler 类:由 startup/scheduler_initializer 在导入期注册。
+# Scheduler 类:由 startup/initializers/scheduler.py 在导入期注册。
_scheduler_class: Any = None
diff --git a/app/cli.py b/app/cli.py
index bc5f805e1..988e337b6 100644
--- a/app/cli.py
+++ b/app/cli.py
@@ -21,7 +21,7 @@ from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.state import SystemHelper
from app.application.backup import BackupArtifact
-from app.startup.database import build_database_governance
+from app.startup.composition.database import build_database_governance
from version import APP_VERSION
BACKEND_RUNTIME_FILE = settings.TEMP_PATH / "moviepilot.runtime.json"
diff --git a/app/db/adapters/__init__.py b/app/db/adapters/__init__.py
new file mode 100644
index 000000000..8631fcd40
--- /dev/null
+++ b/app/db/adapters/__init__.py
@@ -0,0 +1 @@
+"""实现 Application 持久化端口的 SQLAlchemy 适配器。"""
diff --git a/app/startup/chain_events.py b/app/db/adapters/chain.py
similarity index 97%
rename from app/startup/chain_events.py
rename to app/db/adapters/chain.py
index f51dfdd4d..637882a30 100644
--- a/app/startup/chain_events.py
+++ b/app/db/adapters/chain.py
@@ -1,4 +1,4 @@
-"""Chain durable 事件写入端口的 SQLAlchemy 启动适配器。"""
+"""Chain durable 事件写入端口的 SQLAlchemy 适配器。"""
from __future__ import annotations
@@ -17,10 +17,10 @@ from app.application.chain.durable_events import (
)
from app.application.history import TransferHistoryRecord, TransferHistoryWriter
from app.application.outbox import DurableEventCommand, OutboxIntent
+from app.db.adapters.outbox import SqlAlchemyOutboxRepository
from app.db.oper.downloadhistory import DownloadHistoryOper
from app.db.oper.transferhistory import TransferHistoryOper
from app.db.uow import SqlAlchemyUnitOfWork
-from app.startup.outbox import SqlAlchemyOutboxRepository
class _StagingTransferHistoryWriter:
diff --git a/app/startup/download_failure.py b/app/db/adapters/download.py
similarity index 100%
rename from app/startup/download_failure.py
rename to app/db/adapters/download.py
diff --git a/app/startup/outbox.py b/app/db/adapters/outbox.py
similarity index 98%
rename from app/startup/outbox.py
rename to app/db/adapters/outbox.py
index c51d7de32..c05b4b4e0 100644
--- a/app/startup/outbox.py
+++ b/app/db/adapters/outbox.py
@@ -1,4 +1,4 @@
-"""启动组合层使用的 SQLAlchemy outbox 持久化适配器。"""
+"""Application outbox 端口的 SQLAlchemy 持久化适配器。"""
from datetime import datetime
diff --git a/app/startup/site.py b/app/db/adapters/site.py
similarity index 100%
rename from app/startup/site.py
rename to app/db/adapters/site.py
diff --git a/app/startup/subscription.py b/app/db/adapters/subscription.py
similarity index 58%
rename from app/startup/subscription.py
rename to app/db/adapters/subscription.py
index 7d643f79f..c8d34b4b6 100644
--- a/app/startup/subscription.py
+++ b/app/db/adapters/subscription.py
@@ -1,9 +1,8 @@
-"""订阅写入事务适配器的启动装配。"""
+"""订阅写入端口的 SQLAlchemy 事务适配器。"""
from collections.abc import Callable
-from contextlib import AbstractAsyncContextManager, asynccontextmanager, contextmanager
+from contextlib import AbstractAsyncContextManager
from datetime import datetime, timezone
-from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
@@ -17,30 +16,12 @@ from app.application.subscription.write import (
subscription_added_notification_key,
subscription_added_report_key,
)
-from app.application.subscription.delete import (
- DeleteSubscribeCommand,
- configure_delete_subscribe_scope,
-)
-from app.application.subscription.complete import (
- CompleteSubscriptionCommand,
- configure_subscription_completion_scope,
-)
-from app.application.subscription.mutation import (
- SubscriptionMutationService,
- configure_subscription_mutation_scope,
-)
-from app.adapters.external.server import MoviePilotServerHelper
-from app.db.oper.subscribe import SubscribeOper
-from app.db.oper.subscribehistory import SubscribeHistoryOper
-from app.db.session import async_session_scope
-from app.db.session import SessionFactory
-from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
-from app.startup.outbox import (
+from app.db.adapters.outbox import (
SqlAlchemyAsyncOutboxStager,
SqlAlchemyOutboxRepository,
)
-from app.runtime.events import EventManager
-from app.schemas.types import EventType
+from app.db.oper.subscribe import SubscribeOper
+from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
class TransactionalSubscribeWriter:
@@ -146,66 +127,3 @@ class TransactionalSubscribeWriter:
delivered,
notification,
)
-
-
-async def _publish_modified(payload: dict[str, Any]) -> None:
- """发布事务已提交的订阅修改事件。"""
- await EventManager().async_send_event(EventType.SubscribeModified, payload)
-
-
-async def _publish_deleted(payload: dict[str, Any]) -> None:
- """发布事务已提交的订阅删除事件。"""
- await EventManager().async_send_event(EventType.SubscribeDeleted, payload)
-
-
-def _publish_completed(payload: dict[str, Any]) -> None:
- """发布已提交的订阅完成事件。"""
- EventManager().send_event(EventType.SubscribeComplete, payload)
-
-
-@contextmanager
-def subscription_completion_scope():
- """为同步完成链创建独占 Session、UoW 与 durable outbox。"""
- session = SessionFactory()
- try:
- yield CompleteSubscriptionCommand(
- repository=SubscribeOper(session),
- unit_of_work=SqlAlchemyUnitOfWork(session),
- outbox=SqlAlchemyOutboxRepository(session),
- publish=_publish_completed,
- )
- finally:
- session.close()
-
-
-@asynccontextmanager
-async def subscription_mutation_scope():
- """为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。"""
- async with async_session_scope() as session:
- yield SubscriptionMutationService(
- repository=SubscribeOper(session),
- history_repository=SubscribeHistoryOper(session),
- unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
- outbox=SqlAlchemyAsyncOutboxStager(session),
- publish_modified=_publish_modified,
- )
-
-
-@asynccontextmanager
-async def delete_subscribe_scope():
- """为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。"""
- async with async_session_scope() as session:
- yield DeleteSubscribeCommand(
- repository=SubscribeOper(session),
- unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
- publish_deleted=_publish_deleted,
- report_deleted=MoviePilotServerHelper.async_sub_done_durable,
- outbox=SqlAlchemyAsyncOutboxStager(session),
- )
-
-
-def configure_transactional_subscription_scopes() -> None:
- """登记 Agent 等非 HTTP 入口复用的订阅事务作用域。"""
- configure_subscription_mutation_scope(subscription_mutation_scope)
- configure_delete_subscribe_scope(delete_subscribe_scope)
- configure_subscription_completion_scope(subscription_completion_scope)
diff --git a/app/startup/transaction.py b/app/db/adapters/transaction.py
similarity index 100%
rename from app/startup/transaction.py
rename to app/db/adapters/transaction.py
diff --git a/app/startup/workflow.py b/app/db/adapters/workflow.py
similarity index 100%
rename from app/startup/workflow.py
rename to app/db/adapters/workflow.py
diff --git a/app/db/base.py b/app/db/base.py
index 70ba83643..f28043057 100644
--- a/app/db/base.py
+++ b/app/db/base.py
@@ -1,7 +1,7 @@
"""
ORM 基类与数据访问基类。
-Base 提供声明式基类与通用的行为(字典转换、增删改查便利方法);
+Base 提供声明式基类与兼容行为(字典转换、旧增删改查便利方法);
DbOper 是各业务 Oper 的基类,持有一个可注入的会话。
"""
from collections.abc import Awaitable, Callable
@@ -12,9 +12,14 @@ from sqlalchemy import (CursorResult, Executable, Identity, Integer, Sequence,
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, declared_attr, mapped_column
-from app.runtime.config import settings
-from app.db.decorators import async_db_query, async_db_update, db_query, db_update
+from app.db.decorators import (
+ legacy_async_db_query,
+ legacy_async_db_update,
+ legacy_db_query,
+ legacy_db_update,
+)
from app.db.uow import run_async_transaction, run_sync_transaction
+from app.runtime.config import settings
T = TypeVar("T")
@@ -64,88 +69,104 @@ class Base(DeclarativeBase): # type: ignore[misc] # SQLAlchemy 无 py.typed
继承本类的模型一律使用 mapped_column() + Mapped[] 注解;确需非映射的类级属性时
用 ClassVar 显式声明,而不是把这个标志加回来。
+
+ create/get/update/delete/list/truncate 及其异步版本仅保留旧插件 ABI。宿主新代码应由
+ Application Command 定义事务边界,经显式 Session 调用 Oper,不得新增对这些方法的依赖。
"""
# 由 get_id_column() 在各模型中提供实际的列定义,这里只声明类型供 IDE 使用
id: Mapped[int]
- @db_update
+ @legacy_db_update
def create(self, db: Session) -> None:
+ """兼容旧插件调用:新增当前模型并提交。"""
db.add(self)
- @async_db_update
+ @legacy_async_db_update
async def async_create(self, db: AsyncSession) -> Self:
+ """兼容旧插件调用:异步新增当前模型、刷新主键并提交。"""
db.add(self)
await db.flush()
return self
@classmethod
- @db_query
+ @legacy_db_query
def get(cls, db: Session, rid: int) -> Optional[Self]:
+ """兼容旧插件调用:按主键查询当前模型。"""
return cast(
Optional[Self],
db.execute(select(cls).where(and_(cls.id == rid))).scalars().first(),
)
@classmethod
- @async_db_query
+ @legacy_async_db_query
async def async_get(cls, db: AsyncSession, rid: int) -> Optional[Self]:
+ """兼容旧插件调用:异步按主键查询当前模型。"""
result = await db.execute(select(cls).where(and_(cls.id == rid)))
return cast(Optional[Self], result.scalars().first())
- @db_update
+ @legacy_db_update
def update(self, db: Session, payload: dict[str, Any]) -> None:
+ """兼容旧插件调用:更新当前模型字段并提交。"""
for key, value in payload.items():
setattr(self, key, value)
if inspect(self).detached:
db.add(self)
- @async_db_update
+ @legacy_async_db_update
async def async_update(
self,
db: AsyncSession,
payload: dict[str, Any],
) -> None:
+ """兼容旧插件调用:异步更新当前模型字段并提交。"""
for key, value in payload.items():
setattr(self, key, value)
if inspect(self).detached:
db.add(self)
@classmethod
- @db_update
+ @legacy_db_update
def delete(cls, db: Session, rid: Any) -> None:
+ """兼容旧插件调用:按主键删除当前模型并提交。"""
db.execute(delete(cls).where(and_(cls.id == rid)))
@classmethod
- @async_db_update
+ @legacy_async_db_update
async def async_delete(cls, db: AsyncSession, rid: Any) -> None:
+ """兼容旧插件调用:异步按主键删除当前模型并提交。"""
result = await db.execute(select(cls).where(and_(cls.id == rid)))
user = result.scalars().first()
if user:
await db.delete(user)
@classmethod
- @db_update
+ @legacy_db_update
def truncate(cls, db: Session) -> None:
+ """兼容旧插件调用:清空当前模型表并提交。"""
db.execute(delete(cls))
@classmethod
- @async_db_update
+ @legacy_async_db_update
async def async_truncate(cls, db: AsyncSession) -> None:
+ """兼容旧插件调用:异步清空当前模型表并提交。"""
await db.execute(delete(cls))
@classmethod
- @db_query
+ @legacy_db_query
def list(cls, db: Session) -> List[Self]:
+ """兼容旧插件调用:查询当前模型的全部记录。"""
return list(db.execute(select(cls)).scalars().all())
@classmethod
- @async_db_query
+ @legacy_async_db_query
async def async_list(cls, db: AsyncSession) -> List[Self]:
+ """兼容旧插件调用:异步查询当前模型的全部记录。"""
result = await db.execute(select(cls))
return list(result.scalars().all())
def to_dict(self) -> dict[str, Any]:
+ """把当前模型的映射列转换为字典。"""
return {c.name: getattr(self, c.name, None) for c in self.__table__.columns} # noqa
@declared_attr.directive # type: ignore[misc] # SQLAlchemy decorator 缺少类型信息
diff --git a/app/db/decorators.py b/app/db/decorators.py
index 168f497e7..3a858418e 100644
--- a/app/db/decorators.py
+++ b/app/db/decorators.py
@@ -5,8 +5,8 @@
未显式传入会话时自动创建,并在结束时归还——异步路径经 async_session_scope 收口,
连接池与配额都在那里生效。
-收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛,四个装饰器
-的处理一致。理由与代价都要写明,别当成漏写的 raise:
+收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛,正式装饰器
+和 legacy 兼容壳的处理一致。理由与代价都要写明,别当成漏写的 raise:
- 连接断开、事务已失效这类故障恰恰最容易发生在「出错之后」的收尾阶段。裸写收尾语句时
它一抛错就顶替掉原始异常,调用方看到的只剩「connection reset」,业务异常连类型都被
@@ -28,31 +28,12 @@ from app.runtime.log import logger
_R = TypeVar("_R")
-# 四个装饰器都会重写实参列表:未传会话时自行创建一个并塞回 db 位置。因此包装后的可调用
+# 正式装饰器会重写实参列表:未传会话时自行创建一个并塞回 db 位置。因此包装后的可调用
# 对象接受的实参与被包装函数的签名并不一致——用 Callable[..., _R] 如实表达「参数由装饰器
# 接管、返回值原样透传」。否则调用方传 None 或传异步会话都会被判成类型不符,而这恰恰是
# 装饰器存在的理由(各 Oper 的 self._db 常态就是 None)。
-def run_legacy_sync_query(operation: Callable[[Session], _R]) -> _R:
- """为已移除查询装饰器的旧 Model ABI 提供一次性同步会话。"""
- db = ScopedSession()
- try:
- return operation(db)
- finally:
- try:
- db.close()
- except Exception as close_err: # noqa: BLE001 兼容查询释放失败不改变返回语义
- logger.error(f"释放数据库会话失败:{close_err}")
-
-
-async def run_legacy_async_query(
- operation: Callable[[AsyncSession], Awaitable[_R]],
-) -> _R:
- """为移除异步查询装饰器的旧 Model ABI 提供一次性异步会话。"""
- async with async_session_scope() as db:
- return await operation(db)
-
def _get_args_db(
args: tuple[Any, ...],
kwargs: dict[str, Any],
@@ -345,6 +326,77 @@ def legacy_async_db_query(
return wrapper
+def legacy_db_update(func: Callable[..., _R]) -> Callable[..., _R]:
+ """保留旧 Model 同步写 ABI,并维持历史自动提交语义。
+
+ 该装饰器只供已经公开的 Model/Base 方法兼容仓外插件。宿主新写路径必须
+ 通过 Application Command、显式 Session 和 UnitOfWork 完成事务收口。
+ """
+
+ @wraps(func)
+ def wrapper(*args: Any, **kwargs: Any) -> _R:
+ db = _get_args_db(args, kwargs)
+ owns_session = db is None
+ if db is None:
+ db = ScopedSession()
+ args, kwargs = _inject_legacy_db(func, args, kwargs, db)
+ try:
+ result = func(*args, **kwargs)
+ db.commit()
+ return result
+ except Exception:
+ try:
+ db.rollback()
+ except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常
+ logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}")
+ raise
+ finally:
+ if owns_session:
+ try:
+ db.close()
+ except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果
+ logger.error(f"释放数据库会话失败:{close_err}")
+
+ return wrapper
+
+
+def legacy_async_db_update(
+ func: Callable[..., Awaitable[_R]],
+) -> Callable[..., Awaitable[_R]]:
+ """保留旧 Model 异步写 ABI,并维持历史自动提交语义。
+
+ 该装饰器只承接既有兼容面;新宿主代码不得用它创建隐式事务。
+ """
+
+ @wraps(func)
+ async def wrapper(*args: Any, **kwargs: Any) -> _R:
+ db = _get_args_async_db(args, kwargs)
+ owns_session = db is None
+ scope = None
+ if db is None:
+ scope = async_session_scope()
+ db = await scope.__aenter__()
+ args, kwargs = _inject_legacy_db(func, args, kwargs, db)
+ try:
+ result = await func(*args, **kwargs)
+ await db.commit()
+ return result
+ except Exception:
+ try:
+ await db.rollback()
+ except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常
+ logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}")
+ raise
+ finally:
+ if owns_session and scope is not None:
+ try:
+ await scope.__aexit__(None, None, None)
+ except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果
+ logger.error(f"释放数据库会话失败:{close_err}")
+
+ return wrapper
+
+
def _inject_legacy_db(
func: Callable[..., _R],
args: tuple[Any, ...],
diff --git a/app/db/models/agenttask.py b/app/db/models/agenttask.py
index 947c91cf9..0073dbccb 100644
--- a/app/db/models/agenttask.py
+++ b/app/db/models/agenttask.py
@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Index, Integer, String, Text, select, update
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
-from app.db.decorators import run_legacy_sync_query
+from app.db.decorators import legacy_db_query
def _get_for_user_statement(
@@ -85,6 +85,7 @@ class AgentTask(Base):
return task.id
@classmethod
+ @legacy_db_query
def get_for_user(
cls,
db: Session | int | None = None,
@@ -105,11 +106,10 @@ class AgentTask(Base):
_get_for_user_statement(cls, task_id=task_id, user_id=user_id)
).scalars().first()
- if isinstance(db, Session):
- return query(db)
- return run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_db_query
def list_for_user(
cls,
db: Session | None = None,
@@ -125,9 +125,7 @@ class AgentTask(Base):
_list_for_user_statement(cls, user_id=user_id, enabled=enabled)
).scalars().all())
- if isinstance(db, Session):
- return query(db)
- return run_legacy_sync_query(query)
+ return query(db)
@classmethod
def update_task(
diff --git a/app/db/models/message.py b/app/db/models/message.py
index 31193b20f..034adf974 100644
--- a/app/db/models/message.py
+++ b/app/db/models/message.py
@@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
-from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
+from app.db.decorators import legacy_async_db_query, legacy_db_query
class Message(Base):
@@ -49,6 +49,7 @@ class Message(Base):
return self.to_dict()
@classmethod
+ @legacy_db_query
def list_by_page(
cls,
db: Session | None = None,
@@ -67,9 +68,10 @@ class Message(Base):
.limit(count)
).scalars().all())
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_db_query
def exists_by_source(
cls,
db: Session | str | None = None,
@@ -93,9 +95,10 @@ class Message(Base):
select(cls.id).where(cls.source == source).limit(1)
).scalars().first() is not None
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_list_by_page(
cls, db: AsyncSession | None = None, page: int = 1, count: int = 30
) -> List["Message"]:
@@ -112,9 +115,10 @@ class Message(Base):
)
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_async_db_query
async def async_list_sent_by_page(
cls,
db: AsyncSession | None = None,
@@ -155,7 +159,7 @@ class Message(Base):
)
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
def delete_before(
diff --git a/app/db/models/passkey.py b/app/db/models/passkey.py
index 158e9caec..06179467e 100644
--- a/app/db/models/passkey.py
+++ b/app/db/models/passkey.py
@@ -8,7 +8,6 @@ from app.db.base import Base, get_id_column
from app.db.decorators import (
legacy_async_db_query,
legacy_db_query,
- run_legacy_sync_query,
)
@@ -55,6 +54,7 @@ class PassKey(Base):
transports: Mapped[Optional[str]] = mapped_column(String, nullable=True)
@classmethod
+ @legacy_db_query
def get_by_user_id(
cls,
db: Session | int | None = None,
@@ -72,9 +72,7 @@ class PassKey(Base):
_get_by_user_id_statement(cls, user_id)
).scalars().all())
- if isinstance(db, Session):
- return query(db)
- return run_legacy_sync_query(query)
+ return query(db)
@classmethod
@legacy_async_db_query
@@ -86,6 +84,7 @@ class PassKey(Base):
return list(result.scalars().all())
@classmethod
+ @legacy_db_query
def get_by_credential_id(
cls,
db: Session | str | None = None,
@@ -103,9 +102,7 @@ class PassKey(Base):
_get_by_credential_id_statement(cls, credential_id)
).scalars().first()
- if isinstance(db, Session):
- return query(db)
- return run_legacy_sync_query(query)
+ return query(db)
@classmethod
@legacy_async_db_query
diff --git a/app/db/models/plugindata.py b/app/db/models/plugindata.py
index 8bf4bd78b..4aa9c9985 100644
--- a/app/db/models/plugindata.py
+++ b/app/db/models/plugindata.py
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
-from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
+from app.db.decorators import legacy_async_db_query, legacy_db_query
class PluginData(Base):
@@ -21,54 +21,44 @@ class PluginData(Base):
)
@classmethod
+ @legacy_db_query
def get_plugin_data(cls, db: Session | None = None, plugin_id: str | None = None):
"""在调用方 Session 中读取插件全部数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
- if not isinstance(db, Session):
- return run_legacy_sync_query(lambda session: cls.get_plugin_data(session, plugin_id))
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
@classmethod
+ @legacy_async_db_query
async def async_get_plugin_data(
cls, db: AsyncSession | None = None, plugin_id: str | None = None
):
"""在调用方 AsyncSession 中读取插件全部数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
- if not isinstance(db, AsyncSession):
- return await run_legacy_async_query(
- lambda session: cls.async_get_plugin_data(session, plugin_id)
- )
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
return list(result.scalars().all())
@classmethod
+ @legacy_db_query
def get_plugin_data_by_key(
cls, db: Session | None = None, plugin_id: str | None = None, key: str | None = None
):
"""在调用方 Session 中按键读取插件数据,并兼容旧无会话入口。"""
if plugin_id is None or key is None:
raise TypeError("plugin_id and key are required")
- if not isinstance(db, Session):
- return run_legacy_sync_query(
- lambda session: cls.get_plugin_data_by_key(session, plugin_id, key)
- )
return db.execute(
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
).scalars().first()
@classmethod
+ @legacy_async_db_query
async def async_get_plugin_data_by_key(
cls, db: AsyncSession | None = None, plugin_id: str | None = None, key: str | None = None
):
"""在调用方 AsyncSession 中按键读取插件数据,并兼容旧无会话入口。"""
if plugin_id is None or key is None:
raise TypeError("plugin_id and key are required")
- if not isinstance(db, AsyncSession):
- return await run_legacy_async_query(
- lambda session: cls.async_get_plugin_data_by_key(session, plugin_id, key)
- )
result = await db.execute(
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
)
@@ -85,28 +75,22 @@ class PluginData(Base):
db.execute(delete(cls).where(cls.plugin_id == plugin_id))
@classmethod
+ @legacy_db_query
def get_plugin_data_by_plugin_id(
cls, db: Session | None = None, plugin_id: str | None = None
):
"""在调用方 Session 中按插件 ID 读取数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
- if not isinstance(db, Session):
- return run_legacy_sync_query(
- lambda session: cls.get_plugin_data_by_plugin_id(session, plugin_id)
- )
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
@classmethod
+ @legacy_async_db_query
async def async_get_plugin_data_by_plugin_id(
cls, db: AsyncSession | None = None, plugin_id: str | None = None
):
"""在调用方 AsyncSession 中按插件 ID 读取数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
- if not isinstance(db, AsyncSession):
- return await run_legacy_async_query(
- lambda session: cls.async_get_plugin_data_by_plugin_id(session, plugin_id)
- )
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
return list(result.scalars().all())
diff --git a/app/db/models/site.py b/app/db/models/site.py
index 7f6b873be..206880b3e 100644
--- a/app/db/models/site.py
+++ b/app/db/models/site.py
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
-from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
+from app.db.decorators import legacy_async_db_query, legacy_db_query
class Site(Base):
@@ -58,6 +58,7 @@ class Site(Base):
downloader: Mapped[Optional[str]] = mapped_column(String)
@classmethod
+ @legacy_db_query
def get_by_domain(cls, db: Session | str | None = None, domain: str | None = None):
"""按域名查询站点,兼容显式会话和旧插件无会话调用。"""
if domain is None and isinstance(db, str):
@@ -69,9 +70,10 @@ class Site(Base):
"""在给定同步会话中执行域名查询。"""
return session.execute(select(cls).where(cls.domain == domain)).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | str | None = None,
@@ -88,9 +90,10 @@ class Site(Base):
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_async_db_query
async def async_get_by_name(
cls,
db: AsyncSession | str | None = None,
@@ -107,18 +110,20 @@ class Site(Base):
result = await session.execute(select(cls).where(cls.name == name))
return result.scalar_one_or_none()
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def get_actives(cls, db: Session | None = None):
"""查询启用站点,兼容显式会话和旧插件无会话调用。"""
def query(session: Session):
"""在给定同步会话中执行启用站点查询。"""
return list(session.execute(select(cls).where(cls.is_active.is_(True))).scalars().all())
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_get_actives(cls, db: AsyncSession | None = None):
"""异步查询启用站点,兼容显式会话和旧插件无会话调用。"""
async def query(session: AsyncSession):
@@ -126,18 +131,20 @@ class Site(Base):
result = await session.execute(select(cls).where(cls.is_active.is_(True)))
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def list_order_by_pri(cls, db: Session | None = None):
"""按优先级升序查询站点,兼容显式会话和旧插件无会话调用。"""
def query(session: Session):
"""在给定同步会话中执行优先级查询。"""
return list(session.execute(select(cls).order_by(cls.pri)).scalars().all())
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_list_order_by_pri(cls, db: AsyncSession | None = None):
"""异步按优先级升序查询站点,兼容显式会话和旧插件无会话调用。"""
async def query(session: AsyncSession):
@@ -145,9 +152,10 @@ class Site(Base):
result = await session.execute(select(cls).order_by(cls.pri))
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def get_domains_by_ids(
cls,
db: Session | list[int] | None = None,
@@ -165,7 +173,7 @@ class Site(Base):
"""在给定同步会话中执行域名投影查询。"""
return list(session.execute(select(cls.domain).where(cls.id.in_(ids))).scalars().all())
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
def reset(cls, db: Session):
diff --git a/app/db/models/siteicon.py b/app/db/models/siteicon.py
index 3d420755b..2882c5cbd 100644
--- a/app/db/models/siteicon.py
+++ b/app/db/models/siteicon.py
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
-from app.db.decorators import run_legacy_async_query
+from app.db.decorators import legacy_async_db_query
class SiteIcon(Base):
@@ -27,6 +27,7 @@ class SiteIcon(Base):
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
+ @legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | None = None,
@@ -41,6 +42,4 @@ class SiteIcon(Base):
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
- if isinstance(db, AsyncSession):
- return await query(db)
- return await run_legacy_async_query(query)
+ return await query(db)
diff --git a/app/db/models/sitestatistic.py b/app/db/models/sitestatistic.py
index 7d7ddf061..cf187c1bb 100644
--- a/app/db/models/sitestatistic.py
+++ b/app/db/models/sitestatistic.py
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
-from app.db.decorators import run_legacy_async_query
+from app.db.decorators import legacy_async_db_query
class SiteStatistic(Base):
@@ -35,6 +35,7 @@ class SiteStatistic(Base):
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
+ @legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | None = None,
@@ -49,9 +50,7 @@ class SiteStatistic(Base):
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
- if isinstance(db, AsyncSession):
- return await query(db)
- return await run_legacy_async_query(query)
+ return await query(db)
@classmethod
def reset(cls, db: Session):
diff --git a/app/db/models/subscribe.py b/app/db/models/subscribe.py
index ec71b11e2..92c125d60 100644
--- a/app/db/models/subscribe.py
+++ b/app/db/models/subscribe.py
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
-from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
+from app.db.decorators import legacy_async_db_query, legacy_db_query
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
@@ -140,6 +140,7 @@ class Subscribe(Base):
return condition
@classmethod
+ @legacy_db_query
def exists(
cls, db: Session | MediaSource | None = None,
media_source: MediaSource | str | None = None,
@@ -164,9 +165,10 @@ class Subscribe(Base):
return session.execute(
statement.where(cls.episode_group == episode_group)
).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_exists(
cls, db: AsyncSession | MediaSource | None = None,
media_source: MediaSource | str | None = None,
@@ -192,9 +194,10 @@ class Subscribe(Base):
statement.where(cls.episode_group == episode_group)
)
return result.scalars().first()
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def exists_by_username(
cls, db: Session | str | None = None,
username: str | MediaSource | None = None,
@@ -224,9 +227,10 @@ class Subscribe(Base):
return session.execute(
statement.where(cls.episode_group == episode_group)
).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_exists_by_username(
cls, db: AsyncSession | str | None = None,
username: str | MediaSource | None = None,
@@ -256,9 +260,10 @@ class Subscribe(Base):
statement.where(cls.episode_group == episode_group)
)
return result.scalars().first()
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def get_by_state(cls, db: Session | str | None = None, state: str | None = None):
"""按状态列表查询订阅,兼容显式会话和旧插件无会话调用。"""
if not isinstance(db, Session):
@@ -269,9 +274,10 @@ class Subscribe(Base):
if state:
statement = statement.where(cls.state.in_(state.split(',')))
return list(session.execute(statement).scalars().all())
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_get_by_state(
cls, db: AsyncSession | str | None = None, state: str | None = None
):
@@ -285,9 +291,10 @@ class Subscribe(Base):
statement = statement.where(cls.state.in_(state.split(',')))
result = await session.execute(statement)
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def get_by_title(
cls, db: Session | str | None = None, title: str | None = None,
season: Optional[int] = None,
@@ -301,9 +308,10 @@ class Subscribe(Base):
if season is not None:
statement = statement.where(cls.season == season)
return session.execute(statement).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_get_by_title(
cls, db: AsyncSession | str | None = None, title: str | None = None,
season: Optional[int] = None,
@@ -318,9 +326,10 @@ class Subscribe(Base):
statement = statement.where(cls.season == season)
result = await session.execute(statement)
return result.scalars().first()
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_async_db_query
async def async_list_by_title(
cls, db: AsyncSession | str | None = None, title: str | None = None,
season: Optional[int] = None,
@@ -335,9 +344,10 @@ class Subscribe(Base):
statement = statement.where(cls.season == season)
result = await session.execute(statement)
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def list_by_media_identity(
cls, db: Session | MediaSource | None = None,
media_source: MediaSource | str | None = None,
@@ -357,9 +367,10 @@ class Subscribe(Base):
def query(session: Session):
"""在给定会话中执行媒体身份列表查询。"""
return list(session.execute(select(cls).where(condition)).scalars().all())
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_list_by_media_identity(
cls, db: AsyncSession | MediaSource | None = None,
media_source: MediaSource | str | None = None,
@@ -380,9 +391,10 @@ class Subscribe(Base):
"""在给定异步会话中执行媒体身份列表查询。"""
result = await session.execute(select(cls).where(condition))
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def get_by(
cls, db: Session | str | None = None,
type: str | MediaSource | None = None,
@@ -407,9 +419,10 @@ class Subscribe(Base):
def query(session: Session):
"""在给定会话中执行类型媒体查询。"""
return session.execute(statement).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_get_by(
cls, db: AsyncSession | str | None = None,
type: str | MediaSource | None = None,
@@ -435,9 +448,10 @@ class Subscribe(Base):
"""在给定异步会话中执行类型媒体查询。"""
result = await session.execute(query)
return result.scalars().first()
- return await execute_query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(execute_query)
+ return await execute_query(db)
@classmethod
+ @legacy_db_query
def list_by_username(cls, db: Session | str | None = None, username: str | None = None,
state: Optional[str] = None, mtype: Optional[str] = None):
"""按用户筛选订阅,兼容显式会话和旧插件无会话调用。"""
@@ -451,9 +465,10 @@ class Subscribe(Base):
if mtype:
statement = statement.where(cls.type == mtype)
return list(session.execute(statement).scalars().all())
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_list_by_username(cls, db: AsyncSession | str | None = None,
username: str | None = None, state: Optional[str] = None,
mtype: Optional[str] = None):
@@ -469,9 +484,10 @@ class Subscribe(Base):
statement = statement.where(cls.type == mtype)
result = await session.execute(statement)
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def list_by_type(cls, db: Session | str | None = None, mtype: str | None = None, days: int = 7):
"""按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。"""
if not isinstance(db, Session):
@@ -483,9 +499,10 @@ class Subscribe(Base):
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)))
)).scalars().all())
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_list_by_type(cls, db: AsyncSession | str | None = None,
mtype: str | None = None, days: int = 7):
"""异步按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。"""
@@ -499,4 +516,4 @@ class Subscribe(Base):
time.localtime(time.time() - 86400 * int(days)))
))
return list(result.scalars().all())
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
diff --git a/app/db/models/transferhistory.py b/app/db/models/transferhistory.py
index fbcde0361..6d68caffd 100644
--- a/app/db/models/transferhistory.py
+++ b/app/db/models/transferhistory.py
@@ -11,7 +11,6 @@ from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import (
legacy_async_db_query,
legacy_db_query,
- run_legacy_sync_query,
)
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaSource, MediaType
@@ -189,6 +188,7 @@ class TransferHistory(Base):
return list(result.scalars().all())
@classmethod
+ @legacy_db_query
def get_by_hash(
cls,
db: Session | str | None = None,
@@ -206,9 +206,10 @@ class TransferHistory(Base):
select(cls).where(cls.download_hash == download_hash)
).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_db_query
def get_by_src(
cls, db: Session | str | None = None, src: str | None = None,
storage: Optional[str] = None
@@ -235,9 +236,10 @@ class TransferHistory(Base):
statement.order_by(cls.id.desc())
).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_db_query
def get_success_by_src(
cls, db: Session | str | None = None, src: str | None = None,
storage: Optional[str] = None
@@ -266,9 +268,10 @@ class TransferHistory(Base):
statement.order_by(cls.id.desc())
).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_db_query
def get_by_dest(
cls, db: Session | str | None = None, dest: str | None = None,
storage: Optional[str] = None
@@ -295,7 +298,7 @@ class TransferHistory(Base):
statement.order_by(cls.id.desc())
).scalars().first()
- return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
+ return query(db)
@classmethod
@legacy_db_query
diff --git a/app/db/models/user.py b/app/db/models/user.py
index 087687f3a..e77a50aee 100644
--- a/app/db/models/user.py
+++ b/app/db/models/user.py
@@ -4,10 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
-from app.db.decorators import (
- run_legacy_async_query,
- run_legacy_sync_query,
-)
+from app.db.decorators import legacy_async_db_query, legacy_db_query
class User(Base):
@@ -38,6 +35,7 @@ class User(Base):
settings: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
@classmethod
+ @legacy_db_query
def get_by_name(
cls,
db: Session | str | None = None,
@@ -53,11 +51,10 @@ class User(Base):
"""在给定会话中执行用户名查询。"""
return session.execute(select(cls).where(cls.name == name)).scalars().first()
- if isinstance(db, Session):
- return query(db)
- return run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_get_by_name(
cls,
db: AsyncSession | str | None = None,
@@ -74,9 +71,10 @@ class User(Base):
result = await session.execute(select(cls).filter(cls.name == name))
return result.scalars().first()
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
@classmethod
+ @legacy_db_query
def get_by_id(cls, db: Session | int | None = None, user_id: int | None = None):
"""按用户 ID 查询用户,兼容显式会话和旧插件无会话调用。"""
if user_id is None and isinstance(db, int):
@@ -88,11 +86,10 @@ class User(Base):
"""在给定会话中执行用户 ID 查询。"""
return session.execute(select(cls).where(cls.id == user_id)).scalars().first()
- if isinstance(db, Session):
- return query(db)
- return run_legacy_sync_query(query)
+ return query(db)
@classmethod
+ @legacy_async_db_query
async def async_get_by_id(
cls,
db: AsyncSession | int | None = None,
@@ -109,7 +106,7 @@ class User(Base):
result = await session.execute(select(cls).filter(cls.id == user_id))
return result.scalars().first()
- return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
+ return await query(db)
def delete_by_name(self, db: Session, name: str):
user = self.get_by_name(db, name)
diff --git a/app/runtime/compat/manifest.py b/app/runtime/compat/manifest.py
index fb1fdaa83..76149e127 100644
--- a/app/runtime/compat/manifest.py
+++ b/app/runtime/compat/manifest.py
@@ -81,8 +81,8 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = {
owner="db",
),
"app.db.init": ModuleAlias(
- target="app.startup.database_initializer",
- replacement="app.startup.database_initializer",
+ target="app.startup.initializers.database",
+ replacement="app.startup.initializers.database",
introduced="v3.0.0",
owner="startup",
),
diff --git a/app/startup/composition/__init__.py b/app/startup/composition/__init__.py
new file mode 100644
index 000000000..6bb9a6de2
--- /dev/null
+++ b/app/startup/composition/__init__.py
@@ -0,0 +1 @@
+"""宿主运行时对象、配置快照与跨层依赖的组合构建。"""
diff --git a/app/startup/configuration.py b/app/startup/composition/configuration.py
similarity index 100%
rename from app/startup/configuration.py
rename to app/startup/composition/configuration.py
diff --git a/app/startup/context.py b/app/startup/composition/context.py
similarity index 100%
rename from app/startup/context.py
rename to app/startup/composition/context.py
diff --git a/app/startup/database.py b/app/startup/composition/database.py
similarity index 100%
rename from app/startup/database.py
rename to app/startup/composition/database.py
diff --git a/app/startup/composition/subscription.py b/app/startup/composition/subscription.py
new file mode 100644
index 000000000..3c15e8cd0
--- /dev/null
+++ b/app/startup/composition/subscription.py
@@ -0,0 +1,91 @@
+"""订阅事务作用域及提交后回调的组合装配。"""
+
+from contextlib import asynccontextmanager, contextmanager
+from typing import Any
+
+from app.adapters.external.server import MoviePilotServerHelper
+from app.application.subscription.complete import (
+ CompleteSubscriptionCommand,
+ configure_subscription_completion_scope,
+)
+from app.application.subscription.delete import (
+ DeleteSubscribeCommand,
+ configure_delete_subscribe_scope,
+)
+from app.application.subscription.mutation import (
+ SubscriptionMutationService,
+ configure_subscription_mutation_scope,
+)
+from app.db.adapters.outbox import (
+ SqlAlchemyAsyncOutboxStager,
+ SqlAlchemyOutboxRepository,
+)
+from app.db.oper.subscribe import SubscribeOper
+from app.db.oper.subscribehistory import SubscribeHistoryOper
+from app.db.session import SessionFactory, async_session_scope
+from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
+from app.runtime.events import EventManager
+from app.schemas.types import EventType
+
+
+async def _publish_modified(payload: dict[str, Any]) -> None:
+ """发布事务已提交的订阅修改事件。"""
+ await EventManager().async_send_event(EventType.SubscribeModified, payload)
+
+
+async def _publish_deleted(payload: dict[str, Any]) -> None:
+ """发布事务已提交的订阅删除事件。"""
+ await EventManager().async_send_event(EventType.SubscribeDeleted, payload)
+
+
+def _publish_completed(payload: dict[str, Any]) -> None:
+ """发布已提交的订阅完成事件。"""
+ EventManager().send_event(EventType.SubscribeComplete, payload)
+
+
+@contextmanager
+def subscription_completion_scope():
+ """为同步完成链创建独占 Session、UoW 与 durable outbox。"""
+ session = SessionFactory()
+ try:
+ yield CompleteSubscriptionCommand(
+ repository=SubscribeOper(session),
+ unit_of_work=SqlAlchemyUnitOfWork(session),
+ outbox=SqlAlchemyOutboxRepository(session),
+ publish=_publish_completed,
+ )
+ finally:
+ session.close()
+
+
+@asynccontextmanager
+async def subscription_mutation_scope():
+ """为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。"""
+ async with async_session_scope() as session:
+ yield SubscriptionMutationService(
+ repository=SubscribeOper(session),
+ history_repository=SubscribeHistoryOper(session),
+ unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
+ outbox=SqlAlchemyAsyncOutboxStager(session),
+ publish_modified=_publish_modified,
+ )
+
+
+@asynccontextmanager
+async def delete_subscribe_scope():
+ """为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。"""
+ async with async_session_scope() as session:
+ yield DeleteSubscribeCommand(
+ repository=SubscribeOper(session),
+ unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
+ publish_deleted=_publish_deleted,
+ report_deleted=MoviePilotServerHelper.async_sub_done_durable,
+ outbox=SqlAlchemyAsyncOutboxStager(session),
+ )
+
+
+def configure_transactional_subscription_scopes() -> None:
+ """登记 Agent 等非 HTTP 入口复用的订阅事务作用域。"""
+ configure_subscription_mutation_scope(subscription_mutation_scope)
+ configure_delete_subscribe_scope(delete_subscribe_scope)
+ configure_subscription_completion_scope(subscription_completion_scope)
diff --git a/app/startup/initializers/__init__.py b/app/startup/initializers/__init__.py
new file mode 100644
index 000000000..698c070c2
--- /dev/null
+++ b/app/startup/initializers/__init__.py
@@ -0,0 +1 @@
+"""按领域组织的宿主初始化与关闭入口。"""
diff --git a/app/startup/agent_initializer.py b/app/startup/initializers/agent.py
similarity index 100%
rename from app/startup/agent_initializer.py
rename to app/startup/initializers/agent.py
diff --git a/app/startup/cache_initializer.py b/app/startup/initializers/cache.py
similarity index 100%
rename from app/startup/cache_initializer.py
rename to app/startup/initializers/cache.py
diff --git a/app/startup/command_initializer.py b/app/startup/initializers/command.py
similarity index 100%
rename from app/startup/command_initializer.py
rename to app/startup/initializers/command.py
diff --git a/app/startup/database_initializer.py b/app/startup/initializers/database.py
similarity index 98%
rename from app/startup/database_initializer.py
rename to app/startup/initializers/database.py
index 32866e1b7..389dba094 100644
--- a/app/startup/database_initializer.py
+++ b/app/startup/initializers/database.py
@@ -17,7 +17,7 @@ from app.db.base import Base
from app.db.engine import get_engine
from app.db.models import load_all_models
from app.runtime.log import logger
-from app.startup.database import build_database_governance
+from app.startup.composition.database import build_database_governance
def _build_alembic_config(engine: Engine | None = None) -> Config:
diff --git a/app/startup/domain_initializer.py b/app/startup/initializers/domain.py
similarity index 100%
rename from app/startup/domain_initializer.py
rename to app/startup/initializers/domain.py
diff --git a/app/startup/managed_resources_initializer.py b/app/startup/initializers/managed_resources.py
similarity index 100%
rename from app/startup/managed_resources_initializer.py
rename to app/startup/initializers/managed_resources.py
diff --git a/app/startup/modules_initializer.py b/app/startup/initializers/modules.py
similarity index 97%
rename from app/startup/modules_initializer.py
rename to app/startup/initializers/modules.py
index dc1becc12..c093b4cc0 100644
--- a/app/startup/modules_initializer.py
+++ b/app/startup/initializers/modules.py
@@ -53,7 +53,7 @@ from app.application.configuration import (
configure_system_config,
configure_transfer_retry_config,
)
-from app.startup.configuration import (
+from app.startup.composition.configuration import (
build_api_runtime_config,
build_chain_runtime_config,
build_scheduler_runtime_config,
@@ -83,7 +83,7 @@ from app.application.security.userconfig import (
)
from app.application.history import configure_transfer_history_provider
from app.application.outbox import OutboxDispatcher, configure_outbox_dispatcher
-from app.startup.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository
+from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository
from app.application.site.query import SiteQueryService, configure_site_query_service
from app.application.site.health import SiteHealthService, configure_site_health_service
from app.application.workflow import WorkflowQueryService, configure_workflow_query
@@ -129,22 +129,22 @@ from app.command import CommandChain
from app.schemas.message import Message
from app.schemas.message import MessageType
from app.schemas.types import EventType, SystemConfigKey
-from app.startup.agent_initializer import init_agent
-from app.startup.database import build_database_governance
-from app.startup.managed_resources_initializer import (
+from app.startup.initializers.agent import init_agent
+from app.startup.composition.database import build_database_governance
+from app.startup.initializers.managed_resources import (
init_managed_resources,
stop_managed_resources,
)
-from app.startup.subscription import (
- TransactionalSubscribeWriter,
+from app.db.adapters.subscription import TransactionalSubscribeWriter
+from app.startup.composition.subscription import (
configure_transactional_subscription_scopes,
)
-from app.startup.chain_events import TransactionalChainDurableEventWriter
-from app.startup.download_failure import TransactionalDownloadFailureRepository
-from app.startup.site import TransactionalSiteRepository
-from app.startup.workflow import TransactionalWorkflowExecutionService
-from app.startup.transaction import TransactionalWriteRunner
-from app.startup.context import (
+from app.db.adapters.chain import TransactionalChainDurableEventWriter
+from app.db.adapters.download import TransactionalDownloadFailureRepository
+from app.db.adapters.site import TransactionalSiteRepository
+from app.db.adapters.workflow import TransactionalWorkflowExecutionService
+from app.db.adapters.transaction import TransactionalWriteRunner
+from app.startup.composition.context import (
AgentChatRuntime,
AuthenticationRuntime,
HistoryRuntime,
diff --git a/app/startup/monitor_initializer.py b/app/startup/initializers/monitor.py
similarity index 100%
rename from app/startup/monitor_initializer.py
rename to app/startup/initializers/monitor.py
diff --git a/app/startup/plugins_initializer.py b/app/startup/initializers/plugins.py
similarity index 100%
rename from app/startup/plugins_initializer.py
rename to app/startup/initializers/plugins.py
diff --git a/app/startup/routers_initializer.py b/app/startup/initializers/routers.py
similarity index 100%
rename from app/startup/routers_initializer.py
rename to app/startup/initializers/routers.py
diff --git a/app/startup/scheduler_initializer.py b/app/startup/initializers/scheduler.py
similarity index 100%
rename from app/startup/scheduler_initializer.py
rename to app/startup/initializers/scheduler.py
diff --git a/app/startup/transfer_initializer.py b/app/startup/initializers/transfer.py
similarity index 100%
rename from app/startup/transfer_initializer.py
rename to app/startup/initializers/transfer.py
diff --git a/app/startup/workflow_initializer.py b/app/startup/initializers/workflow.py
similarity index 100%
rename from app/startup/workflow_initializer.py
rename to app/startup/initializers/workflow.py
diff --git a/app/startup/lifecycle/__init__.py b/app/startup/lifecycle/__init__.py
index 6c0bcf448..9e06a4471 100644
--- a/app/startup/lifecycle/__init__.py
+++ b/app/startup/lifecycle/__init__.py
@@ -8,7 +8,7 @@ from typing import Callable
from fastapi import FastAPI
-from app.startup.cache_initializer import configure_cache_dependencies
+from app.startup.initializers.cache import configure_cache_dependencies
# 缓存装饰器会在业务模块导入时创建后端,必须先完成适配器装配。
configure_cache_dependencies()
# urllib3-future 覆盖 urllib3 命名空间后删除了 format_header_param,导致 telebot 崩溃,需在加载模块前打补丁
@@ -37,17 +37,17 @@ from app.runtime.tasks import TaskRegistry, configure_task_registry
from app.adapters.external.server import MoviePilotServerHelper
from app.runtime.state import SystemHelper
from app.runtime.log import logger, LoggerManager
-from app.startup.command_initializer import init_command, stop_command, restart_command
-from app.startup.agent_initializer import stop_agent
-from app.startup.domain_initializer import configure_domain_dependencies
-from app.startup.modules_initializer import (
+from app.startup.initializers.command import init_command, stop_command, restart_command
+from app.startup.initializers.agent import stop_agent
+from app.startup.initializers.domain import configure_domain_dependencies
+from app.startup.initializers.modules import (
drain_events,
init_modules,
settle_events,
stop_modules,
)
-from app.startup.monitor_initializer import stop_monitor, init_monitor
-from app.startup.plugins_initializer import (
+from app.startup.initializers.monitor import stop_monitor, init_monitor
+from app.startup.initializers.plugins import (
configure_plugin_services,
execute_task,
finalize_plugins,
@@ -57,18 +57,18 @@ from app.startup.plugins_initializer import (
stop_plugin_monitor,
sync_plugins,
)
-from app.startup.routers_initializer import init_routers
-from app.startup.scheduler_initializer import (
+from app.startup.initializers.routers import init_routers
+from app.startup.initializers.scheduler import (
stop_scheduler,
init_scheduler,
init_plugin_scheduler,
)
from app.db.engine import check_connection_budget, get_engine, get_global_async_engine
-from app.startup.transfer_initializer import (
+from app.startup.initializers.transfer import (
replay_pending_transfers,
stop_transfer_runtime,
)
-from app.startup.workflow_initializer import init_workflow, stop_workflow
+from app.startup.initializers.workflow import init_workflow, stop_workflow
from app.startup.lifecycle.components import (
LifecycleComponent,
LifecycleFailurePolicy,
@@ -236,7 +236,7 @@ async def initialize_modules_component(app: FastAPI) -> None:
try:
runtime = await init_modules()
except BaseException:
- from app.startup.modules_initializer import stop_database_worker
+ from app.startup.initializers.modules import stop_database_worker
try:
await stop_database_worker()
@@ -276,7 +276,7 @@ def prepare_database_component(app: FastAPI) -> None:
"""完成数据库建表、迁移与 head 校验后发布数据库就绪状态。"""
# Alembic 及全部 ORM 元数据只在 lifespan 真正启动时加载,create_app/import 阶段
# 继续保持不建库、不加载迁移运行时的纯 ASGI 结构语义。
- from app.startup.database_initializer import (
+ from app.startup.initializers.database import (
prepare_database,
verify_database_revision,
)
diff --git a/app/testing/bootstrap.py b/app/testing/bootstrap.py
index 5c4e7a030..155b61401 100644
--- a/app/testing/bootstrap.py
+++ b/app/testing/bootstrap.py
@@ -180,7 +180,7 @@ def prepare_backend() -> None:
"""
isolate_config_dir()
ensure_sites_stub()
- from app.startup.database_initializer import init_db
+ from app.startup.initializers.database import init_db
init_db()
from app.db.oper.systemconfig import SystemConfigOper
from app.db.oper.userconfig import UserConfigOper
@@ -188,10 +188,10 @@ def prepare_backend() -> None:
SystemConfigOper().load_snapshot()
UserConfigOper().load_snapshot()
# 缓存装饰器在测试模块导入时即创建后端,先装配隔离配置对应的适配器。
- from app.startup.cache_initializer import configure_cache_dependencies
+ from app.startup.initializers.cache import configure_cache_dependencies
configure_cache_dependencies()
# 测试与生产使用同一组合入口,确保领域解析器获得隔离库和测试 settings。
- from app.startup.domain_initializer import configure_domain_dependencies
+ from app.startup.initializers.domain import configure_domain_dependencies
configure_domain_dependencies()
diff --git a/database/env.py b/database/env.py
index f1091da3e..627d8fa32 100644
--- a/database/env.py
+++ b/database/env.py
@@ -6,7 +6,7 @@ from sqlalchemy import pool
from alembic import context
from app.db import Base
-from app.startup.cache_initializer import configure_cache_dependencies
+from app.startup.initializers.cache import configure_cache_dependencies
# 历史 migration 会调用使用 FileCache 的业务清理链,Alembic 自身也是组合入口。
configure_cache_dependencies()
diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md
index 78e98cd9c..32cb65032 100644
--- a/docs/architecture-overview.md
+++ b/docs/architecture-overview.md
@@ -245,12 +245,12 @@ sequenceDiagram
和 TestClient 因而共享同一 fail-fast 语义。
- **引擎预热 fail-fast**:同步/异步数据库引擎在单线程期完成首次创建,
避免调度器放出大量线程后再创建引擎导致连接锁竞争。
-- **类型化请求装配**:`startup/context.py` 的 frozen slots `HostRuntime` 是 lifespan 内唯一宿主
+- **类型化请求装配**:`startup/composition/context.py` 的 frozen slots `HostRuntime` 是 lifespan 内唯一宿主
上下文,`api/context.py` 从 `app.state` 收窄到具体领域能力。认证、消息、历史、媒体服务器、站点、
订阅、工作流和请求事务均使用命名 runtime 字段,不再通过字符串仓储键定位;API、Scheduler、Chain
从 `HostRuntime.configuration` 获取 frozen 配置快照。系统设置管理 API 通过
`HostRuntime.settings` 的窄服务读写可变部署设置,业务域不接触 Settings 实例;生产与测试组合根统一
- 复用 `startup/configuration.py` 的映射。`ApiDataPorts` 仅保留旧导入 ABI,不参与正式请求链路。
+ 复用 `startup/composition/configuration.py` 的映射。`ApiDataPorts` 仅保留旧导入 ABI,不参与正式请求链路。
- **安全模式**:`MOVIEPILOT_SAFE_MODE` 会跳过插件、定时器、监控器、命令与工作流,用于故障自救。
- **进程拓扑**:全功能 V3 强制 `API_WORKERS=1`,避免每个 worker 重复启动插件和后台控制面;安全模式可临时使用多 worker 诊断,但不是正式扩容方案。
- **健康语义**:`/health/live` 只确认进程和事件循环可响应;`/health/ready` 仅在数据库
@@ -375,7 +375,8 @@ flowchart LR
不再保留主题包之外的第二个写入入口。
- 规范写入口中的 Oper 只 stage mutation,不创建独立 Session、不提交;Application Command
通过请求或任务入口注入的 UnitOfWork 统一 `commit/rollback`,事件、刷新和上报只在 commit
- 成功后执行。订阅新增样板由 `startup/subscription.py` 创建独占 Session,
+ 成功后执行。订阅新增 Port 位于 `application/subscription/write.py`,由
+ `db/adapters/subscription.py` 创建独占 Session,`startup/composition/subscription.py` 只装配回调,
`application/subscription/write.py` 决定事务与 post-commit 边界,`SubscribeOper.stage_add()`
只查重、`add` 和 `flush`。旧 SDK 显式构造的无会话 Oper 暂留兼容自动短会话,不得被新代码复用。
`transaction-debt-baseline.json` 当前要求正式只读查询装饰器保持为 0;原有同步/异步写装饰器
@@ -384,7 +385,7 @@ flowchart LR
`create/update/delete/truncate` 兼容包装器;AST 门禁保证显式 Session 的提交权不会被底层抢走。
- 站点、历史、工作流、Agent 会话删除和插件数据重置已经形成同构事务切片;对应 Application
Command/Service 持有 UoW,Oper 的 `stage_*` 方法只修改当前会话。插件数据重置从
- `startup/plugins_initializer.py` 创建独占会话,插件直接使用 `PluginDataOper` 的旧 ABI 仅作兼容。
+ `startup/initializers/plugins.py` 注入事务能力,插件直接使用 `PluginDataOper` 的旧 ABI 仅作兼容。
- 每次表结构变更必须新增 `database/versions/` 下的 Alembic 迁移。
- 运行期业务配置使用 `SystemConfigKey` 枚举 + `SystemConfigOper`,禁止裸字符串键;
用户级配置使用 `UserConfigOper`。
@@ -489,7 +490,7 @@ Agent 采用**门面 + 惰性物化**设计,避免 `application → agent` 形
```mermaid
flowchart TB
Entry["消息渠道 / API / MCP"] --> Facade["app/application/agent.py
编排门面(get_agent_manager 等)"]
- Reg["app/startup/agent_initializer.py
导入期注册轻量 Provider"]
+ Reg["app/startup/initializers/agent.py
导入期注册轻量 Provider"]
Facade -.能力启用或首次使用时物化.-> RT["app/agent/runtime_loader.py
能力发现与服务物化"]
RT --> ORC["app/agent/orchestrator.py
会话编排"]
ORC --> Tools["app/agent/tools
系统工具(经 application 门面)"]
diff --git a/docs/refactor/backend-architecture-governance.md b/docs/refactor/backend-architecture-governance.md
index 119968c7b..b22cbbecd 100644
--- a/docs/refactor/backend-architecture-governance.md
+++ b/docs/refactor/backend-architecture-governance.md
@@ -119,7 +119,7 @@ MoviePilot V3 已经完成一轮重要基础工作:原 `app/core`、`app/helpe
| 模块 | 静态出度 | 主要原因 |
| --- | ---: | --- |
| `app.agent.tools.factory` | 99 | 一次性导入全部内置工具并维护集中注册表 |
-| `app.startup.modules_initializer` | 55 | 组合根职责,这是合理高出度,但仍需声明式管理 |
+| `app.startup.initializers.modules` | 55 | 组合根职责,这是合理高出度,但仍需声明式管理 |
| `app.api.endpoints.system` | 54 | 系统设置、规则测试、日志、网络测试、运行控制混合 |
| `app.api.deps` | 49 | 认证、插件配置和跨端点依赖装配集中 |
| `app.agent.orchestrator` | 48 | Agent 构建、执行、工具、记忆、审计、用量混合 |
@@ -425,7 +425,7 @@ app/chain/transfer.py # 保持 TransferChain 兼容门面
#### 已有进展
-`app/startup/modules_initializer.py:211-245` 已经承担托管资源、壁纸 Provider、认证载荷、DoH、站点、事件错误通知、模块、Agent 和前端的组合工作。`app/startup/lifecycle.py` 也显式规定数据库预热、路由、模块、插件、调度器、监控器、命令和工作流的顺序。这是正确方向。
+`app/startup/initializers/modules.py` 承担托管资源、壁纸 Provider、认证载荷、DoH、站点、事件错误通知、模块、Agent 和前端的组合工作。`app/startup/lifecycle/` 显式规定数据库预热、路由、模块、插件、调度器、监控器、命令和工作流的顺序。这是正确方向。
#### 历史泄漏与当前收口
@@ -701,7 +701,7 @@ app/application/server/share.py # 订阅/工作流等分享用例
#### 典型证据
-- `app/application/messaging/skill.py` 通过 `SkillCatalogPort` 消费技能目录,`app.startup.agent_initializer` 才导入并注入 `SkillHelper`。
+- `app/application/messaging/skill.py` 通过 `SkillCatalogPort` 消费技能目录,`app.startup.initializers.agent` 才导入并注入 `SkillHelper`。
- `app/application/plugin/routes.py` 持有 `DynamicRouteRegistry` Protocol 和注册/移除用例;FastAPI app、`app.routes`、`openapi_schema` 和 `setup()` 均封装在 `app/adapters/web/plugin/routes.py`。
- 多个 `modules` 直接导入 `app.application.messaging.agent`、`mediaserver`、`storage` 等;其中一部分是合理 SPI 消费,一部分表明应用能力接口和具体实现未区分。
- `SystemConfigOper()` 在大量文件中被直接构造,形成持久化配置服务定位器。
diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md
index fcf436e0f..8fb3314ed 100644
--- a/docs/refactor/backend-architecture-next-stage.md
+++ b/docs/refactor/backend-architecture-next-stage.md
@@ -15,8 +15,8 @@
### 长期整改阶段 0:治理门禁恢复(2026-08-23)
-- 宿主依赖基线已审查 TaskRegistry 与有界后台 owner 接入后的语义差异:当前为 `800` 个模块、`6482` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。
-- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistry;normal/safe 组件数分别为 `20`/`10`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。
+- 宿主依赖基线已审查本阶段归位后的语义差异:当前为 `805` 个模块、`6514` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。
+- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistry;normal/safe 组件数分别为 `23`/`11`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。
- 官方插件快照覆盖 `plugins.v3`、`plugins.v2` 以及 V3 实际会从 `package.json` 回退加载的 31 个默认实现;`app/plugins/**` 仍只是宿主运行副本,不进入扫描。
- SDK 快照以各模块显式 `__all__` 为公开合同,能够记录赋值别名;`typing`、`__future__` 等实现期导入不再被误冻结,既有数据库备份门面已补精确导出清单。
- async 阻塞实际债务已由 fixture 中的 10 项下降到 1 项并固化低水位;剩余项是 Scheduler Agent task 查询,后续阶段迁入异步查询边界后归零。
@@ -72,7 +72,7 @@
- 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。
- `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。
-- 依赖图当前为 `800` 个 Python 模块、`6479` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
+- 依赖图当前为 `805` 个 Python 模块、`6514` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
- 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。
综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。
@@ -82,7 +82,7 @@
1. **后台任务的统一所有权已覆盖 API 入口,但仍有更深层任务机制待分级。** `app/runtime/tasks.py` 已建立 lifespan 级 TaskRegistry,启动收尾、插件 Release 刷新、Webhook E0 广播、CookieCloud E1 手工调度、消息入口、Seerr 订阅、整理历史 AI 重做、OpenAI/Anthropic 协议流和 WebAgent 断线后执行/快照保存均不再维护端点模块级任务集合或 Starlette 回调,shutdown 会停止接收、取消并有限等待,且生命周期清单明确登记其顺序。主仓 `app/` 已无裸 FastAPI `BackgroundTasks`;当前仍有约 `50` 个更底层 `create_task`/等价任务创建点,与线程池和 APScheduler 并存,后续需逐项确认 owner、取消、等待、重试、幂等和是否 durable,关键业务副作用优先接入已有 Outbox/恢复表。
2. **动态模块契约仍以 legacy 聚合语义为主。** 当前登记 `212` 个模块方法,其中 `194` 个仍使用 `legacy` aggregation,只有 `14` 个 `first_non_empty`、`4` 个 `ordered_list_merge`。`app/runtime/extensions/module/contracts.py:422-455` 已能登记 family、输入/结果标签和基础签名诊断,但 `193` 个方法没有 required parameters,调度器 `app/runtime/extensions/module/dispatcher.py:109-260` 仍主要依赖运行时反射、返回值形状和短路规则。未知第三方方法保留 legacy fallback 是兼容要求,不应删除;宿主高频能力则应逐族补齐可执行的输入校验、结果校验、超时和错误语义。
3. **查询侧数据库兼容 ABI 已完成正式装饰器清零。** 写事务装饰器和正式 `db_query/async_db_query` 均为 `0`。站点、消息、用户、订阅、下载/整理历史、工作流、MediaServer、SiteUserData、AgentChat、AgentTaskRun、TransferPending、SystemConfig、PassKey 和 SubscribeHistory 的宿主查询已迁到显式 Session 路径;对应旧插件 Model 调用由独立 `legacy_*` 外壳保留,可同时接受显式 Session 与无 Session 的位置/关键字参数。后续重点转为减少 ORM 对象跨层流转,并保持正式装饰器零回退。
-4. **组合根和全局状态仍形成复杂的隐式运行时图。** Singleton 实例、模块级 provider、`configure_*` 注册函数和兼容 Facade 同时存在;它们解决了旧 ABI 和启动顺序问题,但增加测试污染、重复装配、实例身份和初始化顺序风险。`app/startup/lifecycle/__init__.py:161-376` 已有声明式生命周期,`app/startup/modules_initializer.py:505-530` 也有分阶段关闭,但尚未做到所有进程级资源都只通过 typed HostRuntime 访问。后续应以“新代码禁止新增 Service Locator/Singleton 依赖、旧入口有命中观测”为 ratchet。
+4. **组合根和全局状态仍形成复杂的隐式运行时图。** Singleton 实例、模块级 provider、`configure_*` 注册函数和兼容 Facade 同时存在;它们解决了旧 ABI 和启动顺序问题,但增加测试污染、重复装配、实例身份和初始化顺序风险。`app/startup/lifecycle/__init__.py` 已有声明式生命周期,`app/startup/initializers/modules.py` 也有分阶段关闭,但尚未做到所有进程级资源都只通过 typed HostRuntime 访问。后续应以“新代码禁止新增 Service Locator/Singleton 依赖、旧入口有命中观测”为 ratchet。
### P2:中长期可演进性债务
@@ -529,7 +529,7 @@ flowchart TB
**实施记录(2026-08-21)**:
-- `app/startup/subscription.py` 为每次规范新增创建独占同步/异步 Session;
+- `app/application/subscription/write.py` 定义用例 Port,`app/db/adapters/subscription.py` 为每次规范新增创建独占同步/异步 Session,`app/startup/composition/subscription.py` 只负责注入;
`CreateSubscriptionCommand` / `AsyncCreateSubscriptionCommand` 持有 UoW,Oper 只执行
查重、`add` 与 `flush`。
- `SubscribeOper.stage_add()` 的查重 SQL 已收口到 Oper,不再调用 Model 自动会话装饰器;
@@ -584,7 +584,7 @@ flowchart TB
**建议结构**:
```text
-app/startup/context.py # HostRuntime 及构建结果
+app/startup/composition/context.py # HostRuntime 及构建结果
app/api/context.py # API 可见的最小 AppState / 读取依赖
app/api/dependencies/ # 按领域拆分依赖工厂
auth.py
@@ -612,7 +612,7 @@ app/api/dependencies/ # 按领域拆分依赖工厂
**实施记录(2026-08-21)**:
-- `app/startup/context.py` 定义 frozen slots `HostRuntime` 与首个窄能力
+- `app/startup/composition/context.py` 定义 frozen slots `HostRuntime` 与首个窄能力
`AgentChatRuntime`,仓储、Session、UoW 字段均为具体 Protocol 工厂,不是字符串字典。
- `init_modules()` 保留零参数兼容签名并返回本次 lifespan 唯一 Runtime;生命周期组件把结果挂到
`app.state.host_runtime`。`app/api/context.py` 只向 Depends 暴露 Agent chat 的最小能力。
@@ -707,7 +707,7 @@ app/api/dependencies/ # 按领域拆分依赖工厂
Startup 路径始终使用 HostRuntime 注入。插件 SDK 的 `app.sdk.config.settings`、动态 API 返回和事件字段未改。
- 收尾批次把 API 与 Chain 余下直接配置读取全部迁入类型化 snapshot;Scheduler 继续保持为零。
`HostRuntime` 新增可变部署设置服务,只供系统设置管理 API 使用,业务 API/Chain 只接收 frozen 字段。
- snapshot 构造集中到 `app/startup/configuration.py`,生产启动与测试组合根复用同一映射,避免测试默认值
+ snapshot 构造集中到 `app/startup/composition/configuration.py`,生产启动与测试组合根复用同一映射,避免测试默认值
漂移。canonical `settings` 直接导入低水位从 154 降到 137,`SystemConfigOper()` 保持 14 个。
- `ApiRuntimeConfig` 已覆盖搜索来源、媒体/字幕/音频后缀、重命名格式、WebPush、CookieCloud、根目录和
版本标识;`ChainRuntimeConfig` 覆盖搜索、下载、整理、刮削、AI、代理、缓存、链接、路径和 TMDB 图片域。
@@ -1080,7 +1080,7 @@ OTel 初始化只能位于 Startup/Adapter;Domain/Application 只依赖 no-op-
- 新增 Application command/port
- `app/runtime/event/`
- `app/runtime/extensions/module/contracts.py`
- - `app/startup/context.py` / `app/api/context.py`
+ - `app/startup/composition/context.py` / `app/api/context.py`
3. 对第三方移植包、旧插件 Facade 和动态 SDK 设置精确豁免,不允许 `app.* = ignore_errors`。
4. CI 先检查严格目录;每次迁移扩大 include 范围。
5. 类型错误不能用无界 `Any`、`cast(Any, ...)` 或全文件 ignore 消音。
@@ -1099,7 +1099,7 @@ OTel 初始化只能位于 Startup/Adapter;Domain/Application 只依赖 no-op-
**扩展实施记录(2026-08-22)**:mypy 目标运行时更新到 Python 3.14,严格清单扩大到 20 个源文件;
新增纳管配置快照和下载失败事务适配器,仍保持零错误、无全局 ignore。
-Workflow 执行状态 UoW 切片将 `app/application/workflow.py` 与 `app/startup/workflow.py` 纳入 strict 清单,
+Workflow 执行状态 UoW 切片将 `app/application/workflow.py` 与 `app/db/adapters/workflow.py` 纳入 strict 清单,
治理范围扩大到 22 个源文件;事务命令、仓储 Protocol 和短会话适配器保持零错误。
异步安全与契约收口继续纳管 scheduling facade、Event error policy、Module dispatcher 和 async blocking
diff --git a/docs/refactor/backend-module-refactor-compatibility.md b/docs/refactor/backend-module-refactor-compatibility.md
index 727673101..f8126ceeb 100644
--- a/docs/refactor/backend-module-refactor-compatibility.md
+++ b/docs/refactor/backend-module-refactor-compatibility.md
@@ -519,7 +519,7 @@ SYMBOL_ALIASES = {
4. Event、模块、插件和安全边界改为由 startup composition root 注入 resolver、回调和错误处理器,迁移模块不再处于强连通分量。
5. 插件稳定入口收敛到 `app.sdk`;存量插件无需同步修改,官方插件可以按正常发布节奏迁移。
6. 站点二进制和数据资源迁到 `app/application/site`,Build 直接生成 `app.application.site.sites`,Build CI、Resources V3 manifest、Docker 和本地 CLI 使用同一目标路径。
-7. 媒体识别领域不再直接读取 DB/settings,也不导入 Rust 适配器;`startup/domain_initializer.py` 统一注入实时规则、后缀策略、TMDB 图片地址、默认媒体来源和加速器。
+7. 媒体识别领域不再直接读取 DB/settings,也不导入 Rust 适配器;`startup/initializers/domain.py` 统一注入实时规则、后缀策略、TMDB 图片地址、默认媒体来源和加速器。
8. 缓存按职责拆为 `runtime/cache.py`(契约、内存实现、装饰器、代理)和 `adapters/cache/backends.py`(Redis、文件 I/O);旧 `app.core.cache` 指向完整 `app.sdk.cache` 门面。
9. `application/mediaserver.py` 集中负责媒体服务器的配置化服务发现、Provider ID 规范化和音乐库匹配;通用媒体身份规则继续复用 `domain/media.py`。
10. GC 归入 `runtime/gc.py`,外部 IP 归属查询归入 `adapters/external/location.py`,安全能力统一在 `app/application/security/`,URL 安全策略为 `url.py`,二次认证文件为 `twofactor.py`。
diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md
index 5de5f6cd7..b45788283 100644
--- a/docs/rules/05-architecture.md
+++ b/docs/rules/05-architecture.md
@@ -72,10 +72,13 @@ to make the directory tree look symmetrical.
| `app/application/messaging/` | Message rendering/routing, interactions and the Agent-to-message bridge: `interaction.py` shared interaction contracts and view helpers; `router.py` unified interaction priority and callback dispatch; `site.py`/`subscribe.py`/`skill.py` per-command sessions, input parsing and views; `media.py` media interaction state while the business workflow stays in `MediaInteractionChain`; `plugin.py` plugin input capture and plugin button callbacks; `agent.py` agent choice state, callback protocol and WebAgent bridge; `message.py` notification rendering, templates and queue. Not a public SDK recommended for direct plugin use |
| `app/application/security/` | Authentication, authorization, cookies, passkeys, OTP/two-factor, path/URL safety, SSRF and signing policy |
-Application services may use domain rules, runtime contracts, Oper classes and
-adapters. Multi-domain workflows still belong in the existing `app/chain/`
-package. `Chain`, `Service` and `Manager` remain class patterns; they do not
-create additional top-level directory categories.
+Application services may use domain rules and runtime contracts. They own the
+persistence Protocol needed by a use case, but must not import `app.db`,
+SQLAlchemy, Session, Oper classes or concrete adapters. `app/db/adapters/`
+implements those Protocols and startup injects the implementation. Multi-domain
+workflows still belong in the existing `app/chain/` package. `Chain`, `Service`
+and `Manager` remain class patterns; they do not create additional top-level
+directory categories.
### Runtime boundaries
@@ -94,8 +97,11 @@ create additional top-level directory categories.
| `app/runtime/compat/` | Standard-library-only exact legacy import routing, resource preflight scanning and DEBUG diagnostics |
`app/startup/` remains the established composition root and is not nested under
-runtime. It injects providers and callbacks, orders initialization/shutdown and
-decides restart policy. Lower-level runtime modules must not import startup.
+runtime. Its root contains only `composition/`, `initializers/` and `lifecycle/`:
+composition constructs and injects cross-layer dependencies, initializers expose
+domain-scoped startup/shutdown hooks, and lifecycle orders those hooks and decides
+restart policy. Reusable persistence implementations belong in `app/db/adapters/`,
+not startup. Lower-level runtime modules must not import startup.
Startup publishes its frozen, slotted `HostRuntime` through FastAPI `app.state`.
API dependencies must narrow that object to a domain runtime (for example,
`AgentChatRuntime`) instead of adding a string key to a global service map.
@@ -105,14 +111,20 @@ API, Scheduler and Chain deployment values are exposed as frozen snapshots from
`HostRuntime.configuration`; canonical callers must not add a fresh direct
`settings` import when the required field belongs to an existing snapshot.
-`app.schemas` and `app.db` are compatibility facades, not implementation
-dependency hubs. Host code imports concrete schema submodules; the schema root
+`app.schemas` and the `app.db` package root are compatibility facades, not
+implementation dependency hubs. Host code imports concrete schema submodules; the schema root
resolves its generated export manifest lazily for plugins and legacy callers.
DB internals import `base`, `decorators`, `engine`, `session`, concrete models
and Oper modules directly. `app.db.models.load_all_models()` is the explicit
composition entry used before metadata creation or migration; importing one
model must not import every table.
+`app/db/oper/` owns table-oriented SQLAlchemy access and receives a caller-owned
+Session. `app/db/adapters/` is the concrete persistence-adapter layer: it may
+depend on Application-owned Protocols, UoW/Session and Oper implementations.
+This deliberate dependency inversion is the only `DB implementation ->
+Application contract` direction; Application must remain free of DB imports.
+
### Adapter boundaries
| Path | Ownership |
@@ -123,6 +135,7 @@ model must not import every table.
| `app/adapters/external/` | CookieCloud, plugin market, OCR, IP-location providers and MoviePilot Server |
| `app/adapters/external/plugin/client.py` | Read-only plugin-market and local-repository client over the established `PluginHelper` implementation |
| `app/adapters/system/plugin/` | Plugin package and dependency I/O (`package.py`, `dependency.py`) |
+| `app/db/adapters/` | SQLAlchemy implementations of Application-owned persistence Protocols |
Generic protocol transport belongs in `adapters/network`; a named product or
ecosystem workflow belongs in `adapters/external`. An adapter may depend on
@@ -381,7 +394,7 @@ policy. `app/db` therefore has no dependency on `app/domain`.
emits no runtime logs; upper-layer owners decide whether failures are
operationally relevant.
- `app/adapters/system/resource.py` only reports whether installation occurred;
- `app/startup/modules_initializer.py` supplies the loaded site-resource
+ `app/startup/initializers/modules.py` supplies the loaded site-resource
versions and decides whether to restart. The adapter never imports the site
application service.
- Configured notification discovery lives in
@@ -407,13 +420,15 @@ policy. `app/db` therefore has no dependency on `app/domain`.
| Direction | Status |
|---|---|
-| `entrypoint -> chain / application / Oper` | Allowed according to workflow complexity |
-| `chain -> module (only via run_module dispatch) / application / Oper / canonical capability` | Allowed; direct `chain -> module` imports forbidden |
-| `chain -> agent implementation` | Forbidden; chains reach Agent runtime only through `app/application/agent.py`; `app/startup/agent_initializer.py` registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used |
+| `entrypoint -> chain / application / injected persistence Port` | Allowed according to workflow complexity |
+| `chain -> module (only via run_module dispatch) / application / injected Port / canonical capability` | Allowed; direct `chain -> module` and `chain -> Oper` imports forbidden |
+| `chain -> agent implementation` | Forbidden; chains reach Agent runtime only through `app/application/agent.py`; `app/startup/initializers/agent.py` registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used |
| `agent.tools -> api / scheduler / command` | Forbidden; tools use `app/application/plugin/routes.py`, `plugin/folders.py`, `scheduling.py` and `commands.py` application services |
| `api -> factory` | Forbidden; the FastAPI route adapter is injected into `app/application/plugin/routes.py` by the composition root after creation |
-| `application -> domain / runtime / adapter / Oper` | Allowed |
-| `module -> canonical capability / Oper` | Allowed |
+| `application -> domain / runtime contract` | Allowed |
+| `application -> DB / Oper / concrete adapter` | Forbidden; define a Protocol in Application and inject an implementation |
+| `db.adapters -> application persistence Protocol / db.oper / UoW` | Allowed; this is dependency inversion, not an upper-layer use-case call |
+| `module -> canonical capability / Application persistence Port` | Allowed; direct Oper imports are forbidden for new code |
| `module -> module / chain` | Forbidden for new code |
| `adapter -> application / runtime.extensions / sdk / compat` | Forbidden |
| `domain -> runtime / adapter / application / DB` | Forbidden |
@@ -426,11 +441,14 @@ policy. `app/db` therefore has no dependency on `app/domain`.
| Path | Purpose |
|---|---|
-| `app/application/agent.py` | Agent orchestration facade (`get_agent_manager` / `get_prompt_manager` / capability queries / prompt builders); lightweight providers register through `app/startup/agent_initializer.py`, with no static `application -> agent` edge |
+| `app/application/agent.py` | Agent orchestration facade (`get_agent_manager` / `get_prompt_manager` / capability queries / prompt builders); lightweight providers register through `app/startup/initializers/agent.py`, with no static `application -> agent` edge |
| `app/agent/runtime_loader.py` | Agent-specific capability discovery and canonical entrypoint/service materialization; reuses the generic Capability Runtime while keeping Agent ownership under `app/agent/` |
| `app/application/subscription/write.py` | Subscription media translation and sync/async write-port orchestration |
-| `app/application/scheduling.py` | Runtime scheduler facade for Agent tools and endpoints; `Scheduler` class registered by `app/startup/scheduler_initializer.py` |
-| `app/application/commands.py` | Command registry facade for Agent tools and endpoints; `Command` class registered by `app/startup/command_initializer.py` |
+| `app/application/scheduling.py` | Runtime scheduler facade for Agent tools and endpoints; `Scheduler` class registered by `app/startup/initializers/scheduler.py` |
+| `app/application/commands.py` | Command registry facade for Agent tools and endpoints; `Command` class registered by `app/startup/initializers/command.py` |
+| `app/db/adapters/` | SQLAlchemy repository/UoW implementations for Application-owned persistence Protocols |
+| `app/startup/composition/` | HostRuntime, configuration snapshots and cross-layer adapter wiring |
+| `app/startup/initializers/` | Domain-scoped initialization and shutdown hooks |
| `app/chain/agent.py` | `AgentChain(ChainBase)`: the chain-layer entry for Agent sessions; Agent runtime stays in `app/agent/` |
| `app/runtime/config.py` | `ConfigModel`, `Settings` and deployment configuration |
| `app/runtime/topology.py` | Single-worker full-runtime policy and safe-mode topology validation |
diff --git a/docs/rules/06-code-styles.md b/docs/rules/06-code-styles.md
index 4d4c32b89..92d382de6 100644
--- a/docs/rules/06-code-styles.md
+++ b/docs/rules/06-code-styles.md
@@ -116,7 +116,9 @@ except:
- Do not introduce new third-party libraries without placing them in the correct `pyproject.toml` dependency group and updating `uv.lock`: runtime packages belong in `[project].dependencies`, test/lint/build tooling in `[dependency-groups].dev`.
- Do not use `requests` or `httpx` directly for external HTTP calls - host code uses `RequestUtils` from `app/adapters/network/http.py`; plugins use `app.sdk.network`.
-- Do not issue raw SQLAlchemy queries from chains, modules, or endpoints — use the Oper classes in `app/db/oper/`.
+- Do not issue raw SQLAlchemy queries or import Oper classes from chains, modules,
+ or endpoints. Define/consume an Application persistence Port; its concrete
+ implementation under `app/db/adapters/` may use Oper classes from `app/db/oper/`.
- Do not add TODO or FIXME without context. Only keep one if it is genuinely deferred and cannot be addressed in the current task.
- Do not add noisy markers like `# change starts here`, `# important`, or `# this is a fix`.
- Do not write comments that restate what the code already clearly says.
diff --git a/docs/rules/10-data-and-persistent.md b/docs/rules/10-data-and-persistent.md
index ac8854afa..2ec63db9c 100644
--- a/docs/rules/10-data-and-persistent.md
+++ b/docs/rules/10-data-and-persistent.md
@@ -81,33 +81,49 @@ the stub.
Oper classes accept and return persistence values. Turning a `MediaInfo` or
`MetaBase` into a row is business logic and lives in `app/application/`.
+Application owns use-case commands and persistence Protocols, but does not import
+`app.db`, SQLAlchemy, Session or Oper. Concrete persistence is used in
+`app/db/adapters/`: adapters implement those Protocols with explicit Session,
+UnitOfWork and Oper objects. `app/startup/composition/` creates and injects the
+adapters; it does not retain reusable repository implementations.
+
### Transaction ownership ratchet
-- `tests/fixtures/architecture/transaction-debt-baseline.json` records the
- existing Model transaction decorators. All formal query and write decorators
- are now zero and must remain zero; compatibility-only `legacy_*` shells must
- not be counted as new transaction ownership.
-- `legacy_db_query` / `legacy_async_db_query` are compatibility-only shells for
- existing plugin-facing Model methods. Host Oper code must pass an explicit
- Session through `_execute_sync_query` / `_execute_async_query`; new Model
- methods must not add either legacy decorator.
+- `tests/fixtures/architecture/transaction-debt-baseline.json` records formal
+ decorators in concrete files under `app/db/models/`. Their count is zero and
+ must remain zero. Compatibility-only `legacy_*` shells are tracked separately
+ and must never be treated as the target design.
+- `legacy_db_query` / `legacy_async_db_query` preserve an existing plugin-facing
+ Model method whose no-Session call shape cannot be removed yet. If a Model
+ method has no external ABI obligation, move the query into its Oper and remove
+ the Model method instead of adding `legacy_*`.
+- `Base.create/get/update/delete/list/truncate` and their async forms are inherited
+ plugin ABI, so `app/db/base.py` deliberately uses legacy query/write wrappers.
+ New host code must not call these convenience methods; Oper staging methods and
+ explicit UoW are the canonical path. Removal requires plugin-usage evidence and
+ a separately announced compatibility break, not a mechanical rename.
+- Host Oper code must pass an explicit Session through `_execute_sync_query` /
+ `_execute_async_query`; new Model methods must not add any legacy decorator.
- New Model methods must not use `db_query`, `db_update`, `async_db_query`, or
`async_db_update`, create a Session, or call `commit()` / `rollback()`.
- Oper receives a caller-owned Session and may query, add, update, delete, or
flush. A composable Oper method must not create its own Session and must not
commit or roll back.
-- The API, Scheduler, Agent, or another logical operation entry creates the
- Session and adapts it through `app/db/uow.py`. Application command code owns
- `commit()` / `rollback()`; events, scheduling refresh, reports, and other
- external effects run only after a successful commit.
+- API, Scheduler, Agent and Chain consume an injected Application Port; they do
+ not import or create a Session. The concrete `app/db/adapters/` implementation
+ creates the Session and adapts it through `app/db/uow.py`. Application command
+ code decides when the injected UoW commits or rolls back; events, scheduling
+ refresh, reports and other external effects run only after a successful commit.
- A synchronous Session is private to one worker thread. An AsyncSession is
private to one asyncio task/operation; neither may be stored in a process
singleton or reused by concurrent work.
-- Subscription creation is the reference slice: `app/startup/subscription.py`
- creates an exclusive Session, `app/application/subscription/write.py` owns the
- UoW and post-commit callback, and `SubscribeOper.stage_add()` only queries,
- adds, and flushes. Preserve `SubscribeOper.add()` only for legacy SDK callers;
- new host code must not use that auto-commit compatibility path.
+- Subscription creation is the reference slice:
+ `app/application/subscription/write.py` owns the command and persistence Port,
+ `app/db/adapters/subscription.py` creates an exclusive Session and adapts Oper/UoW,
+ and `app/startup/composition/subscription.py` only wires scopes and post-commit
+ callbacks. `SubscribeOper.stage_add()` only queries, adds and flushes. Preserve
+ `SubscribeOper.add()` only for legacy SDK callers; new host code must not use
+ that auto-commit compatibility path.
- The same rule applies to `SiteMutationCommand`, history/workflow commands,
`AgentChatService.delete()`, and `DeletePluginDataCommand`: bind the repository
and UoW to one request/operation Session. Legacy plugin-facing Oper methods may
@@ -117,7 +133,18 @@ Run `./.venv/bin/python scripts/architecture/baseline.py --check-host` after
persistence changes. A deliberate debt reduction may refresh the low-water mark
with `--write-host`; never refresh it to accept newly introduced debt.
-**Standard Oper method conventions:**
+**Canonical explicit-session Oper conventions:**
+
+```python
+with SessionFactory() as session:
+ oper = SubscribeOper(session)
+ subscribe = oper.get(sid=1) # Query in caller-owned Session
+ subscribes = oper.list() # List in caller-owned Session
+ oper.stage_add(Subscribe(...)) # Stage only; caller-owned UoW commits
+```
+
+The following no-Session form is legacy plugin ABI only and must not be copied
+into host code:
```python
oper = SubscribeOper()
diff --git a/mypy.ini b/mypy.ini
index 7c9dab870..38e528b31 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -35,12 +35,12 @@ files =
app/db/decorators.py,
app/db/base.py,
app/db/uow.py,
- app/startup/context.py,
- app/startup/configuration.py,
- app/startup/outbox.py,
- app/startup/chain_events.py,
- app/startup/download_failure.py,
- app/startup/workflow.py,
+ app/startup/composition/context.py,
+ app/startup/composition/configuration.py,
+ app/db/adapters/outbox.py,
+ app/db/adapters/chain.py,
+ app/db/adapters/download.py,
+ app/db/adapters/workflow.py,
app/api/context.py,
app/api/dependencies/subscription.py,
scripts/architecture/async_blocking.py
diff --git a/scripts/local_setup.py b/scripts/local_setup.py
index 1c0dcaa16..d45b6a961 100644
--- a/scripts/local_setup.py
+++ b/scripts/local_setup.py
@@ -2380,12 +2380,12 @@ def _apply_local_system_config_inner(config_payload: dict[str, Any]) -> None:
sys.path.insert(0, str(ROOT))
try:
- from app.startup.database_initializer import prepare_database
+ from app.startup.initializers.database import prepare_database
from app.db.oper.systemconfig import SystemConfigOper
from app.schemas.types import SystemConfigKey
from app.db.session import SessionFactory, async_session_scope
from app.db.uow import configure_transaction_runners
- from app.startup.transaction import TransactionalWriteRunner
+ from app.db.adapters.transaction import TransactionalWriteRunner
except ModuleNotFoundError as exc:
raise RuntimeError(
"当前环境尚未安装 MoviePilot 运行依赖,请先执行 moviepilot install deps 或 moviepilot setup"
@@ -2560,7 +2560,7 @@ def _sync_superuser_account_inner() -> None:
sys.path.insert(0, str(ROOT))
try:
- from app.startup.database_initializer import prepare_database
+ from app.startup.initializers.database import prepare_database
except ModuleNotFoundError as exc:
raise RuntimeError(
"当前环境尚未安装 MoviePilot 运行依赖,请先执行 moviepilot install deps 或 moviepilot setup"
@@ -3693,7 +3693,7 @@ def run_agent_request(
sys.path.insert(0, str(ROOT))
try:
- from app.startup.database_initializer import prepare_database
+ from app.startup.initializers.database import prepare_database
from app.agent import MoviePilotAgent
from app.runtime.config import settings
except ModuleNotFoundError as exc:
diff --git a/tests/conftest.py b/tests/conftest.py
index 9a746c245..034f76d56 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -51,7 +51,7 @@ def configure_plugin_system_services():
)
from app.runtime.config import settings
from app.runtime.settings import configure_runtime_setting_provider
- from app.startup.configuration import (
+ from app.startup.composition.configuration import (
build_api_runtime_config,
build_chain_runtime_config,
build_scheduler_runtime_config,
@@ -153,11 +153,11 @@ def configure_plugin_system_services():
from app.db.oper.workflow import WorkflowOper, configure_workflow_legacy_writer
from app.db.oper.message import MessageOper
from app.db.oper.passkey import PassKeyOper
- from app.startup.subscription import TransactionalSubscribeWriter
- from app.startup.download_failure import TransactionalDownloadFailureRepository
- from app.startup.site import TransactionalSiteRepository
- from app.startup.workflow import TransactionalWorkflowExecutionService
- from app.startup.transaction import TransactionalWriteRunner
+ from app.db.adapters.subscription import TransactionalSubscribeWriter
+ from app.db.adapters.download import TransactionalDownloadFailureRepository
+ from app.db.adapters.site import TransactionalSiteRepository
+ from app.db.adapters.workflow import TransactionalWorkflowExecutionService
+ from app.db.adapters.transaction import TransactionalWriteRunner
def compatibility_sync_session() -> Session:
"""动态读取可被存量隔离数据库用例替换的 ScopedSession。"""
diff --git a/tests/fixtures/architecture/configuration-debt-baseline.json b/tests/fixtures/architecture/configuration-debt-baseline.json
index beafebea2..70f792df2 100644
--- a/tests/fixtures/architecture/configuration-debt-baseline.json
+++ b/tests/fixtures/architecture/configuration-debt-baseline.json
@@ -25,7 +25,7 @@
"system_config_oper_constructions": {
"calls": [
{
- "file": "app/startup/modules_initializer.py",
+ "file": "app/startup/initializers/modules.py",
"name": "SystemConfigOper"
}
],
diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json
index 6df30e85c..c009c1f58 100644
--- a/tests/fixtures/architecture/dependency-baseline.json
+++ b/tests/fixtures/architecture/dependency-baseline.json
@@ -13,8 +13,8 @@
"runtime_to_db": [],
"workflow_to_db": []
},
- "edge_count": 6483,
- "edge_sha256": "0a2f82ca189468d5e954b06a2c188c4ea5380a76c7c64039c7f6eb843cb60523",
+ "edge_count": 6514,
+ "edge_sha256": "eda9bc500158baa80dc5013eb4e409aa57cb70446e59a52b7e782cb9c58f9c34",
"edges": [
"app -> app.runtime",
"app -> app.runtime.compat",
@@ -1513,7 +1513,8 @@
"app.api.context -> app.runtime",
"app.api.context -> app.runtime.tasks",
"app.api.context -> app.startup",
- "app.api.context -> app.startup.context",
+ "app.api.context -> app.startup.composition",
+ "app.api.context -> app.startup.composition.context",
"app.api.dependencies.agent -> app.api",
"app.api.dependencies.agent -> app.api.context",
"app.api.dependencies.agent -> app.application",
@@ -1521,7 +1522,8 @@
"app.api.dependencies.agent -> app.application.messaging.chat",
"app.api.dependencies.agent -> app.application.messaging.message",
"app.api.dependencies.agent -> app.startup",
- "app.api.dependencies.agent -> app.startup.context",
+ "app.api.dependencies.agent -> app.startup.composition",
+ "app.api.dependencies.agent -> app.startup.composition.context",
"app.api.dependencies.auth -> app.adapters",
"app.api.dependencies.auth -> app.adapters.web",
"app.api.dependencies.auth -> app.adapters.web.security",
@@ -1536,7 +1538,8 @@
"app.api.dependencies.auth -> app.schemas",
"app.api.dependencies.auth -> app.schemas.token",
"app.api.dependencies.auth -> app.startup",
- "app.api.dependencies.auth -> app.startup.context",
+ "app.api.dependencies.auth -> app.startup.composition",
+ "app.api.dependencies.auth -> app.startup.composition.context",
"app.api.dependencies.data -> app.api",
"app.api.dependencies.data -> app.api.data",
"app.api.dependencies.history -> app.api",
@@ -1554,7 +1557,8 @@
"app.api.dependencies.history -> app.schemas.types",
"app.api.dependencies.history -> app.schemas.workflow",
"app.api.dependencies.history -> app.startup",
- "app.api.dependencies.history -> app.startup.context",
+ "app.api.dependencies.history -> app.startup.composition",
+ "app.api.dependencies.history -> app.startup.composition.context",
"app.api.dependencies.plugin -> app.application",
"app.api.dependencies.plugin -> app.application.commands",
"app.api.dependencies.plugin -> app.application.plugin",
@@ -1582,7 +1586,8 @@
"app.api.dependencies.site -> app.schemas",
"app.api.dependencies.site -> app.schemas.types",
"app.api.dependencies.site -> app.startup",
- "app.api.dependencies.site -> app.startup.context",
+ "app.api.dependencies.site -> app.startup.composition",
+ "app.api.dependencies.site -> app.startup.composition.context",
"app.api.dependencies.subscription -> app.adapters",
"app.api.dependencies.subscription -> app.adapters.external",
"app.api.dependencies.subscription -> app.adapters.external.server",
@@ -1605,7 +1610,8 @@
"app.api.dependencies.subscription -> app.schemas",
"app.api.dependencies.subscription -> app.schemas.types",
"app.api.dependencies.subscription -> app.startup",
- "app.api.dependencies.subscription -> app.startup.context",
+ "app.api.dependencies.subscription -> app.startup.composition",
+ "app.api.dependencies.subscription -> app.startup.composition.context",
"app.api.dependencies.workflow -> app.adapters",
"app.api.dependencies.workflow -> app.adapters.external",
"app.api.dependencies.workflow -> app.adapters.external.server",
@@ -1617,7 +1623,8 @@
"app.api.dependencies.workflow -> app.runtime",
"app.api.dependencies.workflow -> app.runtime.config",
"app.api.dependencies.workflow -> app.startup",
- "app.api.dependencies.workflow -> app.startup.context",
+ "app.api.dependencies.workflow -> app.startup.composition",
+ "app.api.dependencies.workflow -> app.startup.composition.context",
"app.api.dependencies.workflow -> app.workflow",
"app.api.deps -> app.api",
"app.api.deps -> app.api.dependencies",
@@ -2666,6 +2673,8 @@
"app.application.notification -> app.schemas",
"app.application.notification -> app.schemas.system",
"app.application.notification -> app.schemas.types",
+ "app.application.plugin.config -> app.schemas",
+ "app.application.plugin.config -> app.schemas.exception",
"app.application.plugin.folders -> app.application",
"app.application.plugin.folders -> app.application.configuration",
"app.application.plugin.folders -> app.runtime",
@@ -3457,7 +3466,8 @@
"app.cli -> app.runtime.settings",
"app.cli -> app.runtime.state",
"app.cli -> app.startup",
- "app.cli -> app.startup.database",
+ "app.cli -> app.startup.composition",
+ "app.cli -> app.startup.composition.database",
"app.command -> app.application",
"app.command -> app.application.messaging",
"app.command -> app.application.messaging.message",
@@ -3484,6 +3494,49 @@
"app.command -> app.schemas.event",
"app.command -> app.schemas.message",
"app.command -> app.schemas.types",
+ "app.db.adapters.chain -> app.application",
+ "app.db.adapters.chain -> app.application.chain",
+ "app.db.adapters.chain -> app.application.chain.durable_events",
+ "app.db.adapters.chain -> app.application.history",
+ "app.db.adapters.chain -> app.application.outbox",
+ "app.db.adapters.chain -> app.db",
+ "app.db.adapters.chain -> app.db.adapters",
+ "app.db.adapters.chain -> app.db.adapters.outbox",
+ "app.db.adapters.chain -> app.db.oper",
+ "app.db.adapters.chain -> app.db.oper.downloadhistory",
+ "app.db.adapters.chain -> app.db.oper.transferhistory",
+ "app.db.adapters.chain -> app.db.uow",
+ "app.db.adapters.download -> app.db",
+ "app.db.adapters.download -> app.db.oper",
+ "app.db.adapters.download -> app.db.oper.downloadfailure",
+ "app.db.adapters.download -> app.db.uow",
+ "app.db.adapters.outbox -> app.application",
+ "app.db.adapters.outbox -> app.application.outbox",
+ "app.db.adapters.outbox -> app.db",
+ "app.db.adapters.outbox -> app.db.base",
+ "app.db.adapters.outbox -> app.db.models",
+ "app.db.adapters.outbox -> app.db.models.outbox",
+ "app.db.adapters.site -> app.db",
+ "app.db.adapters.site -> app.db.oper",
+ "app.db.adapters.site -> app.db.oper.site",
+ "app.db.adapters.site -> app.db.uow",
+ "app.db.adapters.subscription -> app.application",
+ "app.db.adapters.subscription -> app.application.subscription",
+ "app.db.adapters.subscription -> app.application.subscription.write",
+ "app.db.adapters.subscription -> app.db",
+ "app.db.adapters.subscription -> app.db.adapters",
+ "app.db.adapters.subscription -> app.db.adapters.outbox",
+ "app.db.adapters.subscription -> app.db.oper",
+ "app.db.adapters.subscription -> app.db.oper.subscribe",
+ "app.db.adapters.subscription -> app.db.uow",
+ "app.db.adapters.transaction -> app.db",
+ "app.db.adapters.transaction -> app.db.uow",
+ "app.db.adapters.workflow -> app.application",
+ "app.db.adapters.workflow -> app.application.workflow",
+ "app.db.adapters.workflow -> app.db",
+ "app.db.adapters.workflow -> app.db.oper",
+ "app.db.adapters.workflow -> app.db.oper.workflow",
+ "app.db.adapters.workflow -> app.db.uow",
"app.db.base -> app.db",
"app.db.base -> app.db.decorators",
"app.db.base -> app.db.uow",
@@ -5587,6 +5640,8 @@
"app.runtime.extensions.module_manager -> app.runtime.settings",
"app.runtime.extensions.module_manager -> app.schemas",
"app.runtime.extensions.module_manager -> app.schemas.types",
+ "app.runtime.extensions.plugin.admission -> app.schemas",
+ "app.runtime.extensions.plugin.admission -> app.schemas.exception",
"app.runtime.extensions.plugin.catalog -> app.foundation",
"app.runtime.extensions.plugin.catalog -> app.foundation.version",
"app.runtime.extensions.plugin.catalog -> app.runtime",
@@ -5653,6 +5708,7 @@
"app.runtime.extensions.plugin_manager -> app.runtime.extensions",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.access",
+ "app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.admission",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.catalog",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.clone",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.dependency",
@@ -5673,6 +5729,7 @@
"app.runtime.extensions.plugin_manager -> app.runtime.settings",
"app.runtime.extensions.plugin_manager -> app.runtime.thread",
"app.runtime.extensions.plugin_manager -> app.schemas",
+ "app.runtime.extensions.plugin_manager -> app.schemas.exception",
"app.runtime.extensions.plugin_manager -> app.schemas.plugin",
"app.runtime.extensions.plugin_manager -> app.schemas.types",
"app.runtime.extensions.service_config -> app.runtime",
@@ -5962,109 +6019,295 @@
"app.sdk.utilities -> app.runtime.scheduling",
"app.sdk.utilities -> app.sdk",
"app.sdk.utilities -> app.sdk.string",
- "app.startup.agent_initializer -> app.agent",
- "app.startup.agent_initializer -> app.agent.llm",
- "app.startup.agent_initializer -> app.agent.llm.capability",
- "app.startup.agent_initializer -> app.agent.llm.gateway",
- "app.startup.agent_initializer -> app.agent.llm.helper",
- "app.startup.agent_initializer -> app.agent.llm.provider",
- "app.startup.agent_initializer -> app.agent.prompt",
- "app.startup.agent_initializer -> app.agent.prompt.transfer_redo",
- "app.startup.agent_initializer -> app.agent.runtime_loader",
- "app.startup.agent_initializer -> app.agent.skills",
- "app.startup.agent_initializer -> app.agent.skills.registry",
- "app.startup.agent_initializer -> app.agent.tools",
- "app.startup.agent_initializer -> app.agent.tools.base",
- "app.startup.agent_initializer -> app.application",
- "app.startup.agent_initializer -> app.application.agent",
- "app.startup.agent_initializer -> app.application.messaging",
- "app.startup.agent_initializer -> app.application.messaging.skill",
- "app.startup.agent_initializer -> app.runtime",
- "app.startup.agent_initializer -> app.runtime.events",
- "app.startup.agent_initializer -> app.runtime.log",
- "app.startup.agent_initializer -> app.runtime.settings",
- "app.startup.agent_initializer -> app.schemas",
- "app.startup.agent_initializer -> app.schemas.types",
- "app.startup.cache_initializer -> app.adapters",
- "app.startup.cache_initializer -> app.adapters.cache",
- "app.startup.cache_initializer -> app.adapters.cache.backends",
- "app.startup.chain_events -> app.application",
- "app.startup.chain_events -> app.application.chain",
- "app.startup.chain_events -> app.application.chain.durable_events",
- "app.startup.chain_events -> app.application.history",
- "app.startup.chain_events -> app.application.outbox",
- "app.startup.chain_events -> app.db",
- "app.startup.chain_events -> app.db.oper",
- "app.startup.chain_events -> app.db.oper.downloadhistory",
- "app.startup.chain_events -> app.db.oper.transferhistory",
- "app.startup.chain_events -> app.db.uow",
- "app.startup.chain_events -> app.startup",
- "app.startup.chain_events -> app.startup.outbox",
- "app.startup.command_initializer -> app.application",
- "app.startup.command_initializer -> app.application.commands",
- "app.startup.command_initializer -> app.command",
- "app.startup.configuration -> app.application",
- "app.startup.configuration -> app.application.configuration",
- "app.startup.configuration -> app.runtime",
- "app.startup.configuration -> app.runtime.config",
- "app.startup.configuration -> app.schemas",
- "app.startup.configuration -> app.schemas.types",
- "app.startup.context -> app.application",
- "app.startup.context -> app.application.configuration",
- "app.startup.context -> app.application.messaging",
- "app.startup.context -> app.application.messaging.chat",
- "app.startup.context -> app.application.outbox",
- "app.startup.context -> app.application.subscription",
- "app.startup.context -> app.application.subscription.delete",
- "app.startup.context -> app.application.subscription.identity",
- "app.startup.context -> app.application.subscription.mutation",
- "app.startup.context -> app.application.workflow",
- "app.startup.context -> app.runtime",
- "app.startup.context -> app.runtime.tasks",
- "app.startup.database -> app.adapters",
- "app.startup.database -> app.adapters.system",
- "app.startup.database -> app.adapters.system.backup",
- "app.startup.database -> app.adapters.system.backup.database",
- "app.startup.database -> app.application",
- "app.startup.database -> app.application.backup",
- "app.startup.database -> app.application.database",
- "app.startup.database -> app.application.maintenance",
- "app.startup.database -> app.db",
- "app.startup.database -> app.db.engine",
- "app.startup.database -> app.db.health",
- "app.startup.database -> app.db.maintenance",
- "app.startup.database -> app.db.session",
- "app.startup.database -> app.runtime",
- "app.startup.database -> app.runtime.settings",
- "app.startup.database_initializer -> app.db",
- "app.startup.database_initializer -> app.db.base",
- "app.startup.database_initializer -> app.db.engine",
- "app.startup.database_initializer -> app.db.models",
- "app.startup.database_initializer -> app.runtime",
- "app.startup.database_initializer -> app.runtime.log",
- "app.startup.database_initializer -> app.runtime.settings",
- "app.startup.database_initializer -> app.startup",
- "app.startup.database_initializer -> app.startup.database",
- "app.startup.domain_initializer -> app.adapters",
- "app.startup.domain_initializer -> app.adapters.system",
- "app.startup.domain_initializer -> app.adapters.system.rust",
- "app.startup.domain_initializer -> app.application",
- "app.startup.domain_initializer -> app.application.recognition",
- "app.startup.domain_initializer -> app.domain",
- "app.startup.domain_initializer -> app.domain.context",
- "app.startup.domain_initializer -> app.domain.media",
- "app.startup.domain_initializer -> app.domain.meta",
- "app.startup.domain_initializer -> app.domain.meta.customization",
- "app.startup.domain_initializer -> app.domain.meta.releasegroup",
- "app.startup.domain_initializer -> app.domain.meta.runtime",
- "app.startup.domain_initializer -> app.domain.meta.words",
- "app.startup.domain_initializer -> app.domain.metainfo",
- "app.startup.domain_initializer -> app.runtime",
- "app.startup.domain_initializer -> app.runtime.settings",
- "app.startup.download_failure -> app.db",
- "app.startup.download_failure -> app.db.oper",
- "app.startup.download_failure -> app.db.oper.downloadfailure",
- "app.startup.download_failure -> app.db.uow",
+ "app.startup.composition.configuration -> app.application",
+ "app.startup.composition.configuration -> app.application.configuration",
+ "app.startup.composition.configuration -> app.runtime",
+ "app.startup.composition.configuration -> app.runtime.config",
+ "app.startup.composition.configuration -> app.schemas",
+ "app.startup.composition.configuration -> app.schemas.types",
+ "app.startup.composition.context -> app.application",
+ "app.startup.composition.context -> app.application.configuration",
+ "app.startup.composition.context -> app.application.messaging",
+ "app.startup.composition.context -> app.application.messaging.chat",
+ "app.startup.composition.context -> app.application.outbox",
+ "app.startup.composition.context -> app.application.subscription",
+ "app.startup.composition.context -> app.application.subscription.delete",
+ "app.startup.composition.context -> app.application.subscription.identity",
+ "app.startup.composition.context -> app.application.subscription.mutation",
+ "app.startup.composition.context -> app.application.workflow",
+ "app.startup.composition.context -> app.runtime",
+ "app.startup.composition.context -> app.runtime.tasks",
+ "app.startup.composition.database -> app.adapters",
+ "app.startup.composition.database -> app.adapters.system",
+ "app.startup.composition.database -> app.adapters.system.backup",
+ "app.startup.composition.database -> app.adapters.system.backup.database",
+ "app.startup.composition.database -> app.application",
+ "app.startup.composition.database -> app.application.backup",
+ "app.startup.composition.database -> app.application.database",
+ "app.startup.composition.database -> app.application.maintenance",
+ "app.startup.composition.database -> app.db",
+ "app.startup.composition.database -> app.db.engine",
+ "app.startup.composition.database -> app.db.health",
+ "app.startup.composition.database -> app.db.maintenance",
+ "app.startup.composition.database -> app.db.session",
+ "app.startup.composition.database -> app.runtime",
+ "app.startup.composition.database -> app.runtime.settings",
+ "app.startup.composition.subscription -> app.adapters",
+ "app.startup.composition.subscription -> app.adapters.external",
+ "app.startup.composition.subscription -> app.adapters.external.server",
+ "app.startup.composition.subscription -> app.application",
+ "app.startup.composition.subscription -> app.application.subscription",
+ "app.startup.composition.subscription -> app.application.subscription.complete",
+ "app.startup.composition.subscription -> app.application.subscription.delete",
+ "app.startup.composition.subscription -> app.application.subscription.mutation",
+ "app.startup.composition.subscription -> app.db",
+ "app.startup.composition.subscription -> app.db.adapters",
+ "app.startup.composition.subscription -> app.db.adapters.outbox",
+ "app.startup.composition.subscription -> app.db.oper",
+ "app.startup.composition.subscription -> app.db.oper.subscribe",
+ "app.startup.composition.subscription -> app.db.oper.subscribehistory",
+ "app.startup.composition.subscription -> app.db.session",
+ "app.startup.composition.subscription -> app.db.uow",
+ "app.startup.composition.subscription -> app.runtime",
+ "app.startup.composition.subscription -> app.runtime.events",
+ "app.startup.composition.subscription -> app.schemas",
+ "app.startup.composition.subscription -> app.schemas.types",
+ "app.startup.initializers.agent -> app.agent",
+ "app.startup.initializers.agent -> app.agent.llm",
+ "app.startup.initializers.agent -> app.agent.llm.capability",
+ "app.startup.initializers.agent -> app.agent.llm.gateway",
+ "app.startup.initializers.agent -> app.agent.llm.helper",
+ "app.startup.initializers.agent -> app.agent.llm.provider",
+ "app.startup.initializers.agent -> app.agent.prompt",
+ "app.startup.initializers.agent -> app.agent.prompt.transfer_redo",
+ "app.startup.initializers.agent -> app.agent.runtime_loader",
+ "app.startup.initializers.agent -> app.agent.skills",
+ "app.startup.initializers.agent -> app.agent.skills.registry",
+ "app.startup.initializers.agent -> app.agent.tools",
+ "app.startup.initializers.agent -> app.agent.tools.base",
+ "app.startup.initializers.agent -> app.application",
+ "app.startup.initializers.agent -> app.application.agent",
+ "app.startup.initializers.agent -> app.application.messaging",
+ "app.startup.initializers.agent -> app.application.messaging.skill",
+ "app.startup.initializers.agent -> app.runtime",
+ "app.startup.initializers.agent -> app.runtime.events",
+ "app.startup.initializers.agent -> app.runtime.log",
+ "app.startup.initializers.agent -> app.runtime.settings",
+ "app.startup.initializers.agent -> app.schemas",
+ "app.startup.initializers.agent -> app.schemas.types",
+ "app.startup.initializers.cache -> app.adapters",
+ "app.startup.initializers.cache -> app.adapters.cache",
+ "app.startup.initializers.cache -> app.adapters.cache.backends",
+ "app.startup.initializers.command -> app.application",
+ "app.startup.initializers.command -> app.application.commands",
+ "app.startup.initializers.command -> app.command",
+ "app.startup.initializers.database -> app.db",
+ "app.startup.initializers.database -> app.db.base",
+ "app.startup.initializers.database -> app.db.engine",
+ "app.startup.initializers.database -> app.db.models",
+ "app.startup.initializers.database -> app.runtime",
+ "app.startup.initializers.database -> app.runtime.log",
+ "app.startup.initializers.database -> app.runtime.settings",
+ "app.startup.initializers.database -> app.startup",
+ "app.startup.initializers.database -> app.startup.composition",
+ "app.startup.initializers.database -> app.startup.composition.database",
+ "app.startup.initializers.domain -> app.adapters",
+ "app.startup.initializers.domain -> app.adapters.system",
+ "app.startup.initializers.domain -> app.adapters.system.rust",
+ "app.startup.initializers.domain -> app.application",
+ "app.startup.initializers.domain -> app.application.recognition",
+ "app.startup.initializers.domain -> app.domain",
+ "app.startup.initializers.domain -> app.domain.context",
+ "app.startup.initializers.domain -> app.domain.media",
+ "app.startup.initializers.domain -> app.domain.meta",
+ "app.startup.initializers.domain -> app.domain.meta.customization",
+ "app.startup.initializers.domain -> app.domain.meta.releasegroup",
+ "app.startup.initializers.domain -> app.domain.meta.runtime",
+ "app.startup.initializers.domain -> app.domain.meta.words",
+ "app.startup.initializers.domain -> app.domain.metainfo",
+ "app.startup.initializers.domain -> app.runtime",
+ "app.startup.initializers.domain -> app.runtime.settings",
+ "app.startup.initializers.managed_resources -> app.runtime",
+ "app.startup.initializers.managed_resources -> app.runtime.capabilities",
+ "app.startup.initializers.managed_resources -> app.runtime.capabilities.runtime",
+ "app.startup.initializers.managed_resources -> app.runtime.extensions",
+ "app.startup.initializers.managed_resources -> app.runtime.extensions.managed_resource_adapter",
+ "app.startup.initializers.managed_resources -> app.runtime.managed_resources",
+ "app.startup.initializers.modules -> app.adapters",
+ "app.startup.initializers.modules -> app.adapters.cache",
+ "app.startup.initializers.modules -> app.adapters.cache.redis",
+ "app.startup.initializers.modules -> app.adapters.external",
+ "app.startup.initializers.modules -> app.adapters.external.server",
+ "app.startup.initializers.modules -> app.adapters.network",
+ "app.startup.initializers.modules -> app.adapters.network.browser",
+ "app.startup.initializers.modules -> app.adapters.network.doh",
+ "app.startup.initializers.modules -> app.adapters.system",
+ "app.startup.initializers.modules -> app.adapters.system.host",
+ "app.startup.initializers.modules -> app.adapters.system.resource",
+ "app.startup.initializers.modules -> app.adapters.web",
+ "app.startup.initializers.modules -> app.adapters.web.security",
+ "app.startup.initializers.modules -> app.adapters.web.security.access",
+ "app.startup.initializers.modules -> app.api",
+ "app.startup.initializers.modules -> app.api.data",
+ "app.startup.initializers.modules -> app.application",
+ "app.startup.initializers.modules -> app.application.agentdata",
+ "app.startup.initializers.modules -> app.application.chain",
+ "app.startup.initializers.modules -> app.application.chain.context",
+ "app.startup.initializers.modules -> app.application.chain.data",
+ "app.startup.initializers.modules -> app.application.chain.durable_events",
+ "app.startup.initializers.modules -> app.application.configuration",
+ "app.startup.initializers.modules -> app.application.database",
+ "app.startup.initializers.modules -> app.application.history",
+ "app.startup.initializers.modules -> app.application.image",
+ "app.startup.initializers.modules -> app.application.messaging",
+ "app.startup.initializers.modules -> app.application.messaging.agent",
+ "app.startup.initializers.modules -> app.application.messaging.chat",
+ "app.startup.initializers.modules -> app.application.messaging.message",
+ "app.startup.initializers.modules -> app.application.module",
+ "app.startup.initializers.modules -> app.application.outbox",
+ "app.startup.initializers.modules -> app.application.plugin",
+ "app.startup.initializers.modules -> app.application.plugin.runtime",
+ "app.startup.initializers.modules -> app.application.security",
+ "app.startup.initializers.modules -> app.application.security.auth",
+ "app.startup.initializers.modules -> app.application.security.passkeys",
+ "app.startup.initializers.modules -> app.application.security.user",
+ "app.startup.initializers.modules -> app.application.security.userconfig",
+ "app.startup.initializers.modules -> app.application.server",
+ "app.startup.initializers.modules -> app.application.server.report",
+ "app.startup.initializers.modules -> app.application.server.share",
+ "app.startup.initializers.modules -> app.application.service",
+ "app.startup.initializers.modules -> app.application.site",
+ "app.startup.initializers.modules -> app.application.site.health",
+ "app.startup.initializers.modules -> app.application.site.query",
+ "app.startup.initializers.modules -> app.application.subscription",
+ "app.startup.initializers.modules -> app.application.subscription.write",
+ "app.startup.initializers.modules -> app.application.workflow",
+ "app.startup.initializers.modules -> app.chain",
+ "app.startup.initializers.modules -> app.chain.download",
+ "app.startup.initializers.modules -> app.chain.mediaserver",
+ "app.startup.initializers.modules -> app.chain.scraping",
+ "app.startup.initializers.modules -> app.chain.search",
+ "app.startup.initializers.modules -> app.chain.site",
+ "app.startup.initializers.modules -> app.chain.subscribe",
+ "app.startup.initializers.modules -> app.chain.tmdb",
+ "app.startup.initializers.modules -> app.chain.workflow",
+ "app.startup.initializers.modules -> app.command",
+ "app.startup.initializers.modules -> app.db",
+ "app.startup.initializers.modules -> app.db.adapters",
+ "app.startup.initializers.modules -> app.db.adapters.chain",
+ "app.startup.initializers.modules -> app.db.adapters.download",
+ "app.startup.initializers.modules -> app.db.adapters.outbox",
+ "app.startup.initializers.modules -> app.db.adapters.site",
+ "app.startup.initializers.modules -> app.db.adapters.subscription",
+ "app.startup.initializers.modules -> app.db.adapters.transaction",
+ "app.startup.initializers.modules -> app.db.adapters.workflow",
+ "app.startup.initializers.modules -> app.db.oper",
+ "app.startup.initializers.modules -> app.db.oper.agentchat",
+ "app.startup.initializers.modules -> app.db.oper.agenttask",
+ "app.startup.initializers.modules -> app.db.oper.downloadhistory",
+ "app.startup.initializers.modules -> app.db.oper.mediaserver",
+ "app.startup.initializers.modules -> app.db.oper.message",
+ "app.startup.initializers.modules -> app.db.oper.passkey",
+ "app.startup.initializers.modules -> app.db.oper.plugindata",
+ "app.startup.initializers.modules -> app.db.oper.site",
+ "app.startup.initializers.modules -> app.db.oper.subscribe",
+ "app.startup.initializers.modules -> app.db.oper.subscribehistory",
+ "app.startup.initializers.modules -> app.db.oper.systemconfig",
+ "app.startup.initializers.modules -> app.db.oper.transferhistory",
+ "app.startup.initializers.modules -> app.db.oper.transferpending",
+ "app.startup.initializers.modules -> app.db.oper.user",
+ "app.startup.initializers.modules -> app.db.oper.userconfig",
+ "app.startup.initializers.modules -> app.db.oper.workflow",
+ "app.startup.initializers.modules -> app.db.session",
+ "app.startup.initializers.modules -> app.db.uow",
+ "app.startup.initializers.modules -> app.db.worker",
+ "app.startup.initializers.modules -> app.runtime",
+ "app.startup.initializers.modules -> app.runtime.cache",
+ "app.startup.initializers.modules -> app.runtime.config",
+ "app.startup.initializers.modules -> app.runtime.events",
+ "app.startup.initializers.modules -> app.runtime.extensions",
+ "app.startup.initializers.modules -> app.runtime.extensions.module",
+ "app.startup.initializers.modules -> app.runtime.extensions.module.dispatcher",
+ "app.startup.initializers.modules -> app.runtime.extensions.module_manager",
+ "app.startup.initializers.modules -> app.runtime.extensions.plugin_manager",
+ "app.startup.initializers.modules -> app.runtime.extensions.service_config",
+ "app.startup.initializers.modules -> app.runtime.log",
+ "app.startup.initializers.modules -> app.runtime.observability",
+ "app.startup.initializers.modules -> app.runtime.settings",
+ "app.startup.initializers.modules -> app.runtime.state",
+ "app.startup.initializers.modules -> app.runtime.tasks",
+ "app.startup.initializers.modules -> app.runtime.thread",
+ "app.startup.initializers.modules -> app.scheduler",
+ "app.startup.initializers.modules -> app.schemas",
+ "app.startup.initializers.modules -> app.schemas.message",
+ "app.startup.initializers.modules -> app.schemas.types",
+ "app.startup.initializers.modules -> app.startup",
+ "app.startup.initializers.modules -> app.startup.composition",
+ "app.startup.initializers.modules -> app.startup.composition.configuration",
+ "app.startup.initializers.modules -> app.startup.composition.context",
+ "app.startup.initializers.modules -> app.startup.composition.database",
+ "app.startup.initializers.modules -> app.startup.composition.subscription",
+ "app.startup.initializers.modules -> app.startup.initializers",
+ "app.startup.initializers.modules -> app.startup.initializers.agent",
+ "app.startup.initializers.modules -> app.startup.initializers.managed_resources",
+ "app.startup.initializers.monitor -> app.monitor",
+ "app.startup.initializers.monitor -> app.runtime",
+ "app.startup.initializers.monitor -> app.runtime.execution",
+ "app.startup.initializers.plugins -> app.adapters",
+ "app.startup.initializers.plugins -> app.adapters.external",
+ "app.startup.initializers.plugins -> app.adapters.external.market",
+ "app.startup.initializers.plugins -> app.adapters.external.plugin",
+ "app.startup.initializers.plugins -> app.adapters.external.plugin.client",
+ "app.startup.initializers.plugins -> app.adapters.external.server",
+ "app.startup.initializers.plugins -> app.adapters.system",
+ "app.startup.initializers.plugins -> app.adapters.system.host",
+ "app.startup.initializers.plugins -> app.adapters.system.plugin",
+ "app.startup.initializers.plugins -> app.adapters.system.plugin.dependency",
+ "app.startup.initializers.plugins -> app.adapters.system.plugin.manifest",
+ "app.startup.initializers.plugins -> app.adapters.system.plugin.package",
+ "app.startup.initializers.plugins -> app.application",
+ "app.startup.initializers.plugins -> app.application.configuration",
+ "app.startup.initializers.plugins -> app.application.plugin",
+ "app.startup.initializers.plugins -> app.application.plugin.catalog",
+ "app.startup.initializers.plugins -> app.application.plugin.data",
+ "app.startup.initializers.plugins -> app.application.plugin.routes",
+ "app.startup.initializers.plugins -> app.application.site",
+ "app.startup.initializers.plugins -> app.db",
+ "app.startup.initializers.plugins -> app.db.oper",
+ "app.startup.initializers.plugins -> app.db.oper.plugindata",
+ "app.startup.initializers.plugins -> app.db.session",
+ "app.startup.initializers.plugins -> app.db.uow",
+ "app.startup.initializers.plugins -> app.foundation",
+ "app.startup.initializers.plugins -> app.foundation.version",
+ "app.startup.initializers.plugins -> app.runtime",
+ "app.startup.initializers.plugins -> app.runtime.compat",
+ "app.startup.initializers.plugins -> app.runtime.compat.diagnostics",
+ "app.startup.initializers.plugins -> app.runtime.compat.resource_imports",
+ "app.startup.initializers.plugins -> app.runtime.config",
+ "app.startup.initializers.plugins -> app.runtime.execution",
+ "app.startup.initializers.plugins -> app.runtime.extensions",
+ "app.startup.initializers.plugins -> app.runtime.extensions.plugin",
+ "app.startup.initializers.plugins -> app.runtime.extensions.plugin.dependency",
+ "app.startup.initializers.plugins -> app.runtime.extensions.plugin.storage",
+ "app.startup.initializers.plugins -> app.runtime.extensions.plugin.system",
+ "app.startup.initializers.plugins -> app.runtime.extensions.plugin_manager",
+ "app.startup.initializers.plugins -> app.runtime.log",
+ "app.startup.initializers.plugins -> app.runtime.managed_resources",
+ "app.startup.initializers.plugins -> app.runtime.settings",
+ "app.startup.initializers.plugins -> app.schemas",
+ "app.startup.initializers.plugins -> app.schemas.exception",
+ "app.startup.initializers.plugins -> app.schemas.plugin",
+ "app.startup.initializers.plugins -> app.schemas.types",
+ "app.startup.initializers.routers -> app.api",
+ "app.startup.initializers.routers -> app.api.router_specs",
+ "app.startup.initializers.routers -> app.api.servarr",
+ "app.startup.initializers.routers -> app.api.servcookie",
+ "app.startup.initializers.scheduler -> app.application",
+ "app.startup.initializers.scheduler -> app.application.scheduling",
+ "app.startup.initializers.scheduler -> app.scheduler",
+ "app.startup.initializers.transfer -> app.chain",
+ "app.startup.initializers.transfer -> app.chain.transfer",
+ "app.startup.initializers.workflow -> app.workflow",
"app.startup.lifecycle -> app.adapters",
"app.startup.lifecycle -> app.adapters.external",
"app.startup.lifecycle -> app.adapters.external.server",
@@ -6087,233 +6330,20 @@
"app.startup.lifecycle -> app.runtime.tasks",
"app.startup.lifecycle -> app.runtime.topology",
"app.startup.lifecycle -> app.startup",
- "app.startup.lifecycle -> app.startup.agent_initializer",
- "app.startup.lifecycle -> app.startup.cache_initializer",
- "app.startup.lifecycle -> app.startup.command_initializer",
- "app.startup.lifecycle -> app.startup.database_initializer",
- "app.startup.lifecycle -> app.startup.domain_initializer",
+ "app.startup.lifecycle -> app.startup.initializers",
+ "app.startup.lifecycle -> app.startup.initializers.agent",
+ "app.startup.lifecycle -> app.startup.initializers.cache",
+ "app.startup.lifecycle -> app.startup.initializers.command",
+ "app.startup.lifecycle -> app.startup.initializers.database",
+ "app.startup.lifecycle -> app.startup.initializers.domain",
+ "app.startup.lifecycle -> app.startup.initializers.modules",
+ "app.startup.lifecycle -> app.startup.initializers.monitor",
+ "app.startup.lifecycle -> app.startup.initializers.plugins",
+ "app.startup.lifecycle -> app.startup.initializers.routers",
+ "app.startup.lifecycle -> app.startup.initializers.scheduler",
+ "app.startup.lifecycle -> app.startup.initializers.transfer",
+ "app.startup.lifecycle -> app.startup.initializers.workflow",
"app.startup.lifecycle -> app.startup.lifecycle.components",
- "app.startup.lifecycle -> app.startup.modules_initializer",
- "app.startup.lifecycle -> app.startup.monitor_initializer",
- "app.startup.lifecycle -> app.startup.plugins_initializer",
- "app.startup.lifecycle -> app.startup.routers_initializer",
- "app.startup.lifecycle -> app.startup.scheduler_initializer",
- "app.startup.lifecycle -> app.startup.transfer_initializer",
- "app.startup.lifecycle -> app.startup.workflow_initializer",
- "app.startup.managed_resources_initializer -> app.runtime",
- "app.startup.managed_resources_initializer -> app.runtime.capabilities",
- "app.startup.managed_resources_initializer -> app.runtime.capabilities.runtime",
- "app.startup.managed_resources_initializer -> app.runtime.extensions",
- "app.startup.managed_resources_initializer -> app.runtime.extensions.managed_resource_adapter",
- "app.startup.managed_resources_initializer -> app.runtime.managed_resources",
- "app.startup.modules_initializer -> app.adapters",
- "app.startup.modules_initializer -> app.adapters.cache",
- "app.startup.modules_initializer -> app.adapters.cache.redis",
- "app.startup.modules_initializer -> app.adapters.external",
- "app.startup.modules_initializer -> app.adapters.external.server",
- "app.startup.modules_initializer -> app.adapters.network",
- "app.startup.modules_initializer -> app.adapters.network.browser",
- "app.startup.modules_initializer -> app.adapters.network.doh",
- "app.startup.modules_initializer -> app.adapters.system",
- "app.startup.modules_initializer -> app.adapters.system.host",
- "app.startup.modules_initializer -> app.adapters.system.resource",
- "app.startup.modules_initializer -> app.adapters.web",
- "app.startup.modules_initializer -> app.adapters.web.security",
- "app.startup.modules_initializer -> app.adapters.web.security.access",
- "app.startup.modules_initializer -> app.api",
- "app.startup.modules_initializer -> app.api.data",
- "app.startup.modules_initializer -> app.application",
- "app.startup.modules_initializer -> app.application.agentdata",
- "app.startup.modules_initializer -> app.application.chain",
- "app.startup.modules_initializer -> app.application.chain.context",
- "app.startup.modules_initializer -> app.application.chain.data",
- "app.startup.modules_initializer -> app.application.chain.durable_events",
- "app.startup.modules_initializer -> app.application.configuration",
- "app.startup.modules_initializer -> app.application.database",
- "app.startup.modules_initializer -> app.application.history",
- "app.startup.modules_initializer -> app.application.image",
- "app.startup.modules_initializer -> app.application.messaging",
- "app.startup.modules_initializer -> app.application.messaging.agent",
- "app.startup.modules_initializer -> app.application.messaging.chat",
- "app.startup.modules_initializer -> app.application.messaging.message",
- "app.startup.modules_initializer -> app.application.module",
- "app.startup.modules_initializer -> app.application.outbox",
- "app.startup.modules_initializer -> app.application.plugin",
- "app.startup.modules_initializer -> app.application.plugin.runtime",
- "app.startup.modules_initializer -> app.application.security",
- "app.startup.modules_initializer -> app.application.security.auth",
- "app.startup.modules_initializer -> app.application.security.passkeys",
- "app.startup.modules_initializer -> app.application.security.user",
- "app.startup.modules_initializer -> app.application.security.userconfig",
- "app.startup.modules_initializer -> app.application.server",
- "app.startup.modules_initializer -> app.application.server.report",
- "app.startup.modules_initializer -> app.application.server.share",
- "app.startup.modules_initializer -> app.application.service",
- "app.startup.modules_initializer -> app.application.site",
- "app.startup.modules_initializer -> app.application.site.health",
- "app.startup.modules_initializer -> app.application.site.query",
- "app.startup.modules_initializer -> app.application.subscription",
- "app.startup.modules_initializer -> app.application.subscription.write",
- "app.startup.modules_initializer -> app.application.workflow",
- "app.startup.modules_initializer -> app.chain",
- "app.startup.modules_initializer -> app.chain.download",
- "app.startup.modules_initializer -> app.chain.mediaserver",
- "app.startup.modules_initializer -> app.chain.scraping",
- "app.startup.modules_initializer -> app.chain.search",
- "app.startup.modules_initializer -> app.chain.site",
- "app.startup.modules_initializer -> app.chain.subscribe",
- "app.startup.modules_initializer -> app.chain.tmdb",
- "app.startup.modules_initializer -> app.chain.workflow",
- "app.startup.modules_initializer -> app.command",
- "app.startup.modules_initializer -> app.db",
- "app.startup.modules_initializer -> app.db.oper",
- "app.startup.modules_initializer -> app.db.oper.agentchat",
- "app.startup.modules_initializer -> app.db.oper.agenttask",
- "app.startup.modules_initializer -> app.db.oper.downloadhistory",
- "app.startup.modules_initializer -> app.db.oper.mediaserver",
- "app.startup.modules_initializer -> app.db.oper.message",
- "app.startup.modules_initializer -> app.db.oper.passkey",
- "app.startup.modules_initializer -> app.db.oper.plugindata",
- "app.startup.modules_initializer -> app.db.oper.site",
- "app.startup.modules_initializer -> app.db.oper.subscribe",
- "app.startup.modules_initializer -> app.db.oper.subscribehistory",
- "app.startup.modules_initializer -> app.db.oper.systemconfig",
- "app.startup.modules_initializer -> app.db.oper.transferhistory",
- "app.startup.modules_initializer -> app.db.oper.transferpending",
- "app.startup.modules_initializer -> app.db.oper.user",
- "app.startup.modules_initializer -> app.db.oper.userconfig",
- "app.startup.modules_initializer -> app.db.oper.workflow",
- "app.startup.modules_initializer -> app.db.session",
- "app.startup.modules_initializer -> app.db.uow",
- "app.startup.modules_initializer -> app.db.worker",
- "app.startup.modules_initializer -> app.runtime",
- "app.startup.modules_initializer -> app.runtime.cache",
- "app.startup.modules_initializer -> app.runtime.config",
- "app.startup.modules_initializer -> app.runtime.events",
- "app.startup.modules_initializer -> app.runtime.extensions",
- "app.startup.modules_initializer -> app.runtime.extensions.module",
- "app.startup.modules_initializer -> app.runtime.extensions.module.dispatcher",
- "app.startup.modules_initializer -> app.runtime.extensions.module_manager",
- "app.startup.modules_initializer -> app.runtime.extensions.plugin_manager",
- "app.startup.modules_initializer -> app.runtime.extensions.service_config",
- "app.startup.modules_initializer -> app.runtime.log",
- "app.startup.modules_initializer -> app.runtime.observability",
- "app.startup.modules_initializer -> app.runtime.settings",
- "app.startup.modules_initializer -> app.runtime.state",
- "app.startup.modules_initializer -> app.runtime.tasks",
- "app.startup.modules_initializer -> app.runtime.thread",
- "app.startup.modules_initializer -> app.scheduler",
- "app.startup.modules_initializer -> app.schemas",
- "app.startup.modules_initializer -> app.schemas.message",
- "app.startup.modules_initializer -> app.schemas.types",
- "app.startup.modules_initializer -> app.startup",
- "app.startup.modules_initializer -> app.startup.agent_initializer",
- "app.startup.modules_initializer -> app.startup.chain_events",
- "app.startup.modules_initializer -> app.startup.configuration",
- "app.startup.modules_initializer -> app.startup.context",
- "app.startup.modules_initializer -> app.startup.database",
- "app.startup.modules_initializer -> app.startup.download_failure",
- "app.startup.modules_initializer -> app.startup.managed_resources_initializer",
- "app.startup.modules_initializer -> app.startup.outbox",
- "app.startup.modules_initializer -> app.startup.site",
- "app.startup.modules_initializer -> app.startup.subscription",
- "app.startup.modules_initializer -> app.startup.transaction",
- "app.startup.modules_initializer -> app.startup.workflow",
- "app.startup.monitor_initializer -> app.monitor",
- "app.startup.monitor_initializer -> app.runtime",
- "app.startup.monitor_initializer -> app.runtime.execution",
- "app.startup.outbox -> app.application",
- "app.startup.outbox -> app.application.outbox",
- "app.startup.outbox -> app.db",
- "app.startup.outbox -> app.db.base",
- "app.startup.outbox -> app.db.models",
- "app.startup.outbox -> app.db.models.outbox",
- "app.startup.plugins_initializer -> app.adapters",
- "app.startup.plugins_initializer -> app.adapters.external",
- "app.startup.plugins_initializer -> app.adapters.external.market",
- "app.startup.plugins_initializer -> app.adapters.external.plugin",
- "app.startup.plugins_initializer -> app.adapters.external.plugin.client",
- "app.startup.plugins_initializer -> app.adapters.external.server",
- "app.startup.plugins_initializer -> app.adapters.system",
- "app.startup.plugins_initializer -> app.adapters.system.host",
- "app.startup.plugins_initializer -> app.adapters.system.plugin",
- "app.startup.plugins_initializer -> app.adapters.system.plugin.dependency",
- "app.startup.plugins_initializer -> app.adapters.system.plugin.manifest",
- "app.startup.plugins_initializer -> app.adapters.system.plugin.package",
- "app.startup.plugins_initializer -> app.application",
- "app.startup.plugins_initializer -> app.application.configuration",
- "app.startup.plugins_initializer -> app.application.plugin",
- "app.startup.plugins_initializer -> app.application.plugin.catalog",
- "app.startup.plugins_initializer -> app.application.plugin.data",
- "app.startup.plugins_initializer -> app.application.plugin.routes",
- "app.startup.plugins_initializer -> app.application.site",
- "app.startup.plugins_initializer -> app.db",
- "app.startup.plugins_initializer -> app.db.oper",
- "app.startup.plugins_initializer -> app.db.oper.plugindata",
- "app.startup.plugins_initializer -> app.db.session",
- "app.startup.plugins_initializer -> app.db.uow",
- "app.startup.plugins_initializer -> app.foundation",
- "app.startup.plugins_initializer -> app.foundation.version",
- "app.startup.plugins_initializer -> app.runtime",
- "app.startup.plugins_initializer -> app.runtime.compat",
- "app.startup.plugins_initializer -> app.runtime.compat.diagnostics",
- "app.startup.plugins_initializer -> app.runtime.compat.resource_imports",
- "app.startup.plugins_initializer -> app.runtime.config",
- "app.startup.plugins_initializer -> app.runtime.extensions",
- "app.startup.plugins_initializer -> app.runtime.extensions.plugin",
- "app.startup.plugins_initializer -> app.runtime.extensions.plugin.dependency",
- "app.startup.plugins_initializer -> app.runtime.extensions.plugin.storage",
- "app.startup.plugins_initializer -> app.runtime.extensions.plugin.system",
- "app.startup.plugins_initializer -> app.runtime.extensions.plugin_manager",
- "app.startup.plugins_initializer -> app.runtime.log",
- "app.startup.plugins_initializer -> app.runtime.managed_resources",
- "app.startup.plugins_initializer -> app.runtime.settings",
- "app.startup.plugins_initializer -> app.schemas",
- "app.startup.plugins_initializer -> app.schemas.plugin",
- "app.startup.plugins_initializer -> app.schemas.types",
- "app.startup.routers_initializer -> app.api",
- "app.startup.routers_initializer -> app.api.router_specs",
- "app.startup.routers_initializer -> app.api.servarr",
- "app.startup.routers_initializer -> app.api.servcookie",
- "app.startup.scheduler_initializer -> app.application",
- "app.startup.scheduler_initializer -> app.application.scheduling",
- "app.startup.scheduler_initializer -> app.scheduler",
- "app.startup.site -> app.db",
- "app.startup.site -> app.db.oper",
- "app.startup.site -> app.db.oper.site",
- "app.startup.site -> app.db.uow",
- "app.startup.subscription -> app.adapters",
- "app.startup.subscription -> app.adapters.external",
- "app.startup.subscription -> app.adapters.external.server",
- "app.startup.subscription -> app.application",
- "app.startup.subscription -> app.application.subscription",
- "app.startup.subscription -> app.application.subscription.complete",
- "app.startup.subscription -> app.application.subscription.delete",
- "app.startup.subscription -> app.application.subscription.mutation",
- "app.startup.subscription -> app.application.subscription.write",
- "app.startup.subscription -> app.db",
- "app.startup.subscription -> app.db.oper",
- "app.startup.subscription -> app.db.oper.subscribe",
- "app.startup.subscription -> app.db.oper.subscribehistory",
- "app.startup.subscription -> app.db.session",
- "app.startup.subscription -> app.db.uow",
- "app.startup.subscription -> app.runtime",
- "app.startup.subscription -> app.runtime.events",
- "app.startup.subscription -> app.schemas",
- "app.startup.subscription -> app.schemas.types",
- "app.startup.subscription -> app.startup",
- "app.startup.subscription -> app.startup.outbox",
- "app.startup.transaction -> app.db",
- "app.startup.transaction -> app.db.uow",
- "app.startup.transfer_initializer -> app.chain",
- "app.startup.transfer_initializer -> app.chain.transfer",
- "app.startup.workflow -> app.application",
- "app.startup.workflow -> app.application.workflow",
- "app.startup.workflow -> app.db",
- "app.startup.workflow -> app.db.oper",
- "app.startup.workflow -> app.db.oper.workflow",
- "app.startup.workflow -> app.db.uow",
- "app.startup.workflow_initializer -> app.workflow",
"app.testing -> app.testing.stub",
"app.testing.bootstrap -> app.application",
"app.testing.bootstrap -> app.application.site",
@@ -6322,9 +6352,10 @@
"app.testing.bootstrap -> app.db.oper.systemconfig",
"app.testing.bootstrap -> app.db.oper.userconfig",
"app.testing.bootstrap -> app.startup",
- "app.testing.bootstrap -> app.startup.cache_initializer",
- "app.testing.bootstrap -> app.startup.database_initializer",
- "app.testing.bootstrap -> app.startup.domain_initializer",
+ "app.testing.bootstrap -> app.startup.initializers",
+ "app.testing.bootstrap -> app.startup.initializers.cache",
+ "app.testing.bootstrap -> app.startup.initializers.database",
+ "app.testing.bootstrap -> app.startup.initializers.domain",
"app.workflow -> app.application",
"app.workflow -> app.application.chain",
"app.workflow -> app.application.chain.data",
@@ -6500,7 +6531,7 @@
"app.workflow.actions.transfer_file -> app.workflow",
"app.workflow.actions.transfer_file -> app.workflow.actions"
],
- "module_count": 800,
+ "module_count": 805,
"modules": [
"app",
"app.adapters",
@@ -6874,6 +6905,14 @@
"app.cli",
"app.command",
"app.db",
+ "app.db.adapters",
+ "app.db.adapters.chain",
+ "app.db.adapters.download",
+ "app.db.adapters.outbox",
+ "app.db.adapters.site",
+ "app.db.adapters.subscription",
+ "app.db.adapters.transaction",
+ "app.db.adapters.workflow",
"app.db.base",
"app.db.decorators",
"app.db.diagnostics",
@@ -7164,6 +7203,7 @@
"app.runtime.extensions.module_manager",
"app.runtime.extensions.plugin",
"app.runtime.extensions.plugin.access",
+ "app.runtime.extensions.plugin.admission",
"app.runtime.extensions.plugin.catalog",
"app.runtime.extensions.plugin.clone",
"app.runtime.extensions.plugin.contracts",
@@ -7256,31 +7296,27 @@
"app.sdk.string",
"app.sdk.utilities",
"app.startup",
- "app.startup.agent_initializer",
- "app.startup.cache_initializer",
- "app.startup.chain_events",
- "app.startup.command_initializer",
- "app.startup.configuration",
- "app.startup.context",
- "app.startup.database",
- "app.startup.database_initializer",
- "app.startup.domain_initializer",
- "app.startup.download_failure",
+ "app.startup.composition",
+ "app.startup.composition.configuration",
+ "app.startup.composition.context",
+ "app.startup.composition.database",
+ "app.startup.composition.subscription",
+ "app.startup.initializers",
+ "app.startup.initializers.agent",
+ "app.startup.initializers.cache",
+ "app.startup.initializers.command",
+ "app.startup.initializers.database",
+ "app.startup.initializers.domain",
+ "app.startup.initializers.managed_resources",
+ "app.startup.initializers.modules",
+ "app.startup.initializers.monitor",
+ "app.startup.initializers.plugins",
+ "app.startup.initializers.routers",
+ "app.startup.initializers.scheduler",
+ "app.startup.initializers.transfer",
+ "app.startup.initializers.workflow",
"app.startup.lifecycle",
"app.startup.lifecycle.components",
- "app.startup.managed_resources_initializer",
- "app.startup.modules_initializer",
- "app.startup.monitor_initializer",
- "app.startup.outbox",
- "app.startup.plugins_initializer",
- "app.startup.routers_initializer",
- "app.startup.scheduler_initializer",
- "app.startup.site",
- "app.startup.subscription",
- "app.startup.transaction",
- "app.startup.transfer_initializer",
- "app.startup.workflow",
- "app.startup.workflow_initializer",
"app.testing",
"app.testing.bootstrap",
"app.testing.network_guard",
diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json
index 3fe8cdd28..7b912deba 100644
--- a/tests/fixtures/architecture/runtime-contract-baseline.json
+++ b/tests/fixtures/architecture/runtime-contract-baseline.json
@@ -187,8 +187,8 @@
"introduced": "v3.0.0",
"is_package": false,
"owner": "startup",
- "replacement": "app.startup.database_initializer",
- "target": "app.startup.database_initializer"
+ "replacement": "app.startup.initializers.database",
+ "target": "app.startup.initializers.database"
},
"app.db.mediaserver_oper": {
"introduced": "v3.0.0",
@@ -2029,7 +2029,7 @@
"count": 1
},
{
- "caller": "app.startup.agent_initializer",
+ "caller": "app.startup.initializers.agent",
"count": 1
}
],
@@ -2043,7 +2043,7 @@
"count": 2
},
{
- "caller": "app.startup.modules_initializer",
+ "caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2194,7 +2194,7 @@
"count": 2
},
{
- "caller": "app.startup.modules_initializer",
+ "caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2203,11 +2203,11 @@
"consumers": [],
"producers": [
{
- "caller": "app.startup.modules_initializer",
+ "caller": "app.startup.composition.subscription",
"count": 1
},
{
- "caller": "app.startup.subscription",
+ "caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2220,11 +2220,11 @@
"count": 1
},
{
- "caller": "app.startup.modules_initializer",
+ "caller": "app.startup.composition.subscription",
"count": 1
},
{
- "caller": "app.startup.subscription",
+ "caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2241,11 +2241,11 @@
"count": 3
},
{
- "caller": "app.startup.modules_initializer",
+ "caller": "app.startup.composition.subscription",
"count": 1
},
{
- "caller": "app.startup.subscription",
+ "caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2293,7 +2293,7 @@
"count": 2
},
{
- "caller": "app.startup.modules_initializer",
+ "caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2306,7 +2306,7 @@
"count": 2
},
{
- "caller": "app.startup.modules_initializer",
+ "caller": "app.startup.initializers.modules",
"count": 1
}
]
diff --git a/tests/test_agent_api_lazy_imports.py b/tests/test_agent_api_lazy_imports.py
index d1061fffb..14c859548 100644
--- a/tests/test_agent_api_lazy_imports.py
+++ b/tests/test_agent_api_lazy_imports.py
@@ -56,7 +56,7 @@ sites.__file__ = ""
sys.modules["app.application.site.sites"] = sites
from fastapi import FastAPI
-from app.startup.routers_initializer import init_routers
+from app.startup.initializers.routers import init_routers
app = FastAPI()
init_routers(app)
diff --git a/tests/test_agent_lazy_initializer.py b/tests/test_agent_lazy_initializer.py
index df8b9fd69..c0858cda6 100644
--- a/tests/test_agent_lazy_initializer.py
+++ b/tests/test_agent_lazy_initializer.py
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from app.runtime.capabilities.errors import CapabilityRuntimeClosedError
-from app.startup import agent_initializer
+from app.startup.initializers import agent as agent_initializer
@pytest.mark.anyio
diff --git a/tests/test_agent_lazy_runtime_boundary.py b/tests/test_agent_lazy_runtime_boundary.py
index 40827576e..dfda740d6 100644
--- a/tests/test_agent_lazy_runtime_boundary.py
+++ b/tests/test_agent_lazy_runtime_boundary.py
@@ -64,7 +64,7 @@ def test_agent_initializer_import_only_registers_lazy_providers() -> None:
import json
import sys
-import app.startup.agent_initializer
+import app.startup.initializers.agent
forbidden = (
"app.agent.orchestrator",
diff --git a/tests/test_agent_lifecycle.py b/tests/test_agent_lifecycle.py
index e3195bed9..ab4525355 100644
--- a/tests/test_agent_lifecycle.py
+++ b/tests/test_agent_lifecycle.py
@@ -15,7 +15,8 @@ from app.agent.orchestrator import (
AgentManagerUnavailableError,
)
from app.agent.memory import MemoryManager
-from app.startup import agent_initializer, modules_initializer
+from app.startup.initializers import agent as agent_initializer
+from app.startup.initializers import modules as modules_initializer
@pytest.mark.anyio
diff --git a/tests/test_api_response.py b/tests/test_api_response.py
index 4824b0421..dd535fda9 100644
--- a/tests/test_api_response.py
+++ b/tests/test_api_response.py
@@ -512,7 +512,7 @@ def test_v1_model_free_routes_match_audited_native_allowlist():
def test_native_protocol_openapi_has_explicit_response_schemas():
"""OpenAI、Anthropic 与 MCP 原生协议响应必须在 OpenAPI 中明确建模。"""
from app.factory import create_app
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -558,7 +558,7 @@ def test_native_protocol_openapi_has_explicit_response_schemas():
async def test_native_protocol_validation_errors_keep_native_shapes():
"""OpenAI 与 Anthropic 的请求校验错误应保持各自协议的错误结构。"""
from app.factory import create_app
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -603,7 +603,7 @@ async def test_native_protocol_validation_errors_keep_native_shapes():
async def test_mcp_root_auth_error_keeps_jsonrpc_shape():
"""MCP 根端点的依赖异常应保持 JSON-RPC,REST 子端点仍由统一协议处理。"""
from app.factory import create_app
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -678,7 +678,7 @@ async def test_native_ai_http_and_unhandled_errors_keep_protocol_shapes():
def test_servarr_and_cookiecloud_openapi_has_explicit_models():
"""兼容协议成功响应必须显式建模,错误响应必须声明统一结构。"""
from app.factory import create_app
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -712,7 +712,7 @@ def test_servarr_and_cookiecloud_openapi_has_explicit_models():
def test_all_openapi_error_responses_use_json_schemas():
"""所有普通与原生协议错误响应都应在文档中声明 JSON 媒体类型和结构。"""
from app.factory import create_app
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -739,7 +739,7 @@ def test_all_openapi_error_responses_use_json_schemas():
def test_openapi_success_models_have_no_implicit_empty_nested_schemas():
"""2xx 响应可达模型不得包含裸 Any、裸数组或未声明值类型的开放映射。"""
from app.factory import create_app
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py
index 5b3936e0b..02e4d2e7d 100644
--- a/tests/test_architecture_dependencies.py
+++ b/tests/test_architecture_dependencies.py
@@ -12,6 +12,7 @@ IMPLEMENTATION_ROOTS = (
"app.agent.skills",
"app.adapters",
"app.application",
+ "app.db.adapters",
"app.domain",
"app.foundation",
"app.runtime",
@@ -66,6 +67,29 @@ RETIRED_CANONICAL_FILES = (
"app/adapters/network/sites.pyi",
"app/application/plugins.py",
"app/application/subscribe.py",
+ "app/startup/agent_initializer.py",
+ "app/startup/cache_initializer.py",
+ "app/startup/chain_events.py",
+ "app/startup/command_initializer.py",
+ "app/startup/configuration.py",
+ "app/startup/context.py",
+ "app/startup/database.py",
+ "app/startup/database_initializer.py",
+ "app/startup/domain_initializer.py",
+ "app/startup/download_failure.py",
+ "app/startup/managed_resources_initializer.py",
+ "app/startup/modules_initializer.py",
+ "app/startup/monitor_initializer.py",
+ "app/startup/outbox.py",
+ "app/startup/plugins_initializer.py",
+ "app/startup/routers_initializer.py",
+ "app/startup/scheduler_initializer.py",
+ "app/startup/site.py",
+ "app/startup/subscription.py",
+ "app/startup/transaction.py",
+ "app/startup/transfer_initializer.py",
+ "app/startup/workflow.py",
+ "app/startup/workflow_initializer.py",
)
PLUGIN_COMPONENT_ROOTS = (
"app/adapters/external/plugin",
@@ -288,6 +312,20 @@ def test_retired_canonical_filenames_do_not_return():
assert leftovers == []
+def test_startup_root_contains_only_composition_packages():
+ """组合根顶层只保留稳定分区,禁止再次堆叠扁平实现文件。"""
+ startup_root = APP_ROOT / "startup"
+ root_modules = sorted(path.name for path in startup_root.glob("*.py"))
+ python_packages = sorted(
+ path.name
+ for path in startup_root.iterdir()
+ if path.is_dir() and any(path.rglob("*.py"))
+ )
+
+ assert root_modules == ["__init__.py"]
+ assert python_packages == ["composition", "initializers", "lifecycle"]
+
+
def test_retired_canonical_roots_contain_no_python_sources():
"""已收敛的新增目录不得再次以顶级 Python 包形式出现。"""
leftovers = sorted(
@@ -394,6 +432,49 @@ def test_database_internals_do_not_import_db_facades():
assert violations == []
+def test_base_crud_is_explicitly_legacy_only():
+ """Base 便利 CRUD 只能保留兼容壳,不得伪装成新的正式事务入口。"""
+ path = APP_ROOT / "db" / "base.py"
+ tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
+ base_class = next(
+ node
+ for node in tree.body
+ if isinstance(node, ast.ClassDef) and node.name == "Base"
+ )
+ formal_decorators = {
+ "db_query",
+ "db_update",
+ "async_db_query",
+ "async_db_update",
+ }
+ violations: list[str] = []
+ for node in base_class.body:
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ continue
+ decorators = {
+ decorator.id
+ for decorator in node.decorator_list
+ if isinstance(decorator, ast.Name)
+ }
+ if decorators & formal_decorators:
+ violations.append(node.name)
+ assert violations == []
+
+
+def test_models_use_one_legacy_query_compatibility_shell():
+ """旧 Model 查询统一使用 legacy 装饰器,不得再手写隐式会话 runner。"""
+ retired_names = {"run_legacy_sync_query", "run_legacy_async_query"}
+ violations: list[str] = []
+ for path in (APP_ROOT / "db" / "models").glob("*.py"):
+ tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
+ if any(
+ isinstance(node, ast.Name) and node.id in retired_names
+ for node in ast.walk(tree)
+ ):
+ violations.append(str(path.relative_to(PROJECT_ROOT)))
+ assert violations == []
+
+
def test_entry_layers_do_not_import_database_implementations():
"""API、应用、编排、Agent、监控、模块和 Runtime 只能经端口访问持久化。"""
graph = _build_module_graph()
diff --git a/tests/test_cache_system.py b/tests/test_cache_system.py
index 2241cb5fb..9fe3ff8f8 100644
--- a/tests/test_cache_system.py
+++ b/tests/test_cache_system.py
@@ -40,7 +40,7 @@ def test_clear_package_tool_cache_only_removes_pip_and_uv_old_files(tmp_path, mo
"""
包安装工具缓存清理只处理 pip/uv 子目录,不接管整个 .cache 或业务缓存。
"""
- from app.startup.modules_initializer import clear_package_tool_cache
+ from app.startup.initializers.modules import clear_package_tool_cache
old_time = time.time() - 40 * 24 * 3600
cache_root = tmp_path / ".cache"
@@ -68,7 +68,7 @@ def test_clear_package_tool_cache_disabled_when_days_non_positive(tmp_path, monk
"""
PACKAGE_CACHE_DAYS 小于等于 0 时不清理包安装缓存。
"""
- from app.startup.modules_initializer import clear_package_tool_cache
+ from app.startup.initializers.modules import clear_package_tool_cache
old_time = time.time() - 40 * 24 * 3600
old_pip = tmp_path / ".cache" / "pip" / "old.whl"
@@ -88,7 +88,7 @@ def test_clear_package_tool_cache_isolates_subdir_errors(tmp_path, monkeypatch):
"""
单个工具缓存目录清理失败,不影响另一个工具缓存目录。
"""
- from app.startup.modules_initializer import clear_package_tool_cache
+ from app.startup.initializers.modules import clear_package_tool_cache
calls = []
@@ -100,7 +100,7 @@ def test_clear_package_tool_cache_isolates_subdir_errors(tmp_path, monkeypatch):
monkeypatch.setattr(settings, "CONFIG_DIR", str(tmp_path))
monkeypatch.setattr(settings, "PACKAGE_CACHE_ROOT", str(tmp_path / "custom-package-cache"))
monkeypatch.setattr(settings, "PACKAGE_CACHE_DAYS", 30)
- monkeypatch.setattr("app.startup.modules_initializer.SystemUtils.clear", fake_clear)
+ monkeypatch.setattr("app.startup.initializers.modules.SystemUtils.clear", fake_clear)
clear_package_tool_cache()
@@ -110,7 +110,7 @@ def test_clear_package_tool_cache_uses_package_cache_root(tmp_path, monkeypatch)
"""
PACKAGE_CACHE_ROOT 用作 pip/uv 清理根目录,不扩大到配置目录下其他缓存。
"""
- from app.startup.modules_initializer import clear_package_tool_cache
+ from app.startup.initializers.modules import clear_package_tool_cache
old_time = time.time() - 40 * 24 * 3600
package_cache_root = tmp_path / "custom-package-cache"
@@ -134,7 +134,7 @@ def test_init_modules_does_not_clear_package_tool_cache(monkeypatch):
"""
包安装缓存清理由通用临时清理入口触发,模块启动路径不直接执行清理。
"""
- from app.startup import modules_initializer
+ from app.startup.initializers import modules as modules_initializer
called = False
diff --git a/tests/test_chain_durable_events.py b/tests/test_chain_durable_events.py
index df1fc5996..357d07bc3 100644
--- a/tests/test_chain_durable_events.py
+++ b/tests/test_chain_durable_events.py
@@ -22,7 +22,7 @@ from app.domain.metainfo import MetaInfo
from app.schemas.file import FileItem
from app.schemas.transfer import TransferInfo
from app.schemas.types import MediaSource, MediaType
-from app.startup.chain_events import TransactionalChainDurableEventWriter
+from app.db.adapters.chain import TransactionalChainDurableEventWriter
def _session_factory():
diff --git a/tests/test_configuration_initializer.py b/tests/test_configuration_initializer.py
index 24a765f90..5ab7cbb6d 100644
--- a/tests/test_configuration_initializer.py
+++ b/tests/test_configuration_initializer.py
@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock
import pytest
-from app.startup import modules_initializer
+from app.startup.initializers import modules as modules_initializer
from app.startup.lifecycle import initialize_modules_component
from app.application.configuration import configure_runtime_settings
from app.runtime.settings import RuntimeSettingsCompat
diff --git a/tests/test_database_backup_initializer.py b/tests/test_database_backup_initializer.py
index 9f65ac35c..7bba4ef12 100644
--- a/tests/test_database_backup_initializer.py
+++ b/tests/test_database_backup_initializer.py
@@ -3,7 +3,7 @@ from types import SimpleNamespace
from unittest.mock import Mock
from app.application.database import get_database_governance
-from app.startup import database as startup_database
+from app.startup.composition import database as startup_database
def test_builder_uses_cached_engine_as_database_fact_source(monkeypatch) -> None:
diff --git a/tests/test_database_index_migration.py b/tests/test_database_index_migration.py
index 32dd9effa..cf94593e3 100644
--- a/tests/test_database_index_migration.py
+++ b/tests/test_database_index_migration.py
@@ -76,7 +76,7 @@ from sqlalchemy.exc import IntegrityError
from app.runtime.config import settings
from app.db import get_engine
-from app.startup.database_initializer import init_db, update_db
+from app.startup.initializers.database import init_db, update_db
media_tables = {media_tables!r}
legacy_identity_columns = {legacy_identity_columns!r}
diff --git a/tests/test_database_migration_startup.py b/tests/test_database_migration_startup.py
index 8e6f9c8bc..2ac2fb296 100644
--- a/tests/test_database_migration_startup.py
+++ b/tests/test_database_migration_startup.py
@@ -27,8 +27,8 @@ from sqlalchemy import (
)
from sqlalchemy.engine.url import make_url
-from app.startup import database_initializer as db_init
-from app.startup import database as startup_database
+from app.startup.composition import database as startup_database
+from app.startup.initializers import database as db_init
from app.startup import lifecycle
from app.runtime.health import get_application_health
from app.db.models.systemconfig import SystemConfig
diff --git a/tests/test_db_config_user_queries.py b/tests/test_db_config_user_queries.py
index 7ef0483e8..26547f0ed 100644
--- a/tests/test_db_config_user_queries.py
+++ b/tests/test_db_config_user_queries.py
@@ -292,13 +292,9 @@ def test_passkey_lookup_by_credential_id_skips_inactive(db):
assert asyncio.run(PassKey.async_get_by_credential_id(credential_id="cred-dead")) is None
-def test_passkey_model_sync_queries_keep_no_session_plugin_abi(db, monkeypatch):
- """旧插件不传 Session 时仍应获得短会话查询,而不恢复 Model 装饰器。"""
+def test_passkey_model_sync_queries_keep_no_session_plugin_abi(db):
+ """旧插件不传 Session 时仍由统一 legacy 装饰器获得短会话查询。"""
db.add(_passkey(9004, "cred-legacy"))
- monkeypatch.setattr(
- "app.db.models.passkey.run_legacy_sync_query",
- lambda operation: operation(db.session),
- )
assert [item.credential_id for item in PassKey.get_by_user_id(user_id=9004)] == [
"cred-legacy"
diff --git a/tests/test_db_plugin_message_agent_queries.py b/tests/test_db_plugin_message_agent_queries.py
index 9e60c9e98..3074615bf 100644
--- a/tests/test_db_plugin_message_agent_queries.py
+++ b/tests/test_db_plugin_message_agent_queries.py
@@ -352,13 +352,10 @@ def test_agenttask_get_for_user_enforces_ownership(db):
assert AgentTask.get_for_user(db.session, task_id, user_id="bob") is None
-def test_agenttask_model_queries_keep_no_session_plugin_abi(db, monkeypatch):
- """旧插件省略 Session 时仍可按原关键字参数查询 Agent 任务。"""
+def test_agenttask_model_queries_keep_no_session_plugin_abi(db):
+ """旧插件省略 Session 时仍由统一 legacy 装饰器按原参数查询。"""
task_id = AgentTask.add_task(db.session, **_task("legacy", user_id="legacy-user"))
- monkeypatch.setattr(
- "app.db.models.agenttask.run_legacy_sync_query",
- lambda operation: operation(db.session),
- )
+ db.session.commit()
assert AgentTask.get_for_user(
task_id=task_id,
diff --git a/tests/test_event_runtime_components.py b/tests/test_event_runtime_components.py
index 750f95f1a..eb9f67e12 100644
--- a/tests/test_event_runtime_components.py
+++ b/tests/test_event_runtime_components.py
@@ -12,7 +12,7 @@ from app.runtime.event.binding import (
from app.runtime.event.errors import EventErrorPolicy
from app.runtime.events import Event
from app.schemas.types import EventType
-from app.startup.modules_initializer import get_host_event_handler_factories
+from app.startup.initializers.modules import get_host_event_handler_factories
class _UnmanagedHandler:
diff --git a/tests/test_host_runtime_context.py b/tests/test_host_runtime_context.py
index c164e74b5..f4f4dd338 100644
--- a/tests/test_host_runtime_context.py
+++ b/tests/test_host_runtime_context.py
@@ -15,7 +15,7 @@ from app.api.context import (
)
from app.api.dependencies.agent import get_agent_chat_persistence
from app.startup import lifecycle
-from app.startup.context import (
+from app.startup.composition.context import (
AgentChatRuntime,
AuthenticationRuntime,
HistoryRuntime,
diff --git a/tests/test_legacy_plugin_resource_imports.py b/tests/test_legacy_plugin_resource_imports.py
index 5c874f7ab..dc1c040d3 100644
--- a/tests/test_legacy_plugin_resource_imports.py
+++ b/tests/test_legacy_plugin_resource_imports.py
@@ -17,7 +17,7 @@ from app.runtime.compat.resource_imports import (
)
from app.runtime.extensions import plugin_manager as plugin_manager_module
from app.runtime.extensions.plugin_manager import PluginManager
-from app.startup import plugins_initializer
+from app.startup.initializers import plugins as plugins_initializer
_HEADED_CLOAKBROWSER_ENTRYPOINTS = (
diff --git a/tests/test_lifecycle_shutdown.py b/tests/test_lifecycle_shutdown.py
index 056802c62..733cf8966 100644
--- a/tests/test_lifecycle_shutdown.py
+++ b/tests/test_lifecycle_shutdown.py
@@ -6,7 +6,8 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import FastAPI
-from app.startup import lifecycle, modules_initializer
+from app.startup import lifecycle
+from app.startup.initializers import modules as modules_initializer
from app.adapters.network import http as http_utils
from app.runtime.tasks import get_task_registry
diff --git a/tests/test_managed_resources.py b/tests/test_managed_resources.py
index 0ba671c09..72bb9b547 100644
--- a/tests/test_managed_resources.py
+++ b/tests/test_managed_resources.py
@@ -294,7 +294,7 @@ def test_startup_initializer_discovers_manifest_without_importing_resource() ->
script = """
import asyncio
import sys
-from app.startup.managed_resources_initializer import (
+from app.startup.initializers.managed_resources import (
init_managed_resources,
stop_managed_resources,
)
@@ -320,7 +320,7 @@ assert "pyvirtualdisplay" not in sys.modules
def test_startup_shutdown_without_init_does_not_build_registry(monkeypatch) -> None:
"""未执行启动装配时,关闭入口不得通过发现声明反向初始化 Runtime。"""
- from app.startup import managed_resources_initializer
+ from app.startup.initializers import managed_resources as managed_resources_initializer
build_registry = MagicMock(side_effect=AssertionError("must not discover"))
monkeypatch.setattr(
diff --git a/tests/test_monitor_lifecycle.py b/tests/test_monitor_lifecycle.py
index 5520d172c..7e6239eb0 100644
--- a/tests/test_monitor_lifecycle.py
+++ b/tests/test_monitor_lifecycle.py
@@ -12,7 +12,7 @@ import pytest
from app.monitor.monitor import Monitor
from app.monitor.recovery import RecoveryExecutor, RecoveryState
from app.foundation.singleton import SingletonClass
-from app.startup.monitor_initializer import init_monitor, stop_monitor
+from app.startup.initializers.monitor import init_monitor, stop_monitor
def _build_monitor() -> Monitor:
diff --git a/tests/test_plugin_monitor_lifecycle.py b/tests/test_plugin_monitor_lifecycle.py
index 7be90756c..2cc37c60d 100644
--- a/tests/test_plugin_monitor_lifecycle.py
+++ b/tests/test_plugin_monitor_lifecycle.py
@@ -21,7 +21,7 @@ from app.runtime.extensions.plugin.admission import PluginMutationAdmission
from app.runtime.extensions.plugin.system import reset_plugin_system
from app.runtime.extensions.plugin_manager import PluginManager
from app.schemas.plugin import PluginRuntimeStatus
-from app.startup import plugins_initializer
+from app.startup.initializers import plugins as plugins_initializer
def _reset_plugin_manager() -> None:
diff --git a/tests/test_resource_v3.py b/tests/test_resource_v3.py
index ff2fea07d..f80b5dd41 100644
--- a/tests/test_resource_v3.py
+++ b/tests/test_resource_v3.py
@@ -5,7 +5,7 @@ from app.adapters.system.resource import (
ResourceHelper,
configure_resource_version_provider,
)
-from app.startup import modules_initializer
+from app.startup.initializers import modules as modules_initializer
ROOT_DIR = Path(__file__).resolve().parents[1]
diff --git a/tests/test_router_aggregation.py b/tests/test_router_aggregation.py
index c97dba247..ac20de226 100644
--- a/tests/test_router_aggregation.py
+++ b/tests/test_router_aggregation.py
@@ -56,7 +56,7 @@ def _route_contract(route: APIRoute) -> tuple[Any, ...]:
def test_init_routers_directly_includes_endpoint_router_specs(monkeypatch):
"""启动聚合应直接 include 原始端点路由器并一次性附加完整 v1 前缀。"""
from app.api.router_specs import API_V1_ROUTER_SPECS
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = FastAPI()
include_calls = []
@@ -90,7 +90,7 @@ def test_init_routers_directly_includes_endpoint_router_specs(monkeypatch):
def test_direct_v1_routes_and_openapi_match_compatibility_router():
"""最终应用的 v1 路由合同与 OpenAPI 应和兼容聚合结果完全一致。"""
from app.api.apiv1 import api_router
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
compatibility_app = FastAPI()
compatibility_app.include_router(api_router, prefix=settings.API_V1_STR)
@@ -123,7 +123,7 @@ def test_direct_v1_routes_and_openapi_match_compatibility_router():
@pytest.mark.anyio
async def test_direct_routes_honor_application_dependency_overrides():
"""直接聚合后的路由仍应由最终 FastAPI 应用解析依赖覆盖。"""
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = FastAPI()
init_routers(app)
@@ -163,7 +163,7 @@ def test_compatibility_api_router_keeps_public_contract():
def test_init_routers_accepts_composition_root_api_prefix():
"""路由初始化应使用组合根传入的 API 前缀。"""
- from app.startup.routers_initializer import init_routers
+ from app.startup.initializers.routers import init_routers
app = FastAPI()
init_routers(app, "/custom/v1")
diff --git a/tests/test_scheduler_cache_expiry.py b/tests/test_scheduler_cache_expiry.py
index de0a51490..6eb3c1190 100644
--- a/tests/test_scheduler_cache_expiry.py
+++ b/tests/test_scheduler_cache_expiry.py
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock
from app import scheduler as scheduler_module
from app.scheduler import Scheduler
from app.application.configuration import SchedulerRuntimeConfig
-from app.startup import scheduler_initializer
+from app.startup.initializers import scheduler as scheduler_initializer
class _BackgroundSchedulerStub:
diff --git a/tests/test_transactional_download_failure.py b/tests/test_transactional_download_failure.py
index 0b1312aac..89522b4f3 100644
--- a/tests/test_transactional_download_failure.py
+++ b/tests/test_transactional_download_failure.py
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
import pytest
-from app.startup.download_failure import TransactionalDownloadFailureRepository
+from app.db.adapters.download import TransactionalDownloadFailureRepository
def _session_factory(session: MagicMock):
@@ -22,7 +22,7 @@ def test_record_failure_commits_explicit_unit_of_work() -> None:
repository = TransactionalDownloadFailureRepository(_session_factory(session))
with patch(
- "app.startup.download_failure.DownloadFailureOper",
+ "app.db.adapters.download.DownloadFailureOper",
return_value=oper,
):
result = repository.record_failure("fp", "now", "next", title="片名")
@@ -40,7 +40,7 @@ def test_record_failure_rolls_back_explicit_unit_of_work() -> None:
repository = TransactionalDownloadFailureRepository(_session_factory(session))
with patch(
- "app.startup.download_failure.DownloadFailureOper",
+ "app.db.adapters.download.DownloadFailureOper",
return_value=oper,
), pytest.raises(ValueError, match="duplicate"):
repository.record_failure("fp", "now", "next")
diff --git a/tests/test_transfer_worker_lifecycle.py b/tests/test_transfer_worker_lifecycle.py
index fe678fcc1..193d14cc1 100644
--- a/tests/test_transfer_worker_lifecycle.py
+++ b/tests/test_transfer_worker_lifecycle.py
@@ -15,7 +15,7 @@ from app.foundation.singleton import Singleton
from app.runtime.config import global_vars
from app.application.transfer import TransferQueue, TransferTask
from app.schemas.file import FileItem
-from app.startup import transfer_initializer
+from app.startup.initializers import transfer as transfer_initializer
def _build_chain(*, transfer_threads: int = 0) -> TransferChain:
diff --git a/tests/test_type_gate.py b/tests/test_type_gate.py
index 20ae1b9de..b388e8a4a 100644
--- a/tests/test_type_gate.py
+++ b/tests/test_type_gate.py
@@ -28,10 +28,10 @@ def test_mypy_gate_has_explicit_strict_scope_without_global_ignore() -> None:
assert "app/runtime/event/errors.py" in governed_files
assert "app/application/scheduling.py" in governed_files
assert "scripts/architecture/async_blocking.py" in governed_files
- assert "app/startup/context.py" in governed_files
- assert "app/startup/configuration.py" in governed_files
- assert "app/startup/download_failure.py" in governed_files
- assert "app/startup/workflow.py" in governed_files
+ assert "app/startup/composition/context.py" in governed_files
+ assert "app/startup/composition/configuration.py" in governed_files
+ assert "app/db/adapters/download.py" in governed_files
+ assert "app/db/adapters/workflow.py" in governed_files
assert "app/application/workflow.py" in governed_files
assert "app/api/context.py" in governed_files
assert "app/db/base.py" in governed_files