Files
MoviePilot/docs/code-quality.md
DDSRem e0642eafa7 chore(types): reduce v3 host mypy debt (#7057)
* chore(types): clear AniList endpoint mypy debt

* chore(types): clear Douban endpoint mypy debt

* chore(types): clear recommendation endpoint mypy debt

* chore(types): clear subscription notification mypy debt

* chore(types): clear SDK string compatibility mypy debt

* chore(types): ratchet v3 host mypy across remaining files

* chore(types): reduce v3 host mypy debt and retire ledger

* chore(types): reconcile host snapshot with upstream HTTP pool

* test(types): cover host paths exposed by mypy cleanup
2026-10-18 09:37:49 +08:00

114 lines
9.2 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.
# 代码门禁:结构指标与扩围建议
2026-09-30 对 `v3@b4175a4f88421cf3515eeb651b8a4febe836642f` 的门禁进行审计,并在其上调整
复杂度与严格类型检查。提交前衔接到最新 `v3@4a2ef7570659d587c2ebbddf02177443237734dc`,
初始结构基线按该版本重新采集;宿主业务源码与该版本完全一致。以下区分已落地规则与后续建议。
## 行数为什么不再阻断
旧 `complexity.py` v1 对公共 API 入口设置 80 行预算,对 Application/Chain 公共入口设置
150 行预算;v2 对 API、Application、Chain、Scheduler 的函数、类、文件设置
150/500/1000 行预算。函数和类使用 AST `end_lineno - lineno + 1`,会把 docstring、
注释、空行和参数换行计入。它衡量源码尺寸,并不衡量分支复杂度;压缩格式能够降低数值,
补充契约说明却可能触发回退。同一长函数还可能同时消耗函数、类和文件三份预算。
源码尺寸仍适合寻找职责混杂热点,但不适合作为逐行禁止增长的合并条件。拆分应以稳定职责、
可测试行为和清晰依赖为依据,不能为了满足行数而删除说明或搬运代码。
## 当前复杂度合同
`scripts/architecture/complexity.py` 使用 `uv.lock` 锁定的 Ruff,独立执行以下两条规则:
| 指标 | 新增函数上限 | 存量处理 |
| --- | --- | --- |
| C901:McCabe 圈复杂度 | 15 | 超限项按函数登记,只能保持或下降 |
| PLR1702:嵌套控制块深度 | 5 | 同函数多个告警取最大深度,只能保持或下降 |
| 方法/类/文件源码尺寸 | 无硬限制 | 超过 150/500/1000 行出现在观察报告 |
范围为所有 `app/**/*.py`,只排除 `app/plugins/**` 运行时插件副本;包含 Agent、Runtime、
Modules、Adapters、Startup、Domain、DB、Workflow、私有方法、dunder 方法和嵌套函数。
局部 Ruff 配置、`noqa` 与 `.gitignore` 不会隐藏这些指标。
基线身份使用“规则 + 相对文件路径 + 完整词法函数名”,不保存物理行号;同词法名的重复定义
按源码顺序附加序号,避免 property 或条件重定义覆盖预算。移动或重命名超限函数会成为新项,
需要在职责迁移中单独审查,不能自动挪用旧额度。不同函数的增减也不能互相抵消。
首次从行数策略迁移到结构策略的快照为 C901 138 个函数、PLR1702 39 个函数;同一函数可能
同时出现在两类中。这是本次迁移的历史证据,不要求后续数字保持不变。源码观察报告有
62 个长函数、123 个大类和 66 个大文件。没有为建立新基线修改业务源码。
当前 `tests/fixtures/architecture/complexity-baseline.json` 是指标事实源,并记录策略版本
和阈值;旧 `complexity-v2-baseline.json` 与 `--v2` 已退役。门禁行为为:
- 新增超限或既有超限增长:检查失败,`--write` 也拒绝覆盖已有基线。
- 指标降低、回到阈值内或函数删除:先提示低水位未固化,审查后 `--write` 收紧基线。
- 语法错误、工具错误、不完整输出、无法映射的诊断、缺失源码或基线:失败,不能当作零债务。
- `--report` 输出结构指标和源码尺寸。CI 上传 `complexity-report`,失败时也保留已生成报告。
```bash
uv run --locked --no-sync python scripts/architecture/complexity.py --report /tmp/moviepilot-complexity-report.json
uv run --locked --no-sync python scripts/architecture/complexity.py --write
uv run --locked --no-sync python scripts/architecture/complexity.py
```
`--write` 不是接受新债务的审批入口。首次初始化、算法升级、阈值调整和职责迁移需要审查
源码、指标 diff 和相关测试;不得删除已有基线后重建来绕过增长检查。
算法口径跟随锁定的 Ruff,未自行实现另一套复杂度定义。
[C901 的配置](https://docs.astral.sh/ruff/settings/#lint_mccabe_max-complexity) 衡量控制流分支,
不是运行时耗时、内存复杂度或完整认知复杂度。
[PLR1702](https://docs.astral.sh/ruff/rules/too-many-nested-blocks/) 仍属 preview,只有此独立
检查启用 preview;升级 Ruff 时应跑真实 Ruff 回归用例并审查指标 diff,不自动刷新基线。
短路布尔表达式、推导式、跨函数调用链和职责耦合仍需行为测试与架构审查。
## 类型检查的本次扩围
`mypy.ini` 的严格范围从 42 个文件扩大到 45 个,新增:
- `app/runtime/tasks.py`:后台任务登记、提交和关闭。
- `app/scheduler/registry.py`:调度器代际、所有权与句柄。
- `app/startup/lifecycle/components.py`:组件启动与关闭。
三个文件及完整严格清单实测通过,不增加忽略规则或类型债务。全宿主 mypy ratchet 继续保留。
严格清单对项目内部的 `app.*` 统一采用 `follow_imports = silent`,并将此策略应用于
内部 `.pyi`,读取真实接口;FastAPI 和 Click 同样读取供应方已声明的类型,
其它外部依赖沿用 `follow_imports = skip`,保留 `ignore_missing_imports = True`。`silent` 只影响导入的非目标文件,`files` 中的目标仍然
严格检查;全宿主 Linux 分析显式扫描整个 `app`,继续报告未清零文件的全部历史债务。
此前将已有完整分析零错误的文件统一纳入严格清单,保留之前的全部目标,新增 551 个
受保护文件。扩围前后对完整宿主逐文件、逐错误码核对,导入策略没有隐藏任何既有错误;
订阅请求仓储补齐真实查询接口、API 移除恒等 cast 后,另消除 8 项原有类型错误。
门禁扩围表示已清零代码受到保护,不表示同时修复了这些文件的业务实现。
FastAPI/Click 合同治理移除了 107 条已失效的类型忽略,修正 OpenAPI 状态码键和
真实协程返回类型,并从定义方导入 FastAPI 的同一对象。全宿主诊断下降 417 项,
逐文件、逐错误码没有增长;新增清零的 `app/factory.py` 进入严格清单。仍有历史
债务的文件继续受完整分析的低水位约束,不因装饰器类型恢复而宣称整文件已清零。
批量清理类型债务时,仍逐文件确认生产文件在完整宿主分析中清零,才纳入 `mypy.ini`
严格清单。受影响的行为、兼容、Pylint 与适用门禁通过后,按本轮授权统一提交并通过
同一个 PR 交付,核对最终提交的远端必需 CI。验收同时检查完整严格清单和全宿主
Linux 分析,确认其他文件及错误码无增长;
只把经完整扫描确认的真实下降写入类型基线,不以忽略诊断或放宽类型合同接受回退。
严格文件清单以 `mypy.ini` 为准,已记录的逐文件、逐错误码债务以
`tests/fixtures/architecture/mypy-baseline.json` 为准。动态插件与 Agent 工具合同、
Pydantic 可空输入、事件载荷、工作流上下文、文件系统代理及外部类型桩等问题,
在确定行为和兼容合同前不宣称清零或纳入严格清单。
事件运行时的契约、注册、绑定、调度、错误处理和 payload 快照六个实现模块均已纳入
严格范围,完整宿主分析中 `app/runtime/event/` 目录的类型错误已清零。
## 后续补充顺序
| 优先级 | 现有缺口 | 建议落点与验收 |
| --- | --- | --- |
| P0 | GitHub `v3` 查询结果 `protected=false`,生效分支规则为空 | 增加固定名称的 CI 汇总检查,再配置 required checks;汇总要正确处理成功复用、取消和失败,不能把所有 skipped 当成功。远端规则属于仓库策略,当前改动未更改它 |
| P1 | 覆盖率曾只阻断 Application/Domain 的聚合行覆盖率 | 已扩展到除 `app/plugins` 外全部 `app` 文件;存量逐文件低水位只增不减,新文件至少 80%,仍不设置分支下限;macOS 报告只作诊断 |
| P1 | strict frontier 对部分父类和传递依赖仍视为 Any | 按“父类/Port/DTO → lifecycle → 调用者”扩到 Agent session、Scheduler lifecycle、插件管理器;先消除真实类型问题,再纳入,不增添 ignore |
| P1 | 漏洞审计位于正式版/Beta 发布链路,依赖兼容 PR 工作流只做平台安装验证 | 在依赖变更 PR 对同一 locked 运行时依赖做 pip-audit;不要复制一份依赖清单,也无需每个普通源码 PR 重跑多架构镜像扫描 |
| P2 | 常规 Ruff 仅启用 E4/E7/E9/F/I | 优先试运行 B006(可变默认参数)、B023(循环变量闭包捕获)、B018(无效表达式);审查误报后按范围纳入。审计时 app 中 B006 为 0、B023 为 5、B018 为 1,不自动把新诊断认定为已证实的 bug |
| P2 | 部分其它 ratchet 写入器仍能直接覆盖增长,Ruff/mypy 按文件计数可能发生同规则问题抵消 | 统一已有基线的写保护;对重点规则考虑稳定符号/诊断指纹。先补“无法洗入回退”的行为用例,避免只测 fixture 字符串 |
依赖方向、循环依赖、直接出站、异步阻塞、后台任务所有权、原生并发和 service locator
已经有门禁,应保留并按实际漏报补强,不重复添加同义规则。鉴权、数据持久化、幂等、取消、
崩溃恢复和插件 ABI 继续由行为/契约测试保护,源码尺寸或复杂度分值不能代替这些测试。