Przejdź do treści

INW001 layer-dependency

Co robi

Zgłasza import, w pliku należącym do warstwy, własnego kodu, który należy do warstwy zewnętrznej. Warstwy w [tool.inwards] są wymienione od najbardziej wewnętrznej, a moduł może importować własną warstwę i wszystko, co wymieniono przed nią. Warstwy sąsiednie, zagnieżdżona tablica w layers albo a | b w rolach szablonu, też nie mogą importować jedna drugiej; komunikat mówi wtedy from sibling layer.

Liczy się każdy import w pliku: na najwyższym poziomie, wewnątrz funkcji lub klasy i za if TYPE_CHECKING: (ADR-009). Importy względne są rozwiązywane względem pakietu pliku, a from shop import infrastructure jest rozwiązywane do shop.infrastructure. Importami dynamicznymi (importlib.import_module("shop.infrastructure.db")) zajmuje się INW011.

Dlaczego to źle

Import na zewnątrz wiąże warstwę wewnętrzną ze szczegółami, od których ma być niezależna: domeny nie da się już testować, czytać ani użyć ponownie bez kodu bazy danych. Agenci robią taką edycję często, bo zaimportowanie klasy, która już wykonuje zadanie, to najkrótsza droga do działającego kodu. Code review wyłapuje to późno, o ile w ogóle. Pierwsze, co agent próbuje zrobić ze zgłoszonym importem, to go ukryć, w funkcji albo za TYPE_CHECKING, więc reguła sprawdza też te miejsca, a poprawka mówi o tym wprost.

Przykład

Warstwy tego przykładu, których używają też pozostałe strony reguł:

pyproject.toml
[tool.inwards]
layers = [
  { name = "domain",         modules = ["shop.domain"] },
  { name = "application",    modules = ["shop.application"] },
  { name = "infrastructure", modules = ["shop.infrastructure"] },
]

Domena sięga do infrastruktury po klasę, która rozmawia z bazą danych:

shop/domain/order.py
from shop.infrastructure.db import SqlOrders


def total(order_id: int) -> int:
    return SqlOrders().total(order_id)
inwards check
shop/domain/order.py:1:36: INW001 Layer "domain" imports "shop.infrastructure.db.SqlOrders" from outer layer "infrastructure". Allowed direction: domain <- application <- infrastructure.
  fix: Depend on an abstraction owned by "domain" instead of "shop.infrastructure.db.SqlOrders".
    1. Delete `from shop.infrastructure.db import SqlOrders`. Do not move the import into a function or behind TYPE_CHECKING; Inwards checks those too.
    2. Declare a typing.Protocol in `shop.domain` (for example `shop.domain.ports`) that describes only what this module needs from `SqlOrders`.
    3. Type this module against that Protocol and receive the implementation through a constructor or function parameter.
    4. Make the class in "infrastructure" satisfy the Protocol, and wire it in the outermost layer (the composition root).
  docs: https://sircypkowskyy.github.io/inwards/rules/INW001/

Po poprawce domena ma własny port, a infrastruktura go implementuje.

shop/domain/ports.py
from typing import Protocol


class Orders(Protocol):
    def total(self, order_id: int) -> int: ...
shop/domain/order.py
from shop.domain.ports import Orders


def total(orders: Orders, order_id: int) -> int:
    return orders.total(order_id)
shop/infrastructure/db.py
class SqlOrders:
    def total(self, order_id: int) -> int:
        return 0
inwards check

SqlOrders spełnia Orders strukturalnie, więc nie musi importować domeny. Korzeń kompozycji (composition root) w najbardziej zewnętrznej warstwie przekazuje SqlOrders() do total.

Jak naprawić

  1. Usuń import. Przeniesienie go do funkcji, za TYPE_CHECKING albo do importlib tylko przenosi diagnostykę.
  2. Zadeklaruj w warstwie wewnętrznej typing.Protocol, który opisuje tylko to, czego moduł potrzebuje.
  3. Otypuj moduł względem tego Protocolu i przyjmuj implementację jako parametr konstruktora albo funkcji.
  4. Niech klasa z warstwy zewnętrznej spełnia Protocol, a połącz je w najbardziej zewnętrznej warstwie.

Gdy konfiguracja ma warstwę o nazwie ports (rola ports szablonu, np. slice.ports nad shop.*.ports) leżącą wewnątrz warstwy importującej, krok 2 wskazuje moduł tej roli w bieżącym slice, na przykład shop.orders.ports. inwards context nazywa tę rolę tak samo.

Jeśli moduł naprawdę należy do warstwy wewnętrznej, przenieś go tam. Jeśli źle ustawione są same warstwy, decyduje użytkownik: poprawka nigdy nie każe agentowi edytować [tool.inwards], a config guard go powstrzymuje.

Bezpieczeństwo poprawki

Inwards nie ma automatycznej poprawki dla tej reguły (autofix: false). Powyższe kroki zmieniają projekt kodu, więc musi je wykonać człowiek albo agent, a wynik wymaga przeglądu.

Konfiguracja

Poziom: błąd. [tool.inwards.rules] może wyłączyć regułę (ignore = ["INW001"]) albo zmienić ją w ostrzeżenie (severity = { INW001 = "warning" }); zobacz Konfiguracja reguł. Żeby zaakceptować jeden import, wycisz go w jego linii:

from shop.infrastructure.legacy import LegacyClient  # inwards: ignore[INW001] reason="old billing adapter, removed in #210"

Przy imporcie w nawiasach komentarz trafia do linii importowanej nazwy. Hooki Claude Code nie honorują wyciszenia, które agent dodał w trakcie sesji, chyba że ustawiono agent-suppressions = "allow" (ADR-028).

W hookach Claude Code naruszenie blokuje edycję, która je dodaje, i Stop gate. Naruszenie, które plik miał już na starcie sesji, trafia do agenta jako kontekst, więc stary kod nie blokuje niezwiązanej edycji, z dwoma wyjątkami. Plik, który na starcie sesji był niezacommitowany, nieśledzony albo poza gitem i zbyt duży, żeby hooki zachowały jego kopię (ponad 512 KiB albo ponad limit 4 MiB takich plików), nie ma treści startowej, którą Inwards mógłby potwierdzić, więc każde naruszenie w nim blokuje, gdy tylko agent go zedytuje. Przy stop-gate = "project" Stop gate sprawdza cały projekt i blokuje każde naruszenie, którego nie akceptuje baseline, stare czy nowe. Szczegóły opisuje sekcja o Stop gate.

Znane ograniczenia

  • Przynależność do warstwy wynika tylko z prefiksu modułu. Globy takie jak shop.*.domain dla pionowych wycinków przyjdą z konfiguracją w wersji 2 (#51).
  • Nie ma wyjątku dla odwołań do warstwy zewnętrznej używanych tylko w typach (ADR-009).
  • Importy wewnątrz własnego kodu, który nie należy do żadnej warstwy, nie są sprawdzane; ten kod zgłasza INW006.
  • W niejawnym pakiecie przestrzeni nazw (bez __init__.py) importy względne są rozwiązywane tak, jakby katalog był zwykłym pakietem.
  • INW001 rozstrzyga warstwy po nazwie modułu, więc import przez dowiązanie symboliczne w warstwie, które wskazuje do innej warstwy, przechodzi tę regułę. Takie dowiązanie, a także dowiązanie wychodzące poza katalog root, zgłasza INW006 w miejscu samego dowiązania (ADR-013).

Źródła