Files
MoviePilot/docs/refactor/refactor-roadmap.md
2026-09-11 06:20:59 +08:00

619 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MoviePilot V3 架构重构路线图
> 战略目标:完成 S0-S3 与 S5 的宿主架构重构和最终收口;S4 按维护者决定取消,
> S3 完成后直接进入 S5,S5 完成即结束本轮战略任务。
>
> 执行分支:`v3`;每个叶子完成后独立验证、提交、推送并确认远端 SHA。
>
> 排除范围:`app/plugins/**` 是插件运行时副本,不参与宿主重构。
>
> 兼容原则:新插件只使用 `app.sdk`;旧插件导入、符号和行为只由统一 Compat/Legacy 层承接。
## 0.1 当前追踪目标:G-ARCH-RESIDUAL(COMPLETE)
原 G-ARCH 的 S0-S3、S5 交付记录和 S4 `CANCELLED` 决定保持不变。基于 2026-08-30
对 exact-head 的复核,另行建立本次追踪目标 `G-ARCH-RESIDUAL`,只收口审计已经明确的残余
语义和治理缺口,不把 S4 的全量质量债务重新作为隐性依赖。
| Leaf | 状态 | 依赖 | 退出条件 |
|---|---|---|---|
| R0 事实基线刷新 | `DELIVERED` | 无 | `87269610e`:exact-head、远端、工作树、CI 和五类残余问题证据冻结;未修改 `app/plugins/**` |
| R1 System API 边界收口 | `DELIVERED` | R0 | `d50177f45`:日志、Wiki、配置、事件、更新与重启进入 `application/system.py` 和 `composition/system.py`;endpoint 只保留 HTTP/SSE/ZIP/响应映射 |
| R2 复杂度与原生并发门禁 | `DELIVERED` | R0 | `f7ca7e517`、`5cd5780d3`:完整 AST、Scheduler、canonical alias、TaskGroup、稳定 owner/count 与低水位门禁落地;本范围新增债务为零 |
| R3 Outbox after_commit 恢复语义 | `DELIVERED` | R0 | `9d06f91bb`:下载提交后通知、模块和字幕副作用改为同事务持久 intent,具备 handler、claim/fencing、重启回放、幂等与失败观测 |
| R4 Startup/插件市场单一装配 | `DELIVERED` | R0 | `7f5b8b469` 至 `046b0b305`:市场 Transport/Package/Dependency/Health、Domain、网络、站点、Chain、缓存、资源、DoH 和 Workflow 构造归入 composition;Compat 复用同一 owner |
| R5 验收与交付收口 | `DELIVERED` | R1,R2,R3,R4 | 路线图、优化清单、规则和机器门禁一致;Application/Domain 固定覆盖率基线为 `80.00%` / `80.00%`,Pylint `10.00/10`、架构/兼容门禁、真实启动和最终 exact-head GitHub CI 通过;`HEAD == origin/v3` 且 ahead/behind 为 `0/0` |
执行合同:任一时刻只允许一个 `ACTIVE` 叶子;每个叶子必须独立验证、显式提交和推送,
并在交付前复核插件 ABI、canonical 旧实现/重复导出和 `app/plugins/**` 排除边界。
R5 以 2026-08-30 的精确代码头完成验收:四个 GitHub Unit Tests 分片和 Architecture Contract
Gate 在运行 `33293899659` 中通过,Coverage Report 生成并上传同一份 `coverage.json`;该运行只因
Application 低水位从 `81.61%` 提升到 `81.77%` 后尚未固化而失败。R5 使用相同 locked 命令在
本地重新生成报告并写入整数计数,随后由最终文档/基线提交对应的 exact-head CI 复核。真实进程使用
临时配置目录完成启动,`/health/live` 与 `/health/ready` 均返回 HTTP 200,并完成有序关闭。
## 1. 治理合同
### 1.1 Goal 层级
- **G-ARCH(父目标)**:宿主架构、模块职责、持久化、生命周期、合同和质量债务全部达到本路线图终态。
- **Stage(阶段)**:一组有共同退出条件的能力面,可包含多个独立叶子。
- **Leaf(叶子)**:单一所有权面、可独立实现、验证、提交、推送和回滚的最小交付单元。
原生 Codex Goal 保存 G-ARCH 的稳定目标;本文件保存阶段、叶子、依赖、状态和停止条件。
### 1.2 状态
| 状态 | 含义 |
|---|---|
| `ACTIVE` | 当前唯一允许编辑的叶子 |
| `PLANNED` | 已定义边界,依赖满足后可激活 |
| `BLOCKED` | 有明确阻塞证据,且没有可推进的替代叶子 |
| `VERIFIED` | 本地验收完成,尚未推送或尚未确认远端 |
| `DELIVERED` | 已提交、推送,远端 SHA 与本地交付一致,且本叶要求的远端 CI 已闭环 |
| `COMPLETE` | 叶子验收和后续依赖均已结清 |
| `CANCELLED` | 经维护者明确取消,不进入执行、验收或完成计数 |
任何时刻只允许一个 `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 只有在 S3、S5 及以下条件全部满足后才可完成;S4 不属于本轮完成范围:
- [x] `ARCH-001`、`ARCH-101` 至 `ARCH-109`、`ARCH-201` 至 `ARCH-204` 全部完成;`ARCH-110/111` 随 S4 取消且不作为完成依赖。
- [x] 宿主依赖图没有未解释 SCC;只允许精确 containment 的 TMDB 移植包例外,成员不能增长。
- [x] Application/Chain 到具体 Adapter、direct egress,以及本轮迁移资源的并发 owner 例外均精确、可解释、不可增长。
- [x] Chain/Agent 正式数据入口不再注入无 Session Oper,不再以 `Any`/ORM 作为跨层合同。
- [x] 正式业务写入均有单一事务 owner;E2/E3 动作完成语义、幂等、恢复与人工决策路径一致。
- [x] import、普通对象构造不启动进程资源;资源由 bootstrap/lifecycle 显式创建、发布、关闭和重试收口。
- [x] S3、S5 各叶子定义的类型、复杂度、覆盖率和并发债务清零,现有全局 ratchet 不发生回退。
- [x] canonical 主程序无旧实现、重复导出和 legacy import;插件兼容检查基于同步后的官方插件仓 SHA 通过。
- [x] 锁定全量测试、Pylint、依赖一致性和最终远端一致性全部通过;本轮未修改依赖。
## 3. 阶段与叶子
### S0:可信基线与事实源
退出条件:主线门禁全绿;架构规则、总览、机器快照和语义门禁一致;新增债务不能靠更新 fixture 进入。
| Leaf | 状态 | 依赖 | 完成定义 |
|---|---|---|---|
| S0-L1 可信基线恢复 | `DELIVERED` | 无 | `5df388719`:交付架构审计/路线图,修复 `ARCH-001` 两个 mypy 增量错误;远端 `0/0` |
| S0-L2.1 Host Oper/UoW 规范 | `DELIVERED` | S0-L1 | `3bf94ffed`:宿主无 Session Oper 规范债务归零,远端 `0/0` |
| S0-L2.2 完整宿主 SCC policy | `DELIVERED` | S0-L2.1 | `a884ab5c2`:完整宿主 SCC 精确 policy 生效,远端 `0/0` |
| S0-L2.3 Adapter 直连事实 | `DELIVERED` | S0-L2.1 | `e1483e85d`:锁定 28 条原始 Adapter import 事实,远端 `0/0` |
| S0-L2.4 Adapter zero-growth | `DELIVERED` | S0-L2.3 | `2553226f3`:冻结 28 条直连及 owner,收缩/新增/stale policy 门禁生效,远端 `0/0` |
| S0-L2.4b HTTP/Egress 事实与政策 | `DELIVERED` | S0-L2.4 | `47f0de745`、`43d52a35b`、`8d602149f`:冻结 66 条出口事实,消除 CI 类型/覆盖率漂移;远端全绿且 `0/0` |
| S0-L2.5 Event consumer 识别 | `DELIVERED` | S0-L2.1 | `86157be2a`:consumer 只识别可静态证明的 EventManager 注册;全量 6,459 passed / 6 skipped,CI `33029645165`/`33029645254` 全绿,远端 `0/0` |
| S0-L2.6 事实源与 CI 投影 | `DELIVERED` | S0-L2.2,S0-L2.4b,S0-L2.5 | `113355784`:99/17 条逐调用事实、17 条 consumer policy 与 CI 分层交付;Unit Tests `33031697902`、Pylint `33031697785` 全绿,远端 `0/0` |
### S1:可靠性、事务与数据合同
退出条件:Transfer 达到 E3;正式数据 Port 类型化;跨表业务操作有单一 UoW;post-commit/Outbox
竞争、失败呈现、at-least-once 和幂等语义全部闭环。
`S1-L1` 是 ARCH-102 的 Transfer E3 父项,当前状态为 **DELIVERED**。`S1-L1.5` 聚合交付
`S1-L1.1` 至 `S1-L1.4` 的阶段性结果已由最终精确 head 统一交付;真实调用链已完成迁移,旧 fail-open、重复状态和
兼容层外旧入口已退出 canonical 主程序,兼容只经统一 Compat/SDK 门面提供。
| Leaf | 状态 | 依赖 | 完成定义 |
|---|---|---|---|
| S1-L1.1 Durable admission | `DELIVERED` | S0 | Application-owned typed Port + DB adapter + migration 落地;先持久 commit 再入队,入队失败保留可恢复记录;宿主不再通过 raw/`Any` `TransferPendingOper` 处理 admission |
| S1-L1.2 Planning checkpoint | `DELIVERED` | S1-L1.1 | 版本化输入与指纹先持久化;无 legacy provider 时以 `accepted -> planned` CAS 提交完整计划,有 provider 时先提交 `provider_pending`,全部返回空后再以第二次 CAS 提交 `planned`;重放只执行冻结目标,所有文件副作用晚于对应 checkpoint commit |
| S1-L1.3 Lease 与恢复调度 | `DELIVERED` | S1-L1.2 | claim/lease/heartbeat/attempt 与过期接管规则落地;启动回放和同进程恢复共用唯一调度入口,同一任务同时只有一个 worker owner |
| S1-L1.4 幂等执行与终态结算 | `DELIVERED` | S1-L1.3 | 文件操作、历史提交和 checkpoint 可重放;唯一 retry owner 生效,未知外部结果进入 `manual_review`,仅完整终态删除 pending |
| S1-L1.5 E3 全链收口 | `DELIVERED` | S1-L1.4 | `e9de149db`、`a2e249f20`:崩溃矩阵、3.0.17 升降级、重复回放和插件 ABI 验收完整;旧 fail-open、重复状态与兼容层外旧入口删除;Unit Tests `33092427327`、Pylint `33092427348` 全绿,ARCH-102 债务归零 |
| S1-L2 Workflow typed query | `DELIVERED` | S0 | `b4f873654`、`a01a35bcb`:Workflow Application Port 不返回 `Any`/ORM,Session 内投影冻结 DTO,正式调用方全部切换;Unit Tests `33098869736`、Pylint `33098869837` 全绿,覆盖率低水位提升至 Application `78.78%` |
| S1-L3 Chain/Agent typed data ports | `DELIVERED` | S1-L2 | User、History、Site、Subscription 的冻结 DTO/typed Port 已完成;`ChainDataPorts`/`AgentDataPorts` 与 raw Oper/`Any` factory 清零,旧公开 ABI 仅由 Legacy/Compat 承接 |
| S1-L3.1 Workflow typed execution | `DELIVERED` | S1-L2 | `17d8be2af`、`b33b29876`:Chain 直连类型化事务服务且单次执行只取一个 port;canonical Oper 删除旧 writer/无 Session 写方法,旧 ABI 只在 `_legacy/workflow.py` 与 Compat overlay;Unit Tests `33103913838`、Pylint `33103913935` 全绿,Application 覆盖率低水位提升至 `78.79%` |
| S1-L3.2 Chain registry/DI | `DELIVERED` | S1-L3.1 | 显式类型化 context,删除 PortProxy、全局数据 getter 与失效的双重注入,构造器注入真实控制调用 |
| S1-L3.2.1 Registry hygiene | `DELIVERED` | S1-L3.1 | `ac7a20132`:删除零消费者 PortProxy/动态转发和 `ChainRuntimeContext.data_ports` 伪注入;Workflow 退出 Chain registry,只保留 Application owner 单一配置入口;Unit Tests `33120205586`、Pylint `33120205581` 全绿 |
| S1-L3.3 DownloadFailure/MediaServer | `DELIVERED` | S1-L3.2 | `5fb62108a`:两组 raw factory 已替换为冻结 DTO/typed Port;失败冷却在 Session 内投影,媒体库查询只返回标量且每个 upsert/cleanup 独立短事务,远端枚举不持有 Session;旧 Oper 与插件可见 Chain ABI 保持不变;Unit Tests `33127544925`、Pylint `33127544927` 全绿,Application 覆盖率低水位提升至 `78.95%` |
| S1-L3.4 User | `DELIVERED` | S1-L3.3 | User Chain/Agent/认证改用冻结 typed snapshot;创建、更名、删除与最后一个启用超级管理员保护归并到单 UoW;用户名唯一索引及 UserConfig/PassKey 级联迁移落地,UserConfig 在 commit 后持写锁重载数据库事实源并发布内存快照 |
| S1-L3.5 History | `DELIVERED` | S1-L3.4 | DownloadHistory 与 TransferHistory 均已迁入深度冻结 DTO、typed query/write/staging Port 与短 Session adapter;请求级删除和 durable 结算各自保持单一 UoW,canonical 调用方不再接收 raw Oper/ORM,旧写入 ABI 只由 SDK Legacy/Compat 提供 |
| S1-L3.6 Site | `DELIVERED` | S1-L3.5 | Site 配置、用户数据、图标和统计统一为深度冻结 DTO 与 typed query/write/staging Port;请求写入复用 AsyncSession,Chain/Agent 使用独立短事务,canonical 不再接收 raw Oper/ORM,旧插件 ABI 只经 SDK Legacy/Compat |
| S1-L3.7 Subscription | `DELIVERED` | S1-L3.6 | `application/subscription/contract.py` 统一深度冻结 Snapshot/History/Identity/Patch 与 typed query/write/staging Repository;DB adapter 在 Session 内完成 ORM 投影,Chain/API/Agent/Workflow/interaction 不再消费 raw Oper/ORM/`Any`;旧 `SubscribeOper`/`SubscribeHistoryOper` 只经同一 SDK Legacy/Compat 门面保留插件 ABI |
| S1-L3.8 Agent/Transfer locator gate | `DELIVERED` | S1-L3.7 | `chain/data.py`、`agentdata.py` 及其 getter 已删除;Chain/Agent 改用显式 typed context,AST 门禁确认 canonical 无 raw getter/Oper/Any;全量有效 `6955 passed, 9 skipped`,Application 覆盖率提升至 `79.83%`,依赖边降至 `7,062` |
| S1-L4 Subscription mutation UoW | `DELIVERED` | S1-L3.8 | `d9e4a973c`:新增、修改、删除、完成均由显式 Session-bound Command/UoW 拥有事务;Servarr 多季新增在一个请求事务内提交全部订阅和 durable intents,任一季失败整批回滚;canonical 自动提交写入口与 locator 已退出,专项联合测试 327 项通过;当前依赖事实为 7,091 条边,远端 `0/0` |
| S1-L5 站点/规则引用原子清理 | `DELIVERED` | S1-L4 | `d9e4a973c`:站点、规则组及自定义规则改名的 SystemConfig+Subscribe 共享一个 UoW,双事实 CAS 防止过期覆盖,commit 后一次发布配置快照;故障注入、通配重置、并发冲突与事件循环心跳测试通过,远端 `0/0` |
| S1-L6 Outbox 完成语义 | `DELIVERED` | S0 | 事务内 `OutboxStager` 与独立短事务 `OutboxDispatchStore` 已分离;即时投递与 dispatcher 均先 claim,complete/retry 受 attempt fencing;`PostCommitResult` 区分已提交业务、已完成与 pending effect。事件载荷和宿主 correlation context 携带稳定 event key;旧通知插件保持原签名并承认 at-least-once 重复边界 |
### S2:进程生命周期、循环与 Adapter 边界
退出条件:导入/构造零资源副作用;Chain SCC 清零;Application/Chain 只依赖经批准的技术合同,
安全和命名外部能力全部经注入 Port。
| Leaf | 状态 | 依赖 | 完成定义 |
|---|---|---|---|
| S2-L1 日志/消息资源显式生命周期 | `DELIVERED` | S0 | 配置冷导入和非消息 Chain 构造零新增日志/消息线程;lifespan 显式创建共享 owner,启动失败、关闭超时、重试及连续两轮 lifespan 均收口;Chain 轻量客户端不保留首个回调 |
| S2-L2 ChainBase 与 SCC 清零 | `DELIVERED` | S0-L2.2 | canonical `app.chain.base` 已落地,包根无 eager/重复导出,宿主包根导入清零,旧符号只经 `app.sdk.chain` 与 Compat 保持同一类身份;完整宿主 SCC 只剩精确 containment 的 TMDB 移植包 |
| S2-L3 GlobalVar/provider 注册收口 | `DELIVERED` | S2-L1 | 停止、主循环和 WebPush 各有唯一 runtime owner,canonical `global_vars` 消费清零并由 AST 门禁冻结;Workflow、Scheduler、Command、Agent、技能和 LLM provider 均在生命周期显式 configure/reset,冷导入保持未注册;Legacy SDK 门面继续共享同一事实源 |
| S2-L4 Passkey 缓存边界 | `DELIVERED` | S0-L2.4 | `PasskeyChallengeCache` 由 startup 注入,Application 不识别 Redis;严格 `AtomicCacheBackend.store/consume` 由 Memory/Redis backend 分别实现,challenge 仅能被原子领取一次 |
| S2-L5 Backup artifact Port | `DELIVERED` | S0-L2.4 | Application-owned `BackupArtifactStore`/factory 覆盖创建、发布、清理、列举、解析、删除和元数据读取;startup 注入唯一 `BackupFiles` 实现,Application 具体 Adapter 直连由 14 降至 13,完整备份调用链 62 项通过 |
| S2-L6 Application Adapter/DNS 债务清零 | `DELIVERED` | S2-L4,S2-L5 | Application 自有 DNS、图片、消息回环、RSS、站点登录、种子、系统加速 Port,startup 统一注入;13 条具体 Adapter 边及 Application DNS I/O 均归零,未装配和失败回滚语义已验证 |
| S2-L7 Chain Adapter/宿主 HTTP 债务清零 | `DELIVERED` | S2-L6 | Chain 具体 Adapter 与 11 条普通 direct HTTP/Session bridge 均归零;当前 53 条出口事实全部属于精确 SDK/stream/vendor/canonical containment,policy 无 unreviewed、stale 或指纹漂移 |
### S3:大型编排器职责清零
退出条件:每个热点 Facade 只保留稳定公开入口;决策、I/O、状态和生命周期各有单一 owner;
旧实现从 canonical 文件删除,现有复杂度 ratchet 与叶子结构门禁对该所有权面无新增债务。
| Leaf | 状态 | 依赖 | 完成定义 |
|---|---|---|---|
| S3-L1 TransferChain | `DELIVERED` | S1-L1 | 旧 `transfer.py`/`_transfer.py` 单体退役,由 `app.chain.transfer` 同名职责包承接;queue/recovery、plan、execute、settle、history/notify 各有唯一 owner,原 836 行执行方法消失,包根只保留稳定 ABI |
| S3-L2 SubscribeChain | `DELIVERED` | S1-L5 | 旧 `subscribe.py` 单体退役,由 `app.chain.subscribe` 同名职责包承接;search、match、refresh、completion、reference reconciliation、notification 各有唯一 owner,包根只保留稳定 `SubscribeChain` |
| S3-L3 Scheduler | `DELIVERED` | S2-L3 | 旧 `app/scheduler.py` 已退役并迁入 `app.scheduler` 同名包;catalog、execution/bridge/progress、`ExecutionRegistry`、reconciler、lifecycle 与 maintenance 已分离,业务 callable 由 startup 经 `SchedulerServices` 注入,包内无参 Chain 构造清零;功能/生命周期 215 项、架构/兼容/文档 189 项通过,官方插件基线语义未变化 |
| S3-L4 DownloadChain | `DELIVERED` | S1-L6 | `7941fd921`:旧单体退役;selection、submission、batch、history、post-processing 等职责进入同名包单一 owner,提交后动作遵守新完成语义;锁定全量 `7162 passed, 9 skipped`,远端 `0/0` |
| S3-L5 SearchChain | `DELIVERED` | S2-L7 | `1c145d716`:旧单体退役并由同名职责包承接;稳定 ABI、官方插件兼容、类型/复杂度门禁和锁定全量 `7222 passed, 9 skipped` 通过;远端 `0/0` |
| S3-L5.1 Runtime dependencies 命名治理 | `DELIVERED` | S3-L5 | `13fba96fd`:平铺模块退役为 `runtime/dependencies/` 同名包;单词子模块、包根无重复导出、旧快照回滚兼容、命名规则与机器门禁均已交付,锁定全量 `7224 passed, 9 skipped`,远端 `0/0` |
| S3-L6 MediaChain | `DELIVERED` | S2-L2 | `13c4a71f4`:旧单体退役并由同名职责包承接;稳定 ABI、官方插件兼容、缓存并发/别名语义、类型/复杂度门禁和锁定全量 `7244 passed, 9 skipped` 通过;远端 `0/0` |
| S3-L7 Agent/System/Plugin API | `DELIVERED` | S2,S1-L6 | `f38637dce`:WebAgent 会话/file/audio、System nettest 和 Plugin market/rating/release/folders 用例进入 Application;endpoint 只保留传输适配,锁定全量 `7256 passed, 9 skipped`、官方插件基线和远端 `0/0` 通过 |
### S4:可执行合同与质量债务清零
**阶段状态:`CANCELLED`(2026-08-28 维护者决定)**
本阶段不再执行,也不作为 S5 或父目标完成的依赖。S4 原定义保留为取消记录,
不得把 `CANCELLED` 写成 `COMPLETE` 或用 S5 顺带扩张为全宿主质量债务清零。
| Leaf | 状态 | 依赖 | 完成定义 |
|---|---|---|---|
| S4-L1 Module strict contract | `CANCELLED` | — | 原计划取消 |
| S4-L2 Event strict contract | `CANCELLED` | — | 原计划取消 |
| S4-L3 Complexity v2 | `CANCELLED` | — | 原计划取消 |
| S4-L4 全量 mypy 清零 | `CANCELLED` | — | 原计划取消 |
| S4-L5 Ruff 治理债务清零 | `CANCELLED` | — | 原计划取消 |
| S4-L6 Coverage/并发/质量证据 | `CANCELLED` | — | 原计划取消 |
#### S4 Lite:后续增量治理边界(非阶段)
S4 Lite 不是新的 Stage、Leaf 或 Goal,不设置状态、进度或统一完成日期,也不改变 S4
`CANCELLED` 的事实。它只约束后续实际触及相关 owner 时的增量治理,避免取消全量清零后失去
防回退边界:
- Module/Event 合同继续以插件兼容为硬边界。第三方插件的合同校验保持 diagnostic;只有宿主
内置的高风险能力或 durable internal event,才可在独立变更中引入 strict contract
enforcement,并补齐对应行为、生命周期和失败恢复证据。这里的 strict 仅指合同校验策略;
现有 `run_module_strict` / `send_event_strict` 是调用方选择的异常传播接口,仍可触达插件
provider,不属于本条收窄范围。
- 宿主升级不得要求已可用插件先行适配。`run_module` / `async_run_module` 的字符串方法 ABI、
参数兼容、聚合与原始返回形状,以及现有 SDK/Compat/Legacy 行为保持稳定;若增量 strict
治理导致既有插件失效,首先按宿主兼容回归处理,而不是把迁移责任转给插件。
- complexity v2、原生并发、mypy、Ruff 和 coverage 保留现有 zero-growth/低水位 ratchet;
新增或实质修改高风险 owner 时补对应专项证据,但不恢复全宿主历史债务清零目标。
- 每项治理必须在独立变更中说明 owner、风险、兼容影响和退出条件,并通过受影响宿主路径与插件
兼容验证;不得以刷新 baseline、增加文档或只通过静态快照宣称语义完成。
### S5:Plugin、Agent、Domain、Startup 与最终收口
退出条件:剩余 Facade/Manager/Domain/Startup 重复职责清零;官方插件兼容、全量测试和远端交付完成。
| Leaf | 状态 | 依赖 | 完成定义 |
|---|---|---|---|
| S5-L1 PluginHelper/PluginManager | `DELIVERED` | S2,S3 | `fa40f29df`:市场/包/依赖/备份/健康服务各归 owner;Facade 只转发稳定 ABI,构造归 typed PluginRuntime |
| S5-L2 Agent/LLM provider | `DELIVERED` | S3-L7 | `c000c4fff`、`3a33da0b3`、`dbdf7d058` 已在 `origin/v3` 祖先链:catalog、发现、认证、session、runtime 分离;Manager/Facade 只保留稳定 API,最终精确 head CI 已闭环 |
| S5-L3 Domain projection | `DELIVERED` | S3-L6 | `MediaInfo` canonical 路径和 setter ABI 保留;四来源纯投影归入单词 owner,输入不可变、重复业务语义清零 |
| S5-L4 Startup composition | `DELIVERED` | S2,S3,S5-L1,S5-L2 | 领域构造全部归单词型 composition owner;HostRuntime/ApiData 同源;显式逆序 reset manifest 在 worker 收敛后撤销 Provider,失败保留完整诊断边界 |
| S5-L5 Sync/async 重复清零 | `DELIVERED` | S3,S5 | 双 ABI 外壳共享纯逻辑,重复业务实现清零,Session/客户端不跨并发边界复用 |
| S5-L6 最终兼容与交付 | `DELIVERED` | S3,S5-L1,S5-L2,S5-L3,S5-L4,S5-L5 | canonical 旧实现/重复导出清零;官方插件仓、锁定全量、Pylint、架构、类型、覆盖率与远端一致性全部通过,本轮战略任务结束 |
## 4. 叶子合同与交付记录
### S3-L5.1 Runtime dependencies 命名治理
**Status:** `DELIVERED`
**Outcome**
退役平铺且语义相似的 `app/runtime/dependencies.py` 与
`app/runtime/native_dependencies.py`,由同名 `app.runtime.dependencies` 包承接。
`profile.py` 只拥有运行环境依赖分组与同步参数,`native.py` 只拥有已加载原生发行版快照和变更探测;
包根不放实现、不重复导出内部符号,canonical 宿主调用方直接导入职责子模块。
同步把该决策写入 `docs/rules/07-naming-conventions.md`:新增生产模块优先使用准确的单个小写单词;
同一能力需要多个文件时建立同名单词目录包,再使用单词职责文件;禁止在父目录平铺相似复合文件名、
用 `source.py` 保存旧实现,或通过包根重复导出内部 owner。架构测试锁定本包结构和退役文件,
让后续 Agent 的错误命名在 CI 中直接失败。
**Acceptance**
- `app/runtime/dependencies.py` 与 `app/runtime/native_dependencies.py` 删除且不得复活。
- `app/runtime/dependencies/` 只包含 `__init__.py`、`profile.py`、`native.py`;包根没有实现或重复导出。
- 所有宿主 canonical 导入改为明确子模块;最新官方插件仓扫描决定是否需要精确 Compat 映射。
- 命名规范、架构文档、依赖基线和结构门禁同步;`app/plugins/**` 不扫描、不修改。
- 运行依赖分组、插件原生依赖激活、插件安装/包管理专项及架构/质量门禁全部通过。
- 提交推送后确认远端 SHA、祖先关系和 ahead/behind `0/0`。
**Evidence(2026-08-29)**
- 平铺实现已删除,`app/runtime/dependencies/` 仅有 `__init__.py`、`profile.py`、`native.py`;包根只有包说明,宿主导入全部直达职责子模块。
- Docker 更新边界同时覆盖新包载荷与旧版本 `dependencies.py` 快照回滚,未把旧实现恢复到主程序。
- 官方插件仓最新远端 `aa40b961a2093474a77edd9eed88844ff07425ac` 对旧路径及全部公开符号零命中,因此未新增无消费者的 SDK/Compat 债务。
- 运行依赖、原生激活、插件安装/包管理及架构专项 `406 passed`;最终架构/CI/Docker 门禁 `182 passed`。
- 锁定全量 `7224 passed, 9 skipped`;Pylint `10.00/10` 且重复率 `0.000%`;mypy、Ruff(历史诊断降至 `667`)、复杂度和依赖基线门禁全部通过。
- 交付提交 `13fba96fd82f4e94dbda8cff5dc635966e06fa67` 已推送;本地、`origin/v3` 与远端引用一致,ahead/behind `0/0`。
### S3-L5 SearchChain
**Status:** `DELIVERED`
**Outcome**
退役 `app/chain/search.py` 单体,由同名 `app.chain.search` 包承接搜索计划、provider fan-out、
分页、结果状态、AI 推荐、字幕与音乐搜索等职责。包根只惰性保留稳定 `SearchChain`,Facade 只组合
职责 owner 与稳定入口;主程序不保留 `source.py`、旧实现或内部 owner/port 重复导出。
同步、异步和流式入口必须复用同一组纯规则与状态 owner;provider 并发、取消、分页推进和结果发布
各自只有一个执行所有者,不能通过复制现有大方法形成多套业务实现。
**Acceptance**
- 公开 `SearchChain` 类身份、方法签名、MRO、pickle 身份和官方插件调用形态不变。
- `app/plugins/**` 不扫描、不修改;官方插件仓通过独立宿主兼容基线验证。
- 搜索计划、provider fan-out、分页、取消和状态发布的同步/异步/流式行为保持一致并有回归覆盖。
- Search 专项、架构/兼容、mypy/Ruff/complexity、scoped Pylint 全部通过;广泛变更运行锁定全量。
- 本叶提交推送后确认远端 SHA、祖先关系和 ahead/behind `0/0`。
### S3-L6 MediaChain
**Status:** `DELIVERED`
**Outcome**
退役 `app/chain/media.py` 单体,由同名 `app.chain.media` 包承接 recognition、plugin event、
auxiliary、projection、search、catalog、path、album 和 cache。包根只惰性暴露稳定
`MediaChain`;Facade 保持直接 `ChainBase` MRO、Singleton/pickle 类身份和官方插件真实调用签名,
内部 owner 与刮削兼容符号不从包根重复导出。
目录缓存使用有界 LRU、文件内容签名、深拷贝隔离与同步/异步单飞;等待者取消不会反向取消共享
flight,符号链接目录别名不会因物理路径相同而错误共享专辑线索。跨 Chain 音乐来源复用只依赖公开
`MusicMetadataSourceChain`,架构门禁拒绝跨 owner 导入下划线私有合同。
**Evidence**
- Media/架构/插件端点集中回归 `551 passed`;锁定全量 `7244 passed, 9 skipped`。
- Pylint `10.00/10`、零重复;mypy 低水位 `10686 / 597`,Ruff 低水位 `661`,复杂度、宿主
dependency/runtime contract baseline 全部通过。
- 最新官方插件仓 `aa40b961a2093474a77edd9eed88844ff07425ac` 的 MediaChain 消费者 15 个,
official-plugin baseline 与 V3 聚焦测试 `130 passed`;`app/plugins/**` 未扫描、未修改。
- 交付提交 `13c4a71f42d189f618ac8663afb967d1517a79bb` 已推送;本地、`origin/v3` 与远端引用
一致,ahead/behind `0/0`。
### S3-L7 Agent/System/Plugin API
**Status:** `DELIVERED`
**Outcome**
把 WebAgent SSE/file/audio、System nettest/log/update 与 Plugin market 入口中的可复用用例编排下沉到
Application owner;endpoint 只保留请求鉴权、参数/响应映射和流式传输适配,不直接组合具体 Adapter、
Helper、Manager 或跨多个领域状态。
**Acceptance**
- 先以真实入口调用图确认厚方法、I/O、状态和生命周期边界,再按独立用例迁移,不复制 endpoint 实现。
- Application 自有明确 Port/DTO,具体 Adapter 和 Runtime manager 只由 startup/composition 注入。
- 旧插件兼容只经 SDK/Compat;canonical endpoint 不保留旧业务实现或重复导出,`app/plugins/**` 排除。
- Agent/System/Plugin API 专项、架构/外联/类型/复杂度门禁及锁定全量通过后才可交付。
**Evidence**
- `app/api/endpoints/agent.py` 从 2,000 余行收敛为传输层;WebAgent 事件、会话、附件和音频用例由
`app/application/messaging/agent.py` 承担,运行时实现进入单词命名的 `app/agent/web.py`。
- System 网络探测目录、HTTPS/重定向准入和内容校验进入 `app/application/network.py`;Plugin 市场投影、
Release、评分和目录读写分别由 `catalog.py`、`release.py`、`rating.py`、`folders.py` 唯一拥有。
- 提交 `f38637dceaa2b7fedc8ab30a41aaad3d67392ec5` 已推送;本地锁定全量 `7256 passed, 9 skipped`,
Pylint `10.00/10`、架构/外联/类型/复杂度门禁和官方插件基线通过,远端 ahead/behind `0/0`。
### S5-L1 PluginHelper/PluginManager
**Status:** `DELIVERED`
**Outcome**
清除 `PluginHelper`、`PluginManager` 中仍混合的市场、包、依赖、备份、健康检查和运行时构造职责;
每项能力归入现有单一 owner,稳定插件 ABI 只由受控 Facade/SDK/Compat 暴露。
**Acceptance**
- 先审计真实宿主与官方插件调用图;现有 owner 能承接的能力不得新建近义文件或重复 Facade。
- 多文件能力使用同名包和单词子模块,包根不得为宿主提供重复导出;`app/plugins/**` 排除。
- PluginHelper/PluginManager 专项、兼容基线、架构门禁及受影响测试全部通过后才可交付。
**Delivery (2026-08-29)**
- `PluginHelper` 仅保留精确旧 ABI、安装 Gateway 和 owner 委托;市场传输、索引、Release 与本地候选
由 `PluginMarketTransport` 唯一拥有。
- `PluginPackageManager`、`PluginDependencyInstaller`、`PluginRuntimeHealth` 分别唯一拥有包事务、依赖安装
与运行环境检查/恢复;API、Agent 与 Manager 不再保留物理删除或具体 Adapter 构造。
- `PluginManager` 只消费 startup 提前注入的 typed `PluginRuntime` factory;Compat/SDK 无重复 canonical 导出。
- 最新官方插件仓 `d5a786882` 基线与兼容探针通过;架构全集 `220 passed`,锁定覆盖率全量
`7302 passed, 9 skipped`,Pylint `10.00/10` 且零重复,Ruff `654`、mypy `10496`,覆盖率低水位
Application `80.92%`、Domain `79.87%`。实现提交 `fa40f29df` 已推送,远端 ahead/behind `0/0`;
GitHub 工作流已触发,不把 queued/in_progress 状态记作远端绿色。
### S5-L3 Domain projection
**Status:** `DELIVERED`
**Outcome**
`app/domain/context.py` 继续唯一拥有 canonical `MediaInfo` 类型、构造参数、属性和四个历史 setter ABI;
TMDB、豆瓣、Bangumi、AniList 详情到统一字段的规则一次迁入 `app/domain/projection/`,接收只读快照并
返回字段映射,不修改来源或当前输入。TMDB 图片构造器归 `tmdb.py` owner,宿主调用方直接导入来源 owner。
**Compatibility and gates**
- `app.core.context` 继续由现有精确 Compat 映射到 `app.domain.context`,未新增 SDK/Compat 或包根重复导出。
- `app/schemas/context.py` 仍是传输 DTO,不复制 setter 或投影业务;`app/plugins/**` 未改动。
- 架构门禁固定投影包文件集合、空包根、setter 薄委托和宿主禁用 `MediaInfo` helper;golden、输入不可变、
多来源顺序、字段对齐与 setter 签名测试共同守住行为和 ABI。
**Local verification (2026-08-29)**
- 基于 `origin/v3@d17d62e95` 锁定全量 `7375 passed, 9 skipped`;投影/响应/来源/Compat/架构专项全绿。
- scoped Pylint `10.00/10` 且零重复;新 `domain/projection` owner mypy 零错误,strict frontier、
Ruff、mypy、复杂度和宿主依赖低水位均通过。
- 最新官方插件仓 `2d6946eb1` 兼容基线通过;`app/plugins/**` 零改动,包根只有说明且无重复导出。
### S5-L4 Startup composition
**Status:** `DELIVERED`
- configuration、database、network、security、agent、server、outbox、subscription 与 Chain 的具体构造
全部归入单词型 `startup/composition/*` owner;initializer 只消费 composition API。
- `RuntimeDependencies`、HostRuntime、命名领域 Runtime 与旧 `ApiDataPorts` 投影共享同一对象身份,
不再由 initializer、Facade 或兼容入口二次构造。
- lifecycle 通过显式逆序 reset manifest 撤销 Provider;数据库 worker 未收敛时保留 owner 与连接供诊断,
启动失败复用同一正式清理路径。
- 2026-08-29 真实进程启动达到 `Application startup complete`,`/health/live` 与 `/health/ready`
均返回 HTTP 200,随后达到 `Application shutdown complete`;验证未加入 revision 白名单、自动 stamp、
跳过迁移或环境判断脚本。
### S5-L5 Sync/async 重复清零
**Status:** `DELIVERED`
- Search/TVDB、MusicBrainz、TheAudioDB、AcoustID、AniList、Bangumi、ServerReport、Memory、Yema、
通知、豆瓣、媒体识别、目录源、Indexer、TMDB API 与 Recommend 的双 ABI 共用请求计划、响应投影、
回退条件和业务状态机;同步/异步入口只保留真实 I/O 差异。
- 移除 TVDB 整段同步方法的伪异步线程包装;网络客户端和数据库 Session 均按调用或事件循环持有,
不跨事件循环复用。
- 新增或改造的 parity/生命周期测试均为 pytest-native,并恢复 provider、singleton、cache、事件循环与
`sys.modules`;本轮未新增 `unittest.TestCase`。
- `5ba7e6170` 完成本叶最后一批共享决策,mypy 低水位降至 10,008,Ruff 低水位降至 633,
Pylint 保持 `10.00/10` 且重复代码为零。
### S5-L6 最终兼容与交付
**Status:** `DELIVERED`
- 22 个不符合规范的 canonical 文件名已归并为单词文件或同名职责包;旧实现文件和主程序包根重复导出
已删除,宿主调用方直接导入真实 owner,兼容只由精确 SDK/Compat manifest 提供。
- 官方插件仓已同步至 `161fce34caa31deb7d82dd50a31f217d5e6784c2`;285 个 Python 文件的
ABI/导入语义基线不变,仅更新 provenance。
- `app/plugins/**` 始终作为插件副本排除,未修改、未纳入基线写入或提交。
- 锁定串行覆盖率全量 `7659 passed, 9 skipped`;Application/Domain 低水位提升并固化为
`81.61%` / `81.03%`。Host 架构、Event/Egress policy、复杂度、async 阻塞、TaskRegistry、
service locator、mypy/Ruff ratchet、启动性能、Pylint `10.00/10` 与依赖一致性均通过。
- exact-head 失败后已将 canonical `SearchChain` Facade 改为显式类型转发,并修正 API/Workflow 的
Optional、Domain/Schema DTO 边界;2026-08-30 最终本地锁定全量 `7659 passed, 9 skipped`,
CI 同构架构测试 `197 passed`,mypy 低水位降至 `9,987`,Ruff、Pylint `10.00/10`、复杂度、
async、TaskRegistry、service locator、启动性能与依赖快照门禁均通过。
- `4575b11d8` 修复 canonical `SearchChain` 类型转发并更新真实低水位,`c204e2e97` 修正架构测试对
Chain owner 的直接导入;两者均已推送并进入 `origin/v3` 祖先链。精确 head `c204e2e97` 的
Unit Tests `33269394727` 与 Pylint `33269394716` 全绿,本地与远端 ahead/behind 为 `0/0`,
S5-L3 至 S5-L6 的最终远端门禁统一闭环。
### S1-L1.1 Durable admission
**Status:** `DELIVERED`
**Outcome**
把“接受整理任务”变成真正的 durable admission:Application 先通过类型化 Port 在独立事务中
commit pending,再尝试写入进程内队列。队列写入失败或进程在 commit 后退出时,持久记录仍能成为
后续恢复起点;持久化失败则不允许任务进入队列。该叶只结清 admission 所有权和顺序,不预先宣称
planning、lease、幂等执行或终态恢复已经完成。
**Ownership**
- `app/application/transfer/workflow.py` 拥有 admission DTO、Protocol、结果语义与 persist-before-enqueue 编排。
- `app/db/adapters/` 提供短 Session/UoW 的 Transfer pending 持久化实现;`app/db/oper/` 只接收
adapter 拥有的 Session 并 stage/flush。
- `app/startup/` 负责构造并注入 adapter,宿主 Chain 不再取得 raw/`Any` `TransferPendingOper`。
- `app/db/models/transferpending.py` 与配套 Alembic migration 只承载该叶需要的 durable admission
schema,并验证既有记录升级及 downgrade。
- Transfer queue、pending repository、migration、架构边界及兼容回归测试。
**Excluded**
- 不在本叶实现 planning checkpoint、lease/heartbeat、文件操作幂等、历史结算或 `manual_review`;
这些分别由 `S1-L1.2` 至 `S1-L1.4` 完整交付。
- 不修改插件公开 Transfer ABI、旧插件导入路径或第三方插件行为;必要兼容只通过统一 Compat/Legacy
层委托新的 canonical admission 实现。
- 不修改或扫描 `app/plugins/**` 插件副本。
- 不保留宿主 canonical raw/`Any` pending port 与新 typed Port 两套正式入口。
**Acceptance**
```bash
.venv/bin/python -m pytest \
tests/test_transfer_queue_service.py \
tests/test_transfer_pending_replay.py \
tests/test_transfer_admission_migration.py \
tests/test_db_transferpending_queries.py \
tests/test_database_migration_startup.py \
tests/test_architecture_dependencies.py \
tests/test_legacy_import_compat.py -q
.venv/bin/mypy --config-file mypy.ini
.venv/bin/python scripts/architecture/baseline.py --check-host --diagnostics
.venv/bin/python scripts/architecture/ruff_ratchet.py
.venv/bin/python scripts/architecture/mypy_ratchet.py
uv run --locked --no-sync python tests/run.py
git diff --check
```
**Delivery**
- 单一提交主题:交付 Transfer durable admission、类型化持久化边界及可逆 migration。
- 提交前证明 pending commit 发生在 enqueue 之前;持久化失败不入队,enqueue 失败或 commit 后崩溃
均保留可恢复记录;宿主 canonical 路径不再导入或取得 raw/`Any` `TransferPendingOper`。
- 运行锁定全量测试与 scoped Pylint;插件公开 Transfer ABI 和统一兼容导入测试必须保持通过。
- 推送 `origin/v3` 后确认提交祖先关系、远端 SHA 和 ahead/behind `0/0`。
**Local verification (2026-08-27)**
- 锁定全量:`6,496 passed, 7 skipped`;跳过项包含本机未配置隔离库的 PostgreSQL migration
用例,SQLite upgrade/downgrade/re-upgrade 已真实执行。
- scoped Pylint:`10.00/10`;host dependency baseline、Ruff ratchet、mypy ratchet 与
`git diff --check` 全部通过。
- failure injection 已覆盖 admission 失败不入队、batch/enqueue 失败保留记录、批次返回失败、
queue -> worker -> terminal discard 稳定身份,以及 Legacy TransferTask 序列化字段不变。
### S1-L1.2 Planning checkpoint
**Status:** `DELIVERED`
**Outcome**
把 durable admission 推进为可独立恢复的 `accepted -> provider_pending -> planned` 状态:准入时冻结
版本化请求 JSON 和 SHA-256 指纹;存在旧插件 provider 时先 CAS 提交精确身份、顺序和原始 ABI 参数,
全部返回空后才由 FileManager 只读规划目标及有序叶操作,并通过第二次 CAS 提交宿主 checkpoint。
任何对应 checkpoint 提交前都不允许进入其文件副作用;`provider_pending` 重放只消费冻结调用,
`planned` 重放只消费冻结 resolved 上下文、目标和操作,不重新访问在线识别、目录选择或重命名配置。
**Ownership and compatibility**
- `app/application/transfer/workflow.py` 拥有 planning input、plan item、checkpoint 和状态错误合同;JSON 版本、
指纹及 resolved 上下文均可跨进程 round-trip。
- `app/modules/filemanager/transhandler.py` 是唯一目标规划与文件执行实现;`FileManagerModule.transfer`
与 `TransHandler.transfer_media` 已删除,不保留第二套重命名、覆盖或目录递归逻辑。
- `app/db/adapters/transfer/admission.py` 通过短 Session/UoW 提交 checkpoint;Oper 只负责带状态和指纹条件的
stage,3.0.14 migration 可升级、降级并在中断后重跑。
- cleanup intent 随准入输入冻结。宿主路径由 FileManager 在 `TransferIntercept` 放行后、任何文件写入前
执行;legacy provider 路径为保持旧 ABI 顺序,在全部冻结引用解析成功后、调用 provider 前执行。
strict 查询确认目标不存在才视为幂等成功,查询或删除失败抛错并保留对应 checkpoint 供重试;provider
全空后提升的宿主 checkpoint 会记录 cleanup 已完成,禁止二次查询或删除。
- 插件公开 Transfer 方法签名、事件类型和 payload 不变;旧 provider 身份和顺序随 checkpoint
冻结,提交后由统一 dispatcher 精确解析并严格执行,缺失或异常时明确失败而不静默换路;全部返回空
才生成宿主 plan,并以第二次 CAS 提交 `planned` checkpoint 后执行。`ChainBase.transfer` 仅委托启动
组合根注入的 canonical durable
command,内部 plan/execute 合同不向插件调度;新 DTO 不从包根重复导出,`app/plugins/**` 插件
副本不参与改造。
**Excluded**
- 本叶不引入 claim、lease、heartbeat、attempt、执行步骤幂等或 `manual_review`;这些由
`S1-L1.3` 和 `S1-L1.4` 交付。
- 本叶当时不单独承诺文件操作成功后到历史结算前的未知结果;该能力现已由 `S1-L1.4` 和
`S1-L1.5` 的持久步骤账本、严格探测、`manual_review` 与 task-aware settlement 完整交付。
**Local verification (2026-08-27)**
- planning、持久化、迁移、兼容、replay 和 worker 聚焦回归:`224 passed, 2 skipped`;跳过项仅为
本机未配置隔离 PostgreSQL,SQLite upgrade/downgrade/re-upgrade 已覆盖。
- 完整本地套件:`6,578 passed, 8 skipped`;架构回归:`174 passed`;scoped Pylint `10.00/10`;
host baseline、Ruff/mypy ratchet 与 `git diff --check` 通过。
- failure injection 覆盖 commit 前零文件副作用、commit 后崩溃重放、离线 resolved context 恢复、
配置漂移仍使用冻结 target storage、规划失败留痕、旧 provider 提交后短路、严格异常、空结果两阶段
fallback、缺失引用零 cleanup,以及 cleanup 顺序/幂等/瞬时失败。
### S1-L1.3 Lease 与恢复调度
**Status:** `DELIVERED`
**Outcome**
为 durable transfer 增加可过期、可接管且带 fencing token 的执行租约。`owner` 只用于标识执行者,
每次有效 claim 生成的新 token 才是后续 heartbeat、checkpoint、失败留痕和终态删除的授权;所有写入
必须同时匹配当前且未过期的 token,过期 worker 即使稍后恢复也不能修改新 owner 的记录。
`accepted`、`provider_pending`、`planned` 仍是业务 planning phase,lease 与其正交:claim、heartbeat
和 takeover 不改变 phase,恢复 worker 继续按冻结 checkpoint 决定执行路径。启动回放与同进程恢复共用
唯一调度入口:恢复任务在入队前完成原子 claim 并绑定 token,新 admission 由 worker 在任何副作用前
claim。已经 claim 的恢复任务不在 worker 内二次 claim。
**State machine**
| 当前租约 | 操作 | 结果与 fencing 约束 |
|---|---|---|
| 无租约 | `claim` | 生成新 token、设置 owner/到期时间并递增 attempt;phase 不变 |
| 当前租约未过期 | 同 token `heartbeat` | 只延长当前租约;owner、token、attempt 和 phase 不变 |
| 当前租约未过期 | 任意再次 `claim`,包括同 owner | 原子拒绝,不递增 attempt、不入队、不执行副作用 |
| 当前租约已过期 | `heartbeat` 或旧 token 写入 | 原子拒绝;旧租约不可复活 |
| 当前租约已过期 | 新 worker `claim` | 生成新 token 并递增 attempt;旧 token 永久失效,phase 不变 |
| 当前 token 有效 | checkpoint、失败留痕或终态删除 | 仅精确 token CAS 成功;状态提交后不得由旧 worker 覆盖 |
任意时刻一条 pending 记录最多只有一个数据库认可的有效 fencing owner。该保证不等同于外部文件、
插件或历史副作用 exactly-once;租约在不可中断调用期间过期时,旧调用的外部结果仍可能未知。
**Scheduling and shutdown**
- 启动回放和同进程恢复只调用一个 claim-and-schedule 入口;禁止另建 list-then-enqueue 回放路径或
第二个 retry owner。批量恢复必须先原子 claim,再把已绑定 token 的任务交给唯一 worker 队列。
- heartbeat 由 Transfer worker 生命周期拥有,不创建游离后台 owner;停止时先封口新 claim 和调度,
再通知并有限等待 worker,heartbeat 在 worker 存活期间继续维持其租约。worker 收敛后才停止
heartbeat;超时时保留仍存活的 worker/heartbeat owner 并返回失败。
- checkpoint、失败留痕和 pending 删除在同次持久化写入中执行 token CAS;租约丢失后 worker 停止
继续提交状态,不以预查询替代 fencing,也不把 stale mutation 伪装成成功。
- 确定性失败只确保唯一 scheduler 存在,不即时唤醒;新建的失败恢复 owner 首次扫描先等待固定轮询
周期。损坏持久投影按单任务事务回滚,再以无有效租约 CAS 留下去重诊断,不能饿死后续健康记录。
- 精确旧 `app.db.transferpending_oper` 导入只解析到 `app/sdk/_legacy` 门面;canonical Model、Oper、
Application Port 不保留旧 list-then-act 或无 fencing mutation。兼容 `discard/clear` 也不得删除任何
带 token 的有效或过期 claim。
**Excluded**
- 本叶不承诺文件、legacy provider、历史写入或其他外部副作用 exactly-once,也不以延长 lease
掩盖不可判定结果。
- 文件步骤幂等键、逐步结果 checkpoint、崩溃后未知结果判定、唯一 retry owner 和
`manual_review` 终态由 `S1-L1.4` 完整交付。
- 不增加 `processing` 等与 planning phase 重复的业务状态;不修改插件公开 Transfer ABI,不扫描或
修改 `app/plugins/**`,不恢复宿主旧 pending 入口或重复导出。
**Acceptance matrix**
| 场景 | 必须证明 |
|---|---|
| 新 admission 与恢复记录竞争 | 只有 claim 成功者入队并执行;失败者零文件副作用 |
| 两进程同时 claim 同一记录 | 仅一个新 token 成功,attempt 只按真实新 claim 增长 |
| heartbeat 与 takeover 竞争 | 未过期 heartbeat 可续租;过期租约不可复活,接管 token 唯一有效 |
| 旧 worker 延迟提交 | checkpoint、失败留痕和删除均被 token CAS 拒绝,不覆盖新 owner |
| 三种 planning phase 恢复 | claim/续租/接管保持 phase,并消费各自冻结输入或 checkpoint |
| 启动与同进程恢复同时触发 | 只经过唯一 scheduler 入口,同一任务只有一个 worker owner |
| 正常与超时关闭 | 先封口后有限等待;存活 scheduler/heartbeat/worker 不丢失 owner、不报告成功 |
**Failure injection matrix**
| 注入点 | 预期持久结果 |
|---|---|
| claim commit 前崩溃 | 无新 token、attempt 不变,可由后续 worker claim |
| claim commit 后、入队前崩溃 | 租约到期后可接管,新 token fencing 旧 worker |
| worker 执行前 lease 丢失 | 不执行文件副作用,不提交失败或终态状态 |
| heartbeat commit 前后崩溃 | 仅已提交到期时间生效;过期后不可用旧 token 续租 |
| checkpoint/失败留痕提交时被接管 | stale CAS 失败,新 owner 的 phase、错误和 token 不被覆盖 |
| 终态删除提交时被接管 | stale delete 为零行,pending 保留给当前 owner |
| shutdown 时 scheduler、worker 或 lease release 阻塞 | 有限等待返回失败并保留 owner/heartbeat,禁止清句柄后重建重复 owner |
本叶验收还必须运行 lease persistence、replay/worker、startup lifecycle、migration、架构与兼容聚焦
测试,以及锁定全量测试和 scoped Pylint;插件 ABI 只经统一 Compat/SDK 验证。