docs(subscribe): record governance acceptance evidence

This commit is contained in:
jxxghp
2026-09-01 17:01:05 +08:00
parent e57288b5ea
commit 8de5bf5e78
2 changed files with 157 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
# MoviePilot 订阅执行治理
> 状态:`active(2026-09-01 已由 MoviePilot v3 接管)`
> 当前叶:`SUB-GOV-003B`
> 状态:`completed(2026-09-01)`
> 当前叶:`none(全部叶已完成或按条件停止)`
> 迁移来源:`V3-RDY-009A`、`V3-RDY-009A1`、`V3-RDY-009A1B`、
> `V3-RDY-009A1C`、`V3-RDY-009A1D`
> 适用范围:V3 订阅刷新、匹配、兜底搜索、下载提交与用户状态闭环
@@ -236,11 +236,11 @@ RSS/Spider 保持现有逐站点串行刷新,不通过并发化换取几秒收
| `SUB-GOV-002B` | 为兜底搜索建立每站点并发、间隔和错误冷却预算;RSS/Spider 保持基线压力 | 002A | `completed(2026-09-01)` |
| `SUB-GOV-002C` | 联合运行日常 Match、手工搜索和兜底搜索,验证公平性、锁等待、站点压力和失败恢复 | 001D, 002B | `completed(2026-09-01)` |
| `SUB-GOV-003A` | 建立跨入口的订阅级下载幂等、下载器不确定终态和取消补偿 | 001C, 002A | `completed(2026-09-01)` |
| `SUB-GOV-003B` | 后端提供订阅及批次业务状态,前端展示排队、匹配、搜索、提交、完成、失败和取消 | 002A, 003A | `in_progress` |
| `SUB-GOV-004` | 多条订阅记录指向同一媒体时的跨记录季集去重和产品规则 | 003A | `pending(最低优先级)` |
| `SUB-GOV-003B` | 后端提供订阅及批次业务状态,前端展示排队、匹配、搜索、提交、完成、失败和取消 | 002A, 003A | `completed(2026-09-01)` |
| `SUB-GOV-004` | 多条订阅记录指向同一媒体时的跨记录季集去重和产品规则 | 003A | `completed(2026-09-01)` |
当前只激活 `SUB-GOV-003B`。001B–D 是日常 Match 主线;002A–C 是 24 小时兜底搜索主线;003A/B
只有在前两条链路的身份和终态稳定后实施。每个叶子完成验收并更新本表后,才激活下一个满足依赖的叶子。
全部行为叶已完成;条件叶 `SUB-GOV-001E` 因前置治理已消除可重复的本地等待热点而未激活。001B–D 是
日常 Match 主线;002A–C 是 24 小时兜底搜索主线;003A/B 和 004 在身份、终态与提交幂等稳定后完成。
### 6.1 SUB-GOV-001A 验收证据
@@ -360,6 +360,45 @@ RSS/Spider 保持现有逐站点串行刷新,不通过并发化换取几秒收
- 验证:下载幂等、取消补偿、订阅提交与搜索治理 `131 passed`;架构合同、依赖、复杂度和质量 ratchet
`284 passed`;Ruff 低水位 `568`、mypy 低水位 `9,589`,Host 架构基线与 `git diff --check` 通过。
### 6.10 SUB-GOV-003B 验收证据
- 后端新增持久订阅执行状态,将搜索任务、站点预算等待和下载提交账本统一映射为稳定业务状态;订阅列表返回
`execution_status`,批次列表、批次详情和取消使用独立执行端点,并按可访问订阅集合校验批次权限;
- 状态覆盖 queued、matching、searching、waiting_site_budget、preparing、submitting、accepted、retryable、
reconcile_required、failed、cancelling、cancelled 和 completed,且只在真实可取消边界暴露取消能力;
- Alembic `f3c8a1d6b2e9` 持久化任务阶段和状态,旧数据库升级后可继续恢复队列;最终迁移链唯一 head 为
`a7d9e2c4f6b1`;
- 前端提交 `2cb37c6f` 在订阅卡片展示稳定状态和安全错误详情,在列表顶部展示批次进度与取消入口;轮询只在页面
活跃且存在活动执行时运行,终态自动停止;中文简体、中文繁体和英文文案同步;
- 前端验证:`yarn typecheck`、`yarn eslint . --max-warnings=0`、卡片/列表专项 `74 passed`、`yarn build`
通过;使用真实组件和模拟 API 在桌面及 `390x844` 视口检查批次横幅、进度、取消按钮和三类卡片状态,未见
重叠、截断或容器溢出。
### 6.11 SUB-GOV-004 验收证据
- 下载准入在单条订阅幂等键之外增加 canonical 媒体范围身份;同媒体来源、媒体 ID、类型、季集覆盖和下载模式
相同的多条订阅记录共享跨记录防重事实,不依赖标题字符串碰撞;
- 第一条记录已成功时,第二条记录复用成功结果而不再次调用下载器;第一条处于提交中或结果不确定时,第二条
被冻结等待现有事实,不把不确定结果误判成可重试;
- 普通与洗版、不同季、不同集范围或不同媒体身份继续保持独立,避免跨记录治理改变合法下载产品规则;迁移
`a7d9e2c4f6b1` 增加可查询媒体范围键并保持 SQLite/PostgreSQL 可逆升级;
- `tests/test_subscription_download_governance.py` 覆盖跨订阅行成功复用、活动提交阻断、不确定终态和范围隔离;
全量后端测试与架构门禁共同验证旧插件 ABI 和单条订阅行为未回退。
### 6.12 最终规模与全局验收证据
- 固定受控矩阵和原始证据位于 `docs/refactor/subscription-governance-scale-2026-09-01.json`,对应规模测试提交
`93d0631da`;环境为 macOS、Python 3.14.3 和隔离 SQLite,真实外部请求与真实下载均为 0;
- 中档为 100 条订阅、10 个站点、1000 候选、100 个唯一媒体:候选检查从基线 100000 次降为 1000 次,命中
集合相同,事实加载 100 次;首个匹配本地耗时 0.475ms,批次本地耗时 49.783ms;
- 大档为 200 条订阅、20 个站点、1200 候选、100 个唯一媒体:候选检查从基线 240000 次降为 2400 次,命中
集合相同,事实加载 100 次并命中租约 100 次;首个匹配本地耗时 0.55ms,批次本地耗时 113.729ms;
- 两档持久队列均全部完成,每站点峰值在途为 1;重复站点租约和冷却认领均被阻止,任务/站点租约过期后可恢复,
取消批次进入终态,重叠下载只有一次模拟外部提交,重复提交为 0,不确定下载保持冻结;
- 上述耗时只衡量进程内路由和隔离 SQLite 队列,不是生产网络 SLO。最终后端全量验证为
`7712 passed, 9 skipped`;mypy 低水位 `9,588`、Ruff 低水位 `567`,复杂度、依赖、运行时合同、Host
架构基线、API surface、schema exports、数据库技能文档和 `git diff --check` 全部通过。
## 7. 上线前验证与验收
### 7.1 场景

View File

@@ -0,0 +1,112 @@
{
"durable_cases": [
{
"cancel_requested": true,
"cancelled_batch_state": "cancelled",
"cancelled_task_count": 10,
"cooldown_claims_blocked": 10,
"duplicate_download_reused_success": true,
"duplicate_download_submissions": 0,
"duplicate_site_claims_blocked": 10,
"external_download_submissions": 1,
"first_task_completion_local_ms": 24.648,
"name": "medium",
"queue_created_count": 100,
"queue_finished_count": 100,
"queue_state": "completed",
"recovery_batch_id_stable": true,
"running_task_cancelled": true,
"site_count": 10,
"site_peak_inflight_per_site": 1,
"site_recovered_after_lease_expiry": true,
"task_completion_local_p50_ms": 147.463,
"task_completion_local_p95_ms": 258.077,
"task_recovered_after_lease_expiry": true,
"uncertain_download_frozen": true
},
{
"cancel_requested": true,
"cancelled_batch_state": "cancelled",
"cancelled_task_count": 10,
"cooldown_claims_blocked": 20,
"duplicate_download_reused_success": true,
"duplicate_download_submissions": 0,
"duplicate_site_claims_blocked": 20,
"external_download_submissions": 1,
"first_task_completion_local_ms": 36.372,
"name": "large",
"queue_created_count": 200,
"queue_finished_count": 200,
"queue_state": "completed",
"recovery_batch_id_stable": true,
"running_task_cancelled": true,
"site_count": 20,
"site_peak_inflight_per_site": 1,
"site_recovered_after_lease_expiry": true,
"task_completion_local_p50_ms": 291.708,
"task_completion_local_p95_ms": 510.505,
"task_recovered_after_lease_expiry": true,
"uncertain_download_frozen": true
}
],
"environment": {
"database": "SQLite isolated temporary files",
"external_downloads": 0,
"external_requests": 0,
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
"python": "3.14.3"
},
"gates": {
"cancellation_terminal": true,
"candidate_product_avoided": true,
"download_idempotent": true,
"fresh_facts_bounded_by_unique_media": true,
"match_sets_equal": true,
"queue_completed": true,
"site_pressure_bounded": true,
"task_and_site_recovery": true
},
"generated_at": "2026-09-01T07:35:04+00:00",
"match_cases": [
{
"baseline_comparisons": 100000,
"batch_local_ms": 49.783,
"candidate_check_reduction_percent": 99.0,
"candidate_checks": 1000,
"candidate_count": 1000,
"fact_hits": 0,
"fact_loads": 100,
"first_match_local_ms": 0.475,
"match_sets_equal": true,
"media_count": 100,
"name": "medium",
"routed_candidates": 1000,
"site_count": 10,
"subscription_completion_local_p50_ms": 22.854,
"subscription_completion_local_p95_ms": 43.688,
"subscription_count": 100
},
{
"baseline_comparisons": 240000,
"batch_local_ms": 113.729,
"candidate_check_reduction_percent": 99.0,
"candidate_checks": 2400,
"candidate_count": 1200,
"fact_hits": 100,
"fact_loads": 100,
"first_match_local_ms": 0.55,
"match_sets_equal": true,
"media_count": 100,
"name": "large",
"routed_candidates": 2400,
"site_count": 20,
"subscription_completion_local_p50_ms": 53.507,
"subscription_completion_local_p95_ms": 102.284,
"subscription_count": 200
}
],
"method": "Deterministic local controlled sample. Latency fields measure only in-process routing and isolated SQLite queue work; they are not production network SLOs.",
"passed": true,
"revision": "93d0631da33a9b78137875e9deee0b3c8a660846",
"schema_version": 1
}