Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

238 lines
11 KiB
Python

"""Enforce forbidden *direct* import edges between top-level packages.
Unlike import-linter (which flags transitive chains), this checker looks at
**module-top-level** and **nested** (function/class-body) ``import`` /
``from … import`` statements. Module-top-level uses the same AST walk as
``check_import_cycles``; nested imports close the loophole where lazy
``surfaces.*`` imports bypass the module-level graph.
Used by ``make check-imports`` (and ``check_imports``) alongside
import-linter's config contract.
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
from pathlib import Path
_CI_DIR = Path(__file__).resolve().parent
if str(_CI_DIR) not in sys.path:
sys.path.insert(0, str(_CI_DIR))
from check_import_cycles import ( # noqa: E402
_build_graph,
_nested_imports,
discover_first_party_roots,
module_from_path,
)
_REPO_ROOT = Path(__file__).resolve().parents[2]
# ``source_prefix -> forbidden destination roots`` for direct imports only.
# Enforces the layering contract documented in ``surfaces/__init__.py``:
# "Nothing first-party may import from surfaces/". Adds an explicit bound
# on ``platform``, ``core``, ``gateway`` so the surfaces ban
# is CI-enforced, not just doc-described.
_FORBIDDEN_DIRECT: dict[str, frozenset[str]] = {
"platform": frozenset({"surfaces"}),
"core": frozenset({"surfaces"}),
"gateway": frozenset({"surfaces"}),
"integrations": frozenset({"tools", "surfaces"}),
"tools": frozenset({"surfaces"}),
}
# Known direct violations being burned down — remove entries as fixes land.
# Format: ``"source.module -> dest.module"`` (exact modules from the graph).
_BASELINE_IGNORES: frozenset[str] = frozenset(
{
# Gateway hosts the interactive_shell runtime — pre-existing reuse
# to be burned down by extracting shared runtime primitives out of
# ``surfaces/interactive_shell/`` and into a layer below ``surfaces``.
"gateway.storage.session.resolver -> surfaces.interactive_shell.runtime.context",
# tools/interactive_shell action tools reach UP into surfaces/interactive_shell
# for runtime / command_registry / UI primitives. Clears when the action
# tools themselves are refactored to be UI-agnostic (e.g. return
# "approval-required" sentinels instead of calling execution_confirm
# directly) so the surface owns its own confirmation UX.
"tools.interactive_shell.actions.cli_command -> surfaces.interactive_shell.runtime.subprocess_runner",
# These shell action tools type against the shell ``Session`` (investigation_launch
# reads session.terminal.background_mode_enabled). Clears when they are made
# session-core-agnostic so they no longer import the shell session type.
"tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.session",
"tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.session",
"tools.interactive_shell.shared.investigation_launch -> surfaces.interactive_shell.session",
"tools.interactive_shell.actions.llm_provider -> surfaces.interactive_shell.command_registry",
"tools.interactive_shell.actions.llm_provider -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.command_registry",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.command_registry.slash_catalog",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.ui",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.utils.telemetry.turn_outcome",
"tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.command_registry",
"tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.runtime",
"tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.runtime",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.runtime.subprocess_runner.task_streaming",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.ui",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.utils.error_handling.exception_reporting",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.runtime",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.runtime.subprocess_runner.task_streaming",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.ui",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.utils.error_handling.exception_reporting",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.runtime",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.runtime.subprocess_runner.task_streaming",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.ui",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.utils.error_handling.exception_reporting",
}
)
# Function/class-body lazy imports that bypass the module-level graph.
# Format matches ``_BASELINE_IGNORES``; burn down by moving shared code
# below ``surfaces/`` or making tools UI-agnostic.
_NESTED_BASELINE_IGNORES: frozenset[str] = frozenset(
{
"tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.runtime.background.runner",
"tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.runtime.investigation_adapter",
"tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.runtime.background.runner",
"tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.runtime.investigation_adapter",
"tools.interactive_shell.actions.llm_provider -> surfaces.cli.wizard.config",
# Nested only: runner.py also has a module-level ui import (see _BASELINE_IGNORES).
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.ui",
}
)
@dataclass(frozen=True)
class DirectViolation:
source: str
target: str
@property
def edge(self) -> str:
return f"{self.source} -> {self.target}"
@dataclass(frozen=True)
class NestedViolation:
source: str
target: str
lineno: int
@property
def edge(self) -> str:
return f"{self.source} -> {self.target}"
def _source_root(module: str) -> str:
return module.split(".", 1)[0]
def find_direct_violations(
graph: dict[str, set[str]],
*,
forbidden: dict[str, frozenset[str]] | None = None,
baseline_ignores: frozenset[str] | None = None,
) -> list[DirectViolation]:
rules = forbidden or _FORBIDDEN_DIRECT
ignores = baseline_ignores if baseline_ignores is not None else _BASELINE_IGNORES
violations: list[DirectViolation] = []
for source_module, targets in sorted(graph.items()):
source_root = _source_root(source_module)
forbidden_roots = rules.get(source_root)
if not forbidden_roots:
continue
for target_module in sorted(targets):
target_root = _source_root(target_module)
if target_root not in forbidden_roots:
continue
edge = DirectViolation(source_module, target_module)
if edge.edge in ignores:
continue
violations.append(edge)
return violations
def find_nested_direct_violations(
root: Path,
first_party_roots: tuple[str, ...],
*,
forbidden: dict[str, frozenset[str]] | None = None,
baseline_ignores: frozenset[str] | None = None,
) -> list[NestedViolation]:
rules = forbidden or _FORBIDDEN_DIRECT
ignores = baseline_ignores if baseline_ignores is not None else _NESTED_BASELINE_IGNORES
roots = frozenset(first_party_roots)
violations: list[NestedViolation] = []
for pkg in first_party_roots:
if pkg not in rules:
continue
pkg_path = root / pkg
if not pkg_path.exists():
continue
for py in pkg_path.rglob("*.py"):
if "__pycache__" in py.parts:
continue
source_module = module_from_path(root, py)
source_root = _source_root(source_module)
forbidden_roots = rules.get(source_root)
if not forbidden_roots:
continue
source = py.read_text(encoding="utf-8")
for target_module, lineno in _nested_imports(source, first_party_roots=roots):
target_root = _source_root(target_module)
if target_root not in forbidden_roots:
continue
violation = NestedViolation(source_module, target_module, lineno)
if violation.edge in ignores:
continue
violations.append(violation)
return sorted(violations, key=lambda item: (item.source, item.lineno, item.target))
def main(argv: list[str] | None = None) -> int:
del argv
root = _REPO_ROOT
first_party_roots = discover_first_party_roots(root)
graph = _build_graph(root, first_party_roots)
module_violations = find_direct_violations(graph)
nested_violations = find_nested_direct_violations(root, first_party_roots)
if not module_violations and not nested_violations:
print(
"No forbidden direct import edges found "
f"(module baseline: {len(_BASELINE_IGNORES)}, "
f"nested baseline: {len(_NESTED_BASELINE_IGNORES)})."
)
return 0
if module_violations:
print(f"FAIL: {len(module_violations)} forbidden module-level direct import edge(s):")
for violation in module_violations:
print(f" {violation.edge}")
if nested_violations:
if module_violations:
print()
print(f"FAIL: {len(nested_violations)} forbidden nested direct import edge(s):")
for violation in nested_violations:
print(f" {violation.edge} (line {violation.lineno})")
print(
"\nFix by moving shared code to a lower layer (platform/common, core/contracts) "
"or add a temporary baseline entry in .github/ci/check_direct_imports.py "
"with a linked issue — do not use function-level lazy imports to hide "
"new direct edges."
)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))