Problem
Nothing in the current tooling prevents architectural layering violations between app/ subpackages. We already have violations — e.g. app.services.campaign_management_service and app.services.campaign_query_service import from app.routers.campaign_router, which is a layer inversion.
Current tooling (ruff, flake8, bandit, vulture, radon, basedpyright) covers syntax, style, security, dead code, complexity, and types — but not inter-module dependency direction or architecture boundary enforcement.
Spike Goal
Evaluate tools that can enforce import architecture constraints and recommend one for adoption. Produce a concrete .importlinter (or equivalent) config and a pre-commit/CI integration plan.
Candidates to Evaluate
| Tool |
Approach |
Notes |
| import-linter |
INI-based contracts (forbidden, layers, independence, acyclic) |
Pre-commit hook available. 1k stars. BSD-2. Well documented. Browser UI for exploring architecture. |
pylint import checks |
import-error, cyclic-import, wrong-import-order, wrong-import-position |
Already partly covered by ruff. No directional/layer contracts. |
ruff TID252 |
Banned imports via config |
Narrow — can ban specific modules but no directional or layer constraints. |
| pydeps |
Module dependency graph generation |
Visualization only, no enforcement. Could complement import-linter. |
| module-details-mismatch |
Custom AST checks |
Niche, not actively maintained. |
| Custom ruff plugin |
Write our own rule |
Maximum flexibility, but maintenance burden. |
Recommended Direction
import-linter appears to be the strongest fit:
- Declarative
.importlinter config (checked into repo)
- Pre-commit hook (
repo: https://github.com/seddonym/import-linter)
- Contract types map directly to what we need (layers, forbidden, acyclic)
- Interactive UI for visualizing current dependency graph
Proposed Layer Architecture
Based on current app/ structure:
routers → services → domain ← data
↑
persistence
| Layer |
Allowed to import from |
app.routers |
app.services, app.api_models, app.dependencies, app.schemas |
app.services |
app.domain, app.data, app.persistence, app.repositories, app.common, app.ocr, app.matching, app.files, app.storage, app.settings, app.events |
app.domain |
app.common only (no infrastructure) |
app.data |
app.domain (for type references only), app.common |
app.persistence |
app.data, app.domain, app.common |
app.repositories |
app.data, app.domain, app.common |
Known Violations to Fix First
app.services.campaign_management_service → imports app.routers.campaign_router
app.services.campaign_query_service → imports app.routers.campaign_router
These need refactoring (extract shared types/constants to app.common or app.api_models) before a layers contract can pass.
Implementation Plan (if import-linter adopted)
Acceptance Criteria
Problem
Nothing in the current tooling prevents architectural layering violations between
app/subpackages. We already have violations — e.g.app.services.campaign_management_serviceandapp.services.campaign_query_serviceimport fromapp.routers.campaign_router, which is a layer inversion.Current tooling (ruff, flake8, bandit, vulture, radon, basedpyright) covers syntax, style, security, dead code, complexity, and types — but not inter-module dependency direction or architecture boundary enforcement.
Spike Goal
Evaluate tools that can enforce import architecture constraints and recommend one for adoption. Produce a concrete
.importlinter(or equivalent) config and a pre-commit/CI integration plan.Candidates to Evaluate
importchecksimport-error,cyclic-import,wrong-import-order,wrong-import-positionTID252Recommended Direction
import-linter appears to be the strongest fit:
.importlinterconfig (checked into repo)repo: https://github.com/seddonym/import-linter)Proposed Layer Architecture
Based on current
app/structure:app.routersapp.services,app.api_models,app.dependencies,app.schemasapp.servicesapp.domain,app.data,app.persistence,app.repositories,app.common,app.ocr,app.matching,app.files,app.storage,app.settings,app.eventsapp.domainapp.commononly (no infrastructure)app.dataapp.domain(for type references only),app.commonapp.persistenceapp.data,app.domain,app.commonapp.repositoriesapp.data,app.domain,app.commonKnown Violations to Fix First
app.services.campaign_management_service→ importsapp.routers.campaign_routerapp.services.campaign_query_service→ importsapp.routers.campaign_routerThese need refactoring (extract shared types/constants to
app.commonorapp.api_models) before a layers contract can pass.Implementation Plan (if import-linter adopted)
Phase 1: Install & snip baseline
import-lintertodevdependencies inpyproject.toml.importlinterwith initial contracts (start withforbiddenonly, not full layers)lint-importsand catalog all violations (expect failures)warnpass initiallyPhase 2: Fix existing violations
campaign_routerto a shared moduleservices → routersdependency cyclePhase 3: Enforce layers contract
.importlinterto full layers contractforbiddencontracts for sensitive boundaries (e.g.,app.ocrmust not importapp.routers)Phase 4: CI integration
lint-importsstep to CI pipelineAcceptance Criteria
.importlinter(or equivalent) config committedlint-importspasses clean