Dokumentacja konfiguracji¶
Inwards czyta tabelę [tool.inwards] z najbliższego pliku pyproject.toml, idąc w górę od katalogu roboczego, albo z pliku wskazanego przez --config. W katalogu głównym workspace'u uv czyta zamiast tego własną tabelę każdego członka (Monorepo i workspace'y uv). Ta strona wymienia każdy klucz: jego typ, wartość domyślną i przykład. Rozdział 1 pokazuje całą konfigurację; ADR-005 wyjaśnia, dlaczego mieszka ona w pyproject.toml.
Pełna konfiguracja potrzebuje tylko layers:
[tool.inwards]
root = "src"
layers = [
{ name = "domain", modules = ["shop.domain"] },
{ name = "application", modules = ["shop.application"] },
{ name = "infrastructure", modules = ["shop.infrastructure"] },
]
Nieznany klucz, zły typ albo zła wartość to błąd konfiguracji: inwards check kończy się kodem 2 i podaje klucz, na przykład tool.inwards.contexts[0].depends-on[1]. Tabela jest chroniona jak kod: strażnik konfiguracji w Claude Code odrzuca edycję agenta w każdym jej kluczu (rozdział 4).
Podpowiedzi i sprawdzanie w edytorze¶
JSON Schema dla [tool.inwards] jest opublikowany pod adresem https://sircypkowskyy.github.io/inwards/schema/tool-inwards.json. Ten adres śledzi gałąź develop, tak jak ta dokumentacja. Żeby przypiąć schemat do wydania, którego używasz, weź plik inwards-tool-schema.json dołączony do każdego wydania na GitHubie.
Edytory, które biorą schemat pyproject.toml z SchemaStore, będą podpowiadać i sprawdzać [tool.inwards] bez żadnej konfiguracji, gdy SchemaStore przyjmie schemat Inwards (SchemaStore#6427, #192). Do tego czasu ustaw go ręcznie.
Edytory korzystające z Taplo (Even Better TOML w VS Code i inne) przypisują schematy do całych plików: Taplo pomija keys reguły, gdy wybiera schemat, więc sam schemat tabeli byłby sprawdzany względem całego pyproject.toml. Zamiast niego użyj zbudowanego z niego schematu całego pliku, https://sircypkowskyy.github.io/inwards/schema/pyproject.json, który sprawdza [tool.inwards] i zostawia wszystkie inne tabele w spokoju. Plik .taplo.toml obok pyproject.toml:
[[rule]]
include = ["**/pyproject.toml"]
[rule.schema]
path = "https://sircypkowskyy.github.io/inwards/schema/pyproject.json"
To zastępuje schemat, który Taplo wziąłby dla tego pliku z SchemaStore, więc inne tabele nie są sprawdzane. Gdy SchemaStore będzie miał schemat Inwards, usuń tę regułę i zostaw cały plik schematowi SchemaStore. Wydania dołączają go jako inwards-pyproject-schema.json.
Schemat sprawdza strukturę: klucze, ich typy, dozwolone wartości, kody reguł i kształt nazw oraz wzorców. Inwards sprawdza więcej, gdy wczytuje konfigurację:
- relacje między wpisami: unikalne nazwy warstw i kontekstów, prefiks w dwóch warstwach albo dwóch kontekstach, wpisy
publicnależące do kontekstu, istniejące nazwy wdepends-on, istniejące nazwy wtemplate, rola wymieniona dwa razy; - dokładne nazwy modułów i segmenty selektorów, tam gdzie schemat dopuszcza nieco luźniejszą postać, oraz zakresy w globach zapisane od końca, takie jak
[z-a]; required-versionwzględem uruchomionego programu.
Konfiguracja, którą schemat przyjmuje, może więc nadal być błędna, a komunikat błędu podaje klucz.
Klucze¶
root¶
Typ: tekst. Domyślnie: ".".
Katalog, względem pyproject.toml, od którego liczone są nazwy modułów. Przy root = "src" plik src/shop/domain/order.py to moduł shop.domain.order.
Jedna konfiguracja ma jeden root. W workspace'ie uv, którego członkowie mają własne src/ (src/packages/core/src/core), daj każdemu członkowi własne [tool.inwards]: inwards check w katalogu głównym workspace'u sprawdzi wtedy każdego członka jego własną konfiguracją (Monorepo i workspace'y uv). Pojedyncza konfiguracja w katalogu głównym workspace'u nazwałaby ten pakiet packages.core.src.core, a import core w innych członkach przeszedłby jako import biblioteki zewnętrznej, więc inwards check ostrzega o każdym członku zindeksowanym w ten sposób (INW006).
layers¶
Typ: tablica tabel, co najmniej jedna. Wymagane.
Warstwy, od najbardziej wewnętrznej. Moduł może importować własną warstwę i każdą wymienioną przed nią; import warstwy wymienionej po niej to INW001. Wpis może też być zagnieżdżoną tablicą warstw sąsiednich. Każda warstwa ma:
name: niepusty tekst, unikalny wśród warstw.modules: prefiksy modułów i selektory. Prefiksshop.domainobejmujeshop.domaini wszystko pod nim, ale nieshop.domainx. Gdy do modułu pasuje kilka prefiksów, wygrywa najdłuższy, więc zagnieżdżony pakiet może należeć do innej warstwy niż jego rodzic. Ten sam wpis w dwóch warstwach to błąd konfiguracji. Pusta lista jest dozwolona.allow-libraries,deny-libraries,extend-deny-libraries: które biblioteki warstwa może importować (INW005). Szczegóły są w przewodniku o bibliotekach.template: nazwa szablonu, którego role stają się warstwami wewnątrz modułów tego wpisu. Sam wpis nie jest wtedy warstwą.
Selektory¶
Wpis z * to selektor. Pionowe wycinki powtarzają te same warstwy w każdym wycinku; shop.*.domain mówi raz to, czego shop.orders.domain, shop.billing.domain i każdy kolejny wycinek potrzebowałyby jako osobne prefiksy:
[tool.inwards]
layers = [
{ name = "domain", modules = ["shop.*.domain"] },
{ name = "application", modules = ["shop.*.application"] },
{ name = "infrastructure", modules = ["shop.*.infra", "shop.api"] },
]
- Gramatyka.
*pasuje do dokładnie jednego segmentu, a**do jednego lub więcej, jak w selektorach kształtu. Każdy inny segment to nazwa pakietu. Selektor obejmuje każdy moduł równy jednemu z jego dopasowań i wszystko pod nim:shop.*.domainobejmujeshop.orders.domainishop.orders.domain.order, ale nieshop.ordersanishop.a.b.domain. - Zaczyna się od nazwy pakietu:
shop.*.domain, nie*.domain. Ten pakiet najwyższego poziomuinwards check, Stop gate i serwer języka przechodzą w całości, pomijając tylko ukryte wpisy, więc katalognode_modulesalbo środowiska wirtualnego w jego wnętrzu nie ukryje kodu warstwy. Projekt z kilkoma pakietami najwyższego poziomu wymienia jeden selektor na pakiet. - Błędy konfiguracji: selektor bez nazwy pakietu (
*,**,*.domain), częściowa gwiazdka (shop.dom*),?, nawiasy kwadratowe, puste segmenty i separatory ścieżek. Wpis bez*to dosłowny prefiks, sprawdzany tak łagodnie jak dotąd.
Gdy do modułu pasuje kilka wpisów, wygrywa najbardziej szczegółowy, w tej kolejności:
- najgłębszy ostatni dosłowny segment dopasowania, więc
shop.orders.domainwygrywa zshop.**, który pasuje głębiej, ale jego ostatni dosłowny segment toshop; - potem głębsze dopasowanie;
- potem więcej dosłownych segmentów;
- potem warstwa wymieniona wcześniej.
Dla samych dosłownych prefiksów to po prostu najdłuższy prefiks, jak zawsze. Tabela prawdy, z warstwami a i b w tej kolejności:
a |
b |
Moduł | Właściciel | Dopasowany prefiks |
|---|---|---|---|---|
shop.domain |
shop.domainx |
brak | ||
shop.*.domain |
shop.orders.domain.order |
a |
shop.orders.domain |
|
shop.*.domain |
shop.a.b.domain |
brak: * to jeden segment |
||
shop.*.domain |
shop.orders |
brak: dopasowanie częściowe | ||
shop.* |
shop |
brak: * wymaga segmentu |
||
shop.** |
shop.orders.infra.db |
a |
shop.orders.infra.db |
|
shop.**.domain |
shop.a.b.domain.order |
a |
shop.a.b.domain |
|
shop.**.domain |
shop.domain |
brak: ** to co najmniej jeden |
||
shop.orders.domain |
shop.** |
shop.orders.domain.order |
a: głębszy ostatni dosłowny segment |
shop.orders.domain |
shop.orders.domain |
shop.** |
shop.orders.api |
b |
shop.orders.api |
shop.orders.* |
shop.*.domain |
shop.orders.domain.order |
b: głębszy ostatni dosłowny segment |
shop.orders.domain |
shop.orders.* |
shop.orders.*.* |
shop.orders.x.y |
b: głębsze dopasowanie |
shop.orders.x.y |
shop.*.domain |
shop.orders.domain |
shop.orders.domain.order |
b: więcej dosłownych segmentów |
shop.orders.domain |
shop.**.domain |
shop.*.domain |
shop.orders.domain |
a: wymieniona wcześniej |
shop.orders.domain |
shop |
shop.domain |
shop.domain.order |
b: najdłuższy prefiks |
shop.domain |
Co zmieniają selektory:
- Kroki naprawy nazywają wycinek. INW001, INW005 i INW011 każą agentowi zadeklarować port w dopasowanym prefiksie importującego modułu,
shop.billing.domaindla pliku z billing, i podpowiadająshop.billing.domain.portstylko wtedy, gdy ten prefiks jest pakietem. Warstwa z kilkoma dosłownymi prefiksami jest traktowana tak samo, więc jej naprawa nazywa teraz prefiks, w którym leży plik, a nie pierwszy prefiks warstwy. - INW006 potrzebuje dowodu. Pakiet zawiera warstwę selektora tylko wtedy, gdy projekt ma pod nim moduł, do którego selektor pasuje:
shop.*.domainnie włączashop/util.pydo warstwy, ashop.**.domainnie obejmujeshop.orders.core.order. Selektor, do którego nie pasuje żaden moduł, jest martwy niezależnie od pierwszeństwa. - Sesje sprawdzają każdy wycinek. Wycinek to prefiks dopasowanego modułu aż do ostatniego dosłownego segmentu selektora:
shop.orders.domaindlashop.*.domain,shopdlashop.**. Wycinek, który na starcie sesji miał moduły, a teraz nie ma żadnego, zatrzymuje Stop gate, tak jak dosłowny prefiks. Dlatego zmiana nazwy albo usunięcie wycinka wymaga użytkownika:git mv shop/orders shop/sales, przeniesienie wycinka głębiej podshop.**.domainalbo usunięcie wycinka, którego jedynym modułem jest__init__.py. Usuwanie albo zmiana nazwy modułów wewnątrz wycinka, który zachowuje inne, nie wymaga.
ADR-034 opisuje projekt.
Warstwy sąsiednie¶
Zagnieżdżona tablica dwóch lub więcej warstw zawiera niezależne warstwy sąsiednie, odpowiednik a | b z import-linter. Zajmują jedno miejsce w kolejności: każda może importować warstwy sprzed grupy, warstwy po grupie mogą importować każdą z nich, a żadna nie może importować drugiej. Import z jednej warstwy sąsiedniej do drugiej to INW001, a komunikat mówi wtedy from sibling layer:
[tool.inwards]
layers = [
{ name = "constants", modules = ["app.constants"] },
[
{ name = "models", modules = ["app.models"] },
{ name = "schemas", modules = ["app.schemas"] },
],
{ name = "service", modules = ["app.service"] },
]
Tutaj app.schemas może importować app.constants, ale nie app.models, a app.service może importować oba. Dozwolony kierunek w komunikatach brzmi constants <- models | schemas <- service. Gdy najbardziej wewnętrzne miejsce zajmują warstwy sąsiednie, każda z nich dostaje domyślną listę zakazów INW005. Warstwa sąsiednia nie może wskazywać szablonu, a grupa z jedną warstwą to błąd konfiguracji. inwards init --style hexagonal zapisuje swoje adaptery inbound i outbound jako jedną grupę warstw sąsiednich (Instalacja).
required-version¶
Typ: tekst, "MAJOR.MINOR.PATCH". Domyślnie: brak.
Najstarsza wersja Inwards, która może sprawdzać projekt. Starszy program kończy się błędem konfiguracji, zamiast sprawdzać regułami, których może nie znać. Ustawia go inwards init.
ignore¶
Typ: lista nazw modułów. Domyślnie: brak.
Moduły pominięte w ostrzeżeniu INW006 o kodzie poza wszystkimi warstwami, na przykład tests albo migrations. Wpis dopasowuje całe segmenty nazwy w dowolnym miejscu nazwy modułu: migrations obejmuje shop.orders.migrations.0001_initial, a tests obejmuje shop.tests tak samo jak tests najwyższego poziomu. Wpis zaczynający się od / pasuje tylko na początku nazwy: /tests obejmuje tests.test_order, ale nie shop.tests.test_order, które nadal dostaje ostrzeżenie. inwards init zapisuje domyślne wpisy bez /. Importy z warstwy do pominiętych modułów nadal są sprawdzane, podobnie jak ich treść: reguły treści opt-in nie czytają ignore. Zamiast tego pomijają moduły testowe po nazwie (zob. check-tests w rules).
generated¶
Typ: lista wzorców modułów. Domyślnie: ["*_pb2", "*_pb2_grpc", "_version"].
Moduły zapisywane przez krok budowania, które INW010 traktuje jako istniejące, gdy nie ma ich na dysku. Segmenty mogą używać * i ?; wzorzec dopasowuje całe segmenty w dowolnym miejscu nazwy modułu. Lista, nawet pusta, zastępuje wartość domyślną. Szczegóły są w ADR-029.
namespace-packages¶
Typ: lista nazw pakietów. Domyślnie: brak.
Niejawne pakiety przestrzeni nazw (bez __init__.py), do których dokładają moduły zainstalowane dystrybucje. Przy namespace-packages = ["acme.platform"] INW010 nie zgłasza import acme.platform.auth.tokens, gdy projekt ma tylko acme/platform/billing/: nazwa leżąca bezpośrednio w acme.platform, której nie ma w projekcie, pochodzi z dystrybucji. Zmyślony acme.platform.billing.pricing jest nadal zgłaszany, bo billing jest w projekcie. Takie moduły Inwards sam znajduje w .venv projektu; klucz jest dla uruchomień bez niego, na przykład w CI przed uv sync (Pakiety przestrzeni nazw współdzielone z zainstalowanymi dystrybucjami).
escalate-after¶
Typ: liczba całkowita, co najmniej 1. Domyślnie: 3.
Ile prób naprawienia tego samego naruszenia, zanim hooki przestaną blokować i każą agentowi zapytać użytkownika.
run-log¶
Typ: prawda albo fałsz. Domyślnie: false.
Zapisuj opcjonalny dziennik uruchomień .inwards/runs.jsonl, opisany w rozdziale 8.
stop-gate¶
Typ: "changed" albo "project". Domyślnie: "changed".
Co sprawdza bramka Stop w Claude Code: pliki zmienione w sesji albo cały projekt względem jego baseline'u, tak że naruszenie w pliku, którego sesja nie dotknęła, też blokuje.
agent-suppressions¶
Typ: "deny" albo "allow". Domyślnie: "deny".
Czy hooki Claude Code uwzględniają komentarz wyciszający, który agent dodał w trakcie sesji. inwards check i serwer języka zawsze uwzględniają wyciszenia. Szczegóły są w ADR-028.
rules¶
Typ: tabela. Domyślnie: każda reguła zgłasza z własnym poziomem.
Które reguły zgłaszają i jak głośno:
select: zgłaszają tylko te reguły, także reguły opt-in. Musi zawierać co najmniej jeden kod.extend-select: te reguły też zgłaszają, obokselectalbo reguł domyślnie włączonych. Włącza reguły opt-in.ignore: te reguły nie zgłaszają. Ma pierwszeństwo przedselectiextend-select.severity: tabela kodu reguły na"error"albo"warning". Wszystkie cztery klucze przyjmują kody reguł, takie jak"INW001". Nazwa reguły w ich miejscu, na przykład"layer-dependency", to błąd konfiguracji, który podaje kod do wpisania.<rule-name>: tabela opcji tej reguły, na przykład[tool.inwards.rules.pure-domain]. Każda reguła przyjmujemodules, listę prefiksów modułów albo selektorów zapisanych jak wmoduleswarstwy, która ogranicza regułę do pasujących modułów. Niektóre reguły mają też własne opcje, opisane na stronie reguły:thin-endpoint,async-blocking,ports-abstract,construct-only-in,orm-naming,adapter-conformance,endpoint-metadata,undocumented-error-responseirouter-wiring. Nieznany klucz albo klucz innej reguły to błąd konfiguracji, który go podaje. Tabela nie włącza reguły, a tabela dla reguły wyłączonej dostaje ostrzeżenie. Szablon może też włączyć reguły opt-in dla jednej ze swoich ról, co dodaje się do tych tabel.check-tests, w tabeli opcji reguły treści opt-in (INW012 do INW016, INW020, FAPI001 do FAPI009) albo INW021:truesprawia, że reguła zgłasza też w modułach testowych. Domyślnie te reguły pomijają moduł, którego nazwa mówi, że to test: ostatni segmenttest_*albo*_test, albo segmenttests,testlubconftest(#362). Wyciszenie w takim module nie jest zgłaszane jako nieużywane. Pozostałe reguły sprawdzają moduły testowe jak każde inne, acheck-testsw ich tabelach to błąd konfiguracji.[tool.inwards.rules.pure-domain]przyjmuje teżdeny, listę tabel zmodules(prefiksy albo selektory, jak wyżej) ilibraries(nazwy importu, jak wdeny-librarieswarstwy). Moduły, do których pasuje wpis, nie mogą importować jego bibliotek, niezależnie od tego, czy należą do warstwy, aallow-librarieswarstwy tego nie znosi (INW005, przewodnik po bibliotekach). Służy do pakietu, który nie jest całą warstwą, na przykład do kontraktu import-lintera „mypackage.onenie może importowaćdjango”.
INW000 nie da się wyłączyć, zmienić jego poziomu ani nadać mu opcji. Szczegóły są w ADR-027.
inwards rules pokazuje, co z tej tabeli wynika: każdą regułę z kodem, nazwą, informacją, czy jest włączona, poziomem i kluczem, który o każdym z nich zdecydował, zapisanym jako klucz pod [tool.inwards] (rules.extend-select, rules.severity, templates.domain.rules.router), albo default. Reguła ograniczona przez modules mówi gdzie (only in src.*.router). Czyta najbliższą konfigurację albo --config FILE, a bez konfiguracji wypisuje ustawienia domyślne. --json wypisuje to samo jako inwards/rules@1: każda reguła ma source i severitySource, każde z polem kind (fixed, default, opt-in, select, extend-select, template, ignore, not-selected albo severity) i swoimi keys. inwards/rules@1 jest kontraktem, tak jak inwards/diagnostics@1: w ramach @1 pola są tylko dodawane, a usunięcie albo zmiana nazwy pola podnosi wersję (ADR-007). inwards rule INW013 wypisuje stronę jednej reguły (poradnik MCP).
[tool.inwards.rules]
ignore = ["INW007", "INW008"]
severity = { INW005 = "warning" }
[tool.inwards.rules.pure-domain]
modules = ["shop"]
deny = [{ modules = ["shop.billing"], libraries = ["django"] }]
shape i names¶
Typ: tablice tabel. Domyślnie: brak.
Jakie elementy pakiet może, musi i nie może zawierać (INW007, INW008) oraz gdzie może się pojawić nazwa elementu. Obie tabele oraz składnię ich selektorów i wzorców opisuje przewodnik o kształcie pakietu. Wpis kształtu może też ustawić hints, zdania dodawane do kroków naprawy jego diagnostyk INW007, oraz template, szablon, który dostarcza allow, require, forbid, extra i hints; klucz ustawiony przez sam wpis wygrywa.
contexts¶
Typ: tablica tabel. Domyślnie: brak.
Konteksty ograniczone albo wycinki: co każdy z nich posiada, które z jego modułów mogą importować inne konteksty i od których kontekstów może zależeć. INW002 ogranicza każdy kontekst do kontekstów wymienionych w jego depends-on; INW003 pilnuje, żeby kod spoza kontekstu korzystał tylko z jego modułów public.
[[tool.inwards.contexts]]
name = "orders"
modules = ["shop.orders"]
public = ["shop.orders.api"]
depends-on = ["billing"]
[[tool.inwards.contexts]]
name = "billing"
modules = ["shop.billing"]
public = ["shop.billing.api"]
Każdy kontekst ma:
name: niepusty tekst, z rozróżnianiem wielkości liter, unikalny wśród kontekstów.modules: prefiksy modułów, które kontekst posiada, razem ze wszystkim pod nimi. To dosłowne nazwy z kropkami; gwiazdki są błędem konfiguracji. Gdy do modułu pasują prefiksy kilku kontekstów, wygrywa najdłuższy, więc kolejność tabel nigdy nie ma znaczenia. Ten sam prefiks w dwóch kontekstach to błąd konfiguracji.public(domyślnie[]): prefiksy własnych modułów kontekstu, które mogą importować konteksty od niego zależne. To pełne nazwy modułów, a nie nazwy względne wobec kontekstu:apioznacza moduł najwyższego poziomuapi. Każdy musi należeć do tego kontekstu; prefiks, który dokładniej posiada inny kontekst, to błąd konfiguracji. Moduł jest publiczny, gdy leży na publicznym prefiksie albo pod nim i należy do tego kontekstu. Wpis z wiodącym=jest dokładny:public = ["=shop.billing", "shop.billing.models"]otwiera fasadę pakietushop.billing(jego__init__) orazshop.billing.modelsze wszystkim pod nim, ale nieshop.billing._invoicesani żadnego innego podmodułu. Moduł po=to zwykła nazwa z kropkami i obowiązuje go ta sama zasada przynależności.depends-on(domyślnie[]): konteksty, z których ten może importować. Zależność jest bezpośrednia: nie przechodzi dalej i nie działa w drugą stronę. Nazwa może wskazywać kontekst zadeklarowany niżej. Własna nazwa kontekstu, nieznana nazwa i powtórzona nazwa to błędy konfiguracji.template: szablon, którego nazwypublic, pod każdym zmoduleskontekstu, dołączają do jego listypublic.
Konteksty i warstwy się sumują: zadeklarowana zależność ani moduł publiczny nigdy nie pozwalają na import, którego zabrania kolejność warstw, a przynależność do kontekstu nic nie mówi o warstwie ani odwrotnie. Kontekst korzysta tylko ze swoich własnych depends-on i public, także wtedy, gdy jego prefiksy leżą wewnątrz innego kontekstu. Uzasadnienie jest w ADR-030.
templates¶
Typ: tabela tabel, [tool.inwards.templates.<nazwa>]. Domyślnie: brak.
Szablon raz opisuje, jak wygląda pewien rodzaj pakietu, gdy dzieli go wiele pakietów: każda domena aplikacji FastAPI, każdy wycinek modularnego monolitu. Wpisy warstw, kształtów i kontekstów korzystają z niego przez template = "<nazwa>". Układ fastapi-best-practices z domenami src/orders/ i src/users/ jako jeden szablon:
[tool.inwards]
layers = [
{ name = "core", modules = ["src.config", "src.database", "src.exceptions", "src.models"], deny-libraries = ["fastapi"] },
{ name = "domain", modules = ["src.*"], template = "fastapi-domain" },
{ name = "app", modules = ["src.main"] },
]
# One template says what every domain package looks like, how its modules
# import each other (innermost first; "a | b" are siblings that may not import
# each other), and which modules other domains may import.
[tool.inwards.templates.fastapi-domain]
roles = [
"constants | config",
"exceptions | utils",
"models | schemas",
"service",
"dependencies",
"router",
]
public = ["router", "service", "dependencies", "schemas", "constants", "exceptions"]
allow = []
require = ["__init__", "router", "service"]
hints = ["Other domains may import this one's router, service, dependencies, schemas, constants and exceptions, never its models, config or utils."]
[[tool.inwards.shape]]
packages = ["src.*"]
template = "fastapi-domain"
[[tool.inwards.contexts]]
name = "orders"
modules = ["src.orders"]
depends-on = ["users"]
template = "fastapi-domain"
[[tool.inwards.contexts]]
name = "users"
modules = ["src.users"]
template = "fastapi-domain"
Szablon ma te klucze, wszystkie opcjonalne:
roles: nazwy modułów względne wobec modułów wpisu warstwy, od najbardziej wewnętrznej."models | schemas"umieszcza niezależne warstwy sąsiednie w jednym miejscu. Każda rola występuje raz.public: nazwy modułów względne wobec modułów kontekstu, które mogą importować inne konteksty (INW003).allow,require,forbid,extra: jak we wpisie kształtu. Gdyallowjest ustawione, dochodzi do niego pierwszy segment każdej roli, więcallow = []oznacza tylko role,requirei__init__.hints: zdania dodawane do kroków naprawy diagnostyk INW007 w pakietach, którym szablon nadaje kształt, na przykład gdzie trafia wspólny kod.rules: reguły opt-in, które każda rola włącza w swoich modułach, na przykładrouter = { async-blocking = true }(Reguły dla roli).
Tam, gdzie ustawiono template = "<nazwa>", oznacza to:
| Gdzie | Rozwija się w |
|---|---|
wpis layers |
jedną warstwę na rolę, o nazwie <wpis>.<rola>, obejmującą <moduł>.<rola> dla każdego z modułów wpisu, z listami bibliotek wpisu; warstwy sąsiednie stają się zagnieżdżoną tablicą. Sam wpis nie jest warstwą. |
wpis shape |
allow, require, forbid, extra i hints szablonu; klucz ustawiony przez wpis wygrywa |
wpis contexts |
<moduł>.<nazwa> dla każdego z modułów kontekstu i każdej nazwy z public, dodane do własnego public kontekstu |
W przykładzie wpis domain staje się dziewięcioma warstwami, od domain.constants do domain.router, więc import src.orders.router w src.orders.service to INW001, tak samo jak import src.orders.models, warstwy sąsiedniej, w src.orders.schemas. src.* obejmuje każdą domenę, także następną. src.orders.router może importować src.users.service: orders zależy od users, a service jest publiczny. Każdy import src.users.models z orders to INW003. Routery są publiczne, bo montuje je src.main, który nie należy do żadnego kontekstu.
Szablony są rozwijane przy wczytywaniu konfiguracji, zanim cokolwiek innego zostanie sprawdzone, więc reguły, baseline, opis architektury i edytor widzą tylko wynik, który konfiguracja mogłaby też wypisać ręcznie. Fixture testowy zawiera tę konfigurację i jej ręczny odpowiednik, a testy sprawdzają, że dają tę samą konfigurację i te same diagnostyki. W praktyce:
- Komunikaty nazywają warstwy ról, na przykład
Layer "domain.service" imports "src.orders.router" from outer layer "domain.router". - Rola, której nie ma żaden pakiet, to pusta warstwa. Warstwa roli, do której nie pasuje żaden moduł, dostaje błąd INW006 o pustej warstwie jak każda inna warstwa, więc wymieniaj tylko role, które może mieć każdy pakiet danego rodzaju; opcjonalne mogą trafić do
allow. Błąd wskazuje wpis, który niesie szablon, i nazywa rolę. - W modułach wpisu z szablonem używaj
*, nie**.src.*dajesrc.*.models, które pasuje tylko domodelssamej domeny, więcsrc/orders/service/models.pyzostaje w roliservice. Przysrc.**wzorzecsrc.**.modelspasuje też do tego pliku, który przechodzi wtedy do rolimodels, bo wygrywa najgłębszy ostatni dosłowny segment (Selektory). - Błędy konfiguracji nazywają wpis albo klucz szablonu:
tool.inwards.layers[1].templatedla nieznanego szablonu albo takiego bez ról,tool.inwards.templates.fastapi-domain.roles[2]dla błędnej roli. Problem, który widać dopiero po rozwinięciu, na przykład zajęta już nazwa warstwy roli, nazywa rozwiniętą warstwę.
Reguły dla roli¶
Tabela rules szablonu włącza reguły opt-in w modułach jednej roli, więc „każdy router dostaje INW013” mówi się raz, obok ról:
[tool.inwards]
layers = [
{ name = "core", modules = ["src.database"] },
{ name = "domain", modules = ["src.*"], template = "domain" },
]
[tool.inwards.templates.domain]
roles = ["models", "service", "router"]
[tool.inwards.templates.domain.rules]
router = { async-blocking = true, thin-endpoint = { max-statements = 8 } }
models = { orm-naming = "warning" }
Każdy klucz to rola wymieniona w szablonie (rolę z kropką trzeba wziąć w cudzysłów: "api.v1"), a każda wartość wskazuje reguły po ich nazwach w kebab-case. Wartość reguły to true, poziom ("error" albo "warning") albo tabela opcji reguły bez modules. Szablon rozwija się do [tool.inwards.rules], a wynik to to, co napisałbyś ręcznie:
[tool.inwards.rules]
extend-select = ["INW012", "INW013", "INW016"]
severity = { INW016 = "warning" }
[tool.inwards.rules.async-blocking]
modules = ["src.*.router"]
[tool.inwards.rules.thin-endpoint]
modules = ["src.*.router"]
max-statements = 8
[tool.inwards.rules.orm-naming]
modules = ["src.*.models"]
- Moduły roli to moduły jej warstwy:
<moduł>.<rola>dla każdego modułu każdego wpisulayers, który korzysta z szablonu. Dołączają domodulesreguły, więc reguła zgłasza tylko tam. Wyjątkiem jest INW015: u niej dołączają dorole, czyli chronionych modułów, a jejmoduleszostaje takie, jak ustawia tabela najwyższego poziomu. - Reguła zostaje włączona: jej kod dołącza do
extend-select, za kodami, które już tam są.ignorenadal wygrywa. - Tabela najwyższego poziomu dodaje się do tego. Wpisy
modulesz[tool.inwards.rules.<reguła>](alboroleprzy INW015) idą najpierw, potem wpisy ról. Każda inna opcja ustawiona tam oraz poziom w[tool.inwards.rules] severitywygrywają z wartością szablonu, tak jak własny klucz wpisu kształtu wygrywa z kluczem jego szablonu. Gdy szablon wskaże regułę, tabela najwyższego poziomu bezmodulesnie oznacza już całego projektu: zakresem reguły są role szablonu imodulesz tabeli najwyższego poziomu. - Jedna wartość na opcję. Dwie role albo dwa szablony, które dają tej samej regule różne wartości jednej opcji albo różne poziomy, to błąd konfiguracji podający oba miejsca; ustaw wtedy wartość raz w tabeli najwyższego poziomu.
- Błędy konfiguracji podają klucz, na przykład
tool.inwards.templates.domain.rules.router.async-blocking.modules: rola, której szablon nie wymienia, szablon bezroles, nieznana reguła albo kod reguły zamiast nazwy, reguła domyślnie włączona (i tak zgłasza wszędzie, więc żeby ją zawęzić, ustaw jejmodulesw tabeli najwyższego poziomu), wartość inna niżtrue, poziom albo tabela,modules(alboroleprzy INW015) w tabeli szablonu oraz opcja, której reguła nie przyjmuje. Błędem jest też szablon zrules, z którego nie korzysta żaden wpislayers, bo do jego ról nie pasuje żaden moduł.
Szablon, z którego nikt nie korzysta, jest dozwolony. Cztery presety zapisują szablon i konteksty za ciebie: inwards init --style vertical-slices, bounded-contexts, django i fastapi, ten ostatni jako szablon fastapi-best-practices z przykładu powyżej (Instalacja). Szablon slice presetu vertical-slices ma roles = ["models", "services", "commands | queries", "api"], więc komendy i zapytania wycinka są warstwami sąsiednimi, a INW001 zgłasza import między nimi (ADR-046). ADR-036 opisuje projekt.
cycles¶
Typ: lista wartości "modules" i "contexts". Domyślnie: ["contexts"].
Które cykle importów zgłasza INW004 przy sprawdzaniu całego projektu: między kontekstami, między modułami, jedne i drugie albo żadne ([]). Wartość domyślna ma znaczenie dopiero wtedy, gdy zadeklarowano contexts, więc aktualizacja nie psuje projektu, który żyje z cyklami modułów; dodaj "modules", żeby wyłapywać także je. Każdy preset inwards init --style zapisuje ["modules", "contexts"] (Instalacja).
diagrams¶
Typ: lista ścieżek. Domyślnie: brak.
Pliki Markdown, Mermaid i PlantUML, których diagramy architektury INW017 (nazwy), INW018 (strzałki) i INW019 (importy, których diagram nie rysuje) sprawdzają względem [tool.inwards] i kodu przy sprawdzaniu całego projektu. INW017 i INW018 działają też, gdy wymieniony plik jest edytowany, otwarty w edytorze albo podany (gdzie działają); INW019 tylko przy sprawdzaniu całego projektu. Każdy wpis to ścieżka względem pliku pyproject.toml; segment może używać *, ? i [seq], a ** oznacza dowolną liczbę katalogów. Wpis nie może być ścieżką bezwzględną ani wychodzić wyżej przez ... W wymienionym pliku czytany jest tylko flowchart albo graph w Mermaid, który zaczyna się od %% inwards: layers albo %% inwards: contexts, w bloku kodu mermaid w pliku Markdown albo w całym pliku .mmd lub .mermaid, oraz diagram PlantUML oznaczony ' inwards: layers albo ' inwards: contexts, w bloku kodu plantuml albo w pliku .puml, .plantuml, .pu lub .iuml (PlantUML). Diagram jest sprawdzany, a nigdy nie jest czytany jako konfiguracja. Te reguły są opt-in, więc włącz je przez extend-select; inwards import-diagram zapisuje pierwszą [tool.inwards] na podstawie diagramu, a inwards diagram rysuje diagram na podstawie konfiguracji. Szczegóły w ADR-045.
[tool.inwards]
diagrams = ["docs/architecture.md", "docs/**/*.mmd"]
rules = { extend-select = ["INW017", "INW018"] }
Monorepo i workspace'y uv¶
Inwards idzie za workspace'ami uv: lista członków mieszka w [tool.uv.workspace] w katalogu głównym workspace'u, a każdy członek ma własne [tool.inwards] we własnym pyproject.toml. Inwards nie ma do tego własnego klucza (ADR-035).
# packages/core/pyproject.toml
[project]
name = "acme-core"
[tool.inwards]
root = "src"
layers = [
{ name = "domain", modules = ["acme.core.domain"] },
{ name = "services", modules = ["acme.core.services"] },
]
Bez --config inwards check wybiera konfiguracje tak:
- W katalogu głównym workspace'u (albo w dowolnym katalogu, który zawiera członków) sprawdza każdego członka poniżej, który ma
[tool.inwards], jego własną konfiguracją i jego własnym baseline'em. Konfiguracja w samym katalogu głównym workspace'u sprawdza resztę drzewa z pominięciem katalogów tych członków, a członek zagnieżdżony w innym członku zostaje swojej własnej konfiguracji, więc żaden plik nie jest sprawdzany dwa razy. Członek bez[tool.inwards]trafia na listę niesprawdzonych, chyba że sprawdza go konfiguracja katalogu głównego. Konfiguracja powyżej katalogu głównego workspace'u należy do innego projektu i nie jest używana. - Wewnątrz członka używa najbliższej konfiguracji, tak jak poza workspace'em.
- Ze ścieżkami każda ścieżka trafia do najbliższej konfiguracji nad nią, a katalog zawierający członków z konfiguracją trafia też do każdej z ich konfiguracji.
inwards check packages/api/src packages/core/src/acme/core/domain/order.pysprawdza pierwszą ścieżkę konfiguracjąpackages/api, a drugą konfiguracjąpackages/core. Workspace jest szukany od każdej ścieżki, więc wynik nie zależy od tego, gdzie uruchomisz polecenie, a członkowie pod wskazanym katalogiem, których nie sprawdza żadna konfiguracja, trafiają na listę niesprawdzonych. Ścieżka bez konfiguracji nad nią jest zgłaszana jako niesprawdzona. --configsprawdza każdą ścieżkę tą jedną konfiguracją, jak dotąd.
Członkowie to members minus exclude, a * pasuje wewnątrz jednego segmentu ścieżki (packages/*, libs/acme_*); **, ? i klasy znaków nie pasują do niczego.
Wynikiem jest jeden raport: JSON i SARIF to jeden dokument, a ścieżki są względne wobec katalogu roboczego. Wyjście text i concise kończy się wierszem na każdą konfigurację. Kod wyjścia to najgorszy z kodów konfiguracji: 1, gdy którykolwiek członek ma błąd, 2, gdy konfiguracja członka jest niepoprawna (pozostali członkowie są nadal sprawdzani i raportowani), i 2, gdy żadna wskazana ścieżka nie dała pliku do sprawdzenia. Z --log każda konfiguracja dostaje własny wiersz we własnym logu uruchomień. Fixture używany w testach, sprawdzony z katalogu głównego:
warning: packages/tools has no [tool.inwards] table, so it was not checked.
All clear: 6 files, 0 violations (114.8 ms).
packages/api/pyproject.toml: 2 files, 0 violations, 0 warnings
packages/core/pyproject.toml: 4 files, 0 violations, 0 warnings
Członkowie często współdzielą niejawny pakiet przestrzeni nazw: packages/core/src/acme/core i packages/api/src/acme/api, bez acme/__init__.py. Python scala acme z obu członków, a Inwards nazywa moduły tak samo, więc import względny taki jak from ..core import model oznacza acme.core.model w każdym z nich. Gdy modułu pod takim pakietem brakuje w sprawdzanym członku, INW010 szuka go u pozostałych członków (w ich src/, a bez niego w katalogu członka) i w .venv workspace'u, zanim go zgłosi.
Hooki Claude Code nie potrzebują niczego więcej: każdy edytowany plik jest sprawdzany najbliższą konfiguracją zapisaną na starcie sesji, Stop gate sprawdza każdy zmieniony plik konfiguracją jego członka, a strażnik konfiguracji chroni [tool.inwards] i baseline każdego członka. inwards baseline nadal bierze jedną konfigurację na uruchomienie: inwards baseline --config packages/core/pyproject.toml.