diff --git a/app/chain/media.py b/app/chain/media.py index 4842e0c97..454f5b23c 100644 --- a/app/chain/media.py +++ b/app/chain/media.py @@ -1231,9 +1231,9 @@ class MediaChain(ChainBase, metaclass=Singleton): indexed_tracks = list(enumerate(tracks)) by_title: dict[str, list[int]] = {} for index, track in indexed_tracks: - key = cls._music_track_title_key(track.title) - if key: - by_title.setdefault(key, []).append(index) + title_key = cls._music_track_title_key(track.title) + if title_key: + by_title.setdefault(title_key, []).append(index) pending: list[tuple[Path, MetaMusic]] = [] for file, meta in zip(files, metas): @@ -1266,9 +1266,9 @@ class MediaChain(ChainBase, metaclass=Singleton): ).append(index) unresolved: list[tuple[Path, MetaMusic]] = [] for file, meta in pending: - key = (meta.disc_number or 1, meta.track_number or 0) + position_key = (meta.disc_number or 1, meta.track_number or 0) candidates = [ - index for index in by_position.get(key, []) + index for index in by_position.get(position_key, []) if index not in used ] if meta.track_number else [] if candidates: diff --git a/docs/architecture-optimization-checklist.md b/docs/architecture-optimization-checklist.md new file mode 100644 index 000000000..f30774e3a --- /dev/null +++ b/docs/architecture-optimization-checklist.md @@ -0,0 +1,659 @@ +# MoviePilot V3 架构优化清单 + +> 审计日期:2026-08-26 +> +> 审计基线:`v3@9053db926d20`,与 `origin/v3` 一致 +> +> 文档性质:当前源码的差距清单与分阶段执行说明,不是历史重构结项账本 + +## 1. 结论摘要 + +MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`、`runtime`、 +`adapters`、`application`、`chain`、`db`、`startup`、`sdk` 和 `compat` 的一级所有权 +基本成立。旧物理兼容目录、Model/Oper 自建 Session/提交和直接跨层 DB 依赖等历史问题已有 +硬门禁;门禁还能证明可识别的 TaskRegistry 调用已经声明稳定 owner。 + +当前主要问题不再是“目录没有分层”,而是以下四类更深层的债务: + +1. **边界有形、合同偏弱**:部分 Application Port 仍是 `Callable[[], Any]`,返回 ORM 或 + 动态代理;生产路径因此继续依赖无 Session Oper、全局 provider 和隐式构造。 +2. **用例编排过度集中**:Chain、Scheduler、Plugin、Agent、LLM 和部分 API 文件同时承担决策、 + I/O、状态、生命周期与兼容职责,私有长方法又处于复杂度门禁盲区。 +3. **可靠性声明强于实现**:整理 pending 目前只能做最小重放,尚不满足 ADR 中声明的 E3 + 状态机语义;部分 commit 后副作用仍存在“业务已提交但调用方收到失败”或进程退出后丢失的窗口。 +4. **治理事实源不一致**:架构规则、总览、AST 基线和 CI 语义存在漂移;快照门禁能证明“没有变化”, + 但不能自动证明依赖合理、没有新环或所有运行资源都有生命周期 owner。 + +当前唯一 P0 是主线 `mypy` ratchet 已失败。后续架构优化应先恢复主线,再按可靠性、边界类型化、 +生命周期、复杂度的顺序推进;不建议再做一次大规模目录搬迁。 + +## 2. 范围与证据边界 + +### 2.1 本次范围 + +- 宿主源码 `app/**`,架构统计排除运行时插件副本 `app/plugins/**`。 +- 架构、质量、测试与 CI 门禁,以及当前规则文档之间的一致性。 +- 插件只审查 SDK、Compat、稳定 ABI 和宿主边界,不把官方插件仓代码混入宿主统计。 +- 数据持久化、后台动作完成语义、进程生命周期和外部传输边界。 + +### 2.2 审计方法 + +- 读取当前规则、总览、ADR、关键调用链和组合根。 +- 检查依赖图、SCC、Module/Event Contract、事务/configuration 基线。 +- 对文件、类、方法长度及类型/覆盖率治理范围做静态统计。 +- 运行架构、复杂度、异步阻塞、TaskRegistry owner、service locator、Ruff 和 mypy ratchet。 +- 运行 9 个架构/质量专项测试文件,共 `158 passed`。 + +本次没有运行完整测试套件,也没有用生产流量做动态剖析。因此,清单中的性能收益和低频并发故障 +仍需在对应实施阶段通过专项故障注入、全量测试和运行指标确认。 + +## 3. 当前架构画像 + +### 3.1 分层现状 + +| 层/区域 | 已形成的正确边界 | 当前主要缺口 | +|---|---|---| +| `foundation` | 无状态、无配置、无 I/O 的底层机制 | 当前未发现需要重做的结构性问题 | +| `domain` | 已与 DB、Application、Adapter 保持单向隔离 | `domain/context.py` 等核心对象过大,来源投影与领域模型仍集中 | +| `runtime` | 已有任务、事件、缓存、托管资源和停止合同 | 导入/构造时启动线程;`global_vars` 仍混合停止、主循环和 WebPush 状态 | +| `adapters` | 技术与命名外部生态已有明确目录 | Application/Chain 仍直接识别部分具体 Adapter;例外规则没有形式化 | +| `application` | 已有命令、Port、Outbox 和安全能力 | 部分 Port 是 `Any` 服务定位器,ORM/具体 Adapter 仍会穿透边界 | +| `chain` | 多入口复用的用例编排与 Module 动态分发已稳定 | God object、私有长方法、包根 SCC、无参构造和资源物化过重 | +| `db/oper` | Model/Oper 已不自建 Session、不自行提交 | 无 Session Facade 仍被宿主组合根注入,业务操作可能拆成多个事务 | +| `db/adapters` | 已有显式 Session/UoW 的参考切片 | Port 返回类型不够稳定;Outbox 的 stage 与自提交 dispatcher store 混在一个类型 | +| `startup` | 已是 HostRuntime 和生命周期组合根 | `initializers/modules.py` 高扇出,仍有导入期 provider 注册和具体对象目录装配 | +| API/Command/Scheduler | 多数入口已转向 Chain/Application | Agent/System/Plugin 等入口仍承担较多业务与 I/O 编排 | +| `sdk`/`compat` | 精确映射、稳定插件 ABI 和宿主 canonical 路径已落地 | SDK 仍暴露部分可变全局对象/具体 Manager,只能渐进收窄,不能直接删除 | + +### 3.2 量化快照 + +| 指标 | 当前值 | 解释 | +|---|---:|---| +| 宿主 Python 模块 / 内部依赖边 | 835 / 6,810 | `dependency-baseline.json` 当前快照 | +| 非平凡 SCC | 2 | 新增 Chain 包根环;另一个是隔离的 29 模块 TMDB 移植包环 | +| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 | +| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 | +| Module Contract | 215 specs / 214 methods / 264 calls | 动态方法名为 0;仍有 50 个结果形状为 `ANY` | +| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement | +| Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 | +| 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 | +| 全量 mypy 历史债务 | 11,983 / 601 文件 | strict frontier 当前只覆盖 41 个文件,且 ratchet 已新增 2 个错误 | +| Ruff 历史诊断 | 973 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | +| 覆盖率低水位 | Application 77.76%,Domain 79.24% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 | + +### 3.3 热点文件 + +| 文件 | 行数 | 主要职责混合 | +|---|---:|---| +| `app/chain/subscribe.py` | 3,895 | 搜索、匹配、状态、刷新、规则引用、通知和交互 | +| `app/agent/orchestrator.py` | 3,678 | 会话、运行、流式输出、工具、任务与 Manager | +| `app/agent/llm/provider.py` | 3,528 | provider 目录、认证、模型发现、配置和运行时构建 | +| `app/adapters/external/market.py` | 3,488 | 插件市场、包下载、依赖、备份恢复、健康和兼容入口 | +| `app/chain/transfer.py` | 3,250 | 队列、恢复、规划、执行、历史、通知和清理 | +| `app/chain/search.py` | 2,970 | 搜索计划、并发 fan-out、状态、分页和结果处理 | +| `app/api/endpoints/agent.py` | 2,346 | HTTP/SSE、文件/音频、Agent 会话和事件编排 | +| `app/chain/download.py` | 2,230 | 选择、提交、历史、通知、模块后处理和批量执行 | +| `app/chain/media.py` | 2,191 | 识别、来源投影、缓存、音乐匹配和兼容入口 | +| `app/scheduler.py` | 2,111 | 作业目录、执行状态、恢复、生命周期和领域任务 | + +热点不是按行数机械拆文件的依据。只有在提取出稳定合同、保留旧入口委托并有行为测试时,拆分才算 +降低复杂度;把长方法原样移动到新目录不算完成。 + +## 4. 优化清单总表 + +状态含义:`阻塞` 表示当前门禁已失败;`待执行` 表示尚未开始;`渐进` 表示应按触碰路径逐步收敛。 + +| ID | 优先级 | 状态 | 事项 | 目标结果 | +|---|---|---|---|---| +| ARCH-001 | P0 | 阻塞 | 恢复 mypy ratchet | 主线所有既有 CI gate 重新通过 | +| ARCH-101 | P1 | 待执行 | 统一规则、总览、基线和语义门禁 | 文档声明与机器拒绝条件一一对应 | +| ARCH-102 | P1 | 待执行 | 将 Transfer pending 升级为真实 E3 状态机 | 崩溃窗口可判定恢复,结果未知时进入人工确认 | +| ARCH-103 | P1 | 待执行 | 类型化 Chain/Agent 数据 Port 与 DTO | 宿主主路径不再注入无 Session Oper,不向入口泄漏 ORM | +| ARCH-104 | P1 | 待执行 | 收口跨多次写入的业务事务 | 站点/规则引用清理可整体回滚或幂等恢复 | +| ARCH-105 | P1 | 待执行 | 明确 post-commit 与 Outbox 完成语义 | “业务已提交、后置效果 pending”可被调用方正确识别 | +| ARCH-106 | P1 | 待执行 | 让线程/队列/日志 writer 由 bootstrap/lifecycle 显式构造 | 导入或普通 Chain 构造不再启动进程资源 | +| ARCH-107 | P1 | 待执行 | 消除 Chain SCC,强化循环门禁 | SCC 只剩精确豁免的 TMDB 移植包环 | +| ARCH-108 | P1 | 待执行 | 决策并收口 Application/Chain 到 Adapter 与 HTTP 边界 | 依赖倒置有明确例外、低水位和迁移顺序 | +| ARCH-109 | P1 | 待执行 | 按用例拆分超大 Chain、Scheduler 和厚 API | 稳定 Facade 保留,决策/I/O/状态/生命周期各有 owner | +| ARCH-110 | P1 | 待执行 | Module/Event Contract 分可信级执行 | 宿主 provider 严格,第三方插件仍兼容诊断 | +| ARCH-111 | P1 | 待执行 | 升级复杂度、类型、覆盖率和并发原语门禁 | 高风险私有路径也进入只降不增的治理面 | +| ARCH-201 | P2 | 渐进 | 收窄 PluginHelper/PluginManager 与 SDK 暴露面 | ABI Facade 只委托,构造和具体服务归组合根 | +| ARCH-202 | P2 | 渐进 | 拆分 Agent/LLM provider 职责 | provider catalog、发现、认证、会话和运行时分离 | +| ARCH-203 | P2 | 渐进 | 拆分 Domain 投影与 Startup 高扇出目录 | 保留 canonical 类型和生命周期顺序,降低修改扩散 | +| ARCH-204 | P2 | 渐进 | 合并重复 sync/async 核心逻辑并转换存量测试风格 | 保留双 ABI 外壳,共享纯业务核心 | + +## 5. P0:先恢复主线 + +### ARCH-001 恢复 mypy ratchet + +**问题与证据** + +- `app/chain/media.py:1233` 把局部变量 `key` 推断为 `str`,`1269` 又赋值为 + `tuple[int, int]`,随后 `1271` 触发 `dict.get` overload 错误。 +- 当前 ratchet 报告:`[assignment] 15 -> 16`,并新增 `[call-overload] x1`。 +- `.github/workflows/test.yml:71-72` 在主 CI 中执行该门禁。 + +**执行要求** + +- [ ] 将标题键和位置键使用不同、带语义的变量名,修复真实类型错误。 +- [ ] 不得用 `mypy_ratchet.py --write` 接受增长。 +- [ ] 保留音乐专辑“精确标题优先、碟号/曲序回退”的现有行为。 + +**验收** + +```bash +.venv/bin/python scripts/architecture/mypy_ratchet.py +.venv/bin/python -m pytest tests/test_music_album_match.py -q +``` + +## 6. P1:结构性问题 + +### ARCH-101 统一架构事实源 + +**问题与证据** + +- `docs/rules/04-design-patterns.md:125-131` 仍示范 `SubscribeOper()` 无 Session 调用, + 与 `docs/rules/10-data-and-persistent.md:111-132` 的生产路径显式 UoW 规则冲突。 +- `docs/rules/05-architecture.md:507` 禁止 `application -> concrete adapter`,但同一文档 + `177-183` 又要求 RSS 消费 network adapter;当前测试没有形式化这类例外。 +- 规则 `docs/rules/05-architecture.md:516` 禁止任意模块级环,实际 SCC 测试只覆盖特定根。 +- 架构总览此前仍记录 811 模块、6,572 条边和 1 个 SCC,已经落后于当前基线。 +- Event consumer 扫描把任意名为 `.register()` 的调用都当成事件注册,存在明确误报。 + +**目标与步骤** + +- [ ] 指定一个机器可读事实源,文档指标由 fixture 生成或只保留不易漂移的语义描述。 +- [ ] 修正 Oper 示例,分别展示宿主显式 UoW 与插件兼容 Facade。 +- [ ] 对 Adapter 规则作出明确决策:允许哪些通用技术机制,哪些命名外部/持久化能力必须注入。 +- [ ] 让 SCC 规则、精确 allowlist 和文档声明一致。 +- [ ] Event 扫描只识别 EventManager 实例/别名和事件装饰器。 +- [ ] CI 分开报告“快照一致”与“语义规则通过”,禁止把前者表述为架构完全正确。 + +**验收** + +```bash +.venv/bin/python -m pytest \ + tests/test_architecture_dependencies.py \ + tests/test_architecture_contract_baseline.py \ + tests/test_architecture_baseline_cli.py -q +.venv/bin/python scripts/architecture/baseline.py --check-host --diagnostics +``` + +### ARCH-102 将 Transfer pending 升级为真实 E3 状态机 + +**问题与证据** + +- `app/application/transfer.py:139-145` 当前先把任务接受到内存结构,再执行持久登记回调。 +- `app/chain/transfer.py:1012-1024` 吞掉登记失败并继续执行; + `tests/test_transfer_pending_replay.py:62-71` 固化了这一 fail-open 行为。 +- worker 未知异常最终也会在 `app/chain/transfer.py:1107-1113,1262-1272` 删除 pending。 +- `app/db/models/transferpending.py:9-34` 只有 `storage/src_path/created_at`,没有目标、模式、 + step、lease、attempt、last_error,无法判定“文件已移动、历史未提交”等中间态。 +- 这与 `docs/adr/0007-background-action-reliability.md:123-139` 对 E3 的稳定身份、步骤状态、 + lease/heartbeat 和人工恢复要求不一致。 + +**目标与步骤** + +- [ ] 先在独立持久事务中 commit pending,再尝试放入内存队列;数据库事务不能与 `queue.Queue` + 原子提交,入队失败时必须保留 pending 供重放。 +- [ ] 初始登记保存稳定源身份、模式、状态和 attempt/lease;目标在规划完成后以 planning checkpoint + 更新,不能要求任务刚入队时已经具备尚未计算的目标路径。 +- [ ] 设计幂等文件操作和历史提交;只有所有必要步骤达到持久终态后才能删除记录。 +- [ ] 在持久状态机与现有失败历史/AI retry 之间指定唯一 retry owner,定义旧记录迁移和兼容规则。 +- [ ] E3 失败使用持久 `failed/manual_review`、最后稳定 checkpoint 和补偿边界,不直接套用 E2 + Outbox 的 dead-letter 语义;禁止按年龄通用清理 pending。 +- [ ] 数据模型变更必须配套 Alembic migration,并验证升级与降级路径。 + +**故障注入验收** + +- [ ] 登记后、内存入队前崩溃,重启可继续。 +- [ ] 持久登记成功但内存入队失败,重启可继续。 +- [ ] 文件移动后、历史提交前崩溃,在支持稳定身份/幂等操作的存储上不重复移动且可补齐历史。 +- [ ] worker 未知异常和 lease 超时后保留可诊断状态。 +- [ ] 重复回放、重复消息和人工重试都保持幂等。 +- [ ] 外部存储返回结果未知时进入 `manual_review`,不得伪装成 exactly-once 成功。 + +```bash +.venv/bin/python -m pytest \ + tests/test_transfer_pending_replay.py \ + tests/test_transfer_worker_lifecycle.py \ + tests/test_chain_durable_events.py -q +``` + +### ARCH-103 类型化 Chain/Agent Port 与 DTO + +**问题与证据** + +- `app/application/chain/data.py:14-29,134-176` 的 Oper factory 和 getter 基本都是 `Any`; + `app/application/agentdata.py:91-120` 还通过 `__dict__.update()` 动态组装端口。 +- `app/startup/initializers/modules.py:848-863,896-910` 仍向生产 Chain/Agent 注入多个无 Session Oper。 +- 无 Session Oper 会为单次调用独立创建事务;一个业务操作的“查询后更新”可能被拆成多个事务。 +- Workflow query 和 Chain/Agent raw data port 仍返回 `Any`/ORM,Subscription mutation 内部也消费 ORM, + 因而存在 Session 生命周期外 detached/lazy-load 的潜在风险。公开 Subscription、Site、History + QueryService 已经投影 DTO,属于完成项,不应重做。 + +**目标与步骤** + +- [ ] 按领域定义 Query/Command Protocol,不再使用通用 `OperFactory = Callable[[], Any]`。 +- [ ] 写 Port 由 `db/adapters` 创建单操作 Session/UoW;Oper 的 canonical 写方法只 stage/flush, + 读取方法仍可在调用方 Session 中查询。 +- [ ] 查询 Port 在 adapter Session 内映射为冻结 DTO/Projection,Application 和 API 不接收 ORM。 +- [ ] `ChainDataPorts`/`AgentDataPorts` 可暂时保留为兼容聚合器,但字段必须显式、可类型检查。 +- [ ] 以一个业务纵切面迁移并验证后,再迁移下一组,禁止一次替换所有 Oper。 +- [ ] 增加 AST 门禁,禁止向 `ChainDataPorts`、`AgentDataPorts` 和新的 canonical use-case service + 注入裸 Oper;SystemConfig singleton、legacy transaction runner 等兼容边界使用精确 allowlist。 + +**首批建议** + +1. Workflow query DTO。 +2. Chain/Agent 的 Subscribe/History/User/Site raw port。 +3. Subscription mutation 与站点、规则组引用更新。 +4. Agent 数据能力。 + +**验收** + +- 关闭 adapter Session 后序列化 DTO,确认不会触发 lazy load。 +- 对迁移的 Application Port 运行 strict mypy,并验证 API response contract 与插件 ABI。 + +```bash +.venv/bin/python -m pytest \ + tests/test_db_workflow_queries.py \ + tests/test_agent_data_ports.py \ + tests/test_subscription_query_service.py \ + tests/test_site_query_service.py \ + tests/test_history_query.py -q +.venv/bin/mypy --config-file mypy.ini +``` + +### ARCH-104 收口跨多次写入的业务事务 + +**问题与证据** + +- `app/chain/subscribe.py:3010-3047` 删除站点引用时先写 SystemConfig,再逐条更新订阅。 +- `app/chain/subscribe.py:3073-3110` 清理规则组时依次更新多项配置和多条订阅。 +- 当前无 Session Facade 让这些步骤各自提交;中途失败可能长期残留部分状态,直至同一事件再次触发 + 或人工修复。处理本身具备一定幂等性,但触发事件不是 durable owner。 + +**目标与步骤** + +- [ ] 将引用分析与修改计划提取为纯函数/值对象。 +- [ ] SystemConfig 与 Subscribe 位于同一宿主数据库,首选一个 Application Command/UoW 和批量原子更新; + 复用 `SystemConfigOper.update_atomically()` 的锁行能力,而不是预设必须跨存储补偿。 +- [ ] 同一 UoW 内修改配置表后,在 commit 成功时一次性发布全部进程内配置快照,并明确 + `_write_lock`/`_snapshot_lock` 的顺序;读者只能观察完整旧快照或完整新快照。 +- [ ] 仅当未来确有无法共享事务的外部状态时,才使用持久、幂等、带 checkpoint 的 reconciliation job。 +- [ ] 每一步允许安全重试,返回明确的完成/待恢复状态。 +- [ ] 在第 `k` 次写入注入异常,验证整体回滚或下次能恢复到完整状态。 +- [ ] 增加并发触发和并发读取测试,验证没有丢更新、死锁或配置中间组合。 + +```bash +.venv/bin/python -m pytest \ + tests/test_site_mutation_command.py \ + tests/test_subscription_mutation_outbox.py \ + tests/test_rule_group_media.py -q +``` + +### ARCH-105 明确 post-commit 与 Outbox 完成语义 + +**问题与证据** + +- `app/application/outbox.py:167-192` 在业务提交后直接执行 `after_commit()`、即时 publish 和完成标记; + 某一步抛错时,调用方可能收到失败,但业务行和 intent 已经提交。 +- 通用 `DurableEventCommand` commit 后没有先调用已经定义的 `claim_by_event_key()`;dispatcher 可在 + commit 与请求线程即时 publish 之间先 claim 并投递,随后请求线程再次 publish,形成双发窗口。 + `app/application/subscription/complete.py:111-145` 已提供先 claim 的正确参考。 +- 下载历史及事件 intent 已原子提交;通知在 commit 后同步执行,模块后处理和字幕再投进线程池。 + 进程在提交与这些动作完成之间退出时,未持久化的动作不会自动恢复。 +- `SqlAlchemyOutboxRepository` 同时提供不提交的 `stage()` 和内部自提交的 dispatcher 方法, + 事务所有权没有由类型清楚表达。 + +**目标与步骤** + +- [ ] Command 返回结构化结果:业务是否提交、哪些后置效果完成、哪些处于 pending。 +- [ ] commit 后先取得 delivery lease;未取得时跳过请求线程直投,确保与 dispatcher 排他。 +- [ ] 每个 post-commit effect 使用独立 intent 隔离和结算,避免效果 A 失败导致效果 B 被一起重放。 + 单个外部效果仍是 at-least-once,必须有稳定幂等键和幂等 handler/消费者。 +- [ ] 按完成承诺、可重建性、外部不可逆性和业务重要性划分 E0-E3;用户可见性只是因素之一。 +- [ ] 拆分 `OutboxStager` 与 `OutboxDispatchStore`,避免业务 Session 调用自提交方法。 +- [ ] 在 commit、claim、publish、complete、通知和任务提交各断点注入异常/崩溃,验证声明等级与实际恢复一致。 +- [ ] 增加请求线程即时投递与 dispatcher 并发竞争测试,以及“外部调用成功、complete 前崩溃”的重放测试。 + +```bash +.venv/bin/python -m pytest \ + tests/test_outbox.py \ + tests/test_chain_durable_events.py \ + tests/test_subscription_completion_command.py \ + tests/test_subscription_mutation_outbox.py -q +``` + +### ARCH-106 让进程资源由 bootstrap/lifecycle 显式构造 + +**问题与证据** + +- 导入 `app.runtime.config` 会构造日志 handler,并启动 `LogBatchWriter` 线程;普通 import 已有副作用。 +- `ChainBase.__init__` 无条件构造消息队列 manager,任意一个 Chain 都可能启动消息线程。 +- `global_vars` 同时承担停止兼容、WebPush 订阅和主事件循环 owner,多层代码直接消费。 +- 当前 TaskRegistry 门禁只检查 Registry 调用是否带 owner,不会扫描全部原生 `create_task`、 + `Thread`、`Timer`、`Executor`。 +- 当前 service locator 门禁只覆盖 scheduler/module/plugin/command/workflow 五类 concrete runtime, + 没有覆盖 `global_vars`,门禁通过不能证明全局状态已经收口。 +- Workflow、Scheduler、Command、Agent initializer 仍有导入期 provider 注册;需要明确这是兼容设计, + 还是迁入显式装配阶段并提供 test reset,不能长期处于未声明状态。 + +**目标与步骤** + +- [ ] 日志 writer、消息队列及其他长生命周期资源由 bootstrap/lifecycle 显式构造、发布和关闭。 + 现有关闭路径已能收口部分资源,当前要修的是隐式创建权,而不是重新发明 owner。 +- [ ] startup 唯一创建/启动资源;Chain 只接收无生命周期的发送 Port。 +- [ ] 普通模块 import 和构造 `MediaChain` 等非消息用例不得新增线程。 +- [ ] 将 WebPush registry、主循环 execution gateway 和停止状态拆给各自 owner。 +- [ ] 保留 `global_vars` 作为兼容薄门面,但禁止 canonical 宿主新增依赖。 +- [ ] 将 initializer 的 provider 注册迁入显式 `configure_runtime_ports` 装配阶段并提供测试 reset; + 若某项必须保留导入期兼容行为,需精确记录调用者、原因和退场条件。 +- [ ] 建立原生并发原语清单:结构化局部等待、TaskRegistry、显式生命周期 owner 或受限上下文; + 未分类的新原语由 ratchet 拒绝。 + +**验收** + +- 隔离进程冷导入前后比较线程名。 +- 正常模式和 safe mode 均验证启动顺序、有限关闭、超时保留 owner 和重试关闭。 +- 运行消息、Scheduler、Workflow、Agent、Plugin 和 lifecycle 专项测试。 +- 在 startup 到达 `yield` 前失败时,已经创建的日志/队列资源也必须收口。 + +```bash +.venv/bin/python -m pytest \ + tests/test_lifecycle_shutdown.py \ + tests/test_module_lifecycle.py \ + tests/test_system_notification_dispatch.py \ + tests/test_scheduler_lifecycle.py \ + tests/test_workflow_execution.py \ + tests/test_agent_lifecycle.py \ + tests/test_plugin_lifecycle_coordinator.py -q +``` + +### ARCH-107 消除 Chain SCC 并强化循环门禁 + +**问题与证据** + +- 当前新增 SCC 为 `app.chain -> app.chain._messaging/_recognition -> app.chain`。 +- `app/chain/__init__.py` 同时是 1,000 行以上的 `ChainBase` 实现和插件/宿主导入入口, + 又在包初始化时导入 mixin。 +- `baseline.py --check-host` 会确认该 SCC 与 fixture 一致,但现有语义测试没有拒绝 Chain 包内环。 + +**目标与步骤** + +- [ ] 将 canonical `ChainBase` 实现移到 `app/chain/base.py`。 +- [ ] 宿主全部改用 `app.chain.base.ChainBase`;包根不得 eager re-export,否则父包与子模块仍形成 SCC。 +- [ ] 若 `from app.chain import ChainBase` 必须继续兼容,使用仅限 ABI 的 lazy facade,并以独立测试 + 证明它没有被宿主 canonical 路径使用;同时把这一精确例外写入“不得延迟导入隐藏循环”的规则。 +- [ ] 保证 `_messaging`、`_recognition`、`base` 任意冷导入顺序都不依赖部分初始化包。 +- [ ] 对排除 `app/plugins/**` 的完整宿主图断言 SCC 集合等于精确 allowlist,而不是继续追加根前缀。 +- [ ] TMDB 29 模块移植包允许正常单向包外依赖,但禁止 SCC 成员扩展到 allowlist 外、包外模块 + 通过反向边加入该 SCC,或新增不符合分层方向的边。 +- [ ] 更新兼容、SDK、架构文档和 fixture 前先审查真实依赖变化。 + +**验收** + +- 冷导入 `app.chain.base`、`app.chain._messaging`、`app.chain._recognition` 的全部顺序组合。 +- 验证宿主当前包根导入全部迁移,并保留 `app/plugins/__init__.py` 等插件兼容探针。 + +```bash +.venv/bin/python -m pytest \ + tests/test_architecture_dependencies.py \ + tests/test_architecture_contract_baseline.py \ + tests/test_legacy_import_compat.py \ + tests/test_plugin_sdk.py -q +.venv/bin/python scripts/architecture/baseline.py --check-host +``` + +### ARCH-108 收口具体 Adapter 与 HTTP 边界 + +**问题与证据** + +- 当前 `app/application` 有 10 个文件、15 条直接 Adapter 导入,代表路径包括 + `security/passkey.py`、`backup.py`、`image.py`、`rss.py`、`security/cookie.py`。 +- `app/chain` 有 8 个文件、13 条直接 Adapter 导入,使用 `RequestUtils`、Browser、Cloudflare、 + CookieCloud、ServerHelper 等具体能力。 +- Passkey Application 服务直接判断 Redis 后端并调用 `RedisHelper.pop()`,安全策略识别了具体实现。 +- “所有 HTTP 必须经 RequestUtils”与 LLM streaming、第三方 SDK、移植库中的直接 HTTP 客户端现状 + 没有一份精确例外表,也没有 zero-growth gate。 + +**目标与步骤** + +- [ ] 先建立 `application_to_adapters`、`chain_to_adapters` 和 direct-egress 可审查清单,冻结增长。 +- [ ] 将 Passkey 原子领取提升为 runtime cache contract,由 Memory/Redis backend 分别实现。 +- [ ] 为 Backup 定义 Application-owned artifact store Port,由 startup 注入文件系统实现。 +- [ ] 普通外部请求迁移到统一网络能力;SDK transport、streaming 和 vendor code 例外必须精确到路径与原因。 +- [ ] 命名外部产品和安全敏感能力优先改为注入 Port;通用技术 Adapter 是否可直接使用由 ARCH-101 决策。 +- [ ] 最终把基线收缩到零或少量书面化例外,而不是一次性禁止后再大量豁免。 + +```bash +.venv/bin/python -m pytest \ + tests/test_passkey_challenge.py \ + tests/test_mfa_passkey_transactions.py \ + tests/test_database_backup_service.py \ + tests/test_database_backup_adapters.py \ + tests/test_rss_helper.py -q +.venv/bin/python scripts/architecture/baseline.py --check-host +``` + +### ARCH-109 按用例拆分超大编排器 + +**共同规则** + +- 保留旧 Chain/Manager/Scheduler 的公开方法和插件 ABI,内部委托新服务。 +- 先提取合同和行为测试,再迁移一条主调用链;不按文件行数机械切块。 +- 纯决策进入 Domain/Application policy,I/O 进入 Adapter/Port,运行状态进入明确 owner。 +- 每个切片应能独立验证、提交和回滚。 + +**建议切片** + +| 热点 | 建议所有权拆分 | 首要长方法/耦合点 | +|---|---|---| +| `SubscribeChain` | search、match、refresh、reference reconciliation、notification | 匹配和站点/规则引用更新 | +| `TransferChain` | queue/recovery、plan、execute、settle、history/notify | `_execute_transfer` 约 836 行 | +| `SearchChain` | plan、provider fan-out、result state、pagination | 并发状态与结果处理 | +| `DownloadChain` | selection、submission、history、post-processing | batch download 约 449 行 | +| `MediaChain` | recognition、source projection、music alignment、cache | 保留公共识别 Facade | +| `Scheduler` | JobCatalog、ExecutionRegistry、domain reconciler、lifecycle Facade | `init` 约 336 行且直接构造多个 Chain | +| Agent API | WebAgent session/SSE/file/audio Application service | `_web_agent_stream_impl` 约 361 行 | +| System/Plugin API | nettest、logging、update、market use cases | 入口直接组合多个 Helper/Manager | + +### ARCH-110 分可信级执行 Module/Event Contract + +**问题与证据** + +- Module 动态方法名已经归零,这是完成项;但 50 个结果形状仍是 `ANY`。 +- `app/runtime/extensions/module/dispatcher.py:463-494` 对签名和结果偏差只告警并继续透传。 +- Event 53 个 payload model 已覆盖,但当前全部为 diagnostic enforcement,输出合同也很少。 + +**目标与步骤** + +- [ ] 按“宿主内置 provider / 官方插件 / 第三方未知插件”划分可信级。 +- [ ] 下载、存储、消息等高风险宿主能力使用真实 Protocol/DTO,并在 admission 阶段 fail fast。 +- [ ] 官方插件先以 CI baseline 验证,再逐组切 strict。 +- [ ] 第三方插件继续诊断兼容,错误指标必须包含 method、provider、ABI source。 +- [ ] 不得为了严格化改变 `run_module` 字符串 ABI 或吞掉旧插件原始返回值。 + +```bash +.venv/bin/python -m pytest \ + tests/test_module_method_contracts.py \ + tests/test_module_invocation_dispatcher.py \ + tests/test_module_quality.py \ + tests/test_event_contracts.py -q +.venv/bin/python scripts/architecture/baseline.py --check-host --diagnostics +``` + +### ARCH-111 升级质量与架构趋势门禁 + +**当前盲区** + +- 复杂度脚本只检查 API、Application、Chain 的公共入口;私有长方法、类/文件规模和圈复杂度不受控。 +- strict mypy 仅 41 个文件,高风险 lifecycle、Scheduler、Agent、Plugin Manager 多数不在 frontier。 +- coverage ratchet 只聚合 Application 和 Domain。 +- Task owner gate 不盘点原生并发原语;Event consumer 扫描存在误报。 +- Ruff 仅是有限规则集的历史低水位,不代表整体风格/正确性无债务。 + +**目标与步骤** + +- [ ] complexity v2 增加私有方法、class/file 行数和圈复杂度;先记录低水位,再只降不增。 +- [ ] strict mypy frontier 按 `runtime extensions -> startup -> workflow/scheduler -> messaging -> Agent` + 扩大;每批必须先清零再加入配置。 +- [ ] coverage 增加 Chain、Runtime、Agent、Startup 的高风险子包或关键文件组,不用低价值行数冲百分比。 +- [ ] 原生并发门禁和 Event 扫描按 ARCH-101/106 修正。 +- [ ] Module Quality Scale 增加 capability -> required rules -> evidence tests 映射,避免“已登记”等同“已验证”。 +- [ ] 修改 CI 或门禁脚本时同时运行 `tests/test_architecture_ci.py` 和对应脚本单元测试。 + +## 7. P2:持续可维护性优化 + +### ARCH-201 收窄 Plugin 与 SDK 边界 + +- [ ] `PluginHelper` 继续作为兼容入口,但市场、包、依赖、备份/恢复、健康修复分别委托现有 owner。 +- [ ] `PluginManager` 的服务图构造移到 startup typed `PluginRuntime` factory,Facade 只保留稳定 API。 +- [ ] 新 SDK 提供只读配置、插件查询和主循环提交等窄合同;旧 `settings/global_vars` 和具体 Manager + 只能标记弃用,不能在当前大版本直接删除。 +- [ ] 用官方插件仓 baseline 和真实插件导入探针决定退场,不按宿主“无人引用”判断。 + +### ARCH-202 拆分 Agent 与 LLM provider + +- [ ] 将内置 provider spec 数据从 `LLMProviderManager` 分离为只读 catalog。 +- [ ] 模型发现、认证会话、运行时构建和用户配置分成可替换服务。 +- [ ] 将 WebAgent SSE、文件/音频与会话桥接移出 API endpoint。 +- [ ] 保留 `AgentManager`/`LLMProviderManager` 公开方法为稳定 Facade,并逐批纳入 strict mypy。 + +### ARCH-203 拆分 Domain 投影与 Startup 高扇出 + +- [ ] `MediaInfo`/`domain/context.py` 保留 canonical 类型路径,把各外部来源 setter 的纯投影规则提取到 + Domain 内部模块,旧方法委托,避免 DTO 再复制一套领域语义。 +- [ ] 将 `startup/initializers/modules.py` 的对象构造按领域移到 `startup/composition/*`;initializer + 只负责顺序、注册和是否重启的决策。 +- [ ] 保留生命周期 manifest 和顺序快照;高扇出在组合根是允许的,但业务实现不能继续沉积其中。 + +### ARCH-204 收敛 sync/async 重复与测试存量 + +- [ ] 对重复 sync/async 方法先识别共享纯逻辑;保留双入口 ABI,只共享解析、校验、映射和状态决策。 +- [ ] 不用在线程包装里假装异步,也不因去重而跨事件循环复用 Session/客户端。 +- [ ] 修改到 `unittest.TestCase` 文件时按项目规则渐进转 pytest-native;不发起无行为收益的全库转换。 +- [ ] 每次转换都恢复 monkeypatch 的全局状态、singleton、cache 和 `sys.modules`。 + +## 8. 建议实施顺序 + +### 阶段 0:恢复可信基线 + +1. 单独修复 ARCH-001,不刷新 mypy baseline。 +2. 完成 ARCH-101 的文档/门禁语义决策。 +3. 给 Adapter、原生并发、私有复杂度建立只读清单和 zero-growth 基线。 + +**退出条件**:当前全部 CI gate 通过;文档、fixture 和测试对 SCC、Adapter 例外、Oper 使用方式的描述一致。 + +### 阶段 1:先修可靠性和事务语义 + +1. ARCH-102 设计并迁移 Transfer E3 状态机。 +2. 以站点/规则引用清理为 ARCH-103/104 的第一个 typed Port + UoW 纵切面。 +3. 完成 ARCH-105 的 post-commit 结构化结果与 Outbox 角色拆分。 + +**退出条件**:故障注入覆盖清单列出的崩溃窗口;schema 有 migration;主路径不再 fail-open;调用方能区分 +业务提交与后置效果 pending。 + +### 阶段 2:收口进程边界 + +1. ARCH-106 将日志、消息队列和主循环 gateway 纳入 lifecycle。 +2. ARCH-107 拆出 `chain/base.py` 并消除新增 SCC。 +3. ARCH-108 先迁移 Passkey、Backup,再按风险迁移外部调用。 + +**退出条件**:冷导入不启动线程;SCC 只剩 TMDB 精确豁免;Application/Chain 到 Adapter 的债务只降不增。 + +### 阶段 3:按热点做纵向拆分 + +建议顺序:Transfer -> Subscribe -> Scheduler -> Download/Search -> Agent API。每次只迁移一个完整用例, +旧 Facade 委托并同时运行新旧入口契约测试。 + +**退出条件**:复杂度低水位下降,公开 ABI 未变,文件移动没有新增跨层边或兼容映射滥用。 + +### 阶段 4:扩大强类型与严格合同 + +1. ARCH-110 对宿主高风险 Module/Event 开 strict。 +2. ARCH-111 扩大 strict mypy 和 coverage frontier。 +3. 执行 ARCH-201 至 ARCH-204 的渐进收敛。 + +**退出条件**:新增代码全部位于强治理面;历史低水位持续下降;官方插件兼容验证无回退。 + +## 9. 每个实施切片的统一门禁 + +先运行受影响测试,再按风险扩大。架构、持久化、启动、生命周期、兼容层或跨模块改动必须运行全量套件。 + +```bash +# 当前宿主架构与趋势门禁 +.venv/bin/python scripts/architecture/baseline.py --check-host +.venv/bin/mypy --config-file mypy.ini +.venv/bin/python scripts/architecture/complexity.py +.venv/bin/python scripts/architecture/async_blocking.py +.venv/bin/python scripts/architecture/task_ownership.py +.venv/bin/python scripts/architecture/service_locator.py +.venv/bin/python scripts/architecture/ruff_ratchet.py +.venv/bin/python scripts/architecture/mypy_ratchet.py +.venv/bin/python scripts/startup/performance.py --check --repeat 3 + +# 架构专项 +.venv/bin/python -m pytest \ + tests/test_architecture_dependencies.py \ + tests/test_architecture_contract_baseline.py \ + tests/test_architecture_baseline_cli.py \ + tests/test_complexity_gate.py \ + tests/test_async_blocking_gate.py \ + tests/test_task_ownership_gate.py \ + tests/test_quality_ratchets.py \ + tests/test_mypy_gate.py \ + tests/test_architecture_ci.py -q + +# 广泛变更的最终本地回归 +uv run --locked --no-sync python tests/run.py + +# 需要覆盖率证据时按 CI 串行生成真实报告,再检查低水位 +uv run --locked --no-sync python -m coverage erase +uv run --locked --no-sync python -m coverage run tests/run.py --serial +uv run --locked --no-sync python -m coverage json +uv run --locked --no-sync python scripts/architecture/coverage_ratchet.py + +# Python 源码改动对实际变更路径运行(按切片替换示例路径) +uv run --locked --no-sync pylint app/path/to/changed.py +``` + +附加要求: + +- fixture 只有在依赖/合同变化经过人工审查后才能用 `--write-host` 更新,不能为转绿接受新增债务。 +- 所有 DB schema 变化必须新增 Alembic migration。 +- Plugin ABI、SDK/Compat 映射和官方插件仓验证独立执行;宿主绿灯不能替代插件兼容证据。 +- 正式插件 ABI 验证前先同步独立插件仓并记录所验 SHA,再运行: + `.venv/bin/python scripts/architecture/baseline.py --check-plugins --plugin-repo ../MoviePilot-Plugins`。 +- 每个阶段使用独立、可回滚的提交;不要把一个专项绿灯表述成整个长期目标完成。 +- 完成一次所有权迁移后,同步更新 canonical import、架构规则、SDK/Compat(仅在确有公共迁移时)和测试。 +- 依赖变更还必须运行 `uv lock --check`、锁定环境检查和 + `docs/rules/03-commands.md` 规定的运行依赖漏洞审计。 + +## 10. 明确不做 + +- 不恢复物理 `app/core`、`app/helper`、`app/utils`、`app/log.py`。 +- 不删除精确 Compat 映射、`app.sdk._legacy`、Chain/Manager 稳定 Facade 或旧插件 ABI。 +- 不把 `app/plugins/**` 当作宿主实现重构;官方插件仓单独验证。 +- 不把所有 `run_module` 字符串调用一次性替换成静态 import。 +- 不把所有后台工作改造成持久队列;仅按 ADR 判定为需要跨重启完成的 E2/E3 动作进入 durable 机制。 +- 不机械拆 TMDB 移植包 SCC;当前只做精确 containment,未来通过替换/升级移植库处理。 +- 不为降低行数进行无合同、无行为测试的目录搬运。 +- 不一次性删除 sync/async 双入口、全局兼容对象或无参构造 ABI。 +- 不用更新 baseline、降低门禁或增加宽泛 allowlist 掩盖回归。 + +## 11. 已完成且应继续保护的边界 + +- [x] 旧 `core/helper/utils/log` 物理源码已移除,Compat 使用精确、惰性映射。 +- [x] canonical 实现层当前无 SDK/Compat 反向违规,`app/__init__.py` 和插件启动扫描等 bootstrap + 例外保持精确;插件旧导入仍可用。 +- [x] Model/Oper 自建 Session、自动写事务和直接 commit/rollback 债务为零。 +- [x] HostRuntime、显式 Session/UoW、订阅/站点/Workflow 等参考切片已经落地。 +- [x] TaskRegistry owner、Workflow 有界关闭、Scheduler/Agent/Plugin/Message 生命周期已有专项保护。 +- [x] durable event/outbox 已覆盖关键事件切片;后续是按完成语义扩面,不是推倒重写。 +- [x] Module 动态方法名为零;Event payload model 已覆盖全部登记事件。 +- [x] `app/plugins/**` 与宿主架构基线分离,SDK/Plugin ABI 是独立验收面。 + +这些完成项是后续优化的护栏。任何阶段若重新引入 Model/Oper 自建事务、物理旧路径、无 owner 资源 +或宿主到插件实现的依赖,即使局部测试通过,也应视为架构回退。 diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 6bb13be2c..dbe8937e1 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -59,8 +59,12 @@ flowchart LR ## 二、整体分层架构 -MoviePilot v3 采用严格的单向依赖分层。历史包 `app/core`、`app/helper`、`app/utils` -已不再存在物理目录,仅作为旧插件的**虚拟兼容导入根**保留。 +MoviePilot v3 以单向依赖的模块化单体为目标。历史包 `app/core`、`app/helper`、`app/utils` +已不再存在物理目录,仅作为旧插件的**虚拟兼容导入根**保留。核心边界已有机器门禁, +尚未强制的 Adapter 例外、完整宿主 SCC 等差距记录在本章末的优化清单中。 + +下图同时表达职责关系与运行时调用方向,不等同于 Python 静态 import 图;通过 Port 注入的调用 +会与具体 Adapter 的静态依赖方向相反。 ```mermaid flowchart TB @@ -111,8 +115,7 @@ flowchart TB Chain -->|run_module 分发| Modules Chain -->|Application service / 命名数据端口| App - App --> Modules - App -->|Protocol / 持久化端口| Db + App -->|运行时调用 Protocol / 持久化端口| Db Modules --> Domain App --> Domain @@ -136,13 +139,14 @@ flowchart TB Compat -.精确别名.-> Adapters ``` -图中的 `Chain → Application` 与 `Application → Db` 表示通过应用端口和组合根注入的实现完成持久化, +图中的 `Chain → Application` 与 `Application → Db` 是运行时调用关系,表示通过应用端口和组合根 +注入的实现完成持久化;静态 import 方向是 `db.adapters → application Protocol`。 具体 DB Adapter 再使用 Oper;这不是允许在用例代码中直接创建数据库引擎或拼接 SQL。`compat` 也不是只面向 SDK 的转发层, 它按 `app/runtime/compat/manifest.py` 的白名单把已经删除的旧模块/符号精确映射到各自的 canonical 归属。`app/application/subscribe.py` 与 `app/application/plugins.py` 都是 V3 重构过程中新增、 未形成插件 ABI 的宿主内部聚合文件,主题实现收口后直接删除,不在 manifest 中制造新的兼容债务。 -当前实际的持久化调用路径可概括为: +当前正式目标和已完成参考切片的持久化调用路径可概括为: ```text 入口(API / Agent / CLI / Scheduler / Workflow) @@ -153,13 +157,16 @@ flowchart TB ``` `app/application/chain/data.py`、`app/application/agentdata.py` 和 -`app/application/history.py` 的 `get_*_port()` 是宿主生产代码读取组合根能力的规范入口; -兼容期保留的 `*PortProxy` 不能重新被别名为 Oper。需要跨进程恢复或 commit 后可靠执行的 +`app/application/history.py` 的 `get_*_port()` 是宿主生产代码读取组合根能力的现有接缝;其中 +Chain/Agent 的 `Any` factory 和裸 Oper 仍需迁移为类型化 Port/DTO,不能把当前兼容代理当作最终合同。 +需要跨进程恢复或 commit 后可靠执行的 业务副作用进入 `app/application/outbox.py` 定义的 Outbox 端口,由 `app/db/adapters/outbox.py` 实现;`app/runtime/tasks.py` 的 TaskRegistry 只负责进程内任务所有权、 取消和有限等待,不承担 durable queue 语义。 -**依赖方向的核心约束**(由 `tests/test_architecture_dependencies.py` 强制检查): +**依赖方向的规范目标**如下。`tests/test_architecture_dependencies.py` 已强制其中的核心子集; +Application/Chain 到具体 Adapter 的例外和完整宿主 SCC 仍需按优化清单补齐门禁,不能把表中每一行 +都理解为当前已经被 CI 全量证明: | 方向 | 状态 | |---|---| @@ -655,7 +662,7 @@ flowchart LR ## 十、架构治理 -架构边界不是文档约定,而是**由测试强制执行的门禁**: +架构边界中的核心子集已经由测试强制执行,其余目标不能只停留在文档约定: - `tests/test_architecture_dependencies.py` 构建完整 Python 模块图,拒绝: 物理遗留源码、禁止的上向依赖、SDK/compat 反向引用、包含迁移模块的强连通分量、 @@ -665,23 +672,24 @@ flowchart LR SDK 导出(若公开)、`docs/rules/05-architecture.md` 与上述架构测试。 - 延迟导入不被接受为隐藏循环依赖的手段。 -### 10.1 2026-08-24 当前收口状态与后续边界 +### 10.1 2026-08-26 当前收口状态与后续边界 当前宿主架构基线(排除 `app/plugins/**`)如下;数字来自 `tests/fixtures/architecture/`,更新基线前必须先审查语义变化: | 指标 | 当前值 | |---|---:| -| Python 模块 | 811 | -| 内部导入边 | 6,572 | -| 非平凡 SCC | 1(仅隔离的 TMDB 移植包) | -| Module Contract V2 spec | 212(其中 211 个进入 `run_module` 观察面) | +| Python 模块 | 835 | +| 内部导入边 | 6,810 | +| 非平凡 SCC | 2(Chain 包根环与隔离的 TMDB 移植包环) | +| Module Contract V2 spec | 215(其中 214 个进入 `run_module` 观察面) | | Event Contract | 53 | | Model/Oper 自动事务与自建 Session | 0 | | 组合根外 `SystemConfigOper()` | 0 | 架构专项验证:`tests/test_architecture_dependencies.py` 与 -`tests/test_architecture_contract_baseline.py` 共 68 passed;当前工作树未修改架构 fixture。 +`tests/test_architecture_contract_baseline.py` 共同提供语义断言和快照比较; +`scripts/architecture/baseline.py --check-host` 只验证快照一致,不能替代依赖合理性审查。 本总览与本轮架构治理的关系如下: @@ -696,7 +704,8 @@ flowchart LR 文件夹操作归入 `app/application/plugin/routes.py`、`folders.py`。原 `app/application/subscribe.py`、`app/application/plugins.py` 未形成插件 ABI,已经直接删除, 宿主调用统一改为 canonical 路径。 -- 已完成的运行时可靠性收口:TaskRegistry 统一进程内后台任务 owner;durable-required 事件和 +- 已完成的运行时可靠性收口:已迁移的主干后台任务由 TaskRegistry 或领域 lifecycle owner 管理; + durable-required 事件和 订阅关键副作用经 `app/application/outbox.py` 与 `app/db/adapters/outbox.py` 进入同事务 Outbox; 搜索逐页任务、Agent/消息事件和插件市场子任务均遵守请求或生命周期 owner,不再由入口模块维护 无法追踪的裸任务集合。 @@ -704,6 +713,9 @@ flowchart LR 物理文件仍是稳定入口的,不应为了目录规整新增“自己映射自己”的别名,也不应在 canonical 包中 保留多余导出。 +当前架构问题、优先级、分阶段实施步骤与验收门禁见 +[`docs/architecture-optimization-checklist.md`](architecture-optimization-checklist.md)。 + --- ## 附录:相关文档索引 @@ -718,5 +730,7 @@ flowchart LR | [`docs/rules/10-data-and-persistent.md`](rules/10-data-and-persistent.md) | 数据模型、迁移与缓存规范 | | [`docs/subscribe-lifecycle.md`](subscribe-lifecycle.md) | 订阅生命周期详解 | | [`docs/mcp-api.md`](mcp-api.md) | MCP 工具端点说明 | +| [`docs/architecture-optimization-checklist.md`](architecture-optimization-checklist.md) | 当前架构差距、优先级与可执行优化清单 | +| [`docs/architecture-refactor-roadmap.md`](architecture-refactor-roadmap.md) | 多级 Goal、叶子依赖、清零条件与交付状态 | | [`docs/v3t-runtime-governance.md`](v3t-runtime-governance.md) | V3/V3t 运行依赖、故障恢复、GIL 可观测性与兼容退场门禁 | | [`docs/adr/0007-background-action-reliability.md`](adr/0007-background-action-reliability.md) | 后台动作 E0–E3 可靠性分级与完成语义决策 | diff --git a/docs/architecture-refactor-roadmap.md b/docs/architecture-refactor-roadmap.md new file mode 100644 index 000000000..80c55e7e7 --- /dev/null +++ b/docs/architecture-refactor-roadmap.md @@ -0,0 +1,203 @@ +# MoviePilot V3 架构重构路线图 + +> 战略目标:完成 `docs/architecture-optimization-checklist.md` 的全部宿主架构重构与治理任务。 +> +> 执行分支:`v3`;每个叶子完成后独立验证、提交、推送并确认远端 SHA。 +> +> 排除范围:`app/plugins/**` 是插件运行时副本,不参与宿主重构。 +> +> 兼容原则:新插件只使用 `app.sdk`;旧插件导入、符号和行为只由统一 Compat/Legacy 层承接。 + +## 1. 治理合同 + +### 1.1 Goal 层级 + +- **G-ARCH(父目标)**:宿主架构、模块职责、持久化、生命周期、合同和质量债务全部达到本路线图终态。 +- **Stage(阶段)**:一组有共同退出条件的能力面,可包含多个独立叶子。 +- **Leaf(叶子)**:单一所有权面、可独立实现、验证、提交、推送和回滚的最小交付单元。 + +原生 Codex Goal 保存 G-ARCH 的稳定目标;本文件保存阶段、叶子、依赖、状态和停止条件。 + +### 1.2 状态 + +| 状态 | 含义 | +|---|---| +| `ACTIVE` | 当前唯一允许编辑的叶子 | +| `PLANNED` | 已定义边界,依赖满足后可激活 | +| `BLOCKED` | 有明确阻塞证据,且没有可推进的替代叶子 | +| `VERIFIED` | 本地验收完成,尚未推送或尚未确认远端 | +| `DELIVERED` | 已提交、推送,远端 SHA 与本地交付一致 | +| `COMPLETE` | 叶子验收和后续依赖均已结清 | + +任何时刻只允许一个 `ACTIVE` 叶子。子代理可以并行做只读审计、测试设计和下一叶准备;只有当前 +叶子的明确文件所有者可以写入源码。共享工作树中发现的并发改动必须先确认归属,不能覆盖。 + +### 1.3 叶子完成条件 + +一个叶子只有同时满足以下条件才可标记 `DELIVERED`: + +1. 所有定义在该叶子内的债务计数降为零,或变为有业务理由、精确路径、机器约束的非债务例外。 +2. 真实调用链已切换到新 owner;不能只增加新类、Port、DTO、门禁或测试而保留主路径走旧实现。 +3. canonical 主程序删除被替代的旧实现、兼容分支和重复导出,不保留“新旧两套正式入口”。 +4. 插件兼容只落在 `app/sdk`、`app/sdk/_legacy`、`app/runtime/compat` 或经批准的稳定 Facade; + canonical 实现不得为了兼容反向依赖这些层。 +5. 受影响专项、架构门禁、strict mypy、Ruff/mypy ratchet、scoped pylint 全部通过。 +6. 广泛架构、启动、生命周期、持久化或兼容变更运行锁定全量测试;插件 ABI 变更独立检查官方插件仓。 +7. diff、工作树、提交范围经过主线程审查;提交推送后 `HEAD == origin/v3` 且 ahead/behind 为 `0/0`。 + +不得以以下状态宣称完成:只建立 baseline、只新增抽象未迁移调用方、只留下 TODO、只让新测试通过、 +只完成目录移动、只在兼容层外保留旧导出,或用 `--write` 接受新增债务。 + +## 2. 最终停止条件 + +G-ARCH 只有在以下条件全部满足后才可完成: + +- [ ] `ARCH-001`、`ARCH-101` 至 `ARCH-111`、`ARCH-201` 至 `ARCH-204` 全部完成。 +- [ ] 宿主依赖图没有未解释 SCC;只允许精确 containment 的 TMDB 移植包例外,成员不能增长。 +- [ ] Application/Chain 到具体 Adapter、direct egress、raw concurrency 等所有例外均精确、可解释、不可增长。 +- [ ] Chain/Agent 正式数据入口不再注入无 Session Oper,不再以 `Any`/ORM 作为跨层合同。 +- [ ] 正式业务写入均有单一事务 owner;E2/E3 动作完成语义、幂等、恢复与人工决策路径一致。 +- [ ] import、普通对象构造不启动进程资源;资源由 bootstrap/lifecycle 显式创建、发布、关闭和重试收口。 +- [ ] 宿主 Module/Event 高风险合同 strict;宿主已知 `ANY` 结果形状归零,第三方插件保持诊断兼容。 +- [ ] 全量 mypy 历史错误、当前 Ruff 治理规则诊断和所有新增 complexity-v2 超限归零。 +- [ ] 高风险 Chain、Agent、Runtime、Startup、Adapter 纳入类型、复杂度、覆盖率和并发原语门禁。 +- [ ] canonical 主程序无旧实现、重复导出和 legacy import;插件兼容检查基于同步后的官方插件仓 SHA 通过。 +- [ ] 锁定全量测试、Pylint、依赖一致性/漏洞审计(若涉及依赖)和最终远端一致性全部通过。 + +## 3. 阶段与叶子 + +### S0:可信基线与事实源 + +退出条件:主线门禁全绿;架构规则、总览、机器快照和语义门禁一致;新增债务不能靠更新 fixture 进入。 + +| Leaf | 状态 | 依赖 | 完成定义 | +|---|---|---|---| +| S0-L1 可信基线恢复 | `VERIFIED` | 无 | 交付架构审计/路线图,修复 `ARCH-001` 两个 mypy 增量错误;现有专项和主线门禁通过 | +| S0-L2 规范事实统一 | `PLANNED` | S0-L1 | 修正 Oper、Adapter、HTTP、SCC、调用图规则冲突;规范目标与已执行门禁逐项对应 | +| S0-L3 完整宿主 SCC 门禁 | `PLANNED` | S0-L2 | 完整宿主 SCC 集合只允许 TMDB 精确 allowlist;Chain 环清零前门禁提供可审查低水位,最终由 S2-L2 清零 | +| S0-L4 Adapter/Egress 债务门禁 | `PLANNED` | S0-L2 | Application/Chain Adapter 导入和 direct HTTP egress 有精确分类;未解释项归零且禁止增长 | +| S0-L5 Event 扫描事实修复 | `PLANNED` | S0-L2 | consumer 只识别 EventManager 注册;动态误报归零,台账与真实生产/消费一致 | + +### S1:可靠性、事务与数据合同 + +退出条件:Transfer 达到 E3;正式数据 Port 类型化;跨表业务操作有单一 UoW;post-commit/Outbox +竞争、失败呈现、at-least-once 和幂等语义全部闭环。 + +| Leaf | 状态 | 依赖 | 完成定义 | +|---|---|---|---| +| S1-L1 Transfer E3 状态机 | `PLANNED` | S0 | `ARCH-102` 全部完成:migration、持久状态、checkpoint、lease、唯一 retry owner、重放和 manual review 一次交付 | +| S1-L2 Workflow typed query | `PLANNED` | S0 | Workflow Application Port 不返回 `Any`/ORM,Session 内投影 DTO,正式调用方全部切换 | +| S1-L3 Chain/Agent typed data ports | `PLANNED` | S1-L2 | `ChainDataPorts`/`AgentDataPorts` 的 raw Oper/`Any` factory 全部清零,兼容调用进入 Legacy 层 | +| S1-L4 Subscription mutation UoW | `PLANNED` | S1-L3 | Subscription mutation 不跨 Session 传 ORM,正式写路径一个 UoW,旧自动事务入口退出 canonical 路径 | +| S1-L5 站点/规则引用原子清理 | `PLANNED` | S1-L4 | SystemConfig+Subscribe 同事务更新,commit 后快照原子发布,并发/故障注入无部分状态 | +| S1-L6 Outbox 完成语义 | `PLANNED` | S0 | claim 竞争双发清零;业务提交与 effect pending 可区分;stager/store 分离;handler 幂等与崩溃测试完整 | + +### S2:进程生命周期、循环与 Adapter 边界 + +退出条件:导入/构造零资源副作用;Chain SCC 清零;Application/Chain 只依赖经批准的技术合同, +安全和命名外部能力全部经注入 Port。 + +| Leaf | 状态 | 依赖 | 完成定义 | +|---|---|---|---| +| S2-L1 日志/消息资源显式生命周期 | `PLANNED` | S0 | import 和非消息 Chain 构造零新增线程;bootstrap 显式创建,失败和正常关闭均收口 | +| S2-L2 ChainBase 与 SCC 清零 | `PLANNED` | S0-L3 | canonical `app.chain.base` 落地,包根无 eager/重复导出,宿主包根导入清零,Chain SCC 消失 | +| S2-L3 GlobalVar/provider 注册收口 | `PLANNED` | S2-L1 | `global_vars` canonical 消费清零,provider 注册进入显式装配阶段并可 reset;Legacy 入口精确保留 | +| S2-L4 Passkey 缓存边界 | `PLANNED` | S0-L4 | Application 不识别 Redis;原子 consume 由 runtime cache contract + backend 实现 | +| S2-L5 Backup artifact Port | `PLANNED` | S0-L4 | Application 不构造 `BackupFiles`,文件 I/O 由注入 Adapter 拥有 | +| S2-L6 Application Adapter 债务清零 | `PLANNED` | S2-L4,S2-L5 | Application 到具体 Adapter 的未批准边归零,批准通用机制有精确规则和门禁 | +| S2-L7 Chain Adapter/HTTP 债务清零 | `PLANNED` | S2-L6 | Chain 具体 Adapter 与普通 direct HTTP 依赖归零;SDK/stream/vendor 例外精确 containment | + +### S3:大型编排器职责清零 + +退出条件:每个热点 Facade 只保留稳定公开入口;决策、I/O、状态和生命周期各有单一 owner; +旧实现从 canonical 文件删除,complexity-v2 对该所有权面无债务。 + +| Leaf | 状态 | 依赖 | 完成定义 | +|---|---|---|---| +| S3-L1 TransferChain | `PLANNED` | S1-L1 | queue/recovery、plan、execute、settle、history/notify 完整拆分,原 836 行执行方法消失 | +| S3-L2 SubscribeChain | `PLANNED` | S1-L5 | search、match、refresh、reconciliation、notification 完整拆分,原文件无跨域业务实现 | +| S3-L3 Scheduler | `PLANNED` | S2-L3 | JobCatalog、ExecutionRegistry、reconciler、lifecycle 分离,无参 Chain 构造清零 | +| S3-L4 DownloadChain | `PLANNED` | S1-L6 | selection、submission、history、post-processing 分离,提交后动作遵守新完成语义 | +| S3-L5 SearchChain | `PLANNED` | S2-L7 | plan、provider fan-out、result state、pagination 分离,状态 owner 唯一 | +| S3-L6 MediaChain | `PLANNED` | S2-L2 | recognition、source projection、music alignment、cache 分离,兼容仅经统一层 | +| S3-L7 Agent/System/Plugin API | `PLANNED` | S2,S1-L6 | WebAgent SSE/file/audio、nettest/log/update/market 用例进入 Application,endpoint 只做传输适配 | + +### S4:可执行合同与质量债务清零 + +退出条件:高风险动态合同按信任级执行;所有 Python 源码进入一致的类型、复杂度、覆盖率和并发治理面; +历史 mypy/Ruff/复杂度债务为零。 + +| Leaf | 状态 | 依赖 | 完成定义 | +|---|---|---|---| +| S4-L1 Module strict contract | `PLANNED` | S2-L7 | 宿主 provider `ANY` 结果归零,宿主 admission strict,官方/第三方插件分级兼容 | +| S4-L2 Event strict contract | `PLANNED` | S0-L5,S1-L6 | 宿主事件输入/输出按风险 strict,诊断例外只属于第三方插件兼容 | +| S4-L3 Complexity v2 | `PLANNED` | S3 | 私有方法、class/file、圈复杂度进入门禁;所有超限通过职责拆分归零 | +| S4-L4 全量 mypy 清零 | `PLANNED` | S3,S4-L1,S4-L2 | `mypy-baseline.json` 归零并删除债务接受路径,全宿主 strict 类型通过 | +| S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 973 条诊断归零,规则集扩展经过独立审查且新增诊断为零 | +| S4-L6 Coverage/并发/质量证据 | `PLANNED` | S3,S4-L1,S4-L2 | 高风险包纳入 coverage;raw concurrency 分类清零;Module Quality 有真实 evidence test | + +### S5:Plugin、Agent、Domain、Startup 与最终收口 + +退出条件:剩余 Facade/Manager/Domain/Startup 重复职责清零;官方插件兼容、全量测试和远端交付完成。 + +| Leaf | 状态 | 依赖 | 完成定义 | +|---|---|---|---| +| S5-L1 PluginHelper/PluginManager | `PLANNED` | S2,S4 | 市场/包/依赖/备份/健康服务各归 owner;Facade 只转发稳定 ABI,构造归 typed PluginRuntime | +| S5-L2 Agent/LLM provider | `PLANNED` | S3-L7,S4 | catalog、发现、认证、session、runtime 分离;Manager 只保留稳定 API | +| S5-L3 Domain projection | `PLANNED` | S3-L6,S4 | `MediaInfo` canonical 路径保留,来源投影规则拆分,重复 DTO/业务语义清零 | +| S5-L4 Startup composition | `PLANNED` | S2,S3,S5-L1,S5-L2 | `initializers/modules.py` 仅负责顺序/注册/重启决策,构造按领域进入 composition | +| S5-L5 Sync/async 重复清零 | `PLANNED` | S3,S5 | 双 ABI 外壳共享纯逻辑,重复业务实现清零,Session/客户端不跨并发边界复用 | +| S5-L6 最终兼容与交付 | `PLANNED` | 全部 | canonical 旧实现/重复导出清零;同步官方插件仓验证;锁定全量、Pylint、架构、类型、覆盖率全部通过并推送 | + +## 4. 当前活动叶子 + +### S0-L1 可信基线恢复 + +**Status:** `VERIFIED`(本地验收完成,等待提交、推送和远端一致性确认) + +**Outcome** + +在不接受新债务的前提下恢复当前主线类型门禁,交付审计清单、Goal 路线图和与实际基线一致的架构总览。 + +**Ownership** + +- `app/chain/media.py` 的音乐曲目匹配局部类型回归。 +- `docs/architecture-optimization-checklist.md`。 +- `docs/architecture-refactor-roadmap.md`。 +- `docs/architecture-overview.md` 的当前事实与文档索引。 + +**Excluded** + +- 不在本叶实现 ARCH-102 之后的业务重构。 +- 不刷新 mypy、Ruff、依赖或复杂度 fixture。 +- 不修改 `app/plugins/**` 或独立插件仓。 + +**Acceptance** + +```bash +.venv/bin/python -m pytest tests/test_music_album_match.py -q +.venv/bin/mypy --config-file mypy.ini +.venv/bin/python scripts/architecture/mypy_ratchet.py +.venv/bin/python scripts/architecture/baseline.py --check-host +.venv/bin/python scripts/architecture/complexity.py +.venv/bin/python scripts/architecture/async_blocking.py +.venv/bin/python scripts/architecture/task_ownership.py +.venv/bin/python scripts/architecture/service_locator.py +.venv/bin/python scripts/architecture/ruff_ratchet.py +.venv/bin/python -m pytest \ + tests/test_architecture_dependencies.py \ + tests/test_architecture_contract_baseline.py \ + tests/test_architecture_baseline_cli.py \ + tests/test_complexity_gate.py \ + tests/test_async_blocking_gate.py \ + tests/test_task_ownership_gate.py \ + tests/test_quality_ratchets.py \ + tests/test_mypy_gate.py \ + tests/test_architecture_ci.py -q +git diff --check +``` + +**Delivery** + +- 单一提交主题:恢复可信架构基线并建立长期路线图。 +- 推送 `origin/v3` 后确认提交祖先关系、远端 SHA 和 ahead/behind `0/0`。