Diagramy architektury¶
Inwards trzyma rysunek architektury w Mermaid albo PlantUML w zgodzie z [tool.inwards], w obie strony (ADR-045):
- Dokumentacja idzie za kodem. INW017 zgłasza nazwę w diagramie, której nie zna konfiguracja albo kod, INW018 strzałkę, która rysuje import zabroniony przez konfigurację, a INW019 import w kodzie, którego diagram nie rysuje.
- Dokumentacja steruje kodem.
inwards import-diagramzamienia diagram w pierwszą[tool.inwards], jeden raz. Od tej chwili reguły trzymają je w zgodzie. - Konfiguracja rysuje dokumentację.
inwards diagramwypisuje[tool.inwards]jako oznaczony diagram, który INW017 i INW018 akceptują, aimport-diagramodczytuje z powrotem (Narysuj konfigurację).
[tool.inwards] pozostaje jedynym źródłem prawdy: diagram nigdy nie jest czytany jako konfiguracja w czasie sprawdzania.
Oznacz diagram¶
Liczy się tylko flowchart albo graph w Mermaid, który zaczyna się od komentarza-znacznika, w pliku wymienionym w kluczu diagrams: blok kodu mermaid w Markdownie albo cały plik .mmd. Pozostałe bloki Mermaid to zwykła dokumentacja.
| W diagramie | %% inwards: layers |
%% inwards: contexts |
|---|---|---|
| Identyfikator węzła | nazwa warstwy | poza subgraphem: nazwa kontekstu |
| Subgraph | (grupa, bez znaczenia) | kontekst; jego identyfikator to nazwa kontekstu |
id["pkg.module"], etykieta w cudzysłowie |
prefiks modułu albo selektor warstwy | moduł kontekstu albo moduł wewnątrz niego |
Ciągła strzałka a --> b |
a może importować b, więc b jest wewnętrzna |
kontekst a zależy od kontekstu b |
:::public |
wpis public kontekstu; na samym kontekście cały kontekst |
|
:::external |
pomijany | pomijany |
Przerywana -.->, bez grotu ---, dwukierunkowa <--> |
bez znaczenia | bez znaczenia |
Miejsce warstwy to najdłuższa ścieżka ciągłych strzałek od niej do warstwy, która na nic nie wskazuje; warstwy na tym samym miejscu są sąsiednie i nie mogą importować siebie nawzajem. Cykl ciągłych strzałek nie ma kolejności i jest odrzucany.
Warstwę albo kontekst, którego nazwa nie może być identyfikatorem Mermaid (slices.domain z szablonu), nazywa węzeł, którego etykieta w cudzysłowie jest jednym z jej wpisów modules.
PlantUML¶
Diagram komponentów PlantUML liczy się tak samo, a znacznikiem jest komentarz ': ' inwards: layers albo ' inwards: contexts, wśród linii komentarzy tuż po @startuml. Inwards czyta każde @startuml ... @enduml w pliku .puml, .plantuml, .pu albo .iuml oraz każdy blok kodu plantuml albo puml w Markdownie, w którym @startuml można pominąć.
@startuml
' inwards: layers
component [shop.api] as api
component [shop.orders] as orders
component [shop.domain] as domain
database Postgres as db <<external>>
[api] --> [orders]
[orders] --> [domain]
[api] --> [db]
@enduml
| W diagramie | ' inwards: layers |
' inwards: contexts |
|---|---|---|
[name], component name |
nazwa warstwy | poza pakietem: nazwa kontekstu |
package name { } oraz grupy node, folder, frame, cloud, rectangle |
(grupa, bez znaczenia) | kontekst; jego nazwa to nazwa kontekstu |
[pkg.module] as name, "pkg.module" as name |
prefiks modułu albo selektor warstwy | moduł kontekstu albo moduł wewnątrz niego |
Ciągła strzałka a --> b, a -> b, b <-- a, a -up-> b, a -[#red]-> b |
a może importować b |
kontekst a zależy od kontekstu b |
<<public>> |
wpis public kontekstu; na samym kontekście cały kontekst |
|
<<external>> |
pomijany | pomijany |
Przerywana ..>, bez grotu --, dwukierunkowa <-->, -[dashed]->, --\|> |
bez znaczenia | bez znaczenia |
Przy aliasie as nazwą jest alias, a tekst w nawiasach albo w cudzysłowie to moduł, tak jak etykieta w cudzysłowie w Mermaid; bez aliasu nazwą jest tekst w nawiasach i nie ma modułu. Nazwa ze spacjami ([Order handling] as orders) to podpis. Kolory, skinparam, <style>, notatki, tytuły, left to right direction, interfejsy i linie !include są pomijane, tak samo jak linia, której czytnik nie umie śledzić.
Sprawdzaj diagram¶
Wymień pliki i włącz reguły:
[tool.inwards]
diagrams = ["README.md", "docs/**/*.md", "docs/**/*.mmd", "docs/**/*.puml"]
[tool.inwards.rules]
extend-select = ["INW017", "INW018", "INW019"]
Wszystkie trzy dają ostrzeżenia, więc rysunek, który się rozjechał, nie blokuje Stop gate agenta; severity = { INW017 = "error", INW018 = "error" } sprawia, że blokują. Działają wszędzie tam, gdzie sprawdzany jest wymieniony diagram:
- Sprawdzenie całego projektu:
inwards checkbez ścieżek albo Stop gate zstop-gate = "project". Tylko ono zgłasza wpisdiagrams, do którego nie pasuje żaden plik. - Plik podany z nazwy:
inwards check docs/architecture.md. Plik Markdown, Mermaid albo PlantUML, którego klucz nie wymienia, nie jest sprawdzany, a raport to mówi. - Hook Claude Code, po tym jak agent zmieni wymieniony diagram (PostToolUse). Ostrzeżenie wraca do agenta jako kontekst; błąd blokuje edycję, tak jak naruszenie w kodzie. Edycja każdego innego pliku, który nie jest Pythonem, kosztuje tylko zajrzenie do konfiguracji. Na syntetycznym projekcie benchmarku (2100 plików), na M1 Pro, hook uruchamiany jednorazowo zajął 37 ms p50 dla wymienionego diagramu (listuje moduły projektu dla etykiet INW017) i 16,5 ms dla innego pliku Markdown, wobec 16,1 ms wcześniej.
- Edytor:
inwards serversprawdza otwarty wymieniony diagram przy każdym naciśnięciu klawisza, z niezapisanego tekstu (Edytory). - MCP
check_files, przez ścieżkę albo jako tekst wcontents.
Domyślny zakres Stop gate (stop-gate = "changed") sprawdza pliki Pythona zmienione w sesji i żadnych diagramów. Ostrzeżenie nigdy nie blokuje, więc domyślnie nie ma czego pilnować; gdy reguły podniesiesz do błędów, diagram zepsuty przez agenta zablokował już własną edycję, a stop-gate = "project" sprawdza dodatkowo przy Stop każdy wymieniony diagram. Edycja kodu Pythona nie sprawdza diagramu ponownie: pakiet przemianowany w kodzie pojawi się na rysunku przy następnym sprawdzeniu całego projektu, w CI albo przy Stop z zakresem projektu.
INW019 porównuje diagram z importami, które zbiera uruchomienie: w diagramie warstw import rysuje ścieżka strzałek, w diagramie kontekstów potrzebna jest bezpośrednia strzałka, a import pod if TYPE_CHECKING: się liczy. Działa tylko przy sprawdzaniu całego projektu, bo jeden edytowany albo podany plik nie pokazuje importów reszty projektu.
Zacznij od diagramu¶
Najpierw narysuj architekturę, a potem niech import-diagram zapisze konfigurację:
inwards import-diagram docs/architecture.md # print the table on stdout, notes on stderr
inwards import-diagram docs/architecture.md --write # append it to pyproject.toml
- Wejście. Jeden plik Markdown (każdy oznaczony blok
mermaidiplantumlw nim), jeden plik.mmdalbo jeden plik.puml, z najwyżej jednym diagramem warstw i jednym diagramem kontekstów. - Wyjście. Tabela wymienia plik w
diagramsi włącza INW017 oraz INW018, więc diagram dalej jest sprawdzany. Plik spoza katalogu roboczego nie trafia do listy, bo wpisydiagramsnie mogą wychodzić wyżej przez... --writedopisuje tabelę dopyproject.tomlw katalogu roboczym. Odmawia, z kodem wyjścia 2 i bez zapisu, gdy nie mapyproject.tomlalbo ma on już[tool.inwards]: nigdy niczego nie nadpisuje ani nie scala. Wtedy wypisz tabelę i scal ją ręcznie.root. Gdy pakiety leżą wsrc/, tabela dostajeroot = "src", tak jak wybrałobyinwards init.- Najpierw sprawdzenie. Tabela musi się dać przeczytać jako konfiguracja Inwards, inaczej polecenie kończy się kodem wyjścia 2 i mówi dlaczego: dwie warstwy z tą samą etykietą w cudzysłowie, węzeł
publicpoza modułem swojego kontekstu.
Co jest odrzucane, z numerem linii: cykl między warstwami; kontekst albo węzeł :::public bez etykiety w cudzysłowie, bo kontekst wymaga modułów; drugi diagram tego samego rodzaju w pliku. Warstwa bez etykiety w cudzysłowie dostaje modules = [] i uwagę na stderr: uzupełnij ją przed sprawdzeniem. Plik tylko z diagramem kontekstów dostaje jedną warstwę, app, która obejmuje każdy kontekst.
Narysuj konfigurację¶
inwards diagram działa w drugą stronę: wypisuje [tool.inwards] jako oznaczony diagram, gotowy do pliku .mmd albo .puml lub do bloku kodu w dokumentacji.
inwards diagram # the layers, as Mermaid
inwards diagram --contexts # the contexts
inwards diagram --format plantuml # PlantUML, for either diagram
inwards diagram --config services/pyproject.toml
- Warstwy. Jeden węzeł na warstwę, od najbardziej zewnętrznej, z jej modułem jako etykietą w cudzysłowie, i ciągła strzałka z każdej warstwy do każdej warstwy o jedno miejsce głębiej. Warstwa może importować każdą warstwę wewnątrz niej, więc te strzałki wystarczą, a najdłuższa ścieżka oddaje każdej warstwie jej miejsce: warstwy sąsiednie zostają sąsiednie.
- Konteksty. Kontekst bez modułów
publicjest węzłem. Kontekst z nimi jest subgraphem (w PlantUMLpackage), w którym każdy wpis to węzeł:::public(<<public>>), także wpis dokładny=pkg.module. Każdedepends-onto strzałka z jednego kontekstu do drugiego. - Nazwy. Nazwa, która może być id węzła, jest tym id. Każda inna, na przykład
slices.domainz szablonu, dostaje id zrobione z niej (slices_domain) i jest wskazywana przez swój pierwszy moduł, który INW017 i INW018 rozpoznają (Oznacz diagram). W PlantUML nazwa z łącznikiem albo taka, którą czytnik wziąłby za słowo kluczowe (interface,package), trafia w nawiasy:[interface] as "shop.api". - W obie strony. Względem tej samej konfiguracji INW017 i INW018 nie znajdują w wyniku niczego, o ile istnieje każdy moduł, który konfiguracja wymienia (inaczej INW017 powtarza na diagramie zgłoszenie INW006), a
inwards import-diagramodczytuje go z powrotem do tych samych warstw, miejsc i kontekstów. Testy sprawdzają jedno i drugie dla przykładowego pakietu każdego presetu, obu przykładowych aplikacji i aplikacji ewaluacji, w obu notacjach. - To, czego diagram nie pokaże, trafia na stderr, po linii na sprawę; kod wyjścia zostaje 0:
- Warstwa albo kontekst z dwoma lub więcej modułami. Notatkę dostaje tylko PlantUML: alias nazywa jeden komponent, więc diagram zostawia pierwszy moduł. W Mermaid każdy moduł dostaje własną linię węzła i nie ma notatki;
import-diagramczyta je wszystkie, ale Mermaid rysuje węzeł z ostatnią etykietą. Kontekst narysowany jako subgraph albo pakiet też zostawia pierwszy moduł, z notatką w obu notacjach. - Warstwa albo kontekst nazwany jak słowo kluczowe Mermaid (
class,style,end) nie może być węzłem Mermaid.--format plantumlgo narysuje. - Listy bibliotek, kształty, opcje reguł i
ignore, tak jak przyimport-diagram, oraz szablony, rysowane jako warstwy, na które się rozwijają.
- Warstwa albo kontekst z dwoma lub więcej modułami. Notatkę dostaje tylko PlantUML: alias nazywa jeden komponent, więc diagram zostawia pierwszy moduł. W Mermaid każdy moduł dostaje własną linię węzła i nie ma notatki;
- Kod wyjścia 2 dla
--layersrazem z--contexts,--formatinnego niżmermaidalboplantumloraz--contexts, gdy konfiguracja nie deklaruje kontekstów.
Żeby rysunek był sprawdzany, umieść go w pliku wymienionym w kluczu diagrams. Plik .mmd albo .puml mieści jeden diagram; dla obu umieść każdy w osobnym bloku kodu jednego pliku Markdown, który import-diagram też czyta jako całość.
inwards context nie wypisuje diagramu: opis architektury już wymienia każdą warstwę z tym, co może importować, więc diagram by to powtarzał, a opis presetu fastapi jest już blisko swojego limitu 550 tokenów (opis architektury).
Przykład¶
Projekt z czterema pakietami i jeszcze bez konfiguracji:
Warstwy tak, jak rysuje je zespół, z zamówieniami i rozliczeniami obok siebie:
%% inwards: layers
flowchart TD
api["shop.api"] --> orders["shop.orders"] & billing["shop.billing"]
orders --> domain["shop.domain"]
billing --> domain
api --> db[("Postgres")]:::external
# Converted from docs/layers.mmd by inwards import-diagram.
[tool.inwards]
diagrams = ["docs/layers.mmd"]
layers = [
{ name = "domain", modules = ["shop.domain"] },
[
{ name = "orders", modules = ["shop.orders"] },
{ name = "billing", modules = ["shop.billing"] },
],
{ name = "api", modules = ["shop.api"] },
]
[tool.inwards.rules]
extend-select = ["INW017", "INW018"]
inwards import-diagram: read 4 layers and 0 contexts from docs/layers.mmd.
domain na nic nie wskazuje, więc jest najbardziej wewnętrzna; orders i billing wskazują tylko na nią, więc dzielą następne miejsce i są sąsiednie; api jest najbardziej zewnętrzna. Baza danych jest :::external i nie zostaje warstwą. Zapisz i sprawdź:
Projekt przechodzi, razem z diagramem. Gdyby później ktoś narysował strzałkę z orders do billing, byłoby to INW018, a import w kodzie między nimi INW001.
inwards diagram rysuje konfigurację z powrotem. Każda warstwa wskazuje warstwy o jedno miejsce głębiej, więc api wskazuje obie warstwy sąsiednie, a one wskazują domain:
%% inwards: layers
flowchart TB
api["shop.api"]
orders["shop.orders"]
billing["shop.billing"]
domain["shop.domain"]
api --> orders & billing
orders & billing --> domain
Diagram kontekstów działa tak samo. Ten
%% inwards: contexts
flowchart LR
subgraph sales["shop.sales"]
checkout["shop.sales.checkout"]
end
subgraph catalog["shop.catalog"]
api["shop.catalog.api"]:::public
end
checkout --> api
zamienia się w:
[[tool.inwards.contexts]]
name = "sales"
modules = ["shop.sales"]
public = []
depends-on = ["catalog"]
[[tool.inwards.contexts]]
name = "catalog"
modules = ["shop.catalog"]
public = ["shop.catalog.api"]
depends-on = []
Przejście z ArchUnitPython¶
ArchUnitPython sprawdza kod względem diagramu komponentów PlantUML w teście pytest (project_slices(...).should().adhere_to_diagram_in_file(...)). Inwards czyta te same diagramy: component [x], [a] --> [b] i [a] -> [b], czyli wszystko, co czyta jego parser, a do tego wszystko z tabeli PlantUML. Diagramy z testów i README ArchUnitPython należą do testów czytnika.
Żeby przenieść diagram, oznacz go, daj każdemu komponentowi jego moduł i zaimportuj go raz:
- Dodaj
' inwards: contextspo@startuml. Strzałki ArchUnitPython pozwalają na zależności bezpośrednie, nie przechodnie, i tak samo działadepends-onkontekstu (INW002). - Zamień każde
component [x]nacomponent [package.x] as x <<public>>. ArchUnitPython znajduje kod komponentu przez wzorzec wycinka w teście (defined_by("src/(**)/**")); Inwards potrzebuje modułu w diagramie.<<public>>zachowuje znaczenie z ArchUnitPython, w którym komponent może importować dowolny moduł komponentu, na który wskazuje; bez tego INW003 pozwala tylko na moduły oznaczone jako publiczne. - Uruchom
inwards import-diagram, poteminwards check.
Projekt ułożony jak przykładowy projekt ArchUnitPython, z trzema wycinkami jednego pakietu:
from sample.models.user import User
def find_user() -> User:
return User()
Diagram z ArchUnitPython, oznaczony i z modułami:
@startuml
' inwards: contexts
component [sample.controllers] as controllers <<public>>
component [sample.services] as services <<public>>
component [sample.models] as models <<public>>
[controllers] --> [services]
[services] --> [models]
@enduml
Każdy komponent staje się kontekstem ze strzałkami jako depends-on, jedna warstwa, app, obejmuje je wszystkie, a inwards check przechodzi, razem z diagramem. Później import z sample.models do sample.controllers to INW002 w kodzie, a strzałka [models] --> [controllers] narysowana w diagramie to INW018.
Czym Inwards się różni:
- Diagram nie jest sprawdzeniem. ArchUnitPython ocenia kod względem diagramu przy każdym uruchomieniu testów. Inwards sprawdza kod względem
[tool.inwards]i diagram względem[tool.inwards], więc oba nie rozjadą się bez zgłoszenia, a hook, edytor i opis architektury dla agenta czytają jedną konfigurację. - Kod poza każdym komponentem. ArchUnitPython zgłasza zależność wycinka, którego diagram nie nazywa, chyba że test wywoła
ignoring_orphan_slices(). Inwards pomija importy, których jeden koniec leży poza każdym kontekstem, tak jakignoring_orphan_slices(); INW006 ostrzega o module poza każdą warstwą. - Pakiety i inne strzałki. ArchUnitPython pomija
package,..>i strzałki ze stylem, takie jak-[#red]->. W Inwards pakiet to kontekst w diagramie kontekstów,..>nie znaczy importu, a ciągła strzałka ze stylem jest strzałką. - Także warstwy. Oznaczony
' inwards: layersten sam diagram układa komponenty w warstwy, a warstwa może importować każdą warstwę wewnątrz niej, nie tylko tę, na którą wskazuje jej strzałka. - Bez
should_not(). Diagram kontekstów już zabrania każdej strzałki, której nie rysuje, więc zabroniona zależność nie potrzebuje osobnego diagramu.
Ograniczenia¶
- Tylko flowcharty Mermaid i diagramy komponentów PlantUML. Diagramy C4,
architecture-betai inne rodzaje diagramów PlantUML nie są czytane. - PlantUML jest czytany linia po linii. Instrukcja rozbita na kilka linii (opis komponentu, który przechodzi do następnej linii) nie jest śledzona, a połączenie z komponentem przez jego wyświetlaną nazwę zamiast aliasu nazywa inny węzeł.
- To, czego diagram nie wyrazi, zostaje poza tabelą. Listy bibliotek, kształty, selektory inne niż etykieta węzła, opcje reguł i
ignoredopisuje się ręcznie po imporcie. - Importy, których diagram nie rysuje, zgłasza INW019, tylko przy sprawdzaniu całego projektu, nigdy przy edycji.
- Importy między warstwami i kontekstami, nie modułami. INW019 nie porównuje strzałki między dwoma węzłami modułów wewnątrz jednego kontekstu z importami między tymi modułami.
- Edycja kodu nie sprawdza diagramów. Hook i edytor sprawdzają diagram, gdy jest edytowany; zmiana nazwy w kodzie Pythona dociera do rysunku przy następnym sprawdzeniu całego projektu.
- Ścieżki są porównywane tak, jak je zapisano. Wpis
diagramsjest porównywany ze ścieżką, którą podaje agent albo edytor, z wielkością liter włącznie, więc na systemie plików bez rozróżniania wielkości liter (macOS, Windows)Docs/Architecture.mdnie pasuje w hooku ani w edytorze dodocs/architecture.md. Sprawdzenie całego projektu przechodzi po dysku i tego problemu nie ma.