mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: reorganize startup persistence boundaries
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user