diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8194e3764..d0a13cb71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,6 +50,9 @@ jobs: uv run --locked --no-sync python \ scripts/architecture/baseline.py --check-host + - name: Check governed Python types + run: uv run --locked --no-sync mypy --config-file mypy.ini + pytest: runs-on: ubuntu-latest name: Unit Tests (${{ matrix.shard }}) diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index 33dcb902b..0c0646715 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -786,6 +786,17 @@ OTel 初始化只能位于 Startup/Adapter;Domain/Application 只依赖 no-op- 4. CI 先检查严格目录;每次迁移扩大 include 范围。 5. 类型错误不能用无界 `Any`、`cast(Any, ...)` 或全文件 ignore 消音。 +**实施记录(2026-08-21)**: + +- 选定 mypy 1.18.x 并写入 `pyproject.toml`/`uv.lock`,仓库只保留这一套新增类型门禁;CI architecture job + 使用锁定环境运行 `mypy --config-file mypy.ini`。 +- 首批 strict 清单包含 4 个已满足合同的 Domain value 文件、correlation/observation、Event Contract V2、 + Module Contract V2、typed HostRuntime startup context 和 API context,共 10 个 canonical 文件。 +- `mypy.ini` 不包含 `app.* = ignore_errors`、全文件 ignore、无界 `Any` 或 `cast(Any, ...)` 消音;历史 + `domain/meta` 动态模型只有在逐文件修正后才可加入清单,当前错误不能被 baseline 当作“已通过”。 +- 配置约束测试会检查 strict、关键合同文件和至少一个 Domain 文件均在清单中,并实际启动锁定版本 mypy; + 当前 10 个源文件零错误通过。 + #### ARCH-271:复杂度和端点预算 ratchet **目标**:阻止大方法继续增长,并让拆分对应真实阶段,而不是机械 helper 化。 diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..dc64dabe2 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,19 @@ +[mypy] +python_version = 3.12 +strict = True +warn_unused_configs = True +ignore_missing_imports = True +follow_imports = skip +show_error_codes = True +pretty = True +files = + app/domain/episode.py, + app/domain/media.py, + app/domain/torrent.py, + app/domain/meta/infopath.py, + app/runtime/correlation.py, + app/runtime/observability/__init__.py, + app/runtime/event/contracts.py, + app/runtime/extensions/module/contracts.py, + app/startup/context.py, + app/api/context.py diff --git a/pyproject.toml b/pyproject.toml index cf8f96f82..040dcc8be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,7 @@ dependencies = [ [dependency-groups] dev = [ "cython~=3.2.5", + "mypy~=1.18.2", "pylint~=4.0.6", "pytest~=9.0.3", "pytest-asyncio~=1.4.0", diff --git a/tests/test_type_gate.py b/tests/test_type_gate.py new file mode 100644 index 000000000..9c35963ac --- /dev/null +++ b/tests/test_type_gate.py @@ -0,0 +1,42 @@ +"""渐进式 mypy 门禁配置与可执行性测试。""" + +import configparser +import subprocess +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +MYPY_CONFIG = PROJECT_ROOT / "mypy.ini" + + +def test_mypy_gate_has_explicit_strict_scope_without_global_ignore() -> None: + """门禁必须列出治理文件,且不能用 app 级 ignore_errors 消音。""" + parser = configparser.ConfigParser() + parser.read(MYPY_CONFIG, encoding="utf-8") + settings = parser["mypy"] + governed_files = { + item.strip().rstrip(",") + for item in settings["files"].splitlines() + if item.strip() + } + + assert settings.getboolean("strict") is True + assert "app/runtime/event/contracts.py" in governed_files + assert "app/runtime/extensions/module/contracts.py" in governed_files + assert "app/startup/context.py" in governed_files + assert "app/api/context.py" in governed_files + assert any(path.startswith("app/domain/") for path in governed_files) + assert "ignore_errors" not in MYPY_CONFIG.read_text(encoding="utf-8") + + +def test_mypy_governed_scope_passes() -> None: + """使用锁定到开发依赖的 mypy 执行当前严格文件清单。""" + result = subprocess.run( + [sys.executable, "-m", "mypy", "--config-file", "mypy.ini"], + cwd=PROJECT_ROOT, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stdout + result.stderr diff --git a/uv.lock b/uv.lock index 58d03f720..890a0d7f0 100644 --- a/uv.lock +++ b/uv.lock @@ -3,19 +3,19 @@ revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "python_full_version >= '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", "python_full_version >= '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and platform_machine == 'AMD64' and sys_platform == 'win32'", + "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", "python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'win32'", ] supported-markers = [ @@ -1860,6 +1860,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "cython" }, + { name = "mypy" }, { name = "pylint" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -1968,6 +1969,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "cython", specifier = "~=3.2.5" }, + { name = "mypy", specifier = "~=1.18.2" }, { name = "pylint", specifier = "~=4.0.6" }, { name = "pytest", specifier = "~=9.0.3" }, { name = "pytest-asyncio", specifier = "~=1.4.0" }, @@ -2047,6 +2049,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" }, ] +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "numpy" version = "2.5.2" @@ -2225,6 +2268,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/c5/7c16e99869e1f422629092cfd23e3b58e461988c3f9c36fd3624bb4142e6/parse-1.22.1-py2.py3-none-any.whl", hash = "sha256:20f0925a46f06602485ac90d751764d0697fd8455aaa97489ba8953a4b66de32", size = 20925, upload-time = "2026-05-26T03:44:51.156Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pillow" version = "12.2.0"