From 358863880fa70ef3c9f02d07670ea88700b5cf8c Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 1 Mar 2026 21:56:49 +0100 Subject: [PATCH 1/2] feat: replaced vulture with grimp - better deadcode detection, also bumped up imports --- .github/workflows/{vulture.yml => grimp.yml} | 8 +- .pre-commit-config.yaml | 8 +- AGENTS.md | 6 +- backend/app/core/providers.py | 4 +- backend/app/db/__init__.py | 6 + backend/app/db/repositories/__init__.py | 3 +- backend/app/dlq/manager.py | 2 +- backend/app/events/consumer_group_monitor.py | 126 ------------------ backend/app/events/core/producer.py | 2 +- .../services/admin/admin_events_service.py | 2 +- .../services/admin/admin_execution_service.py | 2 +- .../services/admin/admin_settings_service.py | 2 +- .../app/services/admin/admin_user_service.py | 2 +- backend/app/services/auth_service.py | 4 +- .../services/event_replay/replay_service.py | 4 +- backend/app/services/event_service.py | 2 +- backend/app/services/execution_service.py | 4 +- backend/app/services/k8s_worker/worker.py | 2 +- backend/app/services/kafka_event_service.py | 2 +- .../app/services/notification_scheduler.py | 2 +- backend/app/services/notification_service.py | 2 +- .../services/result_processor/processor.py | 4 +- backend/app/services/runtime_settings.py | 2 +- backend/app/services/saga/execution_saga.py | 4 +- .../app/services/saga/saga_orchestrator.py | 4 +- backend/app/services/saga/saga_service.py | 2 +- backend/app/services/saved_script_service.py | 2 +- backend/app/services/sse/sse_service.py | 2 +- backend/app/services/user_settings_service.py | 2 +- backend/pyproject.toml | 10 +- backend/scripts/check_orphan_modules.py | 58 ++++++++ backend/uv.lock | 86 ++++++++++-- backend/vulture_whitelist.py | 8 -- docs/operations/cicd.md | 12 +- 34 files changed, 186 insertions(+), 205 deletions(-) rename .github/workflows/{vulture.yml => grimp.yml} (79%) delete mode 100644 backend/app/events/consumer_group_monitor.py create mode 100644 backend/scripts/check_orphan_modules.py delete mode 100644 backend/vulture_whitelist.py diff --git a/.github/workflows/vulture.yml b/.github/workflows/grimp.yml similarity index 79% rename from .github/workflows/vulture.yml rename to .github/workflows/grimp.yml index ba288996..0d0fc584 100644 --- a/.github/workflows/vulture.yml +++ b/.github/workflows/grimp.yml @@ -8,8 +8,8 @@ on: workflow_dispatch: jobs: - vulture: - name: Vulture Dead Code Check + grimp: + name: Grimp Orphan Module Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -26,7 +26,7 @@ jobs: uv python install 3.12 uv sync --frozen --group lint --no-dev - - name: Run vulture + - name: Run grimp orphan module check run: | cd backend - uv run vulture app/ vulture_whitelist.py + uv run python scripts/check_orphan_modules.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08fefb65..c77fcbda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,10 +22,10 @@ repos: files: ^backend/.*\.py$ pass_filenames: false - # Vulture - matches CI: cd backend && uv run vulture app/ vulture_whitelist.py - - id: vulture-backend - name: vulture dead code (backend) - entry: bash -c 'cd backend && uv run vulture app/ vulture_whitelist.py' + # Grimp - matches CI: cd backend && uv run python scripts/check_orphan_modules.py + - id: grimp-backend + name: grimp orphan modules (backend) + entry: bash -c 'cd backend && uv run python scripts/check_orphan_modules.py' language: system files: ^backend/.*\.py$ pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md index 1e0c3004..4a230900 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,7 +48,7 @@ uv run pytest tests/unit/ # unit only uv run pytest -k "test_name" -x # single test, stop on failure uv run ruff check . --config pyproject.toml # lint (must pass) uv run mypy --config-file pyproject.toml --strict . # types (must pass) -uv run vulture app/ vulture_whitelist.py # dead code (must pass) +uv run python scripts/check_orphan_modules.py # dead code (must pass) # Frontend (from frontend/) npm run dev # dev server with hot reload @@ -732,14 +732,14 @@ vi.mock('../../../lib/api', () => ({ # Backend uv run ruff check . --config pyproject.toml # rules: E, F, B, I, W; ignore W293 uv run mypy --config-file pyproject.toml --strict . # 318 source files -uv run vulture app/ vulture_whitelist.py # framework patterns in whitelist +uv run python scripts/check_orphan_modules.py # orphan module detection # Frontend npm run check # svelte-check: 0 errors, 0 warnings npm run test # Vitest tests must pass ``` -Vulture whitelist (`backend/vulture_whitelist.py`) covers Dishka providers, FastAPI routes, Beanie documents, and Pydantic models that appear unused but are called at runtime. +Grimp orphan module check (`backend/scripts/check_orphan_modules.py`) detects modules never imported by any other module in the package. --- diff --git a/backend/app/core/providers.py b/backend/app/core/providers.py index f01ffcf3..1acdd9df 100644 --- a/backend/app/core/providers.py +++ b/backend/app/core/providers.py @@ -26,7 +26,7 @@ ) from app.core.security import SecurityService from app.core.tracing import Tracer -from app.db.repositories import ( +from app.db import ( AdminEventsRepository, AdminSettingsRepository, DLQRepository, @@ -42,7 +42,7 @@ ) from app.dlq.manager import DLQManager from app.domain.saga import SagaConfig -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from app.services.admin import AdminEventsService, AdminSettingsService, AdminUserService from app.services.admin.admin_execution_service import AdminExecutionService from app.services.auth_service import AuthService diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py index 64dc753f..0a454bb1 100644 --- a/backend/app/db/__init__.py +++ b/backend/app/db/__init__.py @@ -1,9 +1,12 @@ from app.db.repositories import ( + AdminEventsRepository, AdminSettingsRepository, + DLQRepository, EventRepository, ExecutionRepository, NotificationRepository, ReplayRepository, + ResourceAllocationRepository, SagaRepository, SavedScriptRepository, UserRepository, @@ -11,11 +14,14 @@ ) __all__ = [ + "AdminEventsRepository", "AdminSettingsRepository", + "DLQRepository", "EventRepository", "ExecutionRepository", "NotificationRepository", "ReplayRepository", + "ResourceAllocationRepository", "SagaRepository", "SavedScriptRepository", "UserRepository", diff --git a/backend/app/db/repositories/__init__.py b/backend/app/db/repositories/__init__.py index 686f9343..a35fc303 100644 --- a/backend/app/db/repositories/__init__.py +++ b/backend/app/db/repositories/__init__.py @@ -1,5 +1,4 @@ -from app.db.repositories.admin.admin_events_repository import AdminEventsRepository -from app.db.repositories.admin.admin_settings_repository import AdminSettingsRepository +from app.db.repositories.admin import AdminEventsRepository, AdminSettingsRepository from app.db.repositories.dlq_repository import DLQRepository from app.db.repositories.event_repository import EventRepository from app.db.repositories.execution_repository import ExecutionRepository diff --git a/backend/app/dlq/manager.py b/backend/app/dlq/manager.py index 139fc33e..b7a85075 100644 --- a/backend/app/dlq/manager.py +++ b/backend/app/dlq/manager.py @@ -6,7 +6,7 @@ from faststream.kafka import KafkaBroker from app.core.metrics import DLQMetrics -from app.db.repositories import DLQRepository +from app.db import DLQRepository from app.dlq.models import ( DLQBatchRetryResult, DLQMessage, diff --git a/backend/app/events/consumer_group_monitor.py b/backend/app/events/consumer_group_monitor.py deleted file mode 100644 index 5f81d27f..00000000 --- a/backend/app/events/consumer_group_monitor.py +++ /dev/null @@ -1,126 +0,0 @@ -import logging -from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import Any - -from aiokafka.protocol.api import Response -from aiokafka.protocol.group import MemberAssignment - -from app.core.utils import StringEnum - - -class ConsumerGroupHealth(StringEnum): - """Consumer group health status.""" - - HEALTHY = "healthy" - DEGRADED = "degraded" - UNHEALTHY = "unhealthy" - UNKNOWN = "unknown" - - -# Known consumer group states from Kafka protocol -class ConsumerGroupState(StringEnum): - """Consumer group states from Kafka protocol.""" - - STABLE = "Stable" - PREPARING_REBALANCE = "PreparingRebalance" - COMPLETING_REBALANCE = "CompletingRebalance" - EMPTY = "Empty" - DEAD = "Dead" - UNKNOWN = "Unknown" - - -@dataclass(slots=True) -class ConsumerGroupMember: - """Information about a consumer group member.""" - - member_id: str - client_id: str - host: str - assigned_partitions: list[str] # topic:partition format - - -@dataclass(slots=True) -class ConsumerGroupStatus: - """Comprehensive consumer group status information.""" - - group_id: str - state: ConsumerGroupState - protocol: str - protocol_type: str - coordinator: str - members: list[ConsumerGroupMember] - - # Health metrics - member_count: int - assigned_partitions: int - partition_distribution: dict[str, int] # member_id -> partition count - - # Lag information (if available) - total_lag: int = 0 - partition_lags: dict[str, int] = field(default_factory=dict) # topic:partition -> lag - - # Health assessment - health: ConsumerGroupHealth = ConsumerGroupHealth.UNKNOWN - health_message: str = "" - - timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - - -@dataclass(slots=True) -class DescribedGroup: - """Parsed group from DescribeGroupsResponse.""" - - error_code: int - group_id: str - state: str - protocol_type: str - protocol: str - members: list[dict[str, Any]] - - -def _parse_describe_groups_response(response: Response) -> list[DescribedGroup]: - """Parse DescribeGroupsResponse into typed DescribedGroup objects.""" - obj = response.to_object() - groups_data: list[dict[str, Any]] = obj["groups"] - - result: list[DescribedGroup] = [] - for g in groups_data: - result.append( - DescribedGroup( - error_code=g["error_code"], - group_id=g["group"], - state=g["state"], - protocol_type=g["protocol_type"], - protocol=g["protocol"], - members=g["members"], - ) - ) - return result - - -_logger = logging.getLogger(__name__) - - -def _parse_member_assignment(assignment_bytes: bytes) -> list[tuple[str, list[int]]]: - """Parse member_assignment bytes to list of (topic, partitions).""" - if not assignment_bytes: - return [] - - try: - assignment = MemberAssignment.decode(assignment_bytes) - return [(topic, list(partitions)) for topic, partitions in assignment.assignment] - except Exception as e: - _logger.debug(f"Failed to parse member assignment: {e}") - return [] - - -def _state_from_string(state_str: str) -> ConsumerGroupState: - """Convert state string to ConsumerGroupState enum.""" - try: - return ConsumerGroupState(state_str) - except ValueError: - return ConsumerGroupState.UNKNOWN - - - diff --git a/backend/app/events/core/producer.py b/backend/app/events/core/producer.py index f5fb3bed..f067b7e5 100644 --- a/backend/app/events/core/producer.py +++ b/backend/app/events/core/producer.py @@ -2,7 +2,7 @@ from faststream.kafka import KafkaBroker from app.core.metrics import EventMetrics -from app.db.repositories import EventRepository +from app.db import EventRepository from app.domain.events import DomainEvent diff --git a/backend/app/services/admin/admin_events_service.py b/backend/app/services/admin/admin_events_service.py index 0c87bf68..18ca8848 100644 --- a/backend/app/services/admin/admin_events_service.py +++ b/backend/app/services/admin/admin_events_service.py @@ -7,8 +7,8 @@ import structlog +from app.db import AdminEventsRepository from app.db.docs.replay import ReplaySessionDocument -from app.db.repositories import AdminEventsRepository from app.domain.admin import ReplaySessionData, ReplaySessionStatusDetail, ReplaySessionUpdate from app.domain.enums import ExportFormat, ReplayStatus, ReplayTarget, ReplayType from app.domain.events import ( diff --git a/backend/app/services/admin/admin_execution_service.py b/backend/app/services/admin/admin_execution_service.py index b68d17b8..1624e84a 100644 --- a/backend/app/services/admin/admin_execution_service.py +++ b/backend/app/services/admin/admin_execution_service.py @@ -2,7 +2,7 @@ import structlog -from app.db.repositories import ExecutionRepository +from app.db import ExecutionRepository from app.domain.enums import ExecutionStatus, QueuePriority from app.domain.execution import DomainExecution, ExecutionNotFoundError from app.services.execution_queue import ExecutionQueueService diff --git a/backend/app/services/admin/admin_settings_service.py b/backend/app/services/admin/admin_settings_service.py index d3dc69c3..5a11969b 100644 --- a/backend/app/services/admin/admin_settings_service.py +++ b/backend/app/services/admin/admin_settings_service.py @@ -1,6 +1,6 @@ import structlog -from app.db.repositories import AdminSettingsRepository +from app.db import AdminSettingsRepository from app.domain.admin import SystemSettings from app.services.runtime_settings import RuntimeSettingsLoader diff --git a/backend/app/services/admin/admin_user_service.py b/backend/app/services/admin/admin_user_service.py index bdd61a76..56d68272 100644 --- a/backend/app/services/admin/admin_user_service.py +++ b/backend/app/services/admin/admin_user_service.py @@ -5,7 +5,7 @@ from app.core.metrics import SecurityMetrics from app.core.security import SecurityService -from app.db.repositories import UserRepository +from app.db import UserRepository from app.domain.admin import AdminUserOverviewDomain, DerivedCountsDomain, RateLimitSummaryDomain from app.domain.enums import EventType, ExecutionStatus, UserRole from app.domain.exceptions import ConflictError, NotFoundError diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 5216d106..6233681e 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -6,7 +6,7 @@ from app.core.metrics import SecurityMetrics from app.core.security import SecurityService -from app.db.repositories import UserRepository +from app.db import UserRepository from app.domain.enums import LoginMethod, UserRole from app.domain.events import ( AuthFailedEvent, @@ -25,7 +25,7 @@ LoginResult, User, ) -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from app.services.login_lockout import LoginLockoutService from app.services.runtime_settings import RuntimeSettingsLoader from app.settings import Settings diff --git a/backend/app/services/event_replay/replay_service.py b/backend/app/services/event_replay/replay_service.py index 34508f1c..b92b5e73 100644 --- a/backend/app/services/event_replay/replay_service.py +++ b/backend/app/services/event_replay/replay_service.py @@ -12,7 +12,7 @@ from pydantic import ValidationError from app.core.metrics import ReplayMetrics -from app.db.repositories import ReplayRepository +from app.db import ReplayRepository from app.domain.admin import ReplaySessionUpdate from app.domain.enums import ReplayStatus, ReplayTarget from app.domain.events import DomainEvent, DomainEventAdapter @@ -26,7 +26,7 @@ ReplaySessionState, ) from app.domain.sse import DomainReplaySSEPayload -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from app.services.sse.redis_bus import SSERedisBus diff --git a/backend/app/services/event_service.py b/backend/app/services/event_service.py index 536f8a72..9a27a9b7 100644 --- a/backend/app/services/event_service.py +++ b/backend/app/services/event_service.py @@ -1,6 +1,6 @@ from datetime import datetime -from app.db.repositories import EventRepository +from app.db import EventRepository from app.domain.enums import EventType, UserRole from app.domain.events import ArchivedEvent, DomainEvent, EventListResult, EventReplayInfo, EventStatistics diff --git a/backend/app/services/execution_service.py b/backend/app/services/execution_service.py index 7f021498..7d5a5334 100644 --- a/backend/app/services/execution_service.py +++ b/backend/app/services/execution_service.py @@ -7,7 +7,7 @@ from pydantic import TypeAdapter from app.core.metrics import ExecutionMetrics -from app.db.repositories import ExecutionRepository +from app.db import ExecutionRepository from app.domain.enums import CancelStatus, EventType, ExecutionStatus, QueuePriority from app.domain.events import ( BaseEvent, @@ -26,7 +26,7 @@ ResourceLimitsDomain, ) from app.domain.idempotency import IdempotencyStatus, KeyStrategy -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from app.runtime_registry import RUNTIME_REGISTRY from app.services.idempotency import IdempotencyManager from app.services.runtime_settings import RuntimeSettingsLoader diff --git a/backend/app/services/k8s_worker/worker.py b/backend/app/services/k8s_worker/worker.py index 8ab9610d..d78084af 100644 --- a/backend/app/services/k8s_worker/worker.py +++ b/backend/app/services/k8s_worker/worker.py @@ -15,7 +15,7 @@ ExecutionFailedEvent, PodCreatedEvent, ) -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from app.runtime_registry import RUNTIME_REGISTRY from app.settings import Settings diff --git a/backend/app/services/kafka_event_service.py b/backend/app/services/kafka_event_service.py index a14e8cff..aba951b7 100644 --- a/backend/app/services/kafka_event_service.py +++ b/backend/app/services/kafka_event_service.py @@ -5,7 +5,7 @@ from app.core.metrics import EventMetrics from app.domain.events import DomainEvent -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from app.settings import Settings tracer = trace.get_tracer(__name__) diff --git a/backend/app/services/notification_scheduler.py b/backend/app/services/notification_scheduler.py index a481b83c..f3fd90d2 100644 --- a/backend/app/services/notification_scheduler.py +++ b/backend/app/services/notification_scheduler.py @@ -1,6 +1,6 @@ import structlog -from app.db.repositories import NotificationRepository +from app.db import NotificationRepository from app.services.notification_service import NotificationService diff --git a/backend/app/services/notification_service.py b/backend/app/services/notification_service.py index 069f7dbc..b9a63b83 100644 --- a/backend/app/services/notification_service.py +++ b/backend/app/services/notification_service.py @@ -10,7 +10,7 @@ from opentelemetry import trace from app.core.metrics import NotificationMetrics -from app.db.repositories import NotificationRepository +from app.db import NotificationRepository from app.domain.enums import NotificationChannel, NotificationSeverity, NotificationStatus, UserRole from app.domain.events import ( EventMetadata, diff --git a/backend/app/services/result_processor/processor.py b/backend/app/services/result_processor/processor.py index a765a03f..8a7371e3 100644 --- a/backend/app/services/result_processor/processor.py +++ b/backend/app/services/result_processor/processor.py @@ -1,7 +1,7 @@ import structlog from app.core.metrics import ExecutionMetrics -from app.db.repositories import ExecutionRepository +from app.db import ExecutionRepository from app.domain.enums import ExecutionErrorType, ExecutionStatus, StorageType from app.domain.events import ( DomainEvent, @@ -13,7 +13,7 @@ ResultStoredEvent, ) from app.domain.execution import ExecutionNotFoundError, ExecutionResultDomain -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from app.settings import Settings diff --git a/backend/app/services/runtime_settings.py b/backend/app/services/runtime_settings.py index 59e34c55..eb342389 100644 --- a/backend/app/services/runtime_settings.py +++ b/backend/app/services/runtime_settings.py @@ -2,7 +2,7 @@ import structlog -from app.db.repositories import AdminSettingsRepository +from app.db import AdminSettingsRepository from app.domain.admin import SystemSettings from app.settings import Settings diff --git a/backend/app/services/saga/execution_saga.py b/backend/app/services/saga/execution_saga.py index dc59b8df..d1009f6e 100644 --- a/backend/app/services/saga/execution_saga.py +++ b/backend/app/services/saga/execution_saga.py @@ -3,10 +3,10 @@ import structlog -from app.db.repositories import ResourceAllocationRepository +from app.db import ResourceAllocationRepository from app.domain.events import CreatePodCommandEvent, DeletePodCommandEvent, EventMetadata, ExecutionRequestedEvent from app.domain.saga import DomainResourceAllocationCreate -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from .saga_step import CompensationStep, SagaContext, SagaStep diff --git a/backend/app/services/saga/saga_orchestrator.py b/backend/app/services/saga/saga_orchestrator.py index 62dfa5d2..6cda5730 100644 --- a/backend/app/services/saga/saga_orchestrator.py +++ b/backend/app/services/saga/saga_orchestrator.py @@ -6,7 +6,7 @@ from opentelemetry import trace from opentelemetry.trace import SpanKind -from app.db.repositories import ResourceAllocationRepository, SagaRepository +from app.db import ResourceAllocationRepository, SagaRepository from app.domain.enums import SagaState from app.domain.events import ( DomainEvent, @@ -25,7 +25,7 @@ SagaConfig, SagaContextData, ) -from app.events.core import UnifiedProducer +from app.events import UnifiedProducer from app.services.execution_queue import ExecutionQueueService from app.services.runtime_settings import RuntimeSettingsLoader diff --git a/backend/app/services/saga/saga_service.py b/backend/app/services/saga/saga_service.py index ae80856a..05b8d92e 100644 --- a/backend/app/services/saga/saga_service.py +++ b/backend/app/services/saga/saga_service.py @@ -1,6 +1,6 @@ import structlog -from app.db.repositories import ExecutionRepository, SagaRepository +from app.db import ExecutionRepository, SagaRepository from app.domain.enums import SagaState, UserRole from app.domain.saga import ( Saga, diff --git a/backend/app/services/saved_script_service.py b/backend/app/services/saved_script_service.py index eb526589..da62c259 100644 --- a/backend/app/services/saved_script_service.py +++ b/backend/app/services/saved_script_service.py @@ -1,6 +1,6 @@ import structlog -from app.db.repositories import SavedScriptRepository +from app.db import SavedScriptRepository from app.domain.saved_script import ( DomainSavedScript, DomainSavedScriptCreate, diff --git a/backend/app/services/sse/sse_service.py b/backend/app/services/sse/sse_service.py index edb7a9ca..8a18c9db 100644 --- a/backend/app/services/sse/sse_service.py +++ b/backend/app/services/sse/sse_service.py @@ -5,7 +5,7 @@ import structlog from pydantic import TypeAdapter -from app.db.repositories import ExecutionRepository +from app.db import ExecutionRepository from app.domain.enums import EventType, SSEControlEvent, UserRole from app.domain.exceptions import ForbiddenError from app.domain.execution import ExecutionNotFoundError diff --git a/backend/app/services/user_settings_service.py b/backend/app/services/user_settings_service.py index 7f29706e..82abf731 100644 --- a/backend/app/services/user_settings_service.py +++ b/backend/app/services/user_settings_service.py @@ -5,7 +5,7 @@ import structlog from cachetools import TTLCache -from app.db.repositories import UserSettingsRepository +from app.db import UserSettingsRepository from app.domain.enums import EventType, Theme from app.domain.events import EventMetadata, UserSettingsUpdatedEvent from app.domain.user import ( diff --git a/backend/pyproject.toml b/backend/pyproject.toml index b4efe3ae..c381535b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -143,7 +143,7 @@ lint = [ "mypy_extensions==1.1.0", "ruff==0.14.10", "types-cachetools==6.2.0.20250827", - "vulture==2.14", + "grimp>=3.14", ] load = [ "matplotlib==3.10.8", @@ -170,7 +170,6 @@ exclude = [ "**/.venv/**", "**/site-packages/**", "**/.claude/**", - "vulture_whitelist.py" ] [tool.ruff.lint.flake8-bugbear] @@ -187,7 +186,6 @@ warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true disable_error_code = ["import-untyped", "import-not-found"] -exclude = ["vulture_whitelist\\.py$"] plugins = ["pydantic.mypy"] [tool.pydantic-mypy] @@ -232,9 +230,3 @@ OTEL_SDK_DISABLED = "true" # Prevents teardown delays from OTLP exporter retrie [tool.coverage.run] # Use sysmon for faster coverage (requires Python 3.12+) core = "sysmon" - -# Vulture dead code detection -[tool.vulture] -min_confidence = 80 -paths = ["app"] -exclude = ["tests"] diff --git a/backend/scripts/check_orphan_modules.py b/backend/scripts/check_orphan_modules.py new file mode 100644 index 00000000..c1fbc5cc --- /dev/null +++ b/backend/scripts/check_orphan_modules.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Detect orphan modules in the app package using grimp. + +An orphan module is one that no other module within the package imports. +Entry points are whitelisted since they are invoked directly by gunicorn +or worker runner scripts, not imported by sibling modules. + +Usage: + uv run python scripts/check_orphan_modules.py +""" +import sys +from pathlib import Path + +import grimp + +ENTRY_POINTS: frozenset[str] = frozenset({ + "app.main", +}) + + +def _is_empty_init(module: str) -> bool: + """Return True if module is a package __init__.py with no meaningful code.""" + init_path = Path(module.replace(".", "/")) / "__init__.py" + if not init_path.is_file(): + return False + source = init_path.read_text() + meaningful = [ + line + for line in source.splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] + return len(meaningful) == 0 + + +def main() -> int: + graph = grimp.build_graph("app") + + orphans: list[str] = [] + for module in sorted(graph.modules): + if module in ENTRY_POINTS: + continue + if _is_empty_init(module): + continue + if not graph.find_modules_that_directly_import(module): + orphans.append(module) + + if orphans: + print(f"Found {len(orphans)} orphan module(s) — never imported by any other module in app/:") + for m in orphans: + print(f" {m}") + return 1 + + print("No orphan modules found.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backend/uv.lock b/backend/uv.lock index 563bc408..97d43e24 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -894,6 +894,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] +[[package]] +name = "grimp" +version = "3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/46/79764cfb61a3ac80dadae5d94fb10acdb7800e31fecf4113cf3d345e4952/grimp-3.14.tar.gz", hash = "sha256:645fbd835983901042dae4e1b24fde3a89bf7ac152f9272dd17a97e55cb4f871", size = 830882, upload-time = "2025-12-10T17:55:01.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d6/a35ff62f35aa5fd148053506eddd7a8f2f6afaed31870dc608dd0eb38e4f/grimp-3.14-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ffabc6940301214753bad89ec0bfe275892fa1f64b999e9a101f6cebfc777133", size = 2178573, upload-time = "2025-12-10T17:53:42.836Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/bd2e80273da4d46110969fc62252e5372e0249feb872bc7fe76fdc7f1818/grimp-3.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:075d9a1c78d607792d0ed8d4d3d7754a621ef04c8a95eaebf634930dc9232bb2", size = 2110452, upload-time = "2025-12-10T17:53:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/c3/7307249c657d34dca9d250d73ba027d6cfe15a98fb3119b6e5210bc388b7/grimp-3.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ff52addeb20955a4d6aa097bee910573ffc9ef0d3c8a860844f267ad958156", size = 2283064, upload-time = "2025-12-10T17:52:07.673Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d2/cae4cf32dc8d4188837cc4ab183300d655f898969b0f169e240f3b7c25be/grimp-3.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d10e0663e961fcbe8d0f54608854af31f911f164c96a44112d5173050132701f", size = 2235893, upload-time = "2025-12-10T17:52:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/3f58bc3064fc305dac107d08003ba65713a5bc89a6d327f1c06b30cce752/grimp-3.14-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ab874d7ddddc7a1291259cf7c31a4e7b5c612e9da2e24c67c0eb1a44a624e67", size = 2393376, upload-time = "2025-12-10T17:53:02.397Z" }, + { url = "https://files.pythonhosted.org/packages/06/b8/f476f30edf114f04cb58e8ae162cb4daf52bda0ab01919f3b5b7edb98430/grimp-3.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54fec672ec83355636a852177f5a470c964bede0f6730f9ba3c7b5c8419c9eab", size = 2571342, upload-time = "2025-12-10T17:52:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/2e44d3c4f591f95f86322a8f4dbb5aac17001d49e079f3a80e07e7caaf09/grimp-3.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9e221b5e8070a916c780e88c877fee2a61c95a76a76a2a076396e459511b0bb", size = 2359022, upload-time = "2025-12-10T17:52:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/42b4d6bc0ea119ce2e91e1788feabf32c5433e9617dbb495c2a3d0dc7f12/grimp-3.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea6b495f9b4a8d82f5ce544921e76d0d12017f5d1ac3a3bd2f5ac88ab055b1c", size = 2309424, upload-time = "2025-12-10T17:53:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/6a731989625c1790f4da7602dcbf9d6525512264e853cda77b3b3602d5e0/grimp-3.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:655e8d3f79cd99bb859e09c9dd633515150e9d850879ca71417d5ac31809b745", size = 2462754, upload-time = "2025-12-10T17:53:50.886Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/3d1571c0a39a59dd68be4835f766da64fe64cbab0d69426210b716a8bdf0/grimp-3.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a14f10b1b71c6c37647a76e6a49c226509648107abc0f48c1e3ecd158ba05531", size = 2501356, upload-time = "2025-12-10T17:54:06.014Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/8950b8229095ebda5c54c8784e4d1f0a6e19423f2847289ef9751f878798/grimp-3.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:81685111ee24d3e25f8ed9e77ed00b92b58b2414e1a1c2937236026900972744", size = 2504631, upload-time = "2025-12-10T17:54:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/23bed3da9206138d36d01890b656c7fb7adfb3a37daac8842d84d8777ade/grimp-3.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce8352a8ea0e27b143136ea086582fc6653419aa8a7c15e28ed08c898c42b185", size = 2514751, upload-time = "2025-12-10T17:54:49.384Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/6f1f55c97ee982f133ec5ccb22fc99bf5335aee70c208f4fb86cd833b8d5/grimp-3.14-cp312-cp312-win32.whl", hash = "sha256:3fc0f98b3c60d88e9ffa08faff3200f36604930972f8b29155f323b76ea25a06", size = 1875041, upload-time = "2025-12-10T17:55:13.326Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/03ba01288e2a41a948bc8526f32c2eeaddd683ed34be1b895e31658d5a4c/grimp-3.14-cp312-cp312-win_amd64.whl", hash = "sha256:6bca77d1d50c8dc402c96af21f4e28e2f1e9938eeabd7417592a22bd83cde3c3", size = 2013868, upload-time = "2025-12-10T17:55:05.907Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bd/d12a9c821b79ba31fc52243e564712b64140fc6d011c2bdbb483d9092a12/grimp-3.14-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af8a625554beea84530b98cc471902155b5fc042b42dc47ec846fa3e32b0c615", size = 2178632, upload-time = "2025-12-10T17:53:44.55Z" }, + { url = "https://files.pythonhosted.org/packages/96/8c/d6620dbc245149d5a5a7a9342733556ba91a672f358259c0ab31d889b56b/grimp-3.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0dd1942ffb419ad342f76b0c3d3d2d7f312b264ddc578179d13ce8d5acec1167", size = 2110288, upload-time = "2025-12-10T17:53:21.662Z" }, + { url = "https://files.pythonhosted.org/packages/60/9d/ea51edc4eb295c99786040051c66466bfa235fd1def9f592057b36e03d0f/grimp-3.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:537f784ce9b4acf8657f0b9714ab69a6c72ffa752eccc38a5a85506103b1a194", size = 2282197, upload-time = "2025-12-10T17:52:09.304Z" }, + { url = "https://files.pythonhosted.org/packages/28/6e/7db27818ced6a797f976ca55d981a3af5c12aec6aeda12d63965847cd028/grimp-3.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78ab18c08770aa005bef67b873bc3946d33f65727e9f3e508155093db5fa57d6", size = 2235720, upload-time = "2025-12-10T17:52:21.806Z" }, + { url = "https://files.pythonhosted.org/packages/37/26/0e3bbae4826bd6eaabf404738400414071e73ddb1e65bf487dcce17858c4/grimp-3.14-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28ca58728c27e7292c99f964e6ece9295c2f9cfdefc37c18dea0679c783ffb6f", size = 2393023, upload-time = "2025-12-10T17:53:04.149Z" }, + { url = "https://files.pythonhosted.org/packages/49/f2/7da91db5703da34c7ef4c7cddcbb1a8fc30cd85fe54756eba942c6fb27d8/grimp-3.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b5577de29c6c5ae6e08d4ca0ac361b45dba323aa145796e6b320a6ea35414b7", size = 2571108, upload-time = "2025-12-10T17:52:36.523Z" }, + { url = "https://files.pythonhosted.org/packages/25/5e/4d6278f18032c7208696edf8be24a4b5f7fad80acc20ffca737344bcecb5/grimp-3.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d7d1f9f42306f455abcec34db877e4887ff15f2777a43491f7ccbd6936c449b", size = 2358531, upload-time = "2025-12-10T17:52:50.521Z" }, + { url = "https://files.pythonhosted.org/packages/24/fb/231c32493161ac82f27af6a56965daefa0ec6030fdaf5b948ddd5d68d000/grimp-3.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39bd5c9b7cef59ee30a05535e9cb4cbf45a3c503f22edce34d0aa79362a311a9", size = 2308831, upload-time = "2025-12-10T17:53:12.587Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/f6db325bf5efbbebc9c85cad0af865e821a12a0ba58ee309e938cbd5fedf/grimp-3.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fec3116b4f780a1bc54176b19e6b9f2e36e2ef3164b8fc840660566af35df88", size = 2462138, upload-time = "2025-12-10T17:53:52.403Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/cc3fe29cf07f70364018086840c228a190539ab8105147e34588db590792/grimp-3.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0233a35a5bbb23688d63e1736b54415fa9994ace8dfeb7de8514ed9dee212968", size = 2501393, upload-time = "2025-12-10T17:54:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/e5/eb/54cada9a726455148da23f64577b5cd164164d23a6449e3fa14551157356/grimp-3.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e46b2fef0f1da7e7e2f8129eb93c7e79db716ff7810140a22ce5504e10ed86df", size = 2504514, upload-time = "2025-12-10T17:54:36.34Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/e6afe4f0652df07e8762f61899d1202b73c22c559c804d0a09e5aab2ff17/grimp-3.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e6d9b50623ee1c3d2a1927ec3f5d408995ea1f92f3e91ed996c908bb40e856f", size = 2514018, upload-time = "2025-12-10T17:54:50.76Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/2b8550acc1f010301f02c4fe9664810929fd9277cd032ab608b8534a96fb/grimp-3.14-cp313-cp313-win32.whl", hash = "sha256:fd57c56f5833c99320ec77e8ba5508d56f6fb48ec8032a942f7931cc6ebb80ce", size = 1874922, upload-time = "2025-12-10T17:55:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/46/c7/bc9db5a54ef22972cd17d15ad80a8fee274a471bd3f02300405702d29ea5/grimp-3.14-cp313-cp313-win_amd64.whl", hash = "sha256:173307cf881a126fe5120b7bbec7d54384002e3c83dcd8c4df6ce7f0fee07c53", size = 2013705, upload-time = "2025-12-10T17:55:07.488Z" }, + { url = "https://files.pythonhosted.org/packages/80/7e/02710bf5e50997168c84ac622b10dd41d35515efd0c67549945ad20996a0/grimp-3.14-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebe29f8f13fbd7c314908ed535183a36e6db71839355b04869b27f23c58fa082", size = 2281868, upload-time = "2025-12-10T17:52:10.589Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/2e440c6762cc78bd50582e1b092357d2255f0852ccc6218d8db25170ab31/grimp-3.14-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073d285b00100153fd86064c7726bb1b6d610df1356d33bb42d3fd8809cb6e72", size = 2230917, upload-time = "2025-12-10T17:52:23.212Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bb/2e7dce129b88f07fc525fe5c97f28cfb7ed7b62c59386d39226b4d08969c/grimp-3.14-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6d6efc37e1728bbfcd881b89467be5f7b046292597b3ebe5f8e44e89ea8b6cb", size = 2571371, upload-time = "2025-12-10T17:52:37.84Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2b/8f1be8294af60c953687db7dec25525d87ed9c2aa26b66dcbe5244abaca2/grimp-3.14-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5337d65d81960b712574c41e85b480d4480bbb5c6f547c94e634f6c60d730889", size = 2356980, upload-time = "2025-12-10T17:52:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/ead91e04b3ddd4774ae74601860ea0f0f21bcf6b970b6769ba9571eb2904/grimp-3.14-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:84a7fea63e352b325daa89b0b7297db411b7f0036f8d710c32f8e5090e1fc3ca", size = 2461540, upload-time = "2025-12-10T17:53:53.749Z" }, + { url = "https://files.pythonhosted.org/packages/94/aa/f8a085ff73c37d6e6a37de9f58799a3fea9e16badf267aaef6f11c9a53a3/grimp-3.14-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d0b19a3726377165fe1f7184a8af317734d80d32b371b6c5578747867ab53c0b", size = 2497925, upload-time = "2025-12-10T17:54:23.842Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a3/db3c2d6df07fe74faf5a28fcf3b44fad2831d323ba4a3c2ff66b77a6520c/grimp-3.14-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9caa4991f530750f88474a3f5ecf6ef9f0d064034889d92db00cfb4ecb78aa24", size = 2501794, upload-time = "2025-12-10T17:54:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/095f4e3765e7b60425a41e9fbd2b167f8b0acb957cc88c387f631778a09d/grimp-3.14-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1876efc119b99332a5cc2b08a6bdaada2f0ad94b596f0372a497e2aa8bda4d94", size = 2515203, upload-time = "2025-12-10T17:54:52.555Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5f/ee02a3a1237282d324f596a50923bf9d2cb1b1230ef2fef49fb4d3563c2c/grimp-3.14-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3ccf03e65864d6bc7bf1c003c319f5330a7627b3677f31143f11691a088464c2", size = 2177150, upload-time = "2025-12-10T17:53:46.145Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/2a92889e5fc78e8ef5c548e6a5c6fed78b817eeb0253aca586c28108393a/grimp-3.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9ecd58fa58a270e7523f8bec9e6452f4fdb9c21e4cd370640829f1e43fa87a69", size = 2109280, upload-time = "2025-12-10T17:53:23.345Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/5d0b9ab54821e7fbdeb02f3919fa2cb8b9f0c3869fa6e4b969a5766f0ffa/grimp-3.14-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d75d1f8f7944978b39b08d870315174f1ffcd5123be6ccff8ce90467ace648a", size = 2283367, upload-time = "2025-12-10T17:52:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/c2/96/a77c40c92faf7500f42ac019ab8de108b04ffe3db8ec8d6f90416d2322ce/grimp-3.14-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f70bbb1dd6055d08d29e39a78a11c4118c1778b39d17cd8271e18e213524ca7", size = 2237125, upload-time = "2025-12-10T17:52:24.606Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/3e1483721c83057bff921cf454dd5ff3e661ae1d2e63150a380382d116c2/grimp-3.14-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f21b7c003626c902669dc26ede83a91220cf0a81b51b27128370998c2f247b4", size = 2391735, upload-time = "2025-12-10T17:53:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/25fad4a174fe672d42f3e5616761a8120a3b03c8e9e2ae3f31159561968a/grimp-3.14-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80d9f056415c936b45561310296374c4319b5df0003da802c84d2830a103792a", size = 2571388, upload-time = "2025-12-10T17:52:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/456df7f6a765ce3f160eb32a0f64ed0c1c3cd39b518555dde02087f9b6e4/grimp-3.14-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0332963cd63a45863775d4237e59dedf95455e0a1ea50c356be23100c5fc1d7c", size = 2359637, upload-time = "2025-12-10T17:52:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/3e5005ef21a4e2243f0da489aba86aaaff0bc11d5240d67113482cba88e0/grimp-3.14-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4144350d074f2058fe7c89230a26b34296b161f085b0471a692cb2fe27036f", size = 2308335, upload-time = "2025-12-10T17:53:13.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/03/4e055f756946d6f71ab7e9d1f8536a9e476777093dd7a050f40412d1a2b1/grimp-3.14-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e148e67975e92f90a8435b1b4c02180b9a3f3d725b7a188ba63793f1b1e445a0", size = 2463680, upload-time = "2025-12-10T17:53:55.507Z" }, + { url = "https://files.pythonhosted.org/packages/26/b9/3c76b7c2e1587e4303a6eff6587c2117c3a7efe1b100cd13d8a4a5613572/grimp-3.14-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1093f7770cb5f3ca6f99fb152f9c949381cc0b078dfdfe598c8ab99abaccda3b", size = 2502808, upload-time = "2025-12-10T17:54:25.383Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/ada10b85ad3125ebedea10256d9c568b6bf28339d2f79d2d196a7b94f633/grimp-3.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a213f45ec69e9c2b28ffd3ba5ab12cc9859da17083ba4dc39317f2083b618111", size = 2504013, upload-time = "2025-12-10T17:54:39.762Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/7c369f749d50b0ceac23cd6874ca4695cc1359a96091c7010301e5c8b619/grimp-3.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f003ac3f226d2437a49af0b6036f26edba57f8a32d329275dbde1b2b2a00a56", size = 2515043, upload-time = "2025-12-10T17:54:54.437Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/85135fe83826ce11ae56a340d32a1391b91eed94d25ce7bc318019f735de/grimp-3.14-cp314-cp314-win32.whl", hash = "sha256:eec81be65a18f4b2af014b1e97296cc9ee20d1115529bf70dd7e06f457eac30b", size = 1877509, upload-time = "2025-12-10T17:55:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/db/61/e4a2234edecb3bb3cff8963bc4ec5cc482a9e3c54f8df0946d7d90003830/grimp-3.14-cp314-cp314-win_amd64.whl", hash = "sha256:cd3bab6164f1d5e313678f0ab4bf45955afe7f5bdb0f2f481014aa9cca7e81ba", size = 2014364, upload-time = "2025-12-10T17:55:08.896Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/3d304443fbf1df4d60c09668846d0c8a605c6c95646226e41d8f5c3254da/grimp-3.14-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1df33de479be4d620f69633d1876858a8e64a79c07907d47cf3aaf896af057", size = 2281385, upload-time = "2025-12-10T17:52:13.668Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/493e2648dbb83b3fc517ee675e464beb0154551d726053c7982a3138c6a8/grimp-3.14-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07096d4402e9d5a2c59c402ea3d601f4b7f99025f5e32f077468846fc8d3821b", size = 2231470, upload-time = "2025-12-10T17:52:26.104Z" }, + { url = "https://files.pythonhosted.org/packages/80/84/e772b302385a6b7ec752c88f84ffe35c33d14076245ae27a635aed9c63a2/grimp-3.14-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:712bc28f46b354316af50c469c77953ba3d6cb4166a62b8fb086436a8b05d301", size = 2571579, upload-time = "2025-12-10T17:52:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/69/92/5b23aa7b89c5f4f2cfa636cbeaf33e784378a6b0a823d77a3448670dfacc/grimp-3.14-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abe2bbef1cf8e27df636c02f60184319f138dee4f3a949405c21a4b491980397", size = 2356545, upload-time = "2025-12-10T17:52:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/bcf2116f4b1c3939ab35f9cdddd9ca59e953e57e9a0ac0c143deaf9f29cc/grimp-3.14-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2f9ae3fabb7a7a8468ddc96acc84ecabd84f168e7ca508ee94d8f32ea9bd5de2", size = 2461022, upload-time = "2025-12-10T17:53:56.923Z" }, + { url = "https://files.pythonhosted.org/packages/81/ce/1a076dce6bc22bca4b9ad5d1bbcd7e1023dcf7bf20ea9404c6462d78f049/grimp-3.14-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:efaf11ea73f7f12d847c54a5d6edcbe919e0369dce2d1aabae6c50792e16f816", size = 2498256, upload-time = "2025-12-10T17:54:27.214Z" }, + { url = "https://files.pythonhosted.org/packages/45/ea/ac735bed202c1c5c019e611b92d3861779e0cfbe2d20fdb0dec94266d248/grimp-3.14-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e089c9ab8aa755ff5af88c55891727783b4eb6b228e7bdf278e17209d954aa1e", size = 2502056, upload-time = "2025-12-10T17:54:41.537Z" }, + { url = "https://files.pythonhosted.org/packages/80/8f/774ce522de6a7e70fbeceeaeb6fbe502f5dfb8365728fb3bb4cb23463da8/grimp-3.14-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ad14d5deb56721ac24ab939747f72ab3d378d42e7d1f038317d33b052b77", size = 2515157, upload-time = "2025-12-10T17:54:55.874Z" }, +] + [[package]] name = "grpcio" version = "1.76.0" @@ -1157,6 +1226,7 @@ dependencies = [ dev = [ { name = "async-asgi-testclient" }, { name = "coverage" }, + { name = "grimp" }, { name = "hypothesis" }, { name = "iniconfig" }, { name = "matplotlib" }, @@ -1172,14 +1242,13 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, { name = "types-cachetools" }, - { name = "vulture" }, ] lint = [ + { name = "grimp" }, { name = "mypy" }, { name = "mypy-extensions" }, { name = "ruff" }, { name = "types-cachetools" }, - { name = "vulture" }, ] load = [ { name = "matplotlib" }, @@ -1317,6 +1386,7 @@ requires-dist = [ dev = [ { name = "async-asgi-testclient", specifier = "==1.4.11" }, { name = "coverage", specifier = "==7.13.0" }, + { name = "grimp", specifier = ">=3.14" }, { name = "hypothesis", specifier = "==6.151.6" }, { name = "iniconfig", specifier = "==2.3.0" }, { name = "matplotlib", specifier = "==3.10.8" }, @@ -1332,14 +1402,13 @@ dev = [ { name = "pytest-xdist", specifier = "==3.6.1" }, { name = "ruff", specifier = "==0.14.10" }, { name = "types-cachetools", specifier = "==6.2.0.20250827" }, - { name = "vulture", specifier = "==2.14" }, ] lint = [ + { name = "grimp", specifier = ">=3.14" }, { name = "mypy", specifier = "==1.19.1" }, { name = "mypy-extensions", specifier = "==1.1.0" }, { name = "ruff", specifier = "==0.14.10" }, { name = "types-cachetools", specifier = "==6.2.0.20250827" }, - { name = "vulture", specifier = "==2.14" }, ] load = [{ name = "matplotlib", specifier = "==3.10.8" }] test = [ @@ -3063,15 +3132,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] -[[package]] -name = "vulture" -version = "2.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/25/925f35db758a0f9199113aaf61d703de891676b082bd7cf73ea01d6000f7/vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415", size = 58823, upload-time = "2024-12-08T17:39:43.319Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/56/0cc15b8ff2613c1d5c3dc1f3f576ede1c43868c1bc2e5ccaa2d4bcd7974d/vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9", size = 28915, upload-time = "2024-12-08T17:39:40.573Z" }, -] - [[package]] name = "websocket-client" version = "1.8.0" diff --git a/backend/vulture_whitelist.py b/backend/vulture_whitelist.py deleted file mode 100644 index 09e7763e..00000000 --- a/backend/vulture_whitelist.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Vulture whitelist — framework-required code that appears unused at the AST level. - -Structlog processors must accept (logger, method_name, event_dict) by convention, -even when `method_name` is unused in the body. -""" - -method_name # unused variable (app/core/logging.py:36) -method_name # unused variable (app/core/logging.py:53) diff --git a/docs/operations/cicd.md b/docs/operations/cicd.md index 3255069f..2588889c 100644 --- a/docs/operations/cicd.md +++ b/docs/operations/cicd.md @@ -11,7 +11,7 @@ graph LR subgraph "Code Quality (lightweight)" Ruff["Ruff Linting"] MyPy["MyPy Type Check"] - Vulture["Vulture Dead Code"] + Grimp["Grimp Orphan Modules"] ESLint["ESLint + Svelte Check"] end @@ -49,7 +49,7 @@ graph LR Pages["GitHub Pages"] end - Push["Push / PR"] --> Ruff & MyPy & Vulture & ESLint & Bandit & SBOM & UnitBE & UnitFE & Docs + Push["Push / PR"] --> Ruff & MyPy & Grimp & ESLint & Bandit & SBOM & UnitBE & UnitFE & Docs Build -->|main, all tests pass| Scan Promote -->|main, scans pass| Release Docs -->|main only| Pages @@ -73,7 +73,7 @@ production only updates when everything passes. | MyPy Type Checking | `.github/workflows/mypy.yml` | Push/PR to `main` | Python static type analysis | | Frontend CI | `.github/workflows/frontend-ci.yml` | Push/PR to `main` (frontend changes) | ESLint + Svelte type check | | Security Scanning | `.github/workflows/security.yml` | Push/PR to `main` | Bandit SAST | -| Dead Code Detection | `.github/workflows/vulture.yml` | Push/PR to `main` | Vulture dead code analysis | +| Dead Code Detection | `.github/workflows/grimp.yml` | Push/PR to `main` | Grimp orphan module detection | | Documentation | `.github/workflows/docs.yml` | Push/PR (`docs/`, `mkdocs.yml`) | MkDocs build and GitHub Pages deploy | ## Composite actions @@ -378,7 +378,7 @@ Three lightweight workflows run independently since they catch obvious issues qu - [Ruff](https://docs.astral.sh/ruff/) checks for style violations, import ordering, and common bugs - [mypy](https://mypy.readthedocs.io/) with strict settings catches type mismatches and missing return types -- [Vulture](https://github.com/jendrikseipp/vulture) detects unused functions, classes, methods, imports, and variables. A whitelist file (`backend/vulture_whitelist.py`) excludes framework patterns (Dishka providers, FastAPI routes, Beanie documents, Pydantic models) that look unused but are called at runtime +- [Grimp](https://github.com/seddonym/grimp) detects orphan modules — modules that no other module in the package imports. Unlike symbol-level tools, it catches entire dead files (e.g. removed features) with zero false positives from framework patterns **Frontend (TypeScript/Svelte):** @@ -457,8 +457,8 @@ uv run ruff check . --config pyproject.toml # Type checking uv run mypy --config-file pyproject.toml --strict . -# Dead code detection -uv run vulture app/ vulture_whitelist.py +# Dead code detection (orphan modules) +uv run python scripts/check_orphan_modules.py # Security scan uv tool run bandit -r . -x tests/ -ll From f7a261c8a49b40433b924429b1ce1c34fb96c88a Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 1 Mar 2026 22:13:52 +0100 Subject: [PATCH 2/2] fix: bug detected --- backend/scripts/check_orphan_modules.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/scripts/check_orphan_modules.py b/backend/scripts/check_orphan_modules.py index c1fbc5cc..5319c42c 100644 --- a/backend/scripts/check_orphan_modules.py +++ b/backend/scripts/check_orphan_modules.py @@ -8,6 +8,7 @@ Usage: uv run python scripts/check_orphan_modules.py """ +import ast import sys from pathlib import Path @@ -19,17 +20,21 @@ def _is_empty_init(module: str) -> bool: - """Return True if module is a package __init__.py with no meaningful code.""" + """Return True if module is a package __init__.py with no meaningful code. + + A file counts as empty if its AST body contains nothing beyond + an optional module-level docstring. + """ init_path = Path(module.replace(".", "/")) / "__init__.py" if not init_path.is_file(): return False source = init_path.read_text() - meaningful = [ - line - for line in source.splitlines() - if line.strip() and not line.lstrip().startswith("#") - ] - return len(meaningful) == 0 + tree = ast.parse(source) + for node in tree.body: + if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str): + continue + return False + return True def main() -> int: