diff --git a/app/application/messaging/message.py b/app/application/messaging/message.py index e422e3ceb..b09bcfe49 100644 --- a/app/application/messaging/message.py +++ b/app/application/messaging/message.py @@ -963,13 +963,9 @@ class MessageQueueManager(metaclass=SingletonClass): context = copy_context() call = partial(self._send, *args, **kwargs) loop = asyncio.get_running_loop() + submission = partial(loop.run_in_executor, None, context.run, call) # 默认执行器保持空底层上下文,渠道调用只使用当前消息快照。 - await Context().run( - loop.run_in_executor, - None, - context.run, - call, - ) + await Context().run(submission) return self.queue.put({ "args": args, diff --git a/docs/refactor/backend-architecture-review.md b/docs/refactor/backend-architecture-review.md index 408b9e8c6..a9eed92f0 100644 --- a/docs/refactor/backend-architecture-review.md +++ b/docs/refactor/backend-architecture-review.md @@ -83,8 +83,8 @@ | 批次 | 叶子目标 | 状态 | 当前证据/停止条件 | |---|---|---|---| -| 0 | 历史任务清账、现行架构图、外部契约核对和宿主基线对齐 | 已本地验证 | #6472 遗漏的 dependency fixture 和 Schema 导出清单已对齐;架构契约 `71 passed` | -| 1 | Mypy fail-closed,并把 Ruff/Mypy 已下降债务固化为真实低水位 | 待批次 0 | Mypy 1.18.2 在 `message.py` 内部错误退出 2,但脚本误报通过;Ruff 978 对旧基线 1623 | +| 0 | 历史任务清账、现行架构图、外部契约核对和宿主基线对齐 | 已推送 | `d234c7132`;远端同 SHA;ahead/behind `0/0`;架构契约 `71 passed` | +| 1 | Mypy fail-closed,并把 Ruff/Mypy 已下降债务固化为真实低水位 | 已本地验证 | Mypy 完整低水位 11994、Ruff 976;专项 `33 passed`;架构契约 `71 passed`;Pylint `10.00/10` | | 2 | 用全量串行测试初始化非零 Coverage 低水位,并补齐 CI/文档防回退契约 | 待批次 1 | fixture 当前 Application/Domain 均为 0%;最近 CI 仅作参考,必须在最终代码快照本地重建 | | 3 | 收口阶段 62 遗留的 QQ Gateway heartbeat Timer 所有权 | 待批次 2 | Timer 只 cancel 不 join,Gateway 主线程可能在 heartbeat 仍执行时报告停止成功 | | Final | 全仓回归、插件兼容复核、台账定稿和远端一致性验证 | 待前置批次 | 所有准入项已推送;全量测试和适用门禁通过;本地/远端 0/0 | @@ -106,7 +106,8 @@ * `baseline.py --check-host`、`scripts/schema/exports.py --check` 和相关架构契约测试通过, 测试结果为 `71 passed`。 -待完成:提交并推送本批次,记录远端证据。 +交付证据:提交 `d234c7132` 已推送到 `origin/v3`;`git ls-remote` 返回同一 SHA, +`HEAD...origin/v3` 为 `0/0`,且本地 HEAD 是远端祖先。 ### 批次 1:Ruff/Mypy 低水位闭环 @@ -121,6 +122,20 @@ 完整扫描正常退出并生成真实 Mypy fixture;Ruff/Mypy fixture 与当前结果完全相等; 针对性测试、两条门禁和适用静态检查通过;批次独立提交推送。 +本地验收结果: + +* 消息 executor 提交改为零参数 `partial`,保持空底层上下文提交和 worker 请求快照, + Mypy 1.18.2 不再 INTERNAL ERROR;同步 fake loop 已验证提交阶段无关联 ID、渠道回调可见 ID; +* Mypy runner 固定使用 CI 的 Linux/Python 3.14 分析目标,只接受退出码 0/1,拒绝 stderr、 + 缺失/重复摘要和摘要计数不一致;完整扫描覆盖 601 个文件、11994 项。旧 fixture 的 + 342 个文件、6209 项已确认是截断快照;macOS 与 Linux 目标生成结果逐字一致; +* Ruff/Mypy 将增长与低水位滞后分开;已有基线存在增长时 `--write` 被拒绝, + 只有纯下降或缺失的新基线可以写入; +* Ruff fixture 从 1623 收紧到 976,Mypy 和 Ruff 默认路径复跑均通过; +* 质量/CI/上下文/严格类型专项 `33 passed`,架构契约 `71 passed`,改动文件 Pylint `10.00/10`。 + +待完成:提交推送并记录远端证据。 + ### 批次 2:Coverage 低水位闭环 已确认的既有缺口:Coverage 脚本和 CI 已落地,但 fixture 的 Application/Domain 都是 @@ -178,6 +193,8 @@ git rev-list --left-right --count HEAD...origin/v3 | 2026-08-26 | 复验宿主基线 | 只剩 #6472 引入的两条合法依赖尚未写入 fixture | | 2026-08-26 | 收口 #6472 生成契约 | dependency fixture 与 Schema 导出清单已更新;架构契约 `71 passed` | | 2026-08-26 | 后台 owner 历史余项审计 | 仅 QQ heartbeat Timer 满足阶段 62 既有合同和两天准入条件,其余候选关闭或排除 | +| 2026-08-26 | 批次 0 交付 | `d234c7132` 已推送;远端同 SHA;ahead/behind `0/0` | +| 2026-08-26 | 批次 1 本地验收 | Mypy 11994、Ruff 976;专项 `33 passed`;架构契约 `71 passed`;Pylint `10.00/10` | ## 七、本轮停止条件 diff --git a/scripts/architecture/mypy_ratchet.py b/scripts/architecture/mypy_ratchet.py index 5fb33e0bc..4fea7cad8 100644 --- a/scripts/architecture/mypy_ratchet.py +++ b/scripts/architecture/mypy_ratchet.py @@ -1,9 +1,8 @@ """为宿主源码维护 mypy 类型错误只降不增的基线。 与 ``complexity.py`` 同一模式:AST/subprocess 收集当前计数,与 JSON 基线对比, -删除和减少均合法,新增文件或错误码、既有计数增长都会被拒绝。 -基线按 文件 -> 错误码 -> 数量 三级组织,修复单个文件后可用 ``--write`` 收紧基线, -不影响其他文件的存量豁免。 +基线按 文件 -> 错误码 -> 数量 三级组织。任何增长都会被拒绝;债务下降后也必须用 +``--write`` 固化新的低水位,避免已经修复的错误重新获得回退额度。 """ from __future__ import annotations @@ -22,19 +21,23 @@ DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/mypy-baseline.jso # 只扫描宿主源码;app/plugins 是运行时插件副本,质量由插件市场链路自行管理。 MYPY_TARGETS = ("app",) MYPY_EXCLUDES = ("app/plugins",) +MYPY_PLATFORM = "linux" # 形如 app/foo.py:12: error: 消息说明 [error-code];个别错误可能缺代码。 _ERROR_LINE = re.compile(r"^(?P.+?):\d+(?::\d+)?: error: .+?(?:\s+\[(?P[a-z0-9-]+)\])?$") +_ERROR_SUMMARY = re.compile(r"^Found (?P\d+) errors? in ") def run_mypy() -> str: - """以当前解释器运行全量 mypy 并返回 stdout(非零退出码属于预期结果)。""" + """运行全量 mypy;只接受可完整解析的正常成功或诊断结果。""" command = [ sys.executable, "-m", "mypy", "--no-incremental", "--no-pretty", + "--platform", + MYPY_PLATFORM, *MYPY_TARGETS, ] for pattern in MYPY_EXCLUDES: @@ -46,6 +49,36 @@ def run_mypy() -> str: text=True, check=False, ) + if result.returncode not in {0, 1}: + details = result.stderr.strip() or result.stdout.strip() or "mypy 执行失败" + raise RuntimeError(f"mypy 异常退出({result.returncode}):{details}") + if result.stderr.strip(): + raise RuntimeError(f"mypy 在 stderr 输出异常:{result.stderr.strip()}") + + parsed_total = sum( + count + for codes in parse_errors(result.stdout).values() + for count in codes.values() + ) + summaries = [ + match + for line in result.stdout.splitlines() + if (match := _ERROR_SUMMARY.match(line.strip())) + ] + if result.returncode == 0: + if parsed_total or not any( + line.startswith("Success: no issues found") + for line in result.stdout.splitlines() + ): + raise RuntimeError("mypy 成功输出缺少可验证摘要") + return result.stdout + if len(summaries) != 1: + raise RuntimeError("mypy 错误输出缺少唯一的完整摘要") + summary_total = int(summaries[0].group("count")) + if summary_total != parsed_total: + raise RuntimeError( + f"mypy 摘要与已解析错误数不一致:摘要 {summary_total},解析 {parsed_total}" + ) return result.stdout @@ -66,19 +99,38 @@ def parse_errors(output: str) -> dict[str, dict[str, int]]: return {path: dict(codes) for path, codes in sorted(report.items())} +def classify_counts( + baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]] +) -> tuple[list[str], list[str]]: + """把计数差异分为不可写入的回退和可固化的低水位下降。""" + regressions: list[str] = [] + stale: list[str] = [] + for path in sorted(baseline.keys() | current.keys()): + previous = baseline.get(path, {}) + latest = current.get(path, {}) + for code in sorted(previous.keys() | latest.keys()): + old_count = previous.get(code, 0) + new_count = latest.get(code, 0) + if new_count > old_count: + if old_count == 0: + regressions.append(f"{path}: 新增类型错误 [{code}] x{new_count}") + else: + regressions.append( + f"{path}: 既有错误增长 [{code}] {old_count}->{new_count}" + ) + elif new_count < old_count: + stale.append( + f"{path}: 类型错误低水位未固化 [{code}] {old_count}->{new_count}" + ) + return regressions, stale + + def compare_counts( baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]] ) -> list[str]: - """返回新增文件/错误码或既有计数增长问题;删除与减少均合法。""" - problems = [] - for path, codes in current.items(): - previous = baseline.get(path, {}) - for code, count in codes.items(): - if code not in previous: - problems.append(f"{path}: 新增类型错误 [{code}] x{count}") - elif count > previous[code]: - problems.append(f"{path}: 既有错误增长 [{code}] {previous[code]}->{count}") - return problems + """返回类型错误增长和尚未固化的新低水位。""" + regressions, stale = classify_counts(baseline, current) + return [*regressions, *stale] def main() -> int: @@ -88,22 +140,40 @@ def main() -> int: parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE) args = parser.parse_args() current = parse_errors(run_mypy()) + baseline_exists = args.baseline.exists() + baseline = ( + json.loads(args.baseline.read_text(encoding="utf-8")) + if baseline_exists + else {} + ) + regressions, stale = classify_counts(baseline, current) if args.write: + if baseline_exists and regressions: + print("\n".join(regressions)) + print("拒绝写入:当前结果包含类型错误回退,--write 只能固化下降后的低水位。") + return 1 args.baseline.parent.mkdir(parents=True, exist_ok=True) args.baseline.write_text( json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) - print(f"已写入 {args.baseline.relative_to(PROJECT_ROOT)}") + display_path = ( + args.baseline.relative_to(PROJECT_ROOT) + if args.baseline.is_relative_to(PROJECT_ROOT) + else args.baseline + ) + print(f"已写入 {display_path}") return 0 - baseline = json.loads(args.baseline.read_text(encoding="utf-8")) - problems = compare_counts(baseline, current) + problems = [*regressions, *stale] if problems: print("\n".join(problems)) - print("提示:修复后可用 --write 收紧基线;禁止为绕过门禁放宽基线。") + if regressions: + print("先消除类型错误回退;存在增长时禁止用 --write 覆盖基线。") + else: + print("提示:当前只有债务下降,可用 --write 固化新的低水位。") return 1 total = sum(count for codes in current.values() for count in codes.values()) - print(f"mypy ratchet 通过(存量 {total} 个类型错误,只降不增)") + print(f"mypy ratchet 通过(低水位已同步:{total} 个类型错误)") return 0 diff --git a/scripts/architecture/ruff_ratchet.py b/scripts/architecture/ruff_ratchet.py index ea5c651c7..8cd94b7a8 100644 --- a/scripts/architecture/ruff_ratchet.py +++ b/scripts/architecture/ruff_ratchet.py @@ -26,7 +26,14 @@ def run_ruff() -> list[dict[str, Any]]: ) if result.returncode not in {0, 1}: raise RuntimeError(result.stderr or result.stdout or "Ruff 执行失败") - return json.loads(result.stdout or "[]") + diagnostics = json.loads(result.stdout or "[]") + if not isinstance(diagnostics, list): + raise RuntimeError("Ruff JSON 输出不是诊断列表") + if result.returncode == 0 and diagnostics: + raise RuntimeError("Ruff 返回成功但仍输出诊断") + if result.returncode == 1 and not diagnostics: + raise RuntimeError("Ruff 返回诊断状态但输出为空") + return diagnostics def aggregate_diagnostics( @@ -40,22 +47,40 @@ def aggregate_diagnostics( return {path: dict(sorted(codes.items())) for path, codes in sorted(report.items())} +def classify_counts( + baseline: dict[str, dict[str, int]], + current: dict[str, dict[str, int]], +) -> tuple[list[str], list[str]]: + """把计数差异分为不可写入的回退和可固化的低水位下降。""" + regressions: list[str] = [] + stale: list[str] = [] + for path in sorted(baseline.keys() | current.keys()): + previous = baseline.get(path, {}) + latest = current.get(path, {}) + for code in sorted(previous.keys() | latest.keys()): + old_count = previous.get(code, 0) + new_count = latest.get(code, 0) + if new_count > old_count: + if old_count == 0: + regressions.append(f"{path}: 新增 Ruff 诊断 [{code}] x{new_count}") + else: + regressions.append( + f"{path}: Ruff 诊断增长 [{code}] {old_count}->{new_count}" + ) + elif new_count < old_count: + stale.append( + f"{path}: Ruff 低水位未固化 [{code}] {old_count}->{new_count}" + ) + return regressions, stale + + def compare_counts( baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]], ) -> list[str]: - """返回新增规则或既有数量增长;修复和删除均合法。""" - problems = [] - for path, codes in current.items(): - previous = baseline.get(path, {}) - for code, count in codes.items(): - if code not in previous: - problems.append(f"{path}: 新增 Ruff 诊断 [{code}] x{count}") - elif count > previous[code]: - problems.append( - f"{path}: Ruff 诊断增长 [{code}] {previous[code]}->{count}" - ) - return problems + """返回 Ruff 诊断增长和尚未固化的新低水位。""" + regressions, stale = classify_counts(baseline, current) + return [*regressions, *stale] def main() -> int: @@ -65,22 +90,40 @@ def main() -> int: parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE) args = parser.parse_args() current = aggregate_diagnostics(run_ruff()) + baseline_exists = args.baseline.exists() + baseline = ( + json.loads(args.baseline.read_text(encoding="utf-8")) + if baseline_exists + else {} + ) + regressions, stale = classify_counts(baseline, current) if args.write: + if baseline_exists and regressions: + print("\n".join(regressions)) + print("拒绝写入:当前结果包含 Ruff 回退,--write 只能固化下降后的低水位。") + return 1 args.baseline.parent.mkdir(parents=True, exist_ok=True) args.baseline.write_text( json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) - print(f"已写入 {args.baseline.relative_to(PROJECT_ROOT)}") + display_path = ( + args.baseline.relative_to(PROJECT_ROOT) + if args.baseline.is_relative_to(PROJECT_ROOT) + else args.baseline + ) + print(f"已写入 {display_path}") return 0 - baseline = json.loads(args.baseline.read_text(encoding="utf-8")) - problems = compare_counts(baseline, current) + problems = [*regressions, *stale] if problems: print("\n".join(problems)) - print("提示:修复后可用 --write 收紧基线;禁止为绕过门禁放宽基线。") + if regressions: + print("先消除 Ruff 回退;存在增长时禁止用 --write 覆盖基线。") + else: + print("提示:当前只有债务下降,可用 --write 固化新的低水位。") return 1 total = sum(count for codes in current.values() for count in codes.values()) - print(f"Ruff ratchet 通过(存量 {total} 个诊断,只降不增)") + print(f"Ruff ratchet 通过(低水位已同步:{total} 个诊断)") return 0 diff --git a/tests/fixtures/architecture/mypy-baseline.json b/tests/fixtures/architecture/mypy-baseline.json index 7edb706dd..7f88e844e 100644 --- a/tests/fixtures/architecture/mypy-baseline.json +++ b/tests/fixtures/architecture/mypy-baseline.json @@ -1,6 +1,20 @@ { "app/adapters/cache/backends.py": { - "import-untyped": 1 + "import-untyped": 1, + "no-any-return": 1, + "no-untyped-call": 2, + "no-untyped-def": 4, + "operator": 5, + "override": 16 + }, + "app/adapters/cache/redis.py": { + "arg-type": 9, + "no-any-return": 2, + "no-untyped-call": 21, + "no-untyped-def": 16, + "return-value": 2, + "str-bytes-safe": 2, + "union-attr": 27 }, "app/adapters/external/cookiecloud.py": { "arg-type": 1, @@ -18,7 +32,7 @@ "assignment": 3, "attr-defined": 4, "import-untyped": 1, - "misc": 4, + "misc": 2, "no-any-return": 8, "no-untyped-call": 6, "no-untyped-def": 10, @@ -33,7 +47,6 @@ }, "app/adapters/external/plugin/client.py": { "arg-type": 1, - "no-any-return": 2, "type-arg": 6 }, "app/adapters/external/server.py": { @@ -62,6 +75,11 @@ "app/adapters/network/cloudflare.py": { "no-untyped-def": 1 }, + "app/adapters/network/doh.py": { + "assignment": 1, + "no-any-return": 1, + "no-untyped-def": 3 + }, "app/adapters/network/http.py": { "assignment": 43, "misc": 1, @@ -103,9 +121,6 @@ "app/adapters/system/plugin/dependency.py": { "no-any-return": 3 }, - "app/adapters/system/plugin/package.py": { - "no-any-return": 1 - }, "app/adapters/system/resource.py": { "no-untyped-call": 1, "no-untyped-def": 2, @@ -138,6 +153,12 @@ "misc": 1, "no-any-return": 2 }, + "app/agent/callback/__init__.py": { + "no-untyped-call": 5, + "no-untyped-def": 9, + "return-value": 1, + "type-arg": 1 + }, "app/agent/contracts.py": { "type-arg": 1 }, @@ -158,7 +179,25 @@ "union-attr": 2 }, "app/agent/llm/provider.py": { - "import-untyped": 1 + "arg-type": 3, + "import-untyped": 1, + "no-any-return": 11, + "no-untyped-def": 1 + }, + "app/agent/mcp.py": { + "assignment": 2, + "attr-defined": 3, + "no-any-return": 1, + "no-untyped-def": 6, + "return-value": 2, + "type-arg": 7, + "type-var": 1 + }, + "app/agent/memory/__init__.py": { + "arg-type": 1, + "no-untyped-call": 2, + "no-untyped-def": 8, + "type-arg": 1 }, "app/agent/middleware/activity_log.py": { "misc": 3, @@ -177,11 +216,26 @@ "app/agent/middleware/patch_tool_calls.py": { "misc": 1 }, + "app/agent/middleware/policy.py": { + "misc": 2, + "union-attr": 1 + }, + "app/agent/middleware/runtime_config.py": { + "misc": 1 + }, "app/agent/middleware/skills.py": { "import-untyped": 1, "misc": 3, "valid-type": 1 }, + "app/agent/middleware/subagents.py": { + "arg-type": 2, + "assignment": 1, + "attr-defined": 2, + "misc": 7, + "type-arg": 5, + "var-annotated": 1 + }, "app/agent/middleware/summarization.py": { "misc": 2, "no-any-return": 6, @@ -196,6 +250,18 @@ "misc": 1, "no-any-return": 2 }, + "app/agent/orchestrator.py": { + "arg-type": 17, + "assignment": 14, + "attr-defined": 7, + "dict-item": 1, + "no-any-return": 9, + "no-untyped-call": 7, + "no-untyped-def": 21, + "type-arg": 35, + "union-attr": 3, + "var-annotated": 1 + }, "app/agent/policy/orchestrator.py": { "no-any-return": 1 }, @@ -218,16 +284,43 @@ }, "app/agent/skills/registry.py": { "arg-type": 1, - "attr-defined": 1, "misc": 3, "no-any-return": 7, "no-untyped-def": 5, "type-arg": 14, "union-attr": 13 }, + "app/agent/tools/base.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-def": 7, + "type-arg": 2, + "var-annotated": 1 + }, "app/agent/tools/catalog.py": { "attr-defined": 1 }, + "app/agent/tools/factory.py": { + "arg-type": 1, + "assignment": 5, + "no-any-return": 1, + "no-untyped-def": 1, + "type-arg": 3 + }, + "app/agent/tools/impl/_filter_rule_utils.py": { + "arg-type": 1, + "attr-defined": 2, + "call-overload": 1, + "index": 2, + "no-any-return": 2, + "no-untyped-call": 1, + "type-arg": 13, + "var-annotated": 1 + }, + "app/agent/tools/impl/_plugin_tool_utils.py": { + "arg-type": 2, + "no-any-return": 2 + }, "app/agent/tools/impl/_terminal_session.py": { "assignment": 2, "attr-defined": 1, @@ -240,28 +333,775 @@ "type-arg": 2, "var-annotated": 1 }, + "app/agent/tools/impl/add_custom_filter_rule.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/add_download_tasks.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-def": 2, + "return-value": 1 + }, + "app/agent/tools/impl/add_rule_group.py": { + "arg-type": 1, + "list-item": 1, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/add_subscribe.py": { + "arg-type": 2, + "assignment": 8, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/apply_patch.py": { + "arg-type": 5, + "misc": 1, + "no-untyped-def": 2, + "override": 1, + "type-arg": 1, + "union-attr": 1 + }, + "app/agent/tools/impl/ask_user_choice.py": { + "attr-defined": 1, + "misc": 2, + "no-untyped-def": 4, + "override": 1 + }, + "app/agent/tools/impl/browse_webpage.py": { + "arg-type": 13, + "misc": 1, + "no-any-return": 2, + "no-untyped-def": 12, + "override": 1 + }, "app/agent/tools/impl/create_agent_task.py": { - "import-untyped": 1 + "arg-type": 1, + "assignment": 1, + "import-untyped": 1, + "misc": 2, + "no-any-return": 1, + "override": 1, + "type-arg": 1 + }, + "app/agent/tools/impl/delete_agent_task.py": { + "misc": 1, + "no-any-return": 1, + "override": 1 + }, + "app/agent/tools/impl/delete_custom_filter_rule.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/delete_download_history.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/delete_download_tasks.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/delete_rule_group.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/delete_subscribe.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/delete_transfer_history.py": { + "attr-defined": 1, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/edit_file.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/execute_command.py": { + "arg-type": 2, + "assignment": 1, + "attr-defined": 1, + "misc": 1, + "no-untyped-def": 2, + "type-arg": 3 + }, + "app/agent/tools/impl/get_recommendations.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/get_search_results.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/install_plugin.py": { + "misc": 2, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/list_directory.py": { + "misc": 1, + "no-any-return": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/list_slash_commands.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/mcp.py": { + "arg-type": 3, + "no-untyped-def": 3, + "type-arg": 1 + }, + "app/agent/tools/impl/query_agent_tasks.py": { + "misc": 1, + "type-arg": 1 + }, + "app/agent/tools/impl/query_builtin_filter_rules.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_custom_filter_rules.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_custom_identifiers.py": { + "misc": 1, + "no-untyped-call": 1, + "no-untyped-def": 3 + }, + "app/agent/tools/impl/query_directory_settings.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_doctor_report.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_download_tasks.py": { + "arg-type": 6, + "assignment": 1, + "misc": 1, + "no-any-return": 2, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_downloaders.py": { + "misc": 1, + "no-untyped-call": 1, + "no-untyped-def": 3, + "type-arg": 1 + }, + "app/agent/tools/impl/query_episode_schedule.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/query_installed_plugins.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_library_exists.py": { + "attr-defined": 6, + "dict-item": 1, + "misc": 1, + "no-untyped-call": 1, + "no-untyped-def": 5, + "override": 1, + "type-arg": 4 + }, + "app/agent/tools/impl/query_library_latest.py": { + "misc": 2, + "no-untyped-def": 2, + "type-arg": 1 + }, + "app/agent/tools/impl/query_market_plugins.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_media_detail.py": { + "arg-type": 3, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/query_personas.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_plugin_capabilities.py": { + "misc": 1, + "no-untyped-def": 2, + "type-arg": 1 + }, + "app/agent/tools/impl/query_plugin_config.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/query_plugin_data.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/query_popular_subscribes.py": { + "arg-type": 2, + "assignment": 1, + "misc": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/query_rule_groups.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_schedulers.py": { + "misc": 1 + }, + "app/agent/tools/impl/query_site_userdata.py": { + "misc": 1, + "no-untyped-def": 3, + "override": 1, + "type-arg": 1 + }, + "app/agent/tools/impl/query_sites.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_subscribe_history.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-def": 4, + "type-arg": 1 + }, + "app/agent/tools/impl/query_subscribe_shares.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_subscribes.py": { + "arg-type": 2, + "misc": 1, + "no-untyped-def": 2, + "union-attr": 1 + }, + "app/agent/tools/impl/query_system_settings.py": { + "attr-defined": 1, + "misc": 1, + "no-untyped-def": 6, + "type-arg": 1 + }, + "app/agent/tools/impl/query_transfer_history.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/query_workflows.py": { + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/read_file.py": { + "misc": 1, + "no-any-return": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/recognize_captcha.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/recognize_media.py": { + "arg-type": 1, + "assignment": 1, + "misc": 1, + "no-untyped-call": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/reload_plugin.py": { + "misc": 1, + "no-any-return": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/run_agent_task.py": { + "misc": 1, + "override": 1 + }, + "app/agent/tools/impl/run_scheduler.py": { + "misc": 1, + "override": 1 + }, + "app/agent/tools/impl/run_slash_command.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/run_workflow.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/scrape_metadata.py": { + "arg-type": 6, + "attr-defined": 1, + "misc": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/search_media.py": { + "assignment": 1, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/search_person.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/search_person_credits.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/search_subscribe.py": { + "misc": 1, + "no-untyped-def": 2, + "operator": 1, + "override": 1 + }, + "app/agent/tools/impl/search_torrents.py": { + "arg-type": 2, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/search_web.py": { + "assignment": 1, + "attr-defined": 1, + "misc": 1, + "no-any-return": 1, + "no-untyped-def": 4, + "override": 1, + "type-arg": 8 + }, + "app/agent/tools/impl/send_local_file.py": { + "attr-defined": 1, + "misc": 1, + "no-untyped-def": 3, + "override": 1 + }, + "app/agent/tools/impl/send_message.py": { + "misc": 2, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/send_voice_message.py": { + "attr-defined": 1, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/switch_persona.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/test_site.py": { + "misc": 1, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/transfer_file.py": { + "arg-type": 2, + "attr-defined": 1, + "misc": 1, + "no-any-return": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/uninstall_plugin.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 }, "app/agent/tools/impl/update_agent_task.py": { - "import-untyped": 1 + "assignment": 1, + "dict-item": 3, + "import-untyped": 1, + "misc": 2, + "no-any-return": 2, + "override": 1, + "type-arg": 1 + }, + "app/agent/tools/impl/update_custom_filter_rule.py": { + "arg-type": 5, + "list-item": 1, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/update_custom_identifiers.py": { + "assignment": 1, + "misc": 1, + "no-untyped-def": 2 + }, + "app/agent/tools/impl/update_download_tasks.py": { + "arg-type": 1, + "assignment": 1, + "misc": 1, + "no-untyped-def": 3, + "override": 1, + "var-annotated": 1 + }, + "app/agent/tools/impl/update_persona_definition.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/update_plugin_config.py": { + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/update_rule_group.py": { + "arg-type": 5, + "list-item": 1, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/update_site.py": { + "assignment": 7, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/update_site_cookie.py": { + "misc": 1, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/update_subscribe.py": { + "arg-type": 1, + "assignment": 12, + "misc": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/agent/tools/impl/update_system_settings.py": { + "attr-defined": 1, + "misc": 1, + "no-untyped-def": 5, + "override": 1, + "type-arg": 2 + }, + "app/agent/tools/impl/write_file.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-def": 2, + "override": 1, + "union-attr": 1 + }, + "app/agent/tools/manager.py": { + "assignment": 1, + "attr-defined": 2, + "type-arg": 1 + }, + "app/api/context.py": { + "redundant-cast": 3 + }, + "app/api/dependencies/agent.py": { + "arg-type": 1 + }, + "app/api/dependencies/auth.py": { + "arg-type": 2, + "redundant-cast": 1 + }, + "app/api/dependencies/history.py": { + "arg-type": 11, + "attr-defined": 1 + }, + "app/api/dependencies/site.py": { + "arg-type": 3 + }, + "app/api/dependencies/subscription.py": { + "arg-type": 7 + }, + "app/api/dependencies/workflow.py": { + "arg-type": 4, + "redundant-cast": 2 }, "app/api/endpoints/agent.py": { - "import-untyped": 1 + "arg-type": 9, + "attr-defined": 5, + "dict-item": 3, + "import-untyped": 1, + "misc": 17, + "no-any-return": 8, + "type-arg": 36, + "var-annotated": 1 + }, + "app/api/endpoints/anilist.py": { + "attr-defined": 1, + "misc": 8, + "no-untyped-call": 2 + }, + "app/api/endpoints/anthropic.py": { + "misc": 1, + "no-untyped-call": 1, + "no-untyped-def": 3, + "type-arg": 1, + "var-annotated": 1 + }, + "app/api/endpoints/auth.py": { + "arg-type": 1, + "misc": 3 + }, + "app/api/endpoints/bangumi.py": { + "attr-defined": 1, + "misc": 5, + "no-untyped-call": 3, + "operator": 12 + }, + "app/api/endpoints/dashboard.py": { + "arg-type": 2, + "misc": 19, + "no-untyped-call": 2, + "operator": 12 + }, + "app/api/endpoints/discover.py": { + "arg-type": 20, + "attr-defined": 1, + "misc": 6, + "no-redef": 1, + "no-untyped-call": 5, + "operator": 2 + }, + "app/api/endpoints/douban.py": { + "attr-defined": 1, + "misc": 5, + "no-untyped-call": 3 + }, + "app/api/endpoints/download.py": { + "arg-type": 7, + "assignment": 6, + "attr-defined": 4, + "misc": 10, + "type-arg": 2 + }, + "app/api/endpoints/history.py": { + "arg-type": 4, + "misc": 7, + "no-untyped-call": 2, + "no-untyped-def": 2 + }, + "app/api/endpoints/llm.py": { + "misc": 2, + "no-untyped-def": 2 + }, + "app/api/endpoints/login.py": { + "misc": 3, + "union-attr": 33 + }, + "app/api/endpoints/mcp.py": { + "assignment": 3, + "index": 1, + "misc": 6, + "no-untyped-call": 4, + "no-untyped-def": 1 + }, + "app/api/endpoints/media.py": { + "arg-type": 5, + "assignment": 8, + "attr-defined": 3, + "misc": 14, + "no-untyped-call": 7, + "no-untyped-def": 3, + "type-arg": 1 + }, + "app/api/endpoints/mediaserver.py": { + "arg-type": 4, + "assignment": 1, + "attr-defined": 1, + "misc": 8, + "type-arg": 2, + "union-attr": 2 + }, + "app/api/endpoints/message.py": { + "arg-type": 4, + "assignment": 1, + "misc": 8, + "no-untyped-call": 3, + "no-untyped-def": 8, + "union-attr": 4 + }, + "app/api/endpoints/mfa.py": { + "attr-defined": 7, + "call-overload": 1, + "index": 2, + "misc": 17, + "operator": 1, + "type-arg": 2 + }, + "app/api/endpoints/music.py": { + "arg-type": 6, + "attr-defined": 1, + "misc": 10, + "type-arg": 3 + }, + "app/api/endpoints/notification.py": { + "misc": 1, + "no-untyped-def": 1 + }, + "app/api/endpoints/openai.py": { + "attr-defined": 1, + "has-type": 1, + "misc": 5, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 15, + "type-arg": 8, + "var-annotated": 2 }, "app/api/endpoints/plugin.py": { - "import-untyped": 1 + "assignment": 1, + "import-untyped": 1, + "misc": 33, + "no-any-return": 5, + "no-untyped-call": 1, + "no-untyped-def": 3, + "type-arg": 11 + }, + "app/api/endpoints/recommend.py": { + "attr-defined": 2, + "misc": 16, + "no-redef": 1 + }, + "app/api/endpoints/search.py": { + "arg-type": 14, + "assignment": 1, + "attr-defined": 4, + "misc": 11, + "no-untyped-call": 8, + "no-untyped-def": 4, + "type-arg": 14 + }, + "app/api/endpoints/site.py": { + "arg-type": 7, + "assignment": 2, + "attr-defined": 3, + "misc": 26, + "no-untyped-call": 4, + "no-untyped-def": 2, + "return-value": 3, + "type-arg": 7, + "var-annotated": 1 + }, + "app/api/endpoints/storage.py": { + "arg-type": 3, + "assignment": 1, + "attr-defined": 1, + "misc": 7, + "type-arg": 1 + }, + "app/api/endpoints/subscribe.py": { + "arg-type": 13, + "assignment": 3, + "attr-defined": 2, + "misc": 28, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "type-arg": 2 }, "app/api/endpoints/system.py": { - "import-untyped": 1 + "arg-type": 2, + "attr-defined": 6, + "import-untyped": 1, + "list-item": 1, + "misc": 34, + "no-untyped-call": 7, + "no-untyped-def": 27, + "operator": 1, + "type-arg": 11, + "type-var": 2, + "var-annotated": 1 + }, + "app/api/endpoints/tmdb.py": { + "attr-defined": 1, + "misc": 11, + "no-untyped-call": 4, + "operator": 4, + "type-arg": 3 + }, + "app/api/endpoints/torrent.py": { + "misc": 5, + "no-untyped-call": 1, + "no-untyped-def": 5 + }, + "app/api/endpoints/transfer.py": { + "arg-type": 18, + "assignment": 1, + "attr-defined": 2, + "misc": 8, + "operator": 3, + "type-arg": 3 + }, + "app/api/endpoints/user.py": { + "misc": 10, + "no-untyped-def": 2, + "str-bytes-safe": 1, + "type-arg": 3 + }, + "app/api/endpoints/webhook.py": { + "misc": 2, + "no-untyped-def": 1 + }, + "app/api/endpoints/workflow.py": { + "assignment": 1, + "misc": 16 }, "app/api/response.py": { "arg-type": 1, "misc": 2, "valid-type": 1 }, + "app/api/servarr.py": { + "arg-type": 4, + "assignment": 2, + "attr-defined": 1, + "index": 3, + "misc": 16, + "no-any-return": 2, + "type-arg": 1, + "union-attr": 1 + }, "app/api/servcookie.py": { - "import-untyped": 1 + "import-untyped": 1, + "misc": 7, + "no-any-return": 5, + "return": 1, + "type-arg": 1 }, "app/application/agent.py": { "no-any-return": 4 @@ -272,6 +1112,10 @@ "app/application/agenttask.py": { "type-arg": 1 }, + "app/application/chain/durable_events.py": { + "arg-type": 1, + "no-untyped-call": 1 + }, "app/application/commands.py": { "no-any-return": 1 }, @@ -310,6 +1154,14 @@ "no-any-return": 1, "type-arg": 5 }, + "app/application/image.py": { + "arg-type": 1, + "misc": 8, + "no-any-return": 10, + "no-untyped-call": 1, + "no-untyped-def": 2, + "type-arg": 5 + }, "app/application/maintenance.py": { "assignment": 1 }, @@ -325,6 +1177,9 @@ "type-arg": 15, "var-annotated": 1 }, + "app/application/messaging/chat.py": { + "type-arg": 8 + }, "app/application/messaging/ingress.py": { "arg-type": 2 }, @@ -338,15 +1193,15 @@ }, "app/application/messaging/message.py": { "arg-type": 5, - "assignment": 3, + "assignment": 9, "dict-item": 1, "misc": 1, - "no-any-return": 4, - "no-untyped-call": 2, - "no-untyped-def": 11, + "no-any-return": 5, + "no-untyped-call": 4, + "no-untyped-def": 14, "type-arg": 18, "union-attr": 2, - "var-annotated": 1 + "var-annotated": 2 }, "app/application/messaging/plugin.py": { "arg-type": 5, @@ -391,13 +1246,6 @@ "app/application/plugin/config.py": { "type-arg": 3 }, - "app/application/plugin/install.py": { - "arg-type": 1, - "var-annotated": 1 - }, - "app/application/plugin/lifecycle.py": { - "no-untyped-def": 2 - }, "app/application/rss.py": { "arg-type": 2, "assignment": 5, @@ -412,6 +1260,11 @@ "no-untyped-def": 1, "type-arg": 7 }, + "app/application/security/auth.py": { + "no-untyped-call": 2, + "no-untyped-def": 1, + "type-arg": 1 + }, "app/application/security/cookie.py": { "arg-type": 3, "assignment": 2, @@ -423,6 +1276,9 @@ "app/application/security/otp.py": { "no-any-return": 3 }, + "app/application/security/passkey.py": { + "no-untyped-call": 1 + }, "app/application/security/passkeys.py": { "valid-type": 2 }, @@ -515,6 +1371,13 @@ "type-arg": 14, "union-attr": 47 }, + "app/chain/__init__.py": { + "assignment": 13, + "attr-defined": 1, + "no-any-return": 50, + "no-untyped-def": 7, + "type-arg": 31 + }, "app/chain/_interaction.py": { "assignment": 3, "attr-defined": 1, @@ -522,6 +1385,21 @@ "no-untyped-call": 3, "no-untyped-def": 2 }, + "app/chain/_messaging.py": { + "arg-type": 2, + "attr-defined": 35, + "no-any-return": 6, + "no-untyped-call": 2, + "no-untyped-def": 2, + "type-arg": 4 + }, + "app/chain/_music.py": { + "arg-type": 6, + "assignment": 1, + "attr-defined": 8, + "no-untyped-call": 1, + "union-attr": 8 + }, "app/chain/_recognition.py": { "arg-type": 6, "assignment": 7, @@ -530,6 +1408,242 @@ "no-untyped-def": 5, "type-arg": 4 }, + "app/chain/_transfer.py": { + "arg-type": 9, + "assignment": 4, + "attr-defined": 55, + "no-any-return": 4, + "no-untyped-call": 1, + "no-untyped-def": 8, + "return-value": 2, + "truthy-function": 1, + "type-arg": 3 + }, + "app/chain/anilist.py": { + "no-any-return": 4, + "no-untyped-def": 2, + "type-arg": 2 + }, + "app/chain/bangumi.py": { + "no-any-return": 14, + "no-untyped-def": 2, + "type-arg": 2 + }, + "app/chain/dashboard.py": { + "no-any-return": 2 + }, + "app/chain/douban.py": { + "no-any-return": 28 + }, + "app/chain/download.py": { + "arg-type": 51, + "assignment": 29, + "attr-defined": 7, + "call-overload": 1, + "comparison-overlap": 1, + "misc": 1, + "no-any-return": 4, + "no-redef": 1, + "no-untyped-call": 7, + "no-untyped-def": 6, + "operator": 1, + "return-value": 1, + "type-arg": 10, + "union-attr": 37, + "valid-type": 1 + }, + "app/chain/interaction.py": { + "arg-type": 16, + "assignment": 3, + "no-untyped-call": 1, + "return-value": 1, + "union-attr": 18, + "var-annotated": 1 + }, + "app/chain/media.py": { + "arg-type": 34, + "assignment": 15, + "comparison-overlap": 3, + "no-any-return": 3, + "no-untyped-def": 3, + "return-value": 7, + "type-arg": 11, + "union-attr": 2 + }, + "app/chain/mediaserver.py": { + "arg-type": 3, + "assignment": 2, + "misc": 2, + "no-any-return": 5, + "type-arg": 1 + }, + "app/chain/message.py": { + "arg-type": 23, + "assignment": 2, + "no-any-return": 11, + "no-untyped-call": 10, + "no-untyped-def": 6, + "type-arg": 2, + "union-attr": 1, + "var-annotated": 1 + }, + "app/chain/musicbrainz.py": { + "type-arg": 2 + }, + "app/chain/recommend.py": { + "arg-type": 20, + "misc": 60, + "no-untyped-call": 14, + "no-untyped-def": 1, + "operator": 8, + "type-arg": 31 + }, + "app/chain/scraping.py": { + "arg-type": 30, + "assignment": 13, + "attr-defined": 2, + "call-arg": 1, + "index": 1, + "misc": 2, + "no-any-return": 1, + "no-untyped-def": 14, + "return-value": 1, + "type-arg": 2, + "var-annotated": 1 + }, + "app/chain/search.py": { + "arg-type": 32, + "assignment": 67, + "attr-defined": 2, + "call-overload": 1, + "index": 1, + "misc": 1, + "no-any-return": 6, + "no-redef": 3, + "no-untyped-call": 19, + "no-untyped-def": 5, + "return-value": 1, + "type-arg": 26, + "type-var": 4, + "valid-type": 2 + }, + "app/chain/site.py": { + "arg-type": 14, + "assignment": 7, + "attr-defined": 1, + "misc": 6, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 8, + "return-value": 2, + "type-arg": 5 + }, + "app/chain/storage.py": { + "arg-type": 9, + "assignment": 3, + "attr-defined": 1, + "no-any-return": 11, + "type-arg": 2 + }, + "app/chain/subscribe.py": { + "arg-type": 60, + "assignment": 18, + "attr-defined": 5, + "call-overload": 1, + "dict-item": 2, + "index": 1, + "misc": 7, + "no-any-return": 2, + "no-redef": 3, + "no-untyped-call": 19, + "no-untyped-def": 21, + "operator": 5, + "return-value": 5, + "truthy-function": 3, + "type-arg": 15, + "type-var": 1, + "union-attr": 45, + "var-annotated": 4 + }, + "app/chain/system.py": { + "attr-defined": 1, + "no-untyped-call": 3, + "no-untyped-def": 11 + }, + "app/chain/theaudiodb.py": { + "type-arg": 1 + }, + "app/chain/tmdb.py": { + "no-any-return": 28, + "type-arg": 2 + }, + "app/chain/torrents.py": { + "arg-type": 28, + "assignment": 4, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 15, + "type-arg": 5, + "union-attr": 2 + }, + "app/chain/transfer.py": { + "arg-type": 65, + "assignment": 26, + "attr-defined": 4, + "func-returns-value": 1, + "misc": 2, + "no-any-return": 3, + "no-redef": 2, + "no-untyped-call": 8, + "no-untyped-def": 16, + "operator": 3, + "return-value": 2, + "truthy-function": 5, + "type-arg": 10, + "union-attr": 33, + "var-annotated": 4 + }, + "app/chain/user.py": { + "arg-type": 3, + "index": 1 + }, + "app/chain/workflow.py": { + "arg-type": 2, + "assignment": 1, + "call-arg": 1, + "index": 4, + "misc": 1, + "no-any-return": 10, + "no-untyped-def": 3, + "truthy-function": 2, + "type-arg": 17, + "union-attr": 4, + "var-annotated": 6 + }, + "app/cli.py": { + "attr-defined": 2, + "misc": 57, + "no-any-return": 6, + "type-arg": 3, + "union-attr": 2 + }, + "app/command.py": { + "arg-type": 3, + "assignment": 5, + "misc": 6, + "no-untyped-call": 5, + "no-untyped-def": 4, + "type-arg": 7, + "valid-type": 1, + "var-annotated": 1 + }, + "app/db/adapters/chain.py": { + "return-value": 1 + }, + "app/db/adapters/plugininstallation.py": { + "no-any-return": 1, + "redundant-cast": 2 + }, "app/db/engine.py": { "no-untyped-def": 3, "type-arg": 1 @@ -685,11 +1799,10 @@ "arg-type": 1 }, "app/domain/context.py": { - "arg-type": 7, + "arg-type": 5, "assignment": 133, - "call-overload": 1, "index": 3, - "no-any-return": 2, + "no-any-return": 1, "no-untyped-call": 5, "no-untyped-def": 27, "operator": 1, @@ -773,6 +1886,9 @@ "no-untyped-def": 6, "type-arg": 1 }, + "app/factory.py": { + "misc": 1 + }, "app/foundation/crypto.py": { "no-any-return": 1, "no-untyped-def": 1, @@ -816,6 +1932,55 @@ "app/foundation/url.py": { "type-arg": 1 }, + "app/main.py": { + "misc": 1, + "no-untyped-call": 1, + "no-untyped-def": 5 + }, + "app/modules/__init__.py": { + "assignment": 1, + "attr-defined": 3, + "empty-body": 4, + "misc": 3, + "no-any-return": 1, + "no-untyped-call": 5, + "no-untyped-def": 7, + "union-attr": 4 + }, + "app/modules/_base/downloader.py": { + "assignment": 1, + "attr-defined": 5, + "no-untyped-call": 1 + }, + "app/modules/_base/mediaserver.py": { + "arg-type": 2, + "assignment": 3, + "attr-defined": 5, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "var-annotated": 1 + }, + "app/modules/_base/notification.py": { + "arg-type": 1, + "assignment": 1, + "no-untyped-call": 1, + "no-untyped-def": 3, + "type-arg": 5 + }, + "app/modules/acoustid/__init__.py": { + "arg-type": 1, + "list-item": 1, + "no-untyped-call": 1 + }, + "app/modules/anilist/__init__.py": { + "assignment": 4, + "no-any-return": 2, + "no-untyped-call": 1, + "no-untyped-def": 5, + "return-value": 1, + "type-arg": 12 + }, "app/modules/anilist/anilist.py": { "arg-type": 1, "misc": 15, @@ -823,14 +1988,41 @@ "no-untyped-def": 5, "type-arg": 31 }, + "app/modules/bangumi/__init__.py": { + "arg-type": 2, + "assignment": 4, + "no-any-return": 4, + "no-untyped-call": 11, + "no-untyped-def": 5, + "return-value": 1, + "type-arg": 7 + }, "app/modules/bangumi/bangumi.py": { "misc": 2, "no-untyped-def": 22, "var-annotated": 2 }, + "app/modules/dingtalk/__init__.py": { + "arg-type": 2, + "no-untyped-call": 1, + "no-untyped-def": 1, + "override": 1 + }, "app/modules/dingtalk/dingtalk.py": { "no-untyped-def": 1 }, + "app/modules/discord/__init__.py": { + "arg-type": 11, + "assignment": 10, + "attr-defined": 1, + "empty-body": 1, + "misc": 1, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "truthy-function": 1, + "type-arg": 8 + }, "app/modules/discord/discord.py": { "assignment": 1, "dict-item": 1, @@ -841,6 +2033,18 @@ "type-arg": 14, "union-attr": 18 }, + "app/modules/douban/__init__.py": { + "arg-type": 2, + "assignment": 27, + "empty-body": 1, + "misc": 2, + "no-any-return": 8, + "no-untyped-call": 12, + "no-untyped-def": 11, + "return-value": 2, + "type-arg": 13, + "union-attr": 9 + }, "app/modules/douban/apiv2.py": { "misc": 6, "no-any-return": 4, @@ -856,6 +2060,14 @@ "return-value": 1, "type-arg": 1 }, + "app/modules/emby/__init__.py": { + "arg-type": 3, + "assignment": 13, + "empty-body": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "type-arg": 1 + }, "app/modules/emby/emby.py": { "arg-type": 9, "assignment": 2, @@ -867,6 +2079,24 @@ "union-attr": 1, "var-annotated": 3 }, + "app/modules/fanart/__init__.py": { + "misc": 2, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "type-arg": 10, + "var-annotated": 1 + }, + "app/modules/feishu/__init__.py": { + "arg-type": 4, + "assignment": 6, + "attr-defined": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "return-value": 1, + "type-arg": 5, + "union-attr": 8 + }, "app/modules/feishu/feishu.py": { "arg-type": 5, "assignment": 1, @@ -877,18 +2107,161 @@ "type-arg": 25, "union-attr": 8 }, + "app/modules/filemanager/module.py": { + "arg-type": 21, + "assignment": 9, + "attr-defined": 1, + "empty-body": 1, + "no-any-return": 2, + "no-untyped-call": 6, + "no-untyped-def": 6, + "truthy-function": 4, + "type-arg": 4, + "var-annotated": 3 + }, + "app/modules/filemanager/storages/__init__.py": { + "arg-type": 3, + "assignment": 2, + "attr-defined": 4, + "index": 1, + "no-untyped-call": 2, + "no-untyped-def": 8, + "return-value": 1, + "type-arg": 6, + "var-annotated": 1 + }, + "app/modules/filemanager/storages/alipan.py": { + "arg-type": 19, + "assignment": 7, + "attr-defined": 1, + "empty-body": 2, + "no-any-return": 5, + "no-untyped-call": 4, + "no-untyped-def": 14, + "operator": 1, + "override": 2, + "return-value": 1, + "type-arg": 8, + "union-attr": 25 + }, + "app/modules/filemanager/storages/alist.py": { + "arg-type": 13, + "assignment": 2, + "attr-defined": 1, + "empty-body": 2, + "misc": 1, + "no-any-return": 4, + "no-untyped-call": 2, + "no-untyped-def": 5, + "operator": 4, + "override": 1, + "return-value": 1, + "type-arg": 3 + }, + "app/modules/filemanager/storages/local.py": { + "arg-type": 9, + "assignment": 2, + "attr-defined": 1, + "no-untyped-call": 1, + "no-untyped-def": 3, + "operator": 1, + "override": 1, + "truthy-function": 1, + "union-attr": 2 + }, + "app/modules/filemanager/storages/rclone.py": { + "arg-type": 13, + "assignment": 6, + "attr-defined": 4, + "empty-body": 2, + "no-untyped-call": 13, + "no-untyped-def": 3, + "override": 1, + "type-arg": 2, + "union-attr": 5 + }, + "app/modules/filemanager/storages/smb.py": { + "arg-type": 9, + "assignment": 2, + "attr-defined": 1, + "empty-body": 1, + "no-untyped-call": 15, + "no-untyped-def": 8, + "override": 1, + "return-value": 2, + "union-attr": 5 + }, + "app/modules/filemanager/storages/u115.py": { + "arg-type": 16, + "assignment": 7, + "attr-defined": 1, + "call-overload": 17, + "empty-body": 2, + "no-any-return": 6, + "no-untyped-call": 3, + "no-untyped-def": 5, + "operator": 2, + "override": 2, + "type-arg": 8, + "union-attr": 32 + }, + "app/modules/filemanager/transhandler.py": { + "arg-type": 19, + "assignment": 8, + "attr-defined": 1, + "misc": 1, + "no-any-return": 1, + "no-redef": 1, + "no-untyped-call": 1, + "no-untyped-def": 4, + "operator": 7, + "type-arg": 2, + "union-attr": 2 + }, + "app/modules/filter/__init__.py": { + "arg-type": 2, + "assignment": 4, + "empty-body": 1, + "index": 1, + "no-untyped-call": 3, + "no-untyped-def": 3, + "operator": 2, + "return": 1, + "type-arg": 12, + "var-annotated": 1 + }, + "app/modules/imdb/__init__.py": { + "assignment": 10, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 3, + "override": 1, + "type-arg": 5 + }, "app/modules/imdb/api.py": { "arg-type": 2, "misc": 5, "no-any-return": 2, "type-arg": 18 }, + "app/modules/indexer/__init__.py": { + "arg-type": 34, + "assignment": 8, + "attr-defined": 1, + "empty-body": 1, + "no-any-return": 2, + "no-untyped-call": 4, + "no-untyped-def": 1, + "return-value": 5, + "type-arg": 19, + "var-annotated": 5 + }, "app/modules/indexer/parser/__init__.py": { "arg-type": 20, "assignment": 5, "comparison-overlap": 1, - "no-untyped-call": 5, - "no-untyped-def": 15, + "no-untyped-call": 4, + "no-untyped-def": 14, "return-value": 1, "type-arg": 3, "var-annotated": 3 @@ -1093,6 +2466,14 @@ "no-untyped-def": 1, "type-arg": 8 }, + "app/modules/jellyfin/__init__.py": { + "arg-type": 3, + "assignment": 13, + "empty-body": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "type-arg": 1 + }, "app/modules/jellyfin/jellyfin.py": { "arg-type": 7, "assignment": 4, @@ -1106,6 +2487,29 @@ "union-attr": 1, "var-annotated": 3 }, + "app/modules/listenbrainz/__init__.py": { + "arg-type": 1, + "misc": 2, + "no-any-return": 2, + "no-untyped-call": 1, + "override": 1 + }, + "app/modules/lrclib/__init__.py": { + "arg-type": 1, + "misc": 1, + "no-untyped-call": 1, + "override": 1 + }, + "app/modules/musicbrainz/__init__.py": { + "arg-type": 4, + "assignment": 5, + "misc": 4, + "no-any-return": 1, + "no-untyped-call": 4, + "no-untyped-def": 2, + "override": 1, + "type-arg": 2 + }, "app/modules/musicbrainz/music_cache.py": { "no-any-return": 1, "no-untyped-call": 2, @@ -1113,10 +2517,29 @@ "type-arg": 3, "union-attr": 1 }, + "app/modules/musixmatch/__init__.py": { + "no-any-return": 1, + "no-untyped-call": 1 + }, + "app/modules/navidrome/__init__.py": { + "arg-type": 9, + "no-untyped-call": 1, + "override": 1, + "type-arg": 1, + "union-attr": 1 + }, "app/modules/navidrome/navidrome.py": { "no-any-return": 1, "type-arg": 11 }, + "app/modules/plex/__init__.py": { + "arg-type": 7, + "assignment": 16, + "empty-body": 1, + "no-untyped-call": 2, + "no-untyped-def": 5, + "type-arg": 1 + }, "app/modules/plex/plex.py": { "arg-type": 2, "assignment": 3, @@ -1129,6 +2552,22 @@ "union-attr": 4, "var-annotated": 3 }, + "app/modules/postgresql/__init__.py": { + "empty-body": 1, + "no-untyped-call": 1, + "no-untyped-def": 1 + }, + "app/modules/qbittorrent/__init__.py": { + "arg-type": 7, + "assignment": 16, + "empty-body": 1, + "no-redef": 2, + "no-untyped-call": 1, + "operator": 1, + "str-bytes-safe": 2, + "type-arg": 8, + "var-annotated": 1 + }, "app/modules/qbittorrent/qbittorrent.py": { "assignment": 10, "no-any-return": 3, @@ -1144,12 +2583,39 @@ "no-untyped-def": 4, "type-arg": 2 }, + "app/modules/qqbot/module.py": { + "arg-type": 7, + "assignment": 4, + "attr-defined": 1, + "empty-body": 1, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "type-arg": 4, + "var-annotated": 1 + }, "app/modules/qqbot/qqbot.py": { "assignment": 1, "attr-defined": 1, "no-untyped-def": 4, "type-arg": 10 }, + "app/modules/redis/__init__.py": { + "empty-body": 1, + "no-untyped-call": 2, + "no-untyped-def": 1 + }, + "app/modules/rtorrent/__init__.py": { + "arg-type": 10, + "assignment": 16, + "empty-body": 1, + "no-redef": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "operator": 1, + "str-bytes-safe": 2, + "type-arg": 9 + }, "app/modules/rtorrent/rtorrent.py": { "arg-type": 15, "assignment": 6, @@ -1160,6 +2626,16 @@ "type-arg": 16, "union-attr": 48 }, + "app/modules/slack/__init__.py": { + "arg-type": 14, + "assignment": 9, + "attr-defined": 1, + "empty-body": 1, + "index": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "type-arg": 7 + }, "app/modules/slack/slack.py": { "assignment": 1, "dict-item": 2, @@ -1170,12 +2646,72 @@ "union-attr": 2, "var-annotated": 3 }, + "app/modules/subtitle/__init__.py": { + "arg-type": 2, + "empty-body": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "var-annotated": 1 + }, + "app/modules/synologychat/__init__.py": { + "arg-type": 8, + "assignment": 4, + "attr-defined": 1, + "empty-body": 1, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "type-arg": 4, + "var-annotated": 1 + }, "app/modules/synologychat/synologychat.py": { "assignment": 1, "no-any-return": 3, "no-untyped-call": 7, "no-untyped-def": 3 }, + "app/modules/telegram/module.py": { + "arg-type": 17, + "assignment": 9, + "attr-defined": 1, + "empty-body": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "type-arg": 11, + "union-attr": 1 + }, + "app/modules/telegram/telegram.py": { + "arg-type": 1, + "assignment": 2, + "misc": 4, + "no-any-return": 8, + "no-untyped-call": 2, + "no-untyped-def": 19, + "return-value": 1, + "type-arg": 14, + "union-attr": 1, + "var-annotated": 1 + }, + "app/modules/theaudiodb/__init__.py": { + "assignment": 4, + "misc": 2, + "no-untyped-call": 1, + "no-untyped-def": 2, + "override": 1 + }, + "app/modules/themoviedb/__init__.py": { + "arg-type": 27, + "assignment": 12, + "empty-body": 1, + "index": 2, + "no-any-return": 4, + "no-untyped-call": 7, + "no-untyped-def": 3, + "return-value": 8, + "type-arg": 29, + "union-attr": 2, + "var-annotated": 2 + }, "app/modules/themoviedb/category.py": { "assignment": 2, "no-any-return": 2, @@ -1323,6 +2859,13 @@ "no-untyped-def": 44, "operator": 1 }, + "app/modules/thetvdb/__init__.py": { + "empty-body": 1, + "no-any-return": 2, + "no-untyped-call": 2, + "no-untyped-def": 4, + "type-arg": 2 + }, "app/modules/thetvdb/tvdb_v4_official.py": { "arg-type": 4, "assignment": 118, @@ -1333,6 +2876,17 @@ "return-value": 1, "type-arg": 68 }, + "app/modules/transmission/__init__.py": { + "arg-type": 8, + "assignment": 16, + "empty-body": 1, + "no-any-return": 1, + "no-redef": 2, + "no-untyped-call": 10, + "no-untyped-def": 7, + "str-bytes-safe": 1, + "type-arg": 6 + }, "app/modules/transmission/transmission.py": { "assignment": 19, "misc": 1, @@ -1348,6 +2902,26 @@ "no-untyped-def": 11, "type-arg": 6 }, + "app/modules/trimemedia/module.py": { + "assignment": 1, + "empty-body": 1, + "no-any-return": 3, + "no-untyped-call": 2, + "no-untyped-def": 7, + "return": 1, + "type-arg": 2 + }, + "app/modules/trimemedia/trimemedia.py": { + "arg-type": 5, + "no-any-return": 1, + "no-untyped-call": 8, + "no-untyped-def": 9, + "operator": 1, + "return": 1, + "type-arg": 2, + "union-attr": 32, + "var-annotated": 4 + }, "app/modules/ugreen/api.py": { "arg-type": 1, "no-any-return": 2, @@ -1357,6 +2931,15 @@ "app/modules/ugreen/crypto.py": { "no-any-return": 1 }, + "app/modules/ugreen/module.py": { + "arg-type": 3, + "assignment": 1, + "empty-body": 1, + "no-any-return": 3, + "no-untyped-call": 1, + "no-untyped-def": 7, + "type-arg": 2 + }, "app/modules/ugreen/ugreen.py": { "arg-type": 4, "no-any-return": 1, @@ -1368,12 +2951,42 @@ "union-attr": 7, "var-annotated": 2 }, + "app/modules/vocechat/__init__.py": { + "arg-type": 10, + "assignment": 6, + "attr-defined": 1, + "empty-body": 1, + "no-untyped-call": 1, + "no-untyped-def": 3, + "type-arg": 4, + "union-attr": 1 + }, "app/modules/vocechat/vocechat.py": { "misc": 1, "no-any-return": 3, "no-untyped-call": 1, "no-untyped-def": 3 }, + "app/modules/webpush/__init__.py": { + "arg-type": 1, + "assignment": 1, + "empty-body": 1, + "no-untyped-call": 2, + "no-untyped-def": 2, + "type-arg": 1, + "union-attr": 1 + }, + "app/modules/wechat/__init__.py": { + "arg-type": 14, + "assignment": 5, + "attr-defined": 1, + "empty-body": 1, + "no-any-return": 2, + "no-untyped-call": 3, + "no-untyped-def": 5, + "type-arg": 4, + "union-attr": 4 + }, "app/modules/wechat/wechat.py": { "arg-type": 1, "assignment": 9, @@ -1390,6 +3003,16 @@ "union-attr": 3, "var-annotated": 1 }, + "app/modules/wechatclawbot/__init__.py": { + "arg-type": 9, + "assignment": 3, + "empty-body": 1, + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 3, + "type-arg": 2, + "union-attr": 1 + }, "app/modules/wechatclawbot/wechatclawbot.py": { "arg-type": 2, "assignment": 1, @@ -1399,6 +3022,14 @@ "type-arg": 1, "union-attr": 1 }, + "app/modules/zspace/__init__.py": { + "arg-type": 3, + "assignment": 12, + "empty-body": 1, + "no-untyped-call": 1, + "no-untyped-def": 2, + "type-arg": 1 + }, "app/modules/zspace/zspace.py": { "arg-type": 10, "assignment": 3, @@ -1410,6 +3041,27 @@ "union-attr": 1, "var-annotated": 4 }, + "app/monitor/dispatcher.py": { + "arg-type": 4, + "assignment": 5, + "attr-defined": 1, + "no-untyped-def": 6 + }, + "app/monitor/monitor.py": { + "arg-type": 8, + "assignment": 1, + "attr-defined": 5, + "no-untyped-call": 12, + "no-untyped-def": 2, + "type-arg": 4, + "union-attr": 1, + "var-annotated": 2 + }, + "app/monitor/poller.py": { + "arg-type": 3, + "no-untyped-def": 3, + "type-arg": 2 + }, "app/monitor/snapshot.py": { "type-arg": 5, "valid-type": 1, @@ -1473,7 +3125,7 @@ "type-arg": 5 }, "app/runtime/event/contracts.py": { - "arg-type": 2 + "arg-type": 1 }, "app/runtime/event/dispatch.py": { "type-arg": 5 @@ -1483,7 +3135,7 @@ }, "app/runtime/events.py": { "list-item": 1, - "misc": 2, + "misc": 1, "no-any-return": 2, "no-untyped-call": 3, "no-untyped-def": 19, @@ -1541,13 +3193,21 @@ "type-arg": 3 }, "app/runtime/extensions/plugin/sync.py": { - "arg-type": 1, "type-arg": 2 }, "app/runtime/extensions/plugin/system.py": { "no-any-return": 6, "type-arg": 4 }, + "app/runtime/extensions/plugin_manager.py": { + "arg-type": 2, + "assignment": 8, + "attr-defined": 1, + "no-any-return": 3, + "no-untyped-call": 4, + "no-untyped-def": 14, + "type-arg": 11 + }, "app/runtime/extensions/service_config.py": { "type-arg": 2 }, @@ -1587,13 +3247,30 @@ "valid-type": 2 }, "app/runtime/settings.py": { - "no-any-return": 6 + "no-any-return": 1 + }, + "app/runtime/state.py": { + "attr-defined": 2, + "no-any-return": 2, + "no-untyped-call": 2, + "no-untyped-def": 5, + "return-value": 1, + "type-arg": 1 }, "app/runtime/thread.py": { "unused-ignore": 1 }, "app/scheduler.py": { - "import-untyped": 1 + "arg-type": 8, + "attr-defined": 1, + "import-untyped": 1, + "misc": 1, + "no-any-return": 3, + "no-untyped-call": 6, + "no-untyped-def": 13, + "return-value": 1, + "type-arg": 7, + "union-attr": 1 }, "app/schemas/agent.py": { "misc": 26 @@ -1730,6 +3407,69 @@ "no-untyped-def": 2, "type-arg": 1 }, + "app/startup/composition/database.py": { + "arg-type": 1, + "assignment": 1 + }, + "app/startup/composition/subscription.py": { + "arg-type": 2, + "no-untyped-call": 4 + }, + "app/startup/initializers/agent.py": { + "no-untyped-call": 2, + "no-untyped-def": 1 + }, + "app/startup/initializers/command.py": { + "no-untyped-call": 2, + "no-untyped-def": 1, + "type-arg": 1 + }, + "app/startup/initializers/database.py": { + "no-untyped-call": 1, + "no-untyped-def": 2 + }, + "app/startup/initializers/modules.py": { + "arg-type": 17, + "assignment": 1, + "attr-defined": 2, + "misc": 1, + "no-any-return": 2, + "no-untyped-call": 33, + "no-untyped-def": 13, + "return-value": 5 + }, + "app/startup/initializers/plugins.py": { + "arg-type": 5, + "no-any-return": 2, + "no-untyped-call": 9, + "no-untyped-def": 5 + }, + "app/startup/initializers/routers.py": { + "no-untyped-def": 1 + }, + "app/startup/initializers/scheduler.py": { + "no-untyped-call": 5, + "no-untyped-def": 4 + }, + "app/startup/initializers/transfer.py": { + "no-any-return": 1, + "no-untyped-call": 1, + "no-untyped-def": 1 + }, + "app/startup/initializers/workflow.py": { + "no-untyped-def": 1 + }, + "app/startup/lifecycle/__init__.py": { + "arg-type": 2, + "no-untyped-call": 6, + "no-untyped-def": 2, + "type-arg": 1 + }, + "app/testing/bootstrap.py": { + "no-untyped-call": 3, + "no-untyped-def": 3, + "type-arg": 6 + }, "app/testing/network_guard.py": { "no-untyped-def": 6 }, @@ -1746,5 +3486,99 @@ "type-arg": 9, "union-attr": 2, "valid-type": 1 + }, + "app/workflow/actions/__init__.py": { + "assignment": 1, + "no-untyped-def": 2, + "type-arg": 7, + "var-annotated": 1 + }, + "app/workflow/actions/add_download.py": { + "arg-type": 3, + "attr-defined": 1, + "type-arg": 1, + "union-attr": 9, + "var-annotated": 1 + }, + "app/workflow/actions/add_subscribe.py": { + "type-arg": 1, + "union-attr": 2, + "var-annotated": 1 + }, + "app/workflow/actions/fetch_downloads.py": { + "operator": 1, + "type-arg": 1, + "var-annotated": 1 + }, + "app/workflow/actions/fetch_medias.py": { + "attr-defined": 1, + "no-redef": 1, + "no-untyped-def": 1, + "type-arg": 1, + "union-attr": 2, + "var-annotated": 1 + }, + "app/workflow/actions/fetch_rss.py": { + "arg-type": 6, + "type-arg": 1, + "union-attr": 2, + "var-annotated": 1 + }, + "app/workflow/actions/fetch_torrents.py": { + "arg-type": 6, + "type-arg": 1, + "union-attr": 4, + "var-annotated": 1 + }, + "app/workflow/actions/filter_medias.py": { + "operator": 1, + "type-arg": 1, + "union-attr": 1, + "var-annotated": 1 + }, + "app/workflow/actions/filter_torrents.py": { + "arg-type": 3, + "list-item": 1, + "no-untyped-call": 1, + "type-arg": 1, + "union-attr": 1, + "var-annotated": 1 + }, + "app/workflow/actions/invoke_plugin.py": { + "type-arg": 2, + "var-annotated": 1 + }, + "app/workflow/actions/note.py": { + "type-arg": 1, + "var-annotated": 1 + }, + "app/workflow/actions/scan_file.py": { + "type-arg": 1, + "union-attr": 2, + "var-annotated": 1 + }, + "app/workflow/actions/scrape_file.py": { + "arg-type": 2, + "no-untyped-call": 1, + "type-arg": 1, + "union-attr": 1, + "var-annotated": 1 + }, + "app/workflow/actions/send_event.py": { + "assignment": 1, + "type-arg": 1, + "var-annotated": 1 + }, + "app/workflow/actions/send_message.py": { + "type-arg": 1, + "var-annotated": 1 + }, + "app/workflow/actions/transfer_file.py": { + "arg-type": 1, + "no-untyped-call": 1, + "no-untyped-def": 1, + "type-arg": 1, + "union-attr": 4, + "var-annotated": 1 } } diff --git a/tests/fixtures/architecture/ruff-baseline.json b/tests/fixtures/architecture/ruff-baseline.json index 8ecddc840..f80cc7d0a 100644 --- a/tests/fixtures/architecture/ruff-baseline.json +++ b/tests/fixtures/architecture/ruff-baseline.json @@ -1,10 +1,4 @@ { - "app/adapters/cache/backends.py": { - "I001": 1 - }, - "app/adapters/cache/redis.py": { - "I001": 1 - }, "app/adapters/external/cookiecloud.py": { "I001": 1 }, @@ -27,9 +21,6 @@ "app/adapters/system/display/__init__.py": { "I001": 1 }, - "app/adapters/system/fsproxy.py": { - "E402": 1 - }, "app/adapters/system/host.py": { "I001": 2 }, @@ -39,9 +30,6 @@ "app/adapters/system/plugin/package.py": { "I001": 1 }, - "app/adapters/system/resource.py": { - "I001": 1 - }, "app/adapters/system/rust.py": { "I001": 1 }, @@ -62,14 +50,9 @@ "I001": 1 }, "app/agent/llm/capability.py": { - "E402": 2, "I001": 1 }, - "app/agent/llm/helper.py": { - "E402": 1 - }, "app/agent/llm/provider.py": { - "E402": 4, "I001": 1 }, "app/agent/llm/server_tools.py": { @@ -78,9 +61,6 @@ "app/agent/mcp.py": { "I001": 1 }, - "app/agent/memory/__init__.py": { - "E402": 4 - }, "app/agent/middleware/memory.py": { "I001": 1 }, @@ -100,8 +80,8 @@ "I001": 1 }, "app/agent/orchestrator.py": { - "E402": 14, - "I001": 3 + "E402": 11, + "I001": 2 }, "app/agent/policy/__init__.py": { "I001": 1 @@ -119,7 +99,6 @@ "I001": 1 }, "app/agent/prompt/__init__.py": { - "E402": 6, "I001": 1 }, "app/agent/runtime.py": { @@ -131,13 +110,8 @@ "app/agent/skills/metadata.py": { "I001": 1 }, - "app/agent/skills/registry.py": { - "E402": 4, - "I001": 1 - }, "app/agent/tools/base.py": { - "E402": 5, - "I001": 2 + "I001": 1 }, "app/agent/tools/factory.py": { "I001": 1 @@ -149,12 +123,8 @@ "I001": 1 }, "app/agent/tools/impl/_plugin_tool_utils.py": { - "E402": 8, "I001": 3 }, - "app/agent/tools/impl/_terminal_session.py": { - "E402": 1 - }, "app/agent/tools/impl/_torrent_search_utils.py": { "I001": 1 }, @@ -162,7 +132,6 @@ "I001": 1 }, "app/agent/tools/impl/add_download_tasks.py": { - "E402": 7, "I001": 1 }, "app/agent/tools/impl/add_subscribe.py": { @@ -175,7 +144,6 @@ "I001": 1 }, "app/agent/tools/impl/create_agent_task.py": { - "E402": 3, "I001": 1 }, "app/agent/tools/impl/delete_custom_filter_rule.py": { @@ -202,9 +170,6 @@ "app/agent/tools/impl/list_directory.py": { "I001": 1 }, - "app/agent/tools/impl/query_agent_tasks.py": { - "E402": 1 - }, "app/agent/tools/impl/query_builtin_filter_rules.py": { "I001": 1 }, @@ -260,7 +225,6 @@ "I001": 1 }, "app/agent/tools/impl/recognize_media.py": { - "E402": 6, "I001": 1 }, "app/agent/tools/impl/reload_plugin.py": { @@ -270,7 +234,6 @@ "I001": 1 }, "app/agent/tools/impl/scrape_metadata.py": { - "E402": 6, "I001": 1 }, "app/agent/tools/impl/search_media.py": { @@ -285,14 +248,10 @@ "app/agent/tools/impl/search_torrents.py": { "I001": 1 }, - "app/agent/tools/impl/search_web.py": { - "E402": 1 - }, "app/agent/tools/impl/send_local_file.py": { "I001": 1 }, "app/agent/tools/impl/send_voice_message.py": { - "E402": 3, "I001": 1 }, "app/agent/tools/impl/test_site.py": { @@ -304,9 +263,6 @@ "app/agent/tools/impl/uninstall_plugin.py": { "I001": 1 }, - "app/agent/tools/impl/update_agent_task.py": { - "E402": 2 - }, "app/agent/tools/impl/update_custom_filter_rule.py": { "I001": 1 }, @@ -398,12 +354,6 @@ "app/application/outbox.py": { "I001": 1 }, - "app/application/plugin/catalog.py": { - "I001": 1 - }, - "app/application/plugin/install.py": { - "I001": 1 - }, "app/application/plugin/runtime.py": { "I001": 1 }, @@ -470,10 +420,6 @@ "app/chain/transfer.py": { "E402": 21 }, - "app/cli.py": { - "E402": 4, - "I001": 1 - }, "app/command.py": { "I001": 1 }, @@ -486,9 +432,6 @@ "app/db/adapters/outbox.py": { "I001": 1 }, - "app/db/adapters/site.py": { - "I001": 1 - }, "app/db/adapters/transaction.py": { "I001": 1 }, @@ -597,10 +540,6 @@ "app/doctor/__init__.py": { "I001": 1 }, - "app/doctor/checks.py": { - "E402": 3, - "I001": 1 - }, "app/doctor/dependencies.py": { "I001": 1 }, @@ -608,12 +547,10 @@ "I001": 1 }, "app/doctor/runner.py": { - "E402": 4, "I001": 1 }, "app/domain/context.py": { - "E731": 1, - "I001": 1 + "E731": 1 }, "app/domain/media.py": { "E731": 1 @@ -660,10 +597,6 @@ "app/domain/title.py": { "I001": 1 }, - "app/factory.py": { - "E402": 9, - "I001": 2 - }, "app/foundation/collections.py": { "I001": 1 }, @@ -693,23 +626,18 @@ "I001": 1 }, "app/modules/acoustid/__init__.py": { - "E402": 4, "I001": 1 }, "app/modules/anilist/__init__.py": { - "E402": 8, - "I001": 2 + "I001": 1 }, "app/modules/anilist/anilist.py": { - "E402": 2, "I001": 1 }, "app/modules/bangumi/__init__.py": { - "E402": 9, - "I001": 2 + "I001": 1 }, "app/modules/bangumi/bangumi.py": { - "E402": 1, "I001": 1 }, "app/modules/discord/__init__.py": { @@ -717,17 +645,14 @@ "I001": 1 }, "app/modules/discord/discord.py": { - "E402": 6, "E741": 1, "F541": 2, - "I001": 2 + "I001": 1 }, "app/modules/douban/__init__.py": { - "E402": 16, - "I001": 2 + "I001": 1 }, "app/modules/douban/apiv2.py": { - "E402": 2, "I001": 1 }, "app/modules/douban/scraper.py": { @@ -738,66 +663,36 @@ "I001": 1 }, "app/modules/emby/emby.py": { - "E402": 6, "F541": 33, - "I001": 2 + "I001": 1 }, "app/modules/fanart/__init__.py": { - "E402": 5, - "I001": 2 + "I001": 1 }, "app/modules/feishu/__init__.py": { "F401": 1, "I001": 1 }, "app/modules/feishu/feishu.py": { - "E402": 10, - "I001": 2 + "I001": 1 }, "app/modules/filemanager/__init__.py": { "I001": 1 }, "app/modules/filemanager/module.py": { - "E402": 20, - "I001": 2 - }, - "app/modules/filemanager/storages/alipan.py": { - "E402": 7 - }, - "app/modules/filemanager/storages/alist.py": { - "E402": 7 - }, - "app/modules/filemanager/storages/local.py": { - "E402": 7 - }, - "app/modules/filemanager/storages/rclone.py": { - "E402": 6 - }, - "app/modules/filemanager/storages/smb.py": { - "E402": 5 - }, - "app/modules/filemanager/storages/u115.py": { - "E402": 7 + "I001": 1 }, "app/modules/filemanager/transhandler.py": { - "E402": 21, "F541": 2, - "I001": 2 + "I001": 1 }, "app/modules/filter/__init__.py": { "I001": 1 }, - "app/modules/imdb/__init__.py": { - "E402": 5 - }, - "app/modules/imdb/api.py": { - "E402": 1 - }, "app/modules/indexer/__init__.py": { "I001": 1 }, "app/modules/indexer/parser/__init__.py": { - "E402": 5, "I001": 1 }, "app/modules/indexer/parser/bitpt.py": { @@ -810,9 +705,6 @@ "app/modules/indexer/parser/file_list.py": { "I001": 1 }, - "app/modules/indexer/parser/gazelle.py": { - "I001": 1 - }, "app/modules/indexer/parser/hddolby.py": { "I001": 1 }, @@ -837,7 +729,6 @@ "I001": 1 }, "app/modules/indexer/parser/rousi.py": { - "E402": 4, "I001": 1 }, "app/modules/indexer/parser/small_horse.py": { @@ -862,40 +753,31 @@ "I001": 1 }, "app/modules/indexer/spider/__init__.py": { - "E402": 8, "F541": 1, - "I001": 2 + "I001": 1 }, "app/modules/indexer/spider/haidan.py": { - "E402": 6, - "I001": 2 + "I001": 1 }, "app/modules/indexer/spider/hddolby.py": { - "E402": 5, - "I001": 2 + "I001": 1 }, "app/modules/indexer/spider/mtorrent.py": { - "E402": 6, - "I001": 2 + "I001": 1 }, "app/modules/indexer/spider/rousi.py": { - "E402": 6, "I001": 1 }, "app/modules/indexer/spider/sunnypt.py": { - "E402": 4, "I001": 1 }, "app/modules/indexer/spider/tnode.py": { - "E402": 4, - "I001": 2 + "I001": 1 }, "app/modules/indexer/spider/torrentleech.py": { - "E402": 5, - "I001": 2 + "I001": 1 }, "app/modules/indexer/spider/yema.py": { - "E402": 4, "I001": 1 }, "app/modules/jellyfin/__init__.py": { @@ -903,24 +785,19 @@ "I001": 1 }, "app/modules/jellyfin/jellyfin.py": { - "E402": 7, "F541": 34, - "I001": 2 + "I001": 1 }, "app/modules/listenbrainz/__init__.py": { - "E402": 5, "I001": 1 }, "app/modules/lrclib/__init__.py": { - "E402": 6, "I001": 1 }, "app/modules/musicbrainz/__init__.py": { - "E402": 10, "I001": 1 }, "app/modules/musicbrainz/music_cache.py": { - "E402": 5, "I001": 1 }, "app/modules/navidrome/__init__.py": { @@ -953,10 +830,6 @@ "app/modules/qqbot/module.py": { "I001": 1 }, - "app/modules/qqbot/qqbot.py": { - "E402": 9, - "I001": 2 - }, "app/modules/rtorrent/__init__.py": { "F541": 2, "I001": 1 @@ -970,11 +843,9 @@ "I001": 1 }, "app/modules/slack/slack.py": { - "E402": 6, - "I001": 2 + "I001": 1 }, "app/modules/subtitle/__init__.py": { - "E402": 7, "I001": 1 }, "app/modules/synologychat/__init__.py": { @@ -996,46 +867,32 @@ "I001": 3 }, "app/modules/theaudiodb/__init__.py": { - "E402": 8, "I001": 1 }, "app/modules/themoviedb/__init__.py": { - "E402": 15, - "I001": 2 + "I001": 1 }, "app/modules/themoviedb/category.py": { - "E402": 3, "F541": 1, "I001": 1 }, "app/modules/themoviedb/scraper.py": { - "E402": 5, "I001": 1 }, "app/modules/themoviedb/tmdb_cache.py": { - "E402": 4, "I001": 1 }, "app/modules/themoviedb/tmdbapi.py": { - "E402": 6, - "I001": 2 + "I001": 1 }, "app/modules/themoviedb/tmdbv3api/__init__.py": { "F401": 25 }, - "app/modules/themoviedb/tmdbv3api/objs/discover.py": { - "E402": 1 - }, "app/modules/themoviedb/tmdbv3api/tmdb.py": { - "E402": 2, - "I001": 2 - }, - "app/modules/thetvdb/__init__.py": { - "E402": 4, "I001": 1 }, - "app/modules/thetvdb/tvdb_v4_official.py": { - "E402": 1 + "app/modules/thetvdb/__init__.py": { + "I001": 1 }, "app/modules/transmission/__init__.py": { "F541": 2, @@ -1048,7 +905,6 @@ "I001": 1 }, "app/modules/trimemedia/api.py": { - "E402": 2, "I001": 1 }, "app/modules/trimemedia/module.py": { @@ -1077,8 +933,7 @@ "I001": 1 }, "app/modules/webpush/__init__.py": { - "E402": 4, - "I001": 2 + "I001": 1 }, "app/modules/wechat/__init__.py": { "F541": 3, @@ -1088,15 +943,10 @@ "F541": 1, "I001": 1 }, - "app/modules/wechat/wechatbot.py": { - "E402": 10, - "I001": 2 - }, "app/modules/wechatclawbot/__init__.py": { "I001": 1 }, "app/modules/wechatclawbot/wechatclawbot.py": { - "E402": 6, "I001": 1 }, "app/modules/zspace/__init__.py": { @@ -1149,14 +999,10 @@ "app/runtime/event/errors.py": { "I001": 1 }, - "app/runtime/events.py": { - "I001": 1 - }, "app/runtime/execution.py": { "I001": 1 }, "app/runtime/extensions/host_module_adapter.py": { - "E402": 2, "I001": 1 }, "app/runtime/extensions/managed_resource_adapter.py": { @@ -1166,12 +1012,8 @@ "I001": 1 }, "app/runtime/extensions/module_manager.py": { - "E402": 4, "I001": 1 }, - "app/runtime/extensions/plugin/catalog.py": { - "E402": 5 - }, "app/runtime/extensions/plugin/lifecycle.py": { "I001": 1 }, @@ -1182,8 +1024,7 @@ "I001": 1 }, "app/runtime/extensions/plugin_manager.py": { - "E402": 21, - "I001": 2 + "I001": 1 }, "app/runtime/extensions/service_config.py": { "I001": 1 @@ -1207,12 +1048,8 @@ "app/runtime/reload.py": { "I001": 1 }, - "app/runtime/settings.py": { - "I001": 1 - }, "app/runtime/state.py": { - "E402": 3, - "I001": 2 + "I001": 1 }, "app/schemas/agent.py": { "I001": 1 @@ -1223,9 +1060,6 @@ "app/schemas/context.py": { "I001": 1 }, - "app/schemas/event.py": { - "I001": 1 - }, "app/schemas/file.py": { "I001": 1 }, @@ -1271,9 +1105,6 @@ "app/sdk/config.py": { "I001": 1 }, - "app/sdk/events.py": { - "I001": 1 - }, "app/sdk/logging.py": { "I001": 1 }, @@ -1292,21 +1123,6 @@ "app/sdk/utilities.py": { "I001": 1 }, - "app/startup/initializers/agent.py": { - "E402": 3 - }, - "app/startup/initializers/database.py": { - "E402": 5 - }, - "app/startup/initializers/domain.py": { - "E402": 1 - }, - "app/startup/initializers/modules.py": { - "E402": 79 - }, - "app/startup/initializers/plugins.py": { - "E402": 25 - }, "app/startup/lifecycle/__init__.py": { "E402": 27, "F401": 1 @@ -1314,9 +1130,6 @@ "app/workflow/__init__.py": { "F401": 1 }, - "scripts/architecture/baseline.py": { - "I001": 1 - }, "scripts/architecture/task_ownership.py": { "I001": 1 }, @@ -1432,9 +1245,6 @@ "tests/test_agent_summarization_streaming.py": { "I001": 1 }, - "tests/test_agent_task_run_migration.py": { - "I001": 1 - }, "tests/test_agent_task_runs.py": { "I001": 1 }, @@ -1465,9 +1275,6 @@ "tests/test_api_response.py": { "I001": 1 }, - "tests/test_architecture_ci.py": { - "I001": 1 - }, "tests/test_architecture_contract_baseline.py": { "I001": 1 }, @@ -1605,10 +1412,6 @@ "tests/test_episode_group_recognition.py": { "E402": 4 }, - "tests/test_event_contracts.py": { - "F401": 1, - "I001": 1 - }, "tests/test_event_dispatch_snapshot.py": { "I001": 1 }, @@ -1662,12 +1465,8 @@ "tests/test_legacy_import_compat.py": { "I001": 1 }, - "tests/test_legacy_plugin_resource_imports.py": { - "I001": 1 - }, "tests/test_lifecycle_shutdown.py": { - "F841": 1, - "I001": 1 + "F841": 1 }, "tests/test_llm_provider_bedrock.py": { "I001": 1 @@ -1777,9 +1576,6 @@ "tests/test_module_manager_capability_adapter.py": { "I001": 1 }, - "tests/test_module_method_contracts.py": { - "I001": 1 - }, "tests/test_module_quality.py": { "I001": 1 }, @@ -1838,9 +1634,6 @@ "tests/test_musicbrainz_module.py": { "I001": 1 }, - "tests/test_mypy_gate.py": { - "I001": 1 - }, "tests/test_navidrome_module.py": { "I001": 1 }, @@ -1868,16 +1661,8 @@ "tests/test_plugin_dependency_service.py": { "I001": 1 }, - "tests/test_plugin_endpoint.py": { - "F401": 1, - "I001": 1 - }, "tests/test_plugin_helper.py": { - "F401": 2, - "I001": 11 - }, - "tests/test_plugin_install_command.py": { - "I001": 1 + "F401": 2 }, "tests/test_plugin_lifecycle_status.py": { "F401": 1 @@ -2039,9 +1824,6 @@ "tests/test_testing_bootstrap_plugin_namespace.py": { "I001": 1 }, - "tests/test_tmdb_auxiliary.py": { - "F401": 1 - }, "tests/test_tmdb_cache_management.py": { "I001": 1 }, @@ -2133,16 +1915,9 @@ "tests/test_uvicorn_entrypoint.py": { "I001": 1 }, - "tests/test_web_agent_stream.py": { - "F401": 1, - "I001": 1 - }, "tests/test_webpush_helper.py": { "I001": 1 }, - "tests/test_workflow_actions.py": { - "I001": 1 - }, "tests/test_workflow_authorization.py": { "I001": 1 }, diff --git a/tests/test_architecture_ci.py b/tests/test_architecture_ci.py index dfda0c32d..eb8b31715 100644 --- a/tests/test_architecture_ci.py +++ b/tests/test_architecture_ci.py @@ -4,7 +4,6 @@ from pathlib import Path from ruamel.yaml import YAML - PROJECT_ROOT = Path(__file__).parents[1] WORKFLOW_ROOT = PROJECT_ROOT / ".github" / "workflows" @@ -33,6 +32,10 @@ def test_unit_test_workflow_has_independent_host_architecture_gate(): assert "tests/test_architecture_dependencies.py" in commands assert "tests/test_architecture_contract_baseline.py" in commands assert "scripts/architecture/baseline.py --check-host" in commands + assert "scripts/architecture/ruff_ratchet.py" in commands + assert "scripts/architecture/mypy_ratchet.py" in commands + assert "scripts/architecture/ruff_ratchet.py --write" not in commands + assert "scripts/architecture/mypy_ratchet.py --write" not in commands assert "scripts/architecture/service_locator.py" in commands assert "scripts/startup/performance.py --check --repeat 3" in commands diff --git a/tests/test_mypy_gate.py b/tests/test_mypy_gate.py index b72de56d8..ca3e11897 100644 --- a/tests/test_mypy_gate.py +++ b/tests/test_mypy_gate.py @@ -1,9 +1,13 @@ -"""mypy 错误数只降不增 ratchet 的执行、解析与对比逻辑测试。""" +"""mypy 错误低水位 ratchet 的执行、解析与对比逻辑测试。""" +import json +import subprocess +import sys from unittest.mock import patch -from scripts.architecture.mypy_ratchet import compare_counts, parse_errors, run_mypy +import pytest +from scripts.architecture.mypy_ratchet import compare_counts, main, parse_errors, run_mypy MYPY_SAMPLE = """ app/application/a.py:12: error: Function is missing a type annotation [no-untyped-def] @@ -18,13 +22,86 @@ Found 3 errors in 1 file (checked 500 source files) def test_run_mypy_uses_stable_full_analysis() -> None: """全量门禁不得复用前序检查缓存,也不得输出换行错误码。""" with patch("scripts.architecture.mypy_ratchet.subprocess.run") as run: - run.return_value.stdout = "" + run.return_value = subprocess.CompletedProcess( + args=[], + returncode=0, + stdout="Success: no issues found in 500 source files\n", + stderr="", + ) run_mypy() command = run.call_args.args[0] assert "--no-incremental" in command assert "--no-pretty" in command + assert command[command.index("--platform") + 1] == "linux" + + +def test_run_mypy_rejects_tool_failure() -> None: + """内部错误等退出码 2 必须让门禁失败,不能拿截断输出生成基线。""" + result = subprocess.CompletedProcess( + args=[], + returncode=2, + stdout=MYPY_SAMPLE, + stderr="mypy: INTERNAL ERROR", + ) + with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result): + with pytest.raises(RuntimeError, match="异常退出.*INTERNAL ERROR"): + run_mypy() + + +def test_run_mypy_rejects_unexpected_stderr() -> None: + """正常退出码携带 stderr 时仍须 fail-closed。""" + result = subprocess.CompletedProcess( + args=[], + returncode=1, + stdout=MYPY_SAMPLE, + stderr="analysis incomplete", + ) + with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result): + with pytest.raises(RuntimeError, match="stderr"): + run_mypy() + + +def test_run_mypy_rejects_truncated_error_report() -> None: + """摘要错误数与解析数量不同时必须拒绝不完整报告。""" + result = subprocess.CompletedProcess( + args=[], + returncode=1, + stdout=MYPY_SAMPLE.replace("Found 3 errors", "Found 4 errors"), + stderr="", + ) + with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result): + with pytest.raises(RuntimeError, match="摘要与已解析错误数不一致"): + run_mypy() + + +def test_run_mypy_accepts_complete_error_report() -> None: + """退出码 1 的完整诊断应交给 ratchet 比较,而不是被当作工具失败。""" + result = subprocess.CompletedProcess( + args=[], + returncode=1, + stdout=MYPY_SAMPLE, + stderr="", + ) + with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result): + assert run_mypy() == MYPY_SAMPLE + + +def test_run_mypy_rejects_missing_summary() -> None: + """没有完成摘要的错误输出可能被截断,必须拒绝。""" + result = subprocess.CompletedProcess( + args=[], + returncode=1, + stdout=MYPY_SAMPLE.replace( + "Found 3 errors in 1 file (checked 500 source files)\n", + "", + ), + stderr="", + ) + with patch("scripts.architecture.mypy_ratchet.subprocess.run", return_value=result): + with pytest.raises(RuntimeError, match="缺少唯一的完整摘要"): + run_mypy() def test_parse_errors_aggregates_per_file_and_code() -> None: @@ -46,12 +123,24 @@ def test_parse_errors_buckets_missing_code_as_unknown() -> None: assert report == {"app/b.py": {"unknown": 1}} -def test_ratchet_allows_removal_and_reduction() -> None: - """文件删除或错误数减少必须允许通过。""" +def test_ratchet_requires_reduced_errors_to_be_persisted() -> None: + """错误减少后必须刷新 fixture,避免旧额度允许回退。""" baseline = {"app/a.py": {"arg-type": 5, "misc": 2}} current = {"app/a.py": {"arg-type": 3}} - assert compare_counts(baseline, current) == [] + assert compare_counts(baseline, current) == [ + "app/a.py: 类型错误低水位未固化 [arg-type] 5->3", + "app/a.py: 类型错误低水位未固化 [misc] 2->0", + ] + + +def test_ratchet_requires_deleted_file_to_be_persisted() -> None: + """整文件债务消失也必须写回 fixture。""" + baseline = {"app/a.py": {"arg-type": 1}} + + assert compare_counts(baseline, {}) == [ + "app/a.py: 类型错误低水位未固化 [arg-type] 1->0" + ] def test_ratchet_rejects_growth_and_new_errors() -> None: @@ -71,3 +160,47 @@ def test_ratchet_rejects_growth_and_new_errors() -> None: assert any("app/a.py" in p and "arg-type" in p and "既有错误增长" in p for p in problems) assert any("app/a.py" in p and "type-arg" in p and "新增类型错误" in p for p in problems) assert any("app/b.py" in p and "新增类型错误" in p for p in problems) + + +def test_write_refuses_to_legalize_type_regression(tmp_path, monkeypatch) -> None: + """已有基线出现增长时,--write 不得覆盖原文件。""" + baseline_path = tmp_path / "mypy-baseline.json" + baseline_path.write_text("{}\n", encoding="utf-8") + monkeypatch.setattr( + sys, + "argv", + ["mypy_ratchet.py", "--write", "--baseline", str(baseline_path)], + ) + + with patch("scripts.architecture.mypy_ratchet.run_mypy", return_value=MYPY_SAMPLE): + assert main() == 1 + + assert json.loads(baseline_path.read_text(encoding="utf-8")) == {} + + +def test_write_persists_reduced_type_low_watermark(tmp_path, monkeypatch) -> None: + """没有增长时,--write 应把下降后的完整快照固化。""" + baseline_path = tmp_path / "mypy-baseline.json" + baseline_path.write_text( + json.dumps( + { + "app/application/a.py": { + "no-untyped-def": 3, + "type-arg": 1, + } + } + ), + encoding="utf-8", + ) + monkeypatch.setattr( + sys, + "argv", + ["mypy_ratchet.py", "--write", "--baseline", str(baseline_path)], + ) + + with patch("scripts.architecture.mypy_ratchet.run_mypy", return_value=MYPY_SAMPLE): + assert main() == 0 + + assert json.loads(baseline_path.read_text(encoding="utf-8")) == { + "app/application/a.py": {"no-untyped-def": 2, "type-arg": 1} + } diff --git a/tests/test_quality_ratchets.py b/tests/test_quality_ratchets.py index fc4fb1280..7cc0e2edb 100644 --- a/tests/test_quality_ratchets.py +++ b/tests/test_quality_ratchets.py @@ -1,29 +1,50 @@ """Ruff 与覆盖率增量门禁测试。""" +import json +import subprocess +import sys +from unittest.mock import patch + +import pytest + from scripts.architecture.coverage_ratchet import ( collect_package_coverage, compare_coverage, ) -from scripts.architecture.ruff_ratchet import aggregate_diagnostics, compare_counts +from scripts.architecture.ruff_ratchet import ( + PROJECT_ROOT, + aggregate_diagnostics, + compare_counts, + run_ruff, +) +from scripts.architecture.ruff_ratchet import ( + main as ruff_main, +) def test_ruff_ratchet_rejects_new_rule_and_count_growth() -> None: - """Ruff 基线允许修复,但拒绝新增规则和既有数量增长。""" + """Ruff 基线拒绝新增、增长以及尚未固化的下降。""" baseline = {"app/example.py": {"F401": 2}} - assert compare_counts(baseline, {"app/example.py": {"F401": 1}}) == [] + assert compare_counts(baseline, {"app/example.py": {"F401": 1}}) == [ + "app/example.py: Ruff 低水位未固化 [F401] 2->1" + ] assert compare_counts(baseline, {"app/example.py": {"F401": 3}}) == [ "app/example.py: Ruff 诊断增长 [F401] 2->3" ] - assert compare_counts(baseline, {"app/new.py": {"I001": 1}}) == [ + assert compare_counts( + baseline, + { + "app/example.py": {"F401": 2}, + "app/new.py": {"I001": 1}, + }, + ) == [ "app/new.py: 新增 Ruff 诊断 [I001] x1" ] def test_ruff_diagnostics_are_aggregated_by_relative_file_and_code() -> None: """Ruff JSON 的绝对路径必须归一化为稳定仓库路径。""" - from scripts.architecture.ruff_ratchet import PROJECT_ROOT - path = PROJECT_ROOT / "app" / "example.py" diagnostics = [ {"filename": str(path), "code": "F401"}, @@ -36,6 +57,88 @@ def test_ruff_diagnostics_are_aggregated_by_relative_file_and_code() -> None: } +def test_run_ruff_rejects_tool_failure() -> None: + """Ruff 工具异常必须 fail-closed,不能把空输出当成零诊断。""" + result = subprocess.CompletedProcess( + args=[], + returncode=2, + stdout="", + stderr="ruff failed", + ) + with patch("scripts.architecture.ruff_ratchet.subprocess.run", return_value=result): + with pytest.raises(RuntimeError, match="ruff failed"): + run_ruff() + + +@pytest.mark.parametrize( + ("returncode", "stdout", "message"), + [ + (0, '[{"code": "F401"}]', "成功但仍输出诊断"), + (1, "[]", "诊断状态但输出为空"), + ], +) +def test_run_ruff_rejects_status_payload_mismatch( + returncode: int, + stdout: str, + message: str, +) -> None: + """Ruff 退出状态必须与结构化诊断是否存在一致。""" + result = subprocess.CompletedProcess( + args=[], + returncode=returncode, + stdout=stdout, + stderr="", + ) + with patch("scripts.architecture.ruff_ratchet.subprocess.run", return_value=result): + with pytest.raises(RuntimeError, match=message): + run_ruff() + + +def test_ruff_write_refuses_to_legalize_regression(tmp_path, monkeypatch) -> None: + """已有基线出现增长时,--write 不得覆盖原文件。""" + baseline_path = tmp_path / "ruff-baseline.json" + baseline_path.write_text("{}\n", encoding="utf-8") + monkeypatch.setattr( + sys, + "argv", + ["ruff_ratchet.py", "--write", "--baseline", str(baseline_path)], + ) + diagnostic = { + "filename": str(PROJECT_ROOT / "app" / "example.py"), + "code": "F401", + } + + with patch("scripts.architecture.ruff_ratchet.run_ruff", return_value=[diagnostic]): + assert ruff_main() == 1 + + assert json.loads(baseline_path.read_text(encoding="utf-8")) == {} + + +def test_ruff_write_persists_reduced_low_watermark(tmp_path, monkeypatch) -> None: + """没有增长时,--write 应把下降后的 Ruff 快照固化。""" + baseline_path = tmp_path / "ruff-baseline.json" + baseline_path.write_text( + json.dumps({"app/example.py": {"F401": 2}}), + encoding="utf-8", + ) + monkeypatch.setattr( + sys, + "argv", + ["ruff_ratchet.py", "--write", "--baseline", str(baseline_path)], + ) + diagnostic = { + "filename": str(PROJECT_ROOT / "app" / "example.py"), + "code": "F401", + } + + with patch("scripts.architecture.ruff_ratchet.run_ruff", return_value=[diagnostic]): + assert ruff_main() == 0 + + assert json.loads(baseline_path.read_text(encoding="utf-8")) == { + "app/example.py": {"F401": 1} + } + + def test_coverage_ratchet_aggregates_governed_packages() -> None: """覆盖率门禁只聚合 Application 与 Domain,并按真实语句数加权。""" report = { diff --git a/tests/test_system_notification_dispatch.py b/tests/test_system_notification_dispatch.py index 24e14fab2..df3475b6e 100644 --- a/tests/test_system_notification_dispatch.py +++ b/tests/test_system_notification_dispatch.py @@ -71,22 +71,45 @@ class TestSystemNotificationDispatch(unittest.TestCase): def test_async_send_message_uses_executor_for_immediate_send(self): """异步立即发送不能在事件循环里直接执行同步渠道回调。""" class _FakeLoop: - def __init__(self): - self.called = False + """同步模拟 asyncio 的 executor 提交,并记录提交时的上下文。""" - async def run_in_executor(self, _executor, func, *args): + def __init__(self, loop): + self.called = False + self.loop = loop + self.submission_correlation_id = None + + def run_in_executor(self, _executor, func, *args): + """立即执行 worker 调用并返回已完成 Future。""" self.called = True - func(*args) + self.submission_correlation_id = get_correlation_id() + future = self.loop.create_future() + try: + func(*args) + except BaseException as error: + future.set_exception(error) + else: + future.set_result(None) + return future async def _run(): manager = MessageQueueManager() - fake_loop = _FakeLoop() + fake_loop = _FakeLoop(asyncio.get_running_loop()) + observed = [] with patch( "asyncio.get_running_loop", return_value=fake_loop, - ), patch.object(manager, "_send") as send: - await manager.async_send_message("payload", immediately=True) + ), patch.object( + manager, + "_send", + side_effect=lambda *_args, **_kwargs: observed.append( + get_correlation_id() + ), + ) as send: + with correlation_scope("message-request"): + await manager.async_send_message("payload", immediately=True) self.assertTrue(fake_loop.called) + self.assertIsNone(fake_loop.submission_correlation_id) + self.assertEqual(observed, ["message-request"]) send.assert_called_once_with("payload") asyncio.run(_run())