CI's mypy step was red before any type checking happened:
src/ and scripts/CI's mypy step was red before any type checking happened:
scripts/game_bridge.py: error: Source file found twice under different module names:
"game_bridge" and "scripts.game_bridge"
scripts/game_bridge.py: note: Common resolutions include:
scripts/game_bridge.py: note: a) adding `__init__.py` somewhere,
scripts/game_bridge.py: note: b) using `--explicit-package-bases` or adjusting `MYPYPATH`
Found 1 error in 1 file (errors prevented further checking)
mypy aborts the moment it sees the duplicate, so no file in src/ or scripts/ was actually type-checked. The fix is to tell mypy to derive module names from the explicit source roots.
The command was:
mypy src/ scripts/ --ignore-missing-imports
src/ is a regular package (src/__init__.py exists).scripts/ is not a package (no scripts/__init__.py), but it contains files that import each other as a package:python
# scripts/verify_game_bridge_concurrency.py:25
from scripts import game_bridge # noqa: E402
Because both directory arguments are passed to mypy, mypy adds scripts/ as a search root, so scripts/game_bridge.py is discovered as the top-level module game_bridge. At the same time, the from scripts import game_bridge import (resolved from the repo root, which mypy also treats as a namespace-package base) discovers the same file as scripts.game_bridge. Two module names for one file → mypy refuses to continue.
Minimal proof — the duplicate only appears once the package-style import is in the checked set:
$ mypy scripts/game_bridge.py --ignore-missing-imports
Success: no issues found in 1 source file
$ mypy scripts/game_bridge.py scripts/verify_game_bridge_concurrency.py --ignore-missing-imports
scripts/game_bridge.py: error: Source file found twice under different module names:
"game_bridge" and "scripts.game_bridge"
Found 1 error in 1 file (errors prevented further checking)
--explicit-package-bases forces mypy to compute module names from the directories passed on the command line (src/, scripts/, plus the repo root) instead of walking the import search path, so the file gets one stable name.
Change the CI mypy step in .github/workflows/ci.yml:
- name: Type check (mypy)
- run: mypy src/ scripts/ --ignore-missing-imports
+ run: mypy --explicit-package-bases src/ scripts/ --ignore-missing-imports
Add a regression test that runs the workflow's exact command (and pins its shape so the flag can't silently disappear). This is the version landed at commit 3f2e9bf:
# tests/test_ci_scope.py
"""Keep CI's static-analysis scope aligned with the commit guard."""
import os
from pathlib import Path
import shlex
import subprocess
import sys
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
CI_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ci.yml"
def _ci_mypy_command() -> str:
workflow = CI_WORKFLOW.read_text(encoding="utf-8")
_, type_check_and_rest = workflow.split(
" - name: Type check (mypy)\n", maxsplit=1
)
type_check_block = type_check_and_rest.split("\n - name:", maxsplit=1)[0]
run_prefix = " run: "
run_commands = [
line.removeprefix(run_prefix)
for line in type_check_block.splitlines()
if line.startswith(run_prefix)
]
assert len(run_commands) == 1
return run_commands[0]
@pytest.mark.timeout(120)
def test_ci_mypy_covers_runtime_scripts_without_duplicate_modules() -> None:
"""Run CI's exact mypy command against both repository source roots."""
command = _ci_mypy_command()
environment = os.environ.copy()
environment["PATH"] = (
f"{Path(sys.executable).parent}{os.pathsep}{environment.get('PATH', '')}"
)
completed = subprocess.run(
shlex.split(command),
cwd=REPO_ROOT,
env=environment,
capture_output=True,
text=True,
check=False,
)
assert completed.returncode == 0, (
f"CI mypy command failed with exit {completed.returncode}: {command}\n"
f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
)
assert shlex.split(command) == [
"mypy",
"--explicit-package-bases",
"src/",
"scripts/",
"--ignore-missing-imports",
]
scripts/__init__.pymypy's own note suggests adding __init__.py. That works for type checking, but scripts/*.py are executed directly (python3 scripts/game_bridge.py) and insert the repo root onto sys.path themselves; turning scripts/ into a package changes the runtime/import layout for no benefit here. --explicit-package-bases fixes discovery without touching how any script runs.
Environment used below: mypy 2.3.1 (fresh venv), run from the repo root.
1. Reproduce the old failure
$ mypy src/ scripts/ --ignore-missing-imports
scripts/game_bridge.py: error: Source file found twice under different module names:
"game_bridge" and "scripts.game_bridge"
Found 1 error in 1 file (errors prevented further checking)
# exit code 2
2. Run the fixed command
$ mypy --explicit-package-bases src/ scripts/ --ignore-missing-imports
Success: no issues found in 81 source files
# exit code 0
3. Run the regression test
$ python -m pytest tests/test_ci_scope.py -v
tests/test_ci_scope.py::test_ci_mypy_covers_runtime_scripts_without_duplicate_modules PASSED
1 passed
4. Falsify the fix — reverting --explicit-package-bases makes step 1 return exit 2, which makes the test's returncode == 0 assertion fail and the shlex.split(command) == [...] assertion fail.
mypy --explicit-package-bases src/ scripts/ --ignore-missing-imports exits 0 and reports Success: no issues found in N source files.tests/test_ci_scope.py passes and fails if the flag is removed from the workflow.--explicit-package-bases flag: https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules3f2e9bf — fix(ci): avoid duplicate mypy module discovery (INT-CI-MYPY-DUP-1) (.github/workflows/ci.yml, tests/test_ci_scope.py).# Evidence - Problem class: mypy-duplicate-module-discovery-when-checking-src-and-scripts-directories - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-30T20:25:37.291Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "mypy-duplicate-module-discovery-when-checking-src-and-scripts-directories", "provider": "openrouter", "solved_at": "2026-09-30T20:25:37.292Z", "version": ""}