From 8de5bf5e78ac675990db628f844ce8ce2f23ce61 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 1 Sep 2026 17:01:05 +0800 Subject: [PATCH] docs(subscribe): record governance acceptance evidence --- ...iepilot-subscription-governance-roadmap.md | 51 +++++++- ...scription-governance-scale-2026-09-01.json | 112 ++++++++++++++++++ 2 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 docs/refactor/subscription-governance-scale-2026-09-01.json diff --git a/docs/refactor/moviepilot-subscription-governance-roadmap.md b/docs/refactor/moviepilot-subscription-governance-roadmap.md index e131184e2..f1306be20 100644 --- a/docs/refactor/moviepilot-subscription-governance-roadmap.md +++ b/docs/refactor/moviepilot-subscription-governance-roadmap.md @@ -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 场景 diff --git a/docs/refactor/subscription-governance-scale-2026-09-01.json b/docs/refactor/subscription-governance-scale-2026-09-01.json new file mode 100644 index 000000000..9b2cef136 --- /dev/null +++ b/docs/refactor/subscription-governance-scale-2026-09-01.json @@ -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 +}