ci(architecture): close complexity and concurrency blind spots

This commit is contained in:
jxxghp
2026-08-30 11:40:36 +08:00
parent 87269610e0
commit f7ca7e5172
9 changed files with 1117 additions and 2 deletions

View File

@@ -63,7 +63,12 @@ jobs:
run: uv run --locked --no-sync mypy --config-file mypy.ini
- name: Check complexity ratchet
run: uv run --locked --no-sync python scripts/architecture/complexity.py
run: |
uv run --locked --no-sync python scripts/architecture/complexity.py
uv run --locked --no-sync python scripts/architecture/complexity.py --v2
- name: Check native concurrency ownership
run: uv run --locked --no-sync python scripts/architecture/concurrency.py
- name: Check async blocking ratchet
run: uv run --locked --no-sync python scripts/architecture/async_blocking.py

View File

@@ -20,6 +20,10 @@
| RES-003 | `PLANNED`(R3) | Outbox after_commit 失败只有内存 pending 标记,重启不可恢复 | 持久 intent、唯一 handler、claim/fencing、重启回放、幂等和失败观测完整闭环 |
| RES-004 | `PLANNED`(R4) | Startup initializer 与插件市场存在多套 Transport/Adapter/Manager 构造 | 所有构造回收到 composition root,插件兼容只经 SDK/Compat,canonical 不保留重复正式入口 |
| RES-005 | `PLANNED`(R5) | 审计声明、机器门禁、CI 与远端交付状态需要重新校准 | 文档与事实源一致;专项、锁定全量、Pylint、架构/兼容门禁和 GitHub CI 闭环,远端 `0/0` |
R2 已落地门禁基础:`complexity.py --v2` 覆盖私有/嵌套方法、类、文件和
`app/scheduler/`;`concurrency.py` 覆盖宿主原生并发原语并冻结调用点与词法 owner。
当前基线只允许删除或收缩,新增调用点和 owner 漂移均阻断 CI;现有历史热点仍须在后续
叶子中按真实所有权逐项收口,不得以生成基线伪装为债务清零。
## 1. 结论摘要

View File

@@ -216,6 +216,18 @@ only the historical `Scheduler` and `SchedulerChain` identities. New plugins use
the narrow `app.sdk.scheduler` facade; internal Scheduler owners must not be
re-exported from the package root, SDK or Compat.
Complexity and concurrency governance covers the complete canonical execution
surface rather than only public methods. `scripts/architecture/complexity.py --v2`
ratchets private and nested methods, class/file hotspots, and the
`app/scheduler/` package; its generated baseline is an evidence ledger, not a
permission to add another oversized owner. `scripts/architecture/concurrency.py`
enumerates native Thread, Timer, thread/process executors, Process,
asyncio task/thread helpers, and executor hand-off calls across host roots. Each
fact must have a stable lexical owner and remain in the baseline with no new
call site or owner drift. `app/plugins/**`, `app/sdk/**`, and
`app/runtime/compat/**` are excluded because their compatibility/runtime
contracts are governed by their own exact manifests.
`app.schemas` and the `app.db` package root are compatibility facades, not
implementation dependency hubs. Host code imports concrete schema submodules; the schema root
resolves its generated export manifest lazily for plugins and legacy callers.

View File

@@ -11,6 +11,7 @@ from typing import Iterable
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/complexity-baseline.json"
V2_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/complexity-v2-baseline.json"
@dataclass(frozen=True, slots=True)
@@ -30,6 +31,85 @@ RULES = (
)
def _iter_python_files(root: Path, roots: tuple[str, ...]) -> Iterable[Path]:
"""遍历 canonical 宿主源码,明确排除运行时插件副本。"""
for scan_root in roots:
path = root / scan_root
if path.is_file() and path.suffix == ".py":
yield path
continue
if not path.is_dir():
continue
for candidate in sorted(path.rglob("*.py")):
relative = candidate.relative_to(root).as_posix()
if relative == "app/plugins" or relative.startswith("app/plugins/"):
continue
yield candidate
def _walk_functions(
nodes: Iterable[ast.AST],
prefix: str = "",
) -> Iterable[tuple[str, ast.FunctionDef | ast.AsyncFunctionDef]]:
"""递归收集函数、方法和嵌套编排器,避免只检查公开入口。"""
for node in nodes:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
qualname = f"{prefix}.{node.name}" if prefix else node.name
if not node.name.startswith("__"):
yield qualname, node
yield from _walk_functions(node.body, qualname)
elif isinstance(node, ast.ClassDef):
qualname = f"{prefix}.{node.name}" if prefix else node.name
yield from _walk_functions(node.body, qualname)
elif isinstance(node, ast.If):
yield from _walk_functions(node.body, prefix)
yield from _walk_functions(node.orelse, prefix)
elif isinstance(node, (ast.For, ast.AsyncFor, ast.While, ast.With, ast.AsyncWith)):
yield from _walk_functions(node.body, prefix)
yield from _walk_functions(node.orelse, prefix) if isinstance(node, (ast.For, ast.AsyncFor)) else ()
elif isinstance(node, ast.Try):
yield from _walk_functions(node.body, prefix)
yield from _walk_functions(node.orelse, prefix)
yield from _walk_functions(node.finalbody, prefix)
for handler in node.handlers:
yield from _walk_functions(handler.body, prefix)
def _walk_classes(
nodes: Iterable[ast.AST],
prefix: str = "",
) -> Iterable[tuple[str, ast.ClassDef]]:
"""递归收集类体长度,覆盖 Scheduler 和私有 owner。"""
for node in nodes:
if isinstance(node, ast.ClassDef):
qualname = f"{prefix}.{node.name}" if prefix else node.name
yield qualname, node
yield from _walk_classes(node.body, qualname)
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
yield from _walk_classes(node.body, prefix)
def collect_complexity_v2(root: Path = PROJECT_ROOT) -> dict[str, dict[str, int]]:
"""收集私有方法、类、文件和 Scheduler 的可审计复杂度事实。"""
roots = ("app/api/endpoints", "app/application", "app/chain", "app/scheduler")
report: dict[str, dict[str, int]] = {"method": {}, "class": {}, "file": {}}
for path in _iter_python_files(root, roots):
relative = path.relative_to(root).as_posix()
source_lines = path.read_text(encoding="utf-8").splitlines()
if len(source_lines) > 1000:
report["file"][relative] = len(source_lines)
tree = ast.parse("\n".join(source_lines), filename=str(path))
for qualname, node in _walk_functions(tree.body):
line_count = (node.end_lineno or node.lineno) - node.lineno + 1
if line_count > 150:
report["method"][f"{relative}:{qualname}"] = line_count
for qualname, node in _walk_classes(tree.body):
line_count = (node.end_lineno or node.lineno) - node.lineno + 1
if line_count > 500:
report["class"][f"{relative}:{qualname}"] = line_count
return report
def _is_endpoint(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
"""识别带常见 HTTP method decorator 的 API endpoint。"""
methods = {"get", "post", "put", "patch", "delete", "options", "head"}
@@ -92,12 +172,48 @@ def compare_complexity(
return problems
def compare_complexity_v2(
baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]]
) -> list[str]:
"""返回 v2 私有方法、类和文件事实的新增或增长问题。"""
problems: list[str] = []
for category in baseline.keys() | current.keys():
previous = baseline.get(category, {})
for entry, line_count in current.get(category, {}).items():
if entry not in previous:
problems.append(f"{category}: 新增超限 {entry}={line_count}")
elif line_count > previous[entry]:
problems.append(
f"{category}: 既有超限增长 {entry}={line_count}>{previous[entry]}"
)
return problems
def main() -> int:
"""执行复杂度 baseline check 或显式 write。"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--write", action="store_true", help="写入当前超限基线")
parser.add_argument("--v2", action="store_true", help="检查私有方法、类、文件和 Scheduler")
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
args = parser.parse_args()
if args.v2:
baseline_path = args.baseline if args.baseline != DEFAULT_BASELINE else V2_BASELINE
current_v2 = collect_complexity_v2()
if args.write:
baseline_path.parent.mkdir(parents=True, exist_ok=True)
baseline_path.write_text(
json.dumps(current_v2, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(f"已写入 {baseline_path.relative_to(PROJECT_ROOT)}")
return 0
baseline_v2 = json.loads(baseline_path.read_text(encoding="utf-8"))
problems_v2 = compare_complexity_v2(baseline_v2, current_v2)
if problems_v2:
print("\n".join(problems_v2))
return 1
print("复杂度 v2 ratchet 通过")
return 0
current = collect_complexity()
if args.write:
args.baseline.parent.mkdir(parents=True, exist_ok=True)

View File

@@ -0,0 +1,200 @@
"""枚举宿主原生并发原语并维护稳定 owner 事实。"""
from __future__ import annotations
import argparse
import ast
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/concurrency-baseline.json"
SCAN_ROOTS = (
"app/adapters",
"app/api",
"app/agent",
"app/application",
"app/chain",
"app/command.py",
"app/db",
"app/domain",
"app/foundation",
"app/main.py",
"app/monitor",
"app/modules",
"app/runtime",
"app/scheduler",
"app/startup",
"app/workflow",
)
_CALL_SUFFIXES = frozenset({
"create_task", "ensure_future", "to_thread", "run_in_executor", "run_in_threadpool",
})
_CONSTRUCTOR_NAMES = frozenset({
"Thread", "Timer", "ThreadPoolExecutor", "ProcessPoolExecutor", "Process",
})
@dataclass(frozen=True, slots=True)
class ConcurrencyFact:
"""描述一个原生并发调用及其静态 owner。"""
path: str
line: int
target: str
owner: str
@property
def key(self) -> str:
"""返回跨运行稳定的事实键。"""
return f"{self.path}:{self.line}:{self.target}"
def as_dict(self) -> dict[str, str]:
"""转换为基线文件中的稳定记录。"""
return {"owner": self.owner, "target": self.target}
def _iter_python_files(root: Path, scan_roots: Iterable[str]) -> Iterable[Path]:
"""遍历宿主源码并排除插件副本、SDK 和兼容实现。"""
for scan_root in scan_roots:
path = root / scan_root
if path.is_file():
candidates = [path]
elif path.is_dir():
candidates = sorted(path.rglob("*.py"))
else:
candidates = []
for candidate in candidates:
relative = candidate.relative_to(root).as_posix()
if any(
relative == excluded or relative.startswith(f"{excluded}/")
for excluded in ("app/plugins", "app/sdk", "app/runtime/compat")
):
continue
yield candidate
def _target_name(node: ast.Call) -> str | None:
"""识别约定的原生并发 API,避免按普通方法名误报。"""
if not isinstance(node.func, (ast.Name, ast.Attribute)):
return None
target = ast.unparse(node.func)
terminal = target.rsplit(".", 1)[-1]
if terminal in _CALL_SUFFIXES or terminal in _CONSTRUCTOR_NAMES:
return terminal
return None
class _ConcurrencyVisitor(ast.NodeVisitor):
"""收集函数、类和模块级并发调用的词法 owner。"""
def __init__(self, relative_path: str) -> None:
"""初始化文件位置和 owner 栈。"""
self._relative_path = relative_path
self._owners: list[str] = []
self.facts: list[ConcurrencyFact] = []
@property
def _owner(self) -> str:
"""返回最具体的可定位 owner。"""
return ".".join(self._owners) if self._owners else self._relative_path
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""把类名纳入 owner 路径后继续扫描方法体。"""
self._owners.append(node.name)
self.generic_visit(node)
self._owners.pop()
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
"""把同步函数纳入 owner 路径后扫描其嵌套并发。"""
self._owners.append(node.name)
self.generic_visit(node)
self._owners.pop()
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
"""把异步函数纳入 owner 路径后扫描其嵌套并发。"""
self._owners.append(node.name)
self.generic_visit(node)
self._owners.pop()
def visit_Call(self, node: ast.Call) -> None:
"""记录已识别原语并继续访问参数中的嵌套调用。"""
target = _target_name(node)
if target:
self.facts.append(
ConcurrencyFact(
path=self._relative_path,
line=node.lineno,
target=target,
owner=self._owner,
)
)
self.generic_visit(node)
def collect_concurrency(
root: Path = PROJECT_ROOT,
*,
scan_roots: Iterable[str] = SCAN_ROOTS,
) -> dict[str, dict[str, str]]:
"""收集全部宿主原生并发调用及静态 owner。"""
facts: dict[str, dict[str, str]] = {}
for path in _iter_python_files(root, scan_roots):
relative = path.relative_to(root).as_posix()
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
visitor = _ConcurrencyVisitor(relative)
visitor.visit(tree)
for fact in visitor.facts:
facts[fact.key] = fact.as_dict()
return dict(sorted(facts.items()))
def compare_concurrency(
baseline: dict[str, dict[str, str]],
current: dict[str, dict[str, str]],
) -> list[str]:
"""拒绝新增原语、owner 丢失或事实 owner 漂移。"""
problems: list[str] = []
for key, record in current.items():
if not record.get("owner"):
problems.append(f"并发事实缺少 owner:{key}")
continue
previous = baseline.get(key)
if previous is None:
problems.append(f"新增原生并发事实:{key}")
elif previous.get("owner") != record.get("owner"):
problems.append(
f"并发 owner 漂移:{key}={record.get('owner')}(基线为 {previous.get('owner')})"
)
return problems
def main() -> int:
"""执行原生并发事实 ratchet。"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--write", action="store_true", help="写入当前并发事实基线")
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
args = parser.parse_args()
current = collect_concurrency()
if args.write:
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)}")
return 0
baseline = json.loads(args.baseline.read_text(encoding="utf-8"))
problems = compare_concurrency(baseline, current)
if problems:
print("\n".join(problems))
return 1
print("原生并发事实 ratchet 通过")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,79 @@
{
"class": {
"app/application/formatting.py:EpisodeFormatRuleHelper": 1280,
"app/application/messaging/site.py:SiteInteractionHandler": 579,
"app/application/messaging/skill.py:SkillInteractionHandler": 1101,
"app/application/messaging/subscribe.py:SubscribeInteractionHandler": 694,
"app/application/plugin/install.py:PluginInstallCommand": 739,
"app/application/rss.py:RssHelper": 531,
"app/application/security/url.py:SecurityUtils": 771,
"app/application/torrent/download.py:TorrentHelper": 738,
"app/application/transfer/workflow.py:JobManager": 754,
"app/chain/_recognition.py:RecognitionMixin": 570,
"app/chain/base.py:ChainBase": 1161,
"app/chain/download/batch.py:DownloadBatchOwner": 515,
"app/chain/download/subtitle.py:DownloadSubtitleOwner": 538,
"app/chain/interaction.py:MediaInteractionChain": 1540,
"app/chain/mediaserver.py:MediaServerChain": 568,
"app/chain/message.py:MessageChain": 1876,
"app/chain/recommend.py:RecommendChain": 742,
"app/chain/scraping.py:ScrapingChain": 1802,
"app/chain/search/media.py:SearchMediaOwner": 522,
"app/chain/search/provider.py:SearchProviderOwner": 662,
"app/chain/search/subtitle.py:SearchSubtitleOwner": 528,
"app/chain/site.py:SiteChain": 1003,
"app/chain/subscribe/create.py:SubscribeCreateOwner": 646,
"app/chain/subscribe/match.py:SubscribeMatchOwner": 571,
"app/chain/subscribe/refresh.py:SubscribeRefreshOwner": 760,
"app/chain/torrents.py:TorrentsChain": 792,
"app/chain/transfer/plan.py:TransferPlanningOwner": 909,
"app/chain/transfer/queue.py:TransferQueueOwner": 1572,
"app/chain/transfer/settlement.py:TransferSettlementOwner": 648,
"app/chain/transfer/workflow.py:TransferWorkflowOwner": 1061,
"app/chain/workflow.py:WorkflowExecutor": 1143
},
"file": {
"app/api/endpoints/system.py": 1438,
"app/application/formatting.py": 1523,
"app/application/history.py": 1505,
"app/application/messaging/agent.py": 2195,
"app/application/messaging/message.py": 1314,
"app/application/messaging/skill.py": 1300,
"app/application/security/url.py": 1079,
"app/application/transfer/workflow.py": 1957,
"app/chain/base.py": 1201,
"app/chain/interaction.py": 1571,
"app/chain/message.py": 1984,
"app/chain/scraping.py": 2065,
"app/chain/site.py": 1187,
"app/chain/transfer/queue.py": 1631,
"app/chain/transfer/workflow.py": 1106,
"app/chain/workflow.py": 1391
},
"method": {
"app/api/endpoints/transfer.py:_execute_manual_transfer": 296,
"app/application/formatting.py:EpisodeFormatRuleHelper._auto_recommend": 174,
"app/application/messaging/site.py:SiteInteractionHandler._handle_text_interaction": 227,
"app/application/messaging/skill.py:SkillInteractionHandler._handle_callback_interaction": 158,
"app/application/messaging/skill.py:SkillInteractionHandler._handle_text_interaction": 296,
"app/application/messaging/subscribe.py:SubscribeInteractionHandler._handle_text_interaction": 205,
"app/application/network.py:NetworkTestService._build_rules": 206,
"app/application/rss.py:RssHelper._parse_impl": 213,
"app/application/security/cookie.py:CookieHelper._get_site_cookie_ua_impl": 241,
"app/chain/download/batch.py:DownloadBatchOwner._execute_batch_download": 478,
"app/chain/download/submission.py:DownloadSubmissionOwner._execute_download_single": 176,
"app/chain/message.py:MessageChain._handle_ai_message": 202,
"app/chain/message.py:MessageChain._handle_message_core": 151,
"app/chain/subscribe/match.py:SubscribeMatchOwner._execute_match": 377,
"app/chain/transfer/execution.py:TransferExecutionOwner._TransferChain__perform_transfer": 209,
"app/chain/transfer/queue.py:TransferQueueOwner._TransferChain__replay_pending": 171,
"app/chain/transfer/queue.py:TransferQueueOwner._TransferChain__start_transfer": 180,
"app/chain/transfer/request.py:_TransferCandidatePlanner._plan_file_items": 173,
"app/chain/transfer/settlement.py:TransferSettlementOwner._TransferChain__default_callback": 261,
"app/chain/transfer/workflow.py:TransferWorkflowOwner._build_transfer_tasks": 283,
"app/chain/transfer/workflow.py:TransferWorkflowOwner._execute_transfer_tasks": 172,
"app/chain/transfer/workflow.py:TransferWorkflowOwner._run_transfer_workflow": 232,
"app/scheduler/catalog.py:SchedulerCatalogOwner._initialize_catalog": 336,
"app/scheduler/execution.py:SchedulerExecutionOwner.start": 153
}
}

View File

@@ -0,0 +1,622 @@
{
"app/adapters/external/plugin/client.py:1372:to_thread": {
"owner": "PluginMarketTransport.async_get_plugin_system_version_check_message",
"target": "to_thread"
},
"app/adapters/external/server.py:1005:to_thread": {
"owner": "MoviePilotServerHelper.sub_reg_async",
"target": "to_thread"
},
"app/adapters/external/server.py:1019:to_thread": {
"owner": "MoviePilotServerHelper.sub_done_async",
"target": "to_thread"
},
"app/adapters/network/browser.py:461:ThreadPoolExecutor": {
"owner": "BrowserSessionHelper._get_session_executor",
"target": "ThreadPoolExecutor"
},
"app/adapters/network/http.py:224:create_task": {
"owner": "_get_shared_async_transport",
"target": "create_task"
},
"app/adapters/system/fsproxy.py:399:Thread": {
"owner": "FileSystemProxy._read_line_windows",
"target": "Thread"
},
"app/adapters/system/host.py:1012:Process": {
"owner": "SystemUtils.memory_usage",
"target": "Process"
},
"app/adapters/system/host.py:255:Process": {
"owner": "SystemUtils._process_tree",
"target": "Process"
},
"app/adapters/system/host.py:329:create_task": {
"owner": "SystemUtils.execute_with_subprocess_async",
"target": "create_task"
},
"app/adapters/system/host.py:950:Process": {
"owner": "SystemUtils.dashboard_system_info",
"target": "Process"
},
"app/adapters/system/plugin/dependency.py:414:to_thread": {
"owner": "PluginDependencyInstaller.async_find_missing",
"target": "to_thread"
},
"app/adapters/system/plugin/health.py:403:create_task": {
"owner": "PluginRuntimeHealth.__async_create_runtime_constraints_file",
"target": "create_task"
},
"app/adapters/system/plugin/health.py:404:to_thread": {
"owner": "PluginRuntimeHealth.__async_create_runtime_constraints_file",
"target": "to_thread"
},
"app/adapters/system/plugin/health.py:417:to_thread": {
"owner": "PluginRuntimeHealth.__async_create_runtime_constraints_file.cleanup_created_file",
"target": "to_thread"
},
"app/adapters/system/plugin/health.py:419:create_task": {
"owner": "PluginRuntimeHealth.__async_create_runtime_constraints_file",
"target": "create_task"
},
"app/adapters/system/plugin/package.py:2130:to_thread": {
"owner": "PluginPackageManager.__async_install_from_release",
"target": "to_thread"
},
"app/agent/callback/__init__.py:239:create_task": {
"owner": "StreamingHandler._start_streaming",
"target": "create_task"
},
"app/agent/callback/__init__.py:271:run_in_threadpool": {
"owner": "StreamingHandler._stop_streaming",
"target": "run_in_threadpool"
},
"app/agent/callback/__init__.py:606:run_in_threadpool": {
"owner": "StreamingHandler._flush",
"target": "run_in_threadpool"
},
"app/agent/callback/__init__.py:653:run_in_threadpool": {
"owner": "StreamingHandler._flush",
"target": "run_in_threadpool"
},
"app/agent/callback/__init__.py:691:run_in_threadpool": {
"owner": "StreamingHandler._flush",
"target": "run_in_threadpool"
},
"app/agent/capabilities/adapter.py:117:to_thread": {
"owner": "AgentServiceAdapter.materialize",
"target": "to_thread"
},
"app/agent/lifecycle.py:104:create_task": {
"owner": "AgentLifecycleOwner.close",
"target": "create_task"
},
"app/agent/lifecycle.py:33:create_task": {
"owner": "AgentLifecycleOwner.initialize",
"target": "create_task"
},
"app/agent/llm/discovery.py:195:to_thread": {
"owner": "_ProviderDiscovery._load_models_dev_from_disk",
"target": "to_thread"
},
"app/agent/llm/discovery.py:223:to_thread": {
"owner": "_ProviderDiscovery._write_models_dev_to_disk",
"target": "to_thread"
},
"app/agent/llm/discovery.py:684:to_thread": {
"owner": "_ProviderDiscovery._list_models_from_bedrock",
"target": "to_thread"
},
"app/agent/llm/discovery.py:691:to_thread": {
"owner": "_ProviderDiscovery._list_models_from_bedrock",
"target": "to_thread"
},
"app/agent/mcp.py:215:create_task": {
"owner": "_StdioMcpSession.__aenter__",
"target": "create_task"
},
"app/agent/memory/__init__.py:64:create_task": {
"owner": "MemoryManager.initialize",
"target": "create_task"
},
"app/agent/middleware/subagents.py:1091:create_task": {
"owner": "SubAgentTaskControlMiddleware._run_pipeline",
"target": "create_task"
},
"app/agent/middleware/subagents.py:841:create_task": {
"owner": "SubAgentTaskControlMiddleware._start_tasks",
"target": "create_task"
},
"app/agent/orchestrator.py:1050:run_in_threadpool": {
"owner": "MoviePilotAgent._deliver_private_channel_message",
"target": "run_in_threadpool"
},
"app/agent/session.py:374:create_task": {
"owner": "AgentSessionOwner.process_message",
"target": "create_task"
},
"app/agent/session.py:655:create_task": {
"owner": "AgentSessionOwner._stop_current_task_locked",
"target": "create_task"
},
"app/agent/session.py:71:run_in_threadpool": {
"owner": "_async_start_processing_status",
"target": "run_in_threadpool"
},
"app/agent/session.py:755:create_task": {
"owner": "AgentSessionOwner._schedule_deferred_session_cleanup",
"target": "create_task"
},
"app/agent/session.py:83:run_in_threadpool": {
"owner": "_async_finish_processing_status",
"target": "run_in_threadpool"
},
"app/agent/tools/base.py:199:ThreadPoolExecutor": {
"owner": "_submit_blocking_call",
"target": "ThreadPoolExecutor"
},
"app/agent/tools/impl/_terminal_session.py:257:create_task": {
"owner": "_TerminalSessionManager.start",
"target": "create_task"
},
"app/agent/tools/impl/_terminal_session.py:302:create_task": {
"owner": "_TerminalSessionManager._start_pty_session",
"target": "create_task"
},
"app/agent/tools/impl/_terminal_session.py:303:create_task": {
"owner": "_TerminalSessionManager._start_pty_session",
"target": "create_task"
},
"app/agent/tools/impl/_terminal_session.py:326:create_task": {
"owner": "_TerminalSessionManager._start_pipe_session",
"target": "create_task"
},
"app/agent/tools/impl/_terminal_session.py:330:create_task": {
"owner": "_TerminalSessionManager._start_pipe_session",
"target": "create_task"
},
"app/agent/tools/impl/_terminal_session.py:332:create_task": {
"owner": "_TerminalSessionManager._start_pipe_session",
"target": "create_task"
},
"app/agent/tools/impl/_terminal_session.py:372:to_thread": {
"owner": "_TerminalSessionManager._wait_pty_process",
"target": "to_thread"
},
"app/agent/tools/impl/_terminal_session.py:477:to_thread": {
"owner": "_TerminalSessionManager.write",
"target": "to_thread"
},
"app/agent/tools/impl/execute_command.py:475:create_task": {
"owner": "ExecuteCommandTool._run_once",
"target": "create_task"
},
"app/agent/tools/impl/execute_command.py:477:create_task": {
"owner": "ExecuteCommandTool._run_once",
"target": "create_task"
},
"app/agent/tools/impl/execute_command.py:478:create_task": {
"owner": "ExecuteCommandTool._run_once",
"target": "create_task"
},
"app/api/endpoints/dashboard.py:183:run_in_threadpool": {
"owner": "schedule",
"target": "run_in_threadpool"
},
"app/api/endpoints/dashboard.py:214:run_in_threadpool": {
"owner": "schedule2",
"target": "run_in_threadpool"
},
"app/api/endpoints/history.py:111:to_thread": {
"owner": "_request_batch_durable_retries",
"target": "to_thread"
},
"app/api/endpoints/history.py:447:to_thread": {
"owner": "ai_redo_transfer_history",
"target": "to_thread"
},
"app/api/endpoints/search.py:218:create_task": {
"owner": "_iter_batched_search_events",
"target": "create_task"
},
"app/application/agenttask.py:286:create_task": {
"owner": "AgentTaskExecutionService.claim",
"target": "create_task"
},
"app/application/agenttask.py:293:create_task": {
"owner": "AgentTaskExecutionService.claim",
"target": "create_task"
},
"app/application/agenttask.py:359:create_task": {
"owner": "AgentTaskExecutionService._finalize",
"target": "create_task"
},
"app/application/messaging/agent.py:1023:run_in_threadpool": {
"owner": "get_web_agent_upload_dir",
"target": "run_in_threadpool"
},
"app/application/messaging/agent.py:1047:run_in_threadpool": {
"owner": "save_web_agent_upload",
"target": "run_in_threadpool"
},
"app/application/messaging/agent.py:1263:run_in_threadpool": {
"owner": "prepare_web_agent_audio_attachment_path_async",
"target": "run_in_threadpool"
},
"app/application/messaging/agent.py:1271:run_in_threadpool": {
"owner": "prepare_web_agent_audio_attachment_path_async",
"target": "run_in_threadpool"
},
"app/application/messaging/agent.py:1278:run_in_threadpool": {
"owner": "prepare_web_agent_audio_attachment_path_async",
"target": "run_in_threadpool"
},
"app/application/messaging/agent.py:1289:run_in_threadpool": {
"owner": "prepare_web_agent_audio_attachment_path_async.cleanup_conversion_output",
"target": "run_in_threadpool"
},
"app/application/messaging/agent.py:1311:run_in_threadpool": {
"owner": "prepare_web_agent_audio_attachment_path_async",
"target": "run_in_threadpool"
},
"app/application/messaging/agent.py:1417:to_thread": {
"owner": "transcribe_web_agent_audio_input",
"target": "to_thread"
},
"app/application/messaging/agent.py:1772:run_in_threadpool": {
"owner": "collect_web_agent_traditional_events",
"target": "run_in_threadpool"
},
"app/application/messaging/agent.py:1802:to_thread": {
"owner": "collect_web_agent_traditional_events",
"target": "to_thread"
},
"app/application/messaging/agent.py:1875:create_task": {
"owner": "_build_traditional_web_agent_stream.event_generator",
"target": "create_task"
},
"app/application/messaging/agent.py:715:create_task": {
"owner": "WebAgentEventPublisher.__init__",
"target": "create_task"
},
"app/application/messaging/message.py:892:Thread": {
"owner": "MessageQueueManager.start",
"target": "Thread"
},
"app/application/plugin/catalog.py:195:ThreadPoolExecutor": {
"owner": "PluginCatalogService.collect",
"target": "ThreadPoolExecutor"
},
"app/application/plugin/catalog.py:255:create_task": {
"owner": "PluginCatalogService.async_collect",
"target": "create_task"
},
"app/application/plugin/catalog.py:260:create_task": {
"owner": "PluginCatalogService.async_collect",
"target": "create_task"
},
"app/application/plugin/install.py:661:create_task": {
"owner": "PluginInstallCommand.__resolve_journal_created",
"target": "create_task"
},
"app/application/plugin/install.py:696:create_task": {
"owner": "PluginInstallCommand.__resolve_commit_outcome",
"target": "create_task"
},
"app/application/plugin/install.py:851:create_task": {
"owner": "PluginInstallCommand.__rollback_cancelled",
"target": "create_task"
},
"app/application/plugin/install.py:871:ensure_future": {
"owner": "PluginInstallCommand.__await_side_effect",
"target": "ensure_future"
},
"app/application/plugin/inventory.py:116:create_task": {
"owner": "PluginCandidateInventoryReader.async_load",
"target": "create_task"
},
"app/application/plugin/inventory.py:131:to_thread": {
"owner": "PluginCandidateInventoryReader.async_load",
"target": "to_thread"
},
"app/application/plugin/inventory.py:181:to_thread": {
"owner": "PluginCandidateInventoryReader._async_read_market_generation",
"target": "to_thread"
},
"app/chain/_recognition.py:527:run_in_threadpool": {
"owner": "RecognitionMixin._async_run_recognition_steps",
"target": "run_in_threadpool"
},
"app/chain/media/album.py:180:run_in_threadpool": {
"owner": "MediaAlbumOwner._async_match_music_album_directory",
"target": "run_in_threadpool"
},
"app/chain/media/album.py:216:run_in_threadpool": {
"owner": "MediaAlbumOwner.async_recognize_music_album_directory",
"target": "run_in_threadpool"
},
"app/chain/media/album.py:218:run_in_threadpool": {
"owner": "MediaAlbumOwner.async_recognize_music_album_directory",
"target": "run_in_threadpool"
},
"app/chain/media/path.py:381:run_in_threadpool": {
"owner": "MediaPathOwner._async_music_album_dir_fallback",
"target": "run_in_threadpool"
},
"app/chain/media/path.py:427:run_in_threadpool": {
"owner": "MediaPathOwner.async_recognize_music_by_path",
"target": "run_in_threadpool"
},
"app/chain/search/media.py:734:run_in_threadpool": {
"owner": "SearchMediaOwner._run_media_process_async",
"target": "run_in_threadpool"
},
"app/chain/search/media.py:936:run_in_threadpool": {
"owner": "SearchMediaOwner.async_process_stream",
"target": "run_in_threadpool"
},
"app/chain/search/music.py:236:run_in_threadpool": {
"owner": "SearchMusicOwner._async_process_music",
"target": "run_in_threadpool"
},
"app/chain/search/music.py:282:run_in_threadpool": {
"owner": "SearchMusicOwner._async_process_music_stream",
"target": "run_in_threadpool"
},
"app/chain/search/subtitle.py:429:run_in_threadpool": {
"owner": "SearchSubtitleOwner._async_search_subtitles_for_media",
"target": "run_in_threadpool"
},
"app/chain/search/subtitle.py:526:run_in_threadpool": {
"owner": "SearchSubtitleOwner._async_search_subtitles_for_media_stream",
"target": "run_in_threadpool"
},
"app/chain/search/title.py:248:run_in_threadpool": {
"owner": "SearchTitleOwner._run_title_search_async",
"target": "run_in_threadpool"
},
"app/chain/search/title.py:382:run_in_threadpool": {
"owner": "SearchTitleOwner.async_search_by_title_stream",
"target": "run_in_threadpool"
},
"app/chain/transfer/queue.py:149:Thread": {
"owner": "TransferQueueOwner._TransferChain__init",
"target": "Thread"
},
"app/chain/transfer/queue.py:317:to_thread": {
"owner": "TransferQueueOwner.close",
"target": "to_thread"
},
"app/chain/transfer/queue.py:405:Thread": {
"owner": "TransferQueueOwner._TransferChain__ensure_recovery_scheduler",
"target": "Thread"
},
"app/chain/transfer/queue.py:469:Thread": {
"owner": "TransferQueueOwner._TransferChain__start_lease_heartbeat_owner_locked",
"target": "Thread"
},
"app/chain/transfer/queue.py:717:Thread": {
"owner": "TransferQueueOwner._TransferChain__start_lease_release_owner_locked",
"target": "Thread"
},
"app/db/worker.py:82:ThreadPoolExecutor": {
"owner": "DatabaseWorker.start",
"target": "ThreadPoolExecutor"
},
"app/main.py:179:Thread": {
"owner": "start_tray",
"target": "Thread"
},
"app/modules/acoustid/__init__.py:148:run_in_threadpool": {
"owner": "AcoustIdModule.async_identify_music_by_fingerprint",
"target": "run_in_threadpool"
},
"app/modules/discord/discord.py:220:create_task": {
"owner": "Discord._start.runner",
"target": "create_task"
},
"app/modules/discord/discord.py:248:Thread": {
"owner": "Discord._start",
"target": "Thread"
},
"app/modules/discord/discord.py:731:create_task": {
"owner": "Discord._start_typing_task",
"target": "create_task"
},
"app/modules/discord/discord.py:924:run_in_threadpool": {
"owner": "Discord._send_file",
"target": "run_in_threadpool"
},
"app/modules/feishu/feishu.py:215:Thread": {
"owner": "Feishu._start_ws_client",
"target": "Thread"
},
"app/modules/indexer/spider/__init__.py:400:run_in_threadpool": {
"owner": "SiteSpider.async_get_torrents",
"target": "run_in_threadpool"
},
"app/modules/qqbot/gateway.py:134:Thread": {
"owner": "run_gateway.start_heartbeat",
"target": "Thread"
},
"app/modules/qqbot/qqbot.py:144:Thread": {
"owner": "QQBot._start_gateway",
"target": "Thread"
},
"app/modules/telegram/telegram.py:221:Thread": {
"owner": "Telegram.__init__",
"target": "Thread"
},
"app/modules/telegram/telegram.py:544:Thread": {
"owner": "Telegram._start_typing_task",
"target": "Thread"
},
"app/modules/thetvdb/__init__.py:291:run_in_threadpool": {
"owner": "TheTvDbModule._async_load_auxiliary_candidates",
"target": "run_in_threadpool"
},
"app/modules/wechat/wechatbot.py:122:Thread": {
"owner": "WeChatBot._start_gateway",
"target": "Thread"
},
"app/modules/wechat/wechatbot.py:124:Thread": {
"owner": "WeChatBot._start_gateway",
"target": "Thread"
},
"app/modules/wechatclawbot/wechatclawbot.py:1732:Thread": {
"owner": "WechatClawBot._start_polling",
"target": "Thread"
},
"app/monitor/monitor.py:1111:Thread": {
"owner": "Monitor.__request_scheduler_stop",
"target": "Thread"
},
"app/monitor/monitor.py:735:Thread": {
"owner": "Monitor.__start_compensation",
"target": "Thread"
},
"app/monitor/recovery.py:168:Thread": {
"owner": "RecoveryExecutor._start",
"target": "Thread"
},
"app/monitor/watcher.py:126:Thread": {
"owner": "LocalDirectoryWatcher.start",
"target": "Thread"
},
"app/runtime/capabilities/runtime.py:1514:to_thread": {
"owner": "CapabilityRuntime.shutdown_async",
"target": "to_thread"
},
"app/runtime/coalesce.py:171:create_task": {
"owner": "EventCoalescer._on_flush_timer",
"target": "create_task"
},
"app/runtime/debounce.py:136:Timer": {
"owner": "Debouncer._call_leading",
"target": "Timer"
},
"app/runtime/debounce.py:161:Timer": {
"owner": "Debouncer._call_trailing",
"target": "Timer"
},
"app/runtime/debounce.py:221:create_task": {
"owner": "AsyncDebouncer._call_leading",
"target": "create_task"
},
"app/runtime/debounce.py:241:create_task": {
"owner": "AsyncDebouncer._call_trailing",
"target": "create_task"
},
"app/runtime/event/dispatch.py:260:run_in_threadpool": {
"owner": "EventDispatcher.invoke_async",
"target": "run_in_threadpool"
},
"app/runtime/event/dispatch.py:292:run_in_threadpool": {
"owner": "EventDispatcher.invoke_async_strict",
"target": "run_in_threadpool"
},
"app/runtime/events.py:229:Thread": {
"owner": "EventManager.start",
"target": "Thread"
},
"app/runtime/events.py:262:to_thread": {
"owner": "EventManager.stop_async",
"target": "to_thread"
},
"app/runtime/execution.py:153:create_task": {
"owner": "run_in_threadpool_to_completion",
"target": "create_task"
},
"app/runtime/execution.py:153:run_in_threadpool": {
"owner": "run_in_threadpool_to_completion",
"target": "run_in_threadpool"
},
"app/runtime/extensions/plugin/monitor.py:55:Thread": {
"owner": "PluginMonitorController.start",
"target": "Thread"
},
"app/runtime/extensions/plugin/sync.py:108:ThreadPoolExecutor": {
"owner": "PluginSyncService.sync",
"target": "ThreadPoolExecutor"
},
"app/runtime/extensions/resource.py:129:to_thread": {
"owner": "AsyncManagedResourceAdapter.materialize",
"target": "to_thread"
},
"app/runtime/gc.py:78:Process": {
"owner": "get_memory_usage",
"target": "Process"
},
"app/runtime/log.py:143:Thread": {
"owner": "NonBlockingFileHandler.__init__",
"target": "Thread"
},
"app/runtime/log.py:303:Thread": {
"owner": "NonBlockingFileHandler._close_handlers_bounded",
"target": "Thread"
},
"app/runtime/resources.py:133:to_thread": {
"owner": "acquire_managed_resource_async",
"target": "to_thread"
},
"app/runtime/resources.py:189:to_thread": {
"owner": "stop_managed_resource_async",
"target": "to_thread"
},
"app/runtime/state.py:335:Thread": {
"owner": "SystemHelper._start_graceful_shutdown_monitor",
"target": "Thread"
},
"app/runtime/state.py:99:Process": {
"owner": "SystemHelper._is_local_cli_managed",
"target": "Process"
},
"app/runtime/tasks.py:63:create_task": {
"owner": "TaskRegistry.create",
"target": "create_task"
},
"app/runtime/tasks.py:80:to_thread": {
"owner": "TaskRegistry.create_sync",
"target": "to_thread"
},
"app/scheduler/bridge.py:186:create_task": {
"owner": "SchedulerBridgeOwner._submit_cross_thread.start_on_target_loop",
"target": "create_task"
},
"app/scheduler/bridge.py:248:create_task": {
"owner": "SchedulerBridgeOwner._submit_to_loop",
"target": "create_task"
},
"app/scheduler/catalog.py:187:ThreadPoolExecutor": {
"owner": "SchedulerCatalogOwner._initialize_catalog",
"target": "ThreadPoolExecutor"
},
"app/scheduler/execution.py:224:create_task": {
"owner": "SchedulerExecutionOwner.start.__start_coro",
"target": "create_task"
},
"app/scheduler/execution.py:301:Process": {
"owner": "SchedulerExecutionOwner.start",
"target": "Process"
},
"app/scheduler/lifecycle.py:126:to_thread": {
"owner": "SchedulerLifecycleOwner.stop_async",
"target": "to_thread"
},
"app/scheduler/lifecycle.py:29:to_thread": {
"owner": "SchedulerLifecycleOwner.on_config_changed",
"target": "to_thread"
},
"app/startup/composition/subscription.py:93:to_thread": {
"owner": "async_rule_group_mutation_scope",
"target": "to_thread"
},
"app/startup/lifecycle/__init__.py:161:create_task": {
"owner": "run_shutdown_step",
"target": "create_task"
},
"app/startup/lifecycle/__init__.py:365:create_task": {
"owner": "schedule_plugin_settlement",
"target": "create_task"
}
}

View File

@@ -1,6 +1,6 @@
"""复杂度只降不增 ratchet 测试。"""
from scripts.architecture.complexity import compare_complexity
from scripts.architecture.complexity import compare_complexity, compare_complexity_v2
def test_complexity_ratchet_allows_removal_and_reduction() -> None:
@@ -39,3 +39,18 @@ def test_complexity_ratchet_rejects_growth_and_new_oversize() -> None:
assert any("既有超限增长" in problem for problem in problems)
assert any("新增超限" in problem for problem in problems)
def test_complexity_v2_ratchet_checks_categories_outside_public_rules() -> None:
"""v2 基线必须实际比较私有方法、类和文件类别。"""
baseline = {"method": {"app/scheduler/a.py:Runner.run": 160}, "class": {}, "file": {}}
current = {
"method": {"app/scheduler/a.py:Runner.run": 161},
"class": {"app/scheduler/a.py:Runner": 600},
"file": {"app/scheduler/a.py": 1200},
}
problems = compare_complexity_v2(baseline, current)
assert any("method: 既有超限增长" in problem for problem in problems)
assert any("class: 新增超限" in problem for problem in problems)
assert any("file: 新增超限" in problem for problem in problems)

View File

@@ -0,0 +1,62 @@
"""原生并发事实门禁回归测试。"""
import textwrap
from pathlib import Path
from scripts.architecture.concurrency import collect_concurrency, compare_concurrency
def test_concurrency_gate_tracks_nested_owners_and_known_primitives(tmp_path: Path) -> None:
"""嵌套函数、类方法和常见原生并发入口都必须被枚举。"""
source_path = tmp_path / "app/scheduler/sample.py"
source_path.parent.mkdir(parents=True)
source_path.write_text(
textwrap.dedent(
"""
import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor
class Runner:
async def run(self):
asyncio.create_task(self.child())
threading.Timer(1, self.child).start()
with ThreadPoolExecutor() as executor:
executor.submit(self.child)
async def child(self):
await asyncio.to_thread(lambda: None)
"""
),
encoding="utf-8",
)
facts = collect_concurrency(tmp_path, scan_roots=("app/scheduler",))
assert facts["app/scheduler/sample.py:8:create_task"]["owner"] == "Runner.run"
assert facts["app/scheduler/sample.py:9:Timer"]["owner"] == "Runner.run"
assert facts["app/scheduler/sample.py:10:ThreadPoolExecutor"]["owner"] == "Runner.run"
assert facts["app/scheduler/sample.py:14:to_thread"]["owner"] == "Runner.child"
def test_concurrency_ratchet_rejects_new_facts_and_owner_drift() -> None:
"""新增并发调用和 owner 漂移必须阻断 ratchet。"""
baseline = {
"app/runtime/tasks.py:10:create_task": {
"owner": "Runtime.run",
"target": "create_task",
},
}
current = {
"app/runtime/tasks.py:10:create_task": {
"owner": "Runtime.changed",
"target": "create_task",
},
"app/api/system.py:20:Thread": {
"owner": "system",
"target": "Thread",
},
}
problems = compare_concurrency(baseline, current)
assert any("owner 漂移" in problem for problem in problems)
assert any("新增原生并发事实" in problem for problem in problems)