From 1f656830dbe0aab572c99df22cb0a38f1f795161 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 21 Aug 2026 21:31:42 +0800 Subject: [PATCH] docs: add module quality scale --- app/runtime/extensions/module/quality.py | 77 +++++++++++++++++++ .../backend-architecture-next-stage.md | 9 +++ docs/refactor/module-quality-scale.md | 33 ++++++++ .../architecture/dependency-baseline.json | 3 +- tests/test_module_quality.py | 51 ++++++++++++ 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 app/runtime/extensions/module/quality.py create mode 100644 docs/refactor/module-quality-scale.md create mode 100644 tests/test_module_quality.py diff --git a/app/runtime/extensions/module/quality.py b/app/runtime/extensions/module/quality.py new file mode 100644 index 000000000..44b28f7b5 --- /dev/null +++ b/app/runtime/extensions/module/quality.py @@ -0,0 +1,77 @@ +"""宿主 Module 的渐进式集成质量清单。""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + + +class ModuleQualityLevel(StrEnum): + """描述模块当前接受的质量门禁等级。""" + + LEGACY = "legacy" + ASSESSED = "assessed" + + +@dataclass(frozen=True, slots=True) +class ModuleQualityProfile: + """记录一个模块已验证规则、维护者和精确豁免原因。""" + + module: str + level: ModuleQualityLevel + owner: str + verified_rules: frozenset[str] = frozenset() + exemption_reason: str | None = None + + +QUALITY_RULES = frozenset( + { + "fake-client-or-fixture", + "zero-real-network-tests", + "sync-async-boundary", + "no-blocking-io-in-event-loop", + "auth-rate-timeout-offline-semantics", + "bounded-concurrency-or-polling", + "reload-stop-idempotent", + "module-contract-v2", + "sensitive-log-redaction", + "owner-declared", + } +) + +MODULE_QUALITY_PROFILES = { + "bangumi": ModuleQualityProfile( + module="bangumi", + level=ModuleQualityLevel.ASSESSED, + owner="MoviePilot core", + verified_rules=frozenset( + { + "fake-client-or-fixture", + "zero-real-network-tests", + "sync-async-boundary", + "reload-stop-idempotent", + "module-contract-v2", + "sensitive-log-redaction", + "owner-declared", + } + ), + exemption_reason=( + "外部 Bangumi API 的限流与并发策略仍沿用通用 HTTP adapter;" + "本轮仅对配置快照改动面启用 assessed 门禁" + ), + ), +} + + +def get_module_quality_profile(module: str) -> ModuleQualityProfile: + """返回显式 profile;未迁移模块以带原因的 legacy 视图呈现。""" + return MODULE_QUALITY_PROFILES.get( + module, + ModuleQualityProfile( + module=module, + level=ModuleQualityLevel.LEGACY, + owner="MoviePilot core", + exemption_reason="存量模块尚未在二阶段任务中修改,按渐进策略暂不提升门禁", + ), + ) + diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index c371dc583..39524c4b2 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -620,6 +620,15 @@ ModuleMethodSpec( 质量清单只约束新模块和被修改模块;历史模块以 `legacy`/`exempt + reason` 进入,不允许一次性阻断全部功能。 +**实施记录(2026-08-21)**: + +- `app/runtime/extensions/module/quality.py` 提供十项统一规则、`legacy/assessed` 等级、owner、已验证 + 规则和精确豁免原因;所有存量模块均能生成有 owner/原因的 legacy 视图,不一次性阻断。 +- 本轮修改的 `bangumi` 首个进入 assessed:fake client、零真实网络、sync/async 边界、reload/stop、 + Contract V2、敏感日志和 owner 已登记;限流/并发继续复用通用 HTTP adapter 并明确豁免范围。 +- 详细规则和验收证据见 `docs/refactor/module-quality-scale.md`;自动测试阻止 profile 使用未登记规则, + 并要求今后修改模块时将对应 profile 纳入同一提交。 + ### 阶段 5:定义后台可靠性,不先引入分布式队列 #### ARCH-250:后台动作可靠性分类 ADR diff --git a/docs/refactor/module-quality-scale.md b/docs/refactor/module-quality-scale.md new file mode 100644 index 000000000..0f7cbec60 --- /dev/null +++ b/docs/refactor/module-quality-scale.md @@ -0,0 +1,33 @@ +# Module / Integration 渐进质量清单 + +本清单对应 ARCH-242。机器可检查定义位于 +`app/runtime/extensions/module/quality.py`;它不改变 Module ABI,也不要求一次修完全部历史模块。 + +## 使用规则 + +- 未在本阶段修改的模块解析为 `legacy`,必须携带统一豁免原因和 owner。 +- 新模块或本阶段修改的模块必须新增显式 `ModuleQualityProfile`,只可使用登记规则。 +- `assessed` 表示已明确检查的规则集合,不等于所有规则满分;未覆盖项必须写精确原因。 +- 测试不得访问真实网络。外部错误、限流和超时通过 fake client、fixture 或 adapter stub 验证。 +- profile 不能替代 Module Contract V2;对外能力仍须在 contract registry 单独登记。 + +## 规则说明 + +| 规则 | 验收证据 | +| --- | --- | +| `fake-client-or-fixture` | provider 测试使用 fake client 或稳定录制 fixture | +| `zero-real-network-tests` | 网络守卫下专项测试通过 | +| `sync-async-boundary` | 同步 I/O 与 async 入口的调度策略明确 | +| `no-blocking-io-in-event-loop` | async 专项测试或受控线程池证据 | +| `auth-rate-timeout-offline-semantics` | 鉴权过期、限流、超时、离线结果分别测试 | +| `bounded-concurrency-or-polling` | 并发上限、轮询周期或不适用理由明确 | +| `reload-stop-idempotent` | init/reload/stop 可重复且资源最终释放 | +| `module-contract-v2` | 公开能力进入 Module Contract V2 | +| `sensitive-log-redaction` | token/cookie/password 不进入日志 | +| `owner-declared` | profile 有维护 owner | + +## 当前 assessed 切片 + +`bangumi`:本轮配置快照改造已验证 fake client、零真实网络、同步/异步边界、reload/stop、 +Contract V2、敏感日志和 owner。限流/并发仍复用通用 HTTP adapter,未在本切片重复实现。 + diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index fc312268e..c559fe0f0 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -6225,7 +6225,7 @@ "app.workflow.actions.transfer_file -> app.workflow", "app.workflow.actions.transfer_file -> app.workflow.actions" ], - "module_count": 772, + "module_count": 773, "modules": [ "app", "app.adapters", @@ -6868,6 +6868,7 @@ "app.runtime.extensions.module", "app.runtime.extensions.module.contracts", "app.runtime.extensions.module.dispatcher", + "app.runtime.extensions.module.quality", "app.runtime.extensions.module_manager", "app.runtime.extensions.plugin", "app.runtime.extensions.plugin.access", diff --git a/tests/test_module_quality.py b/tests/test_module_quality.py new file mode 100644 index 000000000..6891ddd1e --- /dev/null +++ b/tests/test_module_quality.py @@ -0,0 +1,51 @@ +"""Module Integration Quality Scale 渐进门禁测试。""" + +from pathlib import Path + +from app.runtime.extensions.module.quality import ( + MODULE_QUALITY_PROFILES, + QUALITY_RULES, + ModuleQualityLevel, + get_module_quality_profile, +) + + +MODULE_ROOT = Path(__file__).parents[1] / "app" / "modules" + + +def test_every_module_has_quality_view_with_owner_and_reason() -> None: + """所有存量模块都必须能解析为 assessed 或有理由的 legacy profile。""" + modules = { + path.name + for path in MODULE_ROOT.iterdir() + if path.is_dir() and not path.name.startswith(("_", ".")) + } + + assert modules + for module in modules: + profile = get_module_quality_profile(module) + assert profile.owner + assert profile.level in ModuleQualityLevel + if profile.level is ModuleQualityLevel.LEGACY: + assert profile.exemption_reason + + +def test_assessed_profiles_only_use_declared_rules() -> None: + """显式 profile 不得通过拼写新规则绕过统一质量维度。""" + for profile in MODULE_QUALITY_PROFILES.values(): + assert profile.verified_rules + assert profile.verified_rules <= QUALITY_RULES + + +def test_bangumi_changed_slice_meets_required_quality_rules() -> None: + """本轮修改的 Bangumi 模块必须满足配置快照切片相关门禁。""" + profile = get_module_quality_profile("bangumi") + + assert profile.level is ModuleQualityLevel.ASSESSED + assert { + "fake-client-or-fixture", + "zero-real-network-tests", + "reload-stop-idempotent", + "module-contract-v2", + "owner-declared", + } <= profile.verified_rules