Architektura (C4)¶
Ten rozdział opisuje Inwards za pomocą modelu C4: kontekst systemu (C1), kontenery (C2) i komponenty (C3). Diagramy to schematy blokowe Mermaid w notacji C4, bo natywna składnia C4 w Mermaid jest wciąż eksperymentalna i słabo się renderuje.
Legenda wspólna dla wszystkich diagramów
zaokrąglone węzły to ludzie · walce to przechowywane dane · przerywane ramki są zaplanowane.
C1: kontekst systemu¶
Kto używa Inwards i z czym Inwards się komunikuje?
flowchart TB
architect(["Architekt<br/><small>deklaruje warstwy</small>"])
dev(["Programista<br/><small>pisze i przegląda kod</small>"])
agent(["Agent kodujący AI<br/><small>Claude Code, Aider, Copilot, Codex</small>"])
inwards["<b>Inwards</b><br/><small>Sprawdza importy Pythona<br/>względem zadeklarowanych warstw</small>"]
repo[("Kod w Pythonie<br/><small>*.py + pyproject.toml</small>")]
editor["Edytor<br/><small>VS Code, Neovim, Helix: pokazuje diagnostyki</small>"]
ci["Runner CI<br/><small>GitHub Actions</small>"]
scanning["GitHub code scanning<br/><small>przyjmuje SARIF</small>"]
architect -- "pisze [tool.inwards]" --> repo
agent -- "edytuje pliki; jego hooki uruchamiają inwards" --> inwards
agent -- "narzędzia MCP" --> inwards
dev -- "pisze w" --> editor
editor -- "LSP" --> inwards
ci -- "uruchamia inwards check --format sarif" --> inwards
inwards -- "czyta" --> repo
ci -- "wysyła SARIF" --> scanning
inwards -. "diagnostyki JSON z krokami naprawy" .-> agent
Do kilku rzeczy ten obraz nas zobowiązuje:
- Inwards tylko czyta kod. Nigdy go nie edytuje. Automatyczne poprawki zostawia agentowi, który ma kontekst, żeby zrobić to dobrze.
- Nie potrzebuje sieci, interpretera Pythona ani importowania kodu użytkownika. To wyklucza podejście z
find_spec, którego używa pytest-archon, i sprawia, że uruchomienia są deterministyczne. - Agent to pełnoprawny użytkownik, na równi z programistą. Format wyjścia jest projektowany przede wszystkim dla niego (zobacz rozdział 4).
C2: kontenery¶
Co jest wdrażane albo instalowane i gdzie działa każdy element?
flowchart TB
agent(["Agent AI"])
dev(["Programista"])
subgraph dist["Inwards"]
cli["<b>inwards CLI</b><br/><small>TypeScript, kompilowany przez bun build --compile<br/>jeden plik binarny na OS/architekturę</small>"]
server["<b>inwards server</b><br/><small>LSP przez stdio, w pliku binarnym CLI</small>"]
mcp["<b>inwards mcp</b><br/><small>MCP przez stdio, w pliku binarnym CLI</small>"]
ext["<b>Rozszerzenie VS Code</b><br/><small>klient LSP, jeden VSIX na platformę<br/>z plikiem binarnym w środku</small>"]
core["<b>Silnik</b> @inwards/core<br/><small>biblioteka TypeScript + tree-sitter WASM<br/>bez operacji wejścia-wyjścia</small>"]
hooks["<b>Zestaw dla agentów</b><br/><small>inwards init --agent: hooki,<br/>sekcja AGENTS.md, lint-cmd dla aidera</small>"]
state[("Stan sesji + run log<br/><small>.inwards/state, runs.jsonl</small>")]
cache[("Pamięć podręczna<br/><small>.inwards/cache</small>")]
end
config[("pyproject.toml<br/><small>[tool.inwards]</small>")]
src[("Źródła w Pythonie")]
vscode["VS Code"]
other["Neovim, Helix, ..."]
agent -- "hook uruchamia" --> cli
hooks -. "instaluje hooki dla" .-> agent
dev --> vscode --> ext -- "uruchamia, stdio" --> server
dev --> other -- "stdio" --> server
cli -- "uruchamia" --> server
agent -- "stdio (MCP)" --> mcp
cli -- "uruchamia" --> mcp
cli -- "zawiera" --> core
cli -- "czyta" --> config
cli -- "czyta" --> src
cli -- "czyta/zapisuje (hook)" --> state
cli -. "czyta/zapisuje" .-> cache
classDef planned stroke-dasharray:5 5
class cache planned
| Kontener | Technologia | Gdzie leży | Stan |
|---|---|---|---|
| Silnik | TypeScript, web-tree-sitter 0.27 + tree-sitter-python 0.25 (WASM) |
src/core |
INW000, INW001, INW002, INW003, INW004, INW005, INW006, INW007, INW008, INW010, INW011, INW012, INW013, INW014, INW015, INW016, INW017, INW018, INW019, INW020, INW021 |
| CLI | Jednoplikowy program wykonywalny Bun 1.4, 6 platform docelowych, opakowany też w 5 wheeli platformowych | src/cli |
check (text/concise/json/sarif/github), init (agenci, presety stylów, scaffold), hook claude-code, daemon, server, mcp |
inwards server |
vscode-languageserver 10 na Bunie, w pliku binarnym CLI |
src/cli/src/lsp/ |
to, co inwards check zgłasza w każdym folderze obszaru roboczego, z niezapisanym tekstem, na starcie, po zapisie, przy zmianie folderów i zdarzeniach plików; naciśnięcie klawisza sprawdza tylko swój dokument (ADR-041) |
inwards mcp |
MCP TypeScript SDK 2 na Bunie, w pliku binarnym CLI | src/cli/src/mcp/ |
check_files, explain_rule i where_should_this_go dla agentów z klientem MCP (poradnik, ADR-042) |
| Rozszerzenie VS Code | vscode-languageclient 10, cienki klient |
src/vscode-extension/src/client/ |
uruchamia inwards server z pliku binarnego w swoim VSIX dla platformy, z inwards.path albo z PATH (ADR-043); VSIX dla każdej platformy w każdym wydaniu; Marketplace i Open VSX, gdy właściciel opublikuje (#64) |
| Zestaw dla agentów | Generowana konfiguracja hooków i Markdown | src/cli/src/init/ |
init --agent dla claude, aider i agents-md |
| Stan sesji i run log | Pliki JSON i JSON Lines, tylko lokalnie | .inwards/state/, .inwards/runs.jsonl |
(run log opcjonalny, rozdział 8) |
| Pamięć podręczna | Listy importów kluczowane hashem zawartości | .inwards/cache |
#56 |
Silnik to jedyne miejsce, w którym żyją reguły. CLI jest jego adapterem: znajduje pliki, czyta je, ładuje gramatyki i wybiera format wyjścia, a inwards server uruchamia to samo sprawdzenie dla edytorów. Dzięki temu podkreślenie w edytorze i błąd w CI nie mogą się rozjechać. Uruchamiają tę samą funkcję na tym samym tekście.
Jedno środowisko uruchomieniowe, trzymane z dala od silnika
Silnik działa na Bunie, w pliku binarnym; od ADR-043 rozszerzenie VS Code uruchamia ten plik binarny, zamiast uruchamiać silnik na Node. Silnik nadal dostaje wszystko z zewnątrz przez porty, a lint tego pilnuje: biome.jsonc włącza noRestrictedGlobals dla src/core/src/** i odrzuca Bun oraz Deno z komunikatem wskazującym port GrammarBinaries. CI przy każdym pushu buduje paczkę klienta rozszerzenia dla platformy node, bo VS Code uruchamia go na Node.
C3: komponenty silnika¶
Silnik to heksagon w miniaturze. Dostaje bajty i tekst, zwraca zwykłe dane i nigdy nie dotyka dysku. Gramatyki wchodzą przez port (GrammarBinaries), więc plik binarny Buna może przekazać osadzone bloby, a serwer VS Code czyta je z katalogu instalacji.
flowchart LR
subgraph driving["Strona sterująca (adaptery wywołują)"]
files["SourceFile[]<br/><small>path, module, text</small>"]
cfgtext["tekst pyproject.toml"]
wasm["GrammarBinaries<br/><small>środowisko + python .wasm</small>"]
pfiles["ProjectFiles<br/><small>sondowanie, lista plików, odczyt</small>"]
end
subgraph enginebox["Silnik: @inwards/core"]
config["<b>Parser konfiguracji</b><br/><small>config/<br/>smol-toml, walidacja</small>"]
pre["<b>Prescan szkieletu importów</b><br/><small>shared/python/prescan.ts<br/>czyści linie niebędące importami</small>"]
parser["<b>Adapter parsera</b><br/><small>shared/python/parser.ts<br/>web-tree-sitter</small>"]
extract["<b>Ekstraktor i resolver importów</b><br/><small>shared/python/parser.ts<br/>względne → bezwzględne</small>"]
rules["<b>Reguły</b><br/><small>shared/meta/registry.ts: rejestr<br/>rules/: jedna na regułę<br/>layer-dependency: INW001<br/>context-independence: INW002<br/>public-api-only: INW003<br/>import-cycles: INW004<br/>pure-domain: INW005<br/>unassigned-module: INW006<br/>package-shape: INW007 + INW008<br/>suppression-comment: INW009<br/>unknown-first-party: INW010<br/>dynamic-import: INW011<br/>thin-endpoint: INW012<br/>async-blocking: INW013<br/>ports-abstract: INW014<br/>construct-only-in: INW015<br/>orm-naming: INW016<br/>diagram-unknown-name: INW017<br/>diagram-forbidden-edge: INW018<br/>diagram-missing-dependency: INW019<br/>adapter-conformance: INW020<br/>private-module: INW021<br/>unsupported-encoding: INW000</small>"]
fix["<b>Kompozytor poprawek</b><br/><small>kroki dla każdego naruszenia</small>"]
report["<b>Reportery</b><br/><small>report/<br/>text · concise · json · sarif · github</small>"]
engine["<b>Fasada silnika</b><br/><small>engine/engine.ts<br/>checkFile / checkFiles / check / checkWith / index</small>"]
modgraph["<b>Indeks modułów</b><br/><small>shared/lookup/project-index.ts: własne moduły,<br/>moduły importujące na żądanie</small>"]
end
cfgtext --> config --> engine
wasm --> parser
files --> engine
engine --> pre --> parser --> extract --> rules
rules --> fix
rules --> engine
engine -. "potwierdź pełnym parsowaniem" .-> parser
engine --> modgraph
pfiles --> modgraph
modgraph -. "importy plików-kandydatów" .-> extract
engine --> report
| Komponent | Odpowiedzialność | Uwagi |
|---|---|---|
| Parser konfiguracji | Czyta [tool.inwards], waliduje je i podaje dokładnie ten klucz, który jest błędny |
Rzuca ConfigError. CLI zamienia go na kod wyjścia 2. Te same klucze opisuje dla edytorów JSON Schema, a testy pilnują zgodności obu |
| Prescan szkieletu importów | Zostawia tylko linie importów, usuwa im wcięcie, a resztę czyści, żeby numery linii się nie przesunęły | Odmawia przetworzenia pliku, gdy import pojawia się w miejscu, którego nie umie wyjaśnić, co wymusza pełne parsowanie. Zobacz ADR-004 |
| Adapter parsera | Inicjalizuje web-tree-sitter z bajtów i parsuje | Jawnie zwalnia każde drzewo, bo pamięć WASM nie jest odśmiecana |
| Ekstraktor importów | Znajduje węzły import / from ... import w dowolnym miejscu drzewa i rozwiązuje importy względne |
from shop import infrastructure jest zapisywane jako shop.infrastructure, więc nie prześlizgnie się |
| Reguły | Czyste funkcje z (file, imports, config) do Diagnostic[]. Kod, nazwa, domyślny poziom, podsumowanie i link do dokumentacji każdej reguły żyją w jednym rejestrze (shared/meta/registry.ts); z niego budowane jest rules[] w SARIF |
INW001, INW005 dla bibliotek, które warstwa może importować, INW006 dla kodu poza wszystkimi warstwami, INW010 dla własnych modułów, które nie istnieją, INW007/INW008 dla kształtu pakietu, INW011 dla importów dynamicznych (dosłownych celów i niesprawdzalnych celów w warstwach wewnętrznych) i INW000 dla plików, których kodowanie mogłoby ukryć importy. Zaplanowane reguły są wymienione niżej |
| Kompozytor poprawek | Buduje ponumerowane kroki naprawy z faktycznych nazw importu i warstw | Kroki podają prawdziwe moduły, a nie symbole zastępcze |
| Reportery | Tekst dla ludzi, JSON inwards/diagnostics@1 dla agentów, SARIF 2.1.0 dla GitHuba |
Pola JSON można dodawać, ale nigdy nie usuwać ani nie zmieniać ich nazw |
| Fasada silnika | Koordynuje prescan, reguły i potwierdzające pełne parsowanie | Jedyne, co wywołują adaptery. Kształt pakietu (INW007) jest sprawdzany najpierw, na podstawie samej ścieżki. Plik poza wszystkimi warstwami nie jest parsowany (dostaje najwyżej ostrzeżenie INW006). Plik w warstwie, którego tekst wymienia loader modułów, pomija prescan (zobacz niżej). checkWith najpierw przekazuje parsowanie do ExtractionBatch, czyli wątków roboczych CLI, a potem sprawdza tak jak check (ADR-040) |
| Indeks modułów | Engine.index(files) opakowuje port ProjectFiles adaptera: ownerOf znajduje własny moduł, do którego trafia import, sondując po jednej ścieżce naraz, każdą tylko raz; modules wypisuje każdy własny moduł; importersOf odpowiada na pytanie „kto importuje moduł X”, parsując tylko pliki, których tekst zawiera ostatni segment nazwy X |
Wejście projektu dla silnika (#44): każdy adapter buduje je i przekazuje do checkFile i checkFiles. Zbudowanie go niczego nie dotyka; każde pytanie wykonuje tylko własne operacje wejścia-wyjścia, więc hook nie płaci za pytania, których nie zadaje żadna reguła. INW006 pyta ownerOf; INW010 pyta ownerOf, czy moduł istnieje, a listDir, co zawiera pakiet, w którym by się znajdował (skompilowane moduły rozszerzeń, najbliższe nazwy); wykrywanie cykli skorzysta z importersOf. Długo działający adapter buduje go od nowa, gdy plik lub katalog powstaje, znika albo zmienia nazwę |
Przebieg jednego sprawdzenia¶
sequenceDiagram
autonumber
participant A as Adapter (CLI / LSP)
participant E as Silnik
participant P as Prescan
participant T as tree-sitter (WASM)
participant R as Reguły
A->>E: checkFile({path, module, text}, index)
alt plik poza wszystkimi warstwami i kontekstami
E-->>A: ostrzeżenie INW006 dla jego pakietu albo [] (bez parsowania)
else zadeklarowane kodowanie, którego Inwards nie umie czytać
E-->>A: INW000
else plik wymienia loader (importlib, runpy, builtins, __import__, exec, eval, compile)
E->>T: parse(pełny tekst)
E->>R: INW001 + INW002 + INW003 + INW005 + INW006 + INW010 + INW011 + INW021
R-->>E: diagnostyki
E-->>A: diagnostyki
else wszystko inne
E->>P: importSkeleton(text)
alt szkielet przyjęty
P-->>E: szkielet (same importy)
E->>T: parse(szkielet)
T-->>E: maleńkie drzewo
E->>R: INW001 + INW002 + INW003 + INW005 + INW006 + INW010 (importy)
alt brak naruszeń (typowy przypadek)
R-->>E: []
E-->>A: []
else znaleziono naruszenia
E->>T: parse(pełny tekst)
T-->>E: pełne drzewo
E->>R: INW001 + INW002 + INW003 + INW005 + INW006 + INW010 (importy z pełnego drzewa)
R-->>E: potwierdzone diagnostyki
E-->>A: diagnostyki
end
else szkielet odrzucony (nietypowe położenie importu)
P-->>E: null
E->>T: parse(pełny tekst)
E->>R: INW001 + INW002 + INW003 + INW005 + INW006 + INW010
R-->>E: diagnostyki
E-->>A: diagnostyki
end
end
Prescan może zgłaszać fałszywe alarmy, na przykład linię wyglądającą jak import wewnątrz docstringa, ale nigdy nie ukryje prawdziwego importu, bo odmawia przetworzenia każdego pliku, którego nie umie w pełni wyjaśnić. Źródłem prawdy jest pełne parsowanie, a uruchamia się ono tylko wtedy, gdy naruszenie wymaga potwierdzenia.
Szkielet zachowuje wyłącznie instrukcje importu, więc plik, którego jedyną zależnością na zewnątrz jest importlib.import_module("shop.infrastructure.db"), by go przeszedł. Zanim uruchomi się prescan, sprawdzenie tekstu szuka nazw, które każde wywołanie ładujące musi zapisać (importlib, runpy, builtins, __import__ albo samo exec, eval lub compile, po normalizacji NFKC). Plik w warstwie, który pasuje, trafia prosto do pełnego parsowania, które szuka też importów dynamicznych. re.compile nie pasuje. W bibliotece standardowej CPythona 3.14 pasuje 209 z 1921 plików. Zobacz ADR-015.
Model kosztów ma jeden zły przypadek: starszy kod, w którym większość plików już narusza reguły. Bez baseline'u prawie każdy plik płaci za parsowanie szkieletu, a potem za pełne parsowanie, co jest wolniejsze niż sparsowanie wszystkiego raz. Z baseline'em CLI przekazuje silnikowi jego klucze i liczniki (reguła, moduł, komunikat bez zdania „Allowed direction”) jako dane. Silnik najpierw skanuje każdy plik. Moduł pomija parsowanie potwierdzające, gdy każdy z jego plików przeszedł przez szkielet, każdy wynik ze szkieletu jest błędem, a dla każdego klucza wyniki ze wszystkich plików modułu (order.py i order.pyi to jeden moduł) nie przekraczają liczby zaakceptowanych kopii. Szkielet nigdy nie pomija importu, a wynik zależy tylko od celu importu, który oba parsowania odczytują tak samo. Prawdziwych wyników nie jest więc więcej niż wyników ze szkieletu, a baseline ukryłby je wszystkie także po pełnym parsowaniu. Błędy zgłaszane przez sprawdzenie są takie same z pominięciem i bez niego; test porównuje oba warianty dla każdej pary zapisów (prawdziwy import, kopia w docstringu albo w napisie) w pliku .py i jego .pyi. Liczniki mogą się różnić: fałszywe alarmy pominiętego modułu (linia wyglądająca jak import w docstringu) są ukrywane razem z jego prawdziwymi wynikami, więc baselined może być wyższe, a resolved niższe niż po pełnym parsowaniu, na przykład gdy import naprawionego naruszenia wciąż siedzi w napisie. Na syntetycznym repozytorium w trybie starszego kodu (bench/generate.py --legacy: 2101 plików, każdy z 2000 modułów z jednym naruszeniem, wszystkie w baseline'ie) zimne inwards check trwało w medianie 3,05 s przed tą zmianą i 0,55 s po niej. Samo pełne parsowanie każdego pliku trwało od 2,0 do 3,1 s, a czyste repozytorium 0,47 s (jeden rdzeń).
C3: komponenty CLI¶
flowchart LR
argv["argv"] --> args["Parser argumentów<br/><small>node:util parseArgs</small>"]
args --> find["Wyszukiwanie konfiguracji<br/><small>idzie w górę do pyproject.toml<br/>z [tool.inwards]</small>"]
find --> walk["Zbieracz plików<br/><small>pomija ukryte katalogi, venvy,<br/>node_modules (nie wewnątrz warstw)</small>"]
walk --> mod["Nazywanie modułów<br/><small>ścieżka → nazwa z kropkami</small>"]
gram["Ładowanie gramatyk<br/><small>osadzone .wasm przez<br/>import ... with type: file</small>"] --> eng
mod --> eng["Silnik"]
eng --> out["Reporter → stdout<br/>exit 0 / 1 / 2"]
pool["Wątki robocze<br/><small>parsują duże pełne sprawdzenie,<br/>INWARDS_THREADS</small>"] --> eng
cache["Pamięć podręczna po hashu zawartości<br/><small>.inwards/cache</small>"] --> eng
Kody wyjścia są takie jak w Ruffie: 0 czysto (ostrzeżenia dozwolone), 1 znaleziono błędy, 2 błąd użycia albo konfiguracji. Agenci i skrypty CI mogą rozgałęziać się na tej podstawie bez parsowania wyjścia. W raporcie JSON summary.violations liczy błędy, a summary.warnings ostrzeżenia. inwards --help wymienia wszystkie polecenia, a inwards COMMAND --help wypisuje opcje jednego polecenia, oba na stdout z kodem 0; nieznane polecenie albo opcja, albo opcja bez wartości, to jedna linia na stderr z kodem 2. Uruchomienie dla całego projektu (bez argumentów ścieżek) sprawdza też każdy prefiks warstwy i selektor kształtu względem znalezionych modułów (INW006, INW007) oraz wymagane elementy każdego pakietu z kształtem (INW008).
Pozostałe polecenia korzystają z tych samych elementów:
inwards hook claude-codeczyta ze stdin dane hooka Claude Code i rozdziela je według zdarzenia: SessionStart zapisuje stan sesji, PreToolUse uruchamia shape guard i config guard, PostToolUse sprawdza edytowany plik, a Stop uruchamia Stop gate dla tego, co zmieniła sesja. Rozdział 4 opisuje każde z nich.inwards daemontrzyma silnik w gotowości dla PostToolUse (ADR-039). Hook wysyła mu przez lokalne gniazdo dane hooka, katalog roboczy i środowisko, po jednej linii JSON w każdą stronę; daemon uruchamia ten sam kodhook claude-codeze środowiskiem zbudowanym z tych danych i strumieniami, które zbierają wyjście, a hook wypisuje to wyjście i kończy się z jego kodem. Daemon trzyma tylko to, co identyfikuje treść: ekstrakcje według skrótu tekstu i odpowiedzi gita według identyfikatora commita. Bez odpowiedzi hook uruchamia się jednorazowo.inwards mcpudostępnia trzy narzędzia klientowi MCP agenta przez stdio (ADR-042).check_filesuruchamia routing i sprawdzenieinwards checkz niezapisanym jeszcze kodem Pythona od agenta nałożonym na dysk,explain_rulezwraca strony reguł wbudowane w plik binarny, awhere_should_this_gosprawdza w każdej warstwie moduł zawierający tylko planowane importy. SDK i strony ładują się z osobnych chunków, tylko dla tego polecenia;inwards rule CODEwypisuje tę samą stronę z tego samego chunka, dla agentów bez klienta MCP.inwards init --agent claude|opencode|aider|agents-mdnajpierw wylicza każdą zmianę plików, więc--dry-runmoże wypisać ją jako diff, a drugie uruchomienie niczego nie zmienia.inwards init --style layered|clean|hexagonal|vertical-slices|bounded-contexts|django|fastapi [--scaffold]zapisuje[tool.inwards]z presetu (i przykładowy pakiet z pasującymi do niego kształtami pakietów) tylko tam, gdzie jeszcze nic nie ma, a potem uruchamia sprawdzenie w tym samym procesie i wypisuje pakiet jako drzewo z opisami. W terminalu bez flag zamiast tego pyta kreator zbudowany na@clack/prompts; jest ładowany importem dynamicznym, który build umieszcza w osobnym fragmencie (ADR-020).
Wdrożenie i dystrybucja¶
flowchart LR
pr["Scalony release PR<br/><small>release-please taguje vX.Y.Z</small>"] --> cd["cd.yml na ubuntu-26.04<br/><small>bun build --compile × 6 platform,<br/>5 wheeli platformowych, 7 VSIX</small>"]
rc["Ręcznie wypchnięty tag rc<br/><small>v0.2.0-rc.1</small>"] --> cd
cd --> verify["macierz weryfikacji<br/><small>linux x64/arm64/musl · macOS arm64/x64 · Windows x64<br/>każdy plik binarny sprawdza przykładową aplikację,<br/>każdy wheel instaluje się przez uvx,<br/>każdy VSIX dla platformy zawiera ten plik binarny</small>"]
verify --> art[("Szkic GitHub Release<br/><small>pliki binarne + wheele + VSIX + SHA256SUMS<br/>+ atestacje pochodzenia, gdy repozytorium będzie publiczne</small>")]
art --> manual["Ręczne pobranie<br/><small>obrazy CI, pre-commit</small>"]
art --> uvurl["uv add --dev z URL-em wheela"]
art -.->|"właściciel publikuje:<br/>pypi.yml"| testpypi["TestPyPI: inwards"]
testpypi -.->|pełne wydania| pypi["PyPI: inwards"]
pypi -.-> dev["uv add --dev inwards"]
art -.->|"właściciel publikuje:<br/>vscode-publish.yml"| market["VS Code Marketplace<br/>i Open VSX"]
classDef planned stroke-dasharray:5 5
class testpypi,pypi,dev,market planned
Wydania przebiegają według ADR-016: release-please utrzymuje otwarty release PR, jego scalenie taguje wersję i uruchamia cd.yml, a właściciel ręcznie publikuje szkic. Opublikowanie szkicu uruchamia pypi.yml i vscode-publish.yml, gdy każdy z nich zostanie włączony (niżej).
Kompilacja skrośna z jednego runnera linuksowego jest możliwa, bo gramatyki są w WASM i nie ma natywnego dodatku do budowania dla każdej platformy (zobacz ADR-002). Macierz weryfikacji uruchamia potem każdy plik binarny na jego prawdziwym systemie, bo skompilowany skrośnie wynik, którego nigdy nie uruchomiono, nie został przetestowany.
Publikowanie na PyPI¶
.github/workflows/pypi.yml wysyła wheele opublikowanego wydania przez trusted publishing (ADR-021). Niczego nie buduje: pobiera pięć wheeli, które cd.yml zbudował, uruchomił na każdej platformie i dołączył do wydania, sprawdza je względem SHA256SUMS wydania (i ich pochodzenie buildu, gdy repozytorium będzie publiczne) i je wysyła. PyPI ufa temu jednemu plikowi workflow w jednym środowisku GitHuba na indeks, więc nigdzie nie jest przechowywany żaden token, a token OIDC mogą wygenerować tylko dwa zadania wysyłające.
| Co uruchamia | TestPyPI | PyPI |
|---|---|---|
Opublikowanie wersji przedpremierowej (przez jej pole wyboru albo tag z przyrostkiem, takim jak -rc.1), przy ustawionym PYPI_PUBLISH |
tak | nie |
Opublikowanie pełnego wydania, przy ustawionym PYPI_PUBLISH |
tak | tak, po nim |
Ręczne uruchomienie, index: testpypi |
tak | nie |
Ręczne uruchomienie z tagu wydania (--ref vX.Y.Z, czego wymaga środowisko pypi), index: pypi, przy ustawionym PYPI_PUBLISH |
tak | tak, po nim |
Szkic nigdy go nie uruchamia, ręczne uruchomienie odrzuca szkic, a pull requesty nigdy go nie uruchamiają. Na żadnym indeksie nie ma jeszcze niczego.
Co naprawdę blokuje wysyłkę. Każdy agent pracuje na koncie GitHub właściciela, więc wszystko po stronie GitHuba jest w zasięgu agenta: zmienna PYPI_PUBLISH, środowiska, tagi (żaden ruleset ich nie chroni) i ręczne uruchomienia. PyPI sprawdza repozytorium, plik workflow i środowisko uruchomienia, a nie jego ref ani commit. Jedyna bramka, do której agent nie sięgnie, to konto właściciela na pypi.org, więc publisher PyPI jest rejestrowany na końcu, przy uruchomieniu produkcyjnym, a usunięcie go tam od razu zatrzymuje każdą wysyłkę na PyPI. PYPI_PUBLISH i reguła tagów v* środowiska pypi chronią przed pomyłkami, a nie przed agentem.
Jednorazowa konfiguracja, przez właściciela, w tej kolejności, żeby żadne środowisko nie istniało bez swoich reguł w chwili, gdy uruchomienie po raz pierwszy się do niego odwoła:
-
Utwórz dwa środowiska (Settings → Environments → New environment):
testpypi: w „Deployment branches and tags” wybierz „Selected branches and tags”, a potem dodaj gałąźdevelop(dla ręcznych uruchomień) i wzorzec tagówv*(dla wydań).pypi: tylko wzorzec tagówv*, żeby wdrażało się wyłącznie z tagu wydania. Dodaj siebie w „Required reviewers” i zostaw wyłączone „Prevent self-review”, bo sam publikujesz i zatwierdzasz. GitHub oferuje wymaganych recenzentów w prywatnym repozytorium tylko z GitHub Enterprise; jeśli tej opcji brakuje, dodaj ją w dniu, w którym repozytorium stanie się publiczne.
To samo przez GitHub CLI (pomiń
reviewersiprevent_self_review, jeśli GitHub je odrzuca, dopóki repozytorium jest prywatne):R=SirCypkowskyy/inwards gh api -X PUT repos/$R/environments/testpypi --input - <<'EOF' {"deployment_branch_policy": {"protected_branches": false, "custom_branch_policies": true}} EOF gh api repos/$R/environments/testpypi/deployment-branch-policies -f name=develop -f type=branch gh api repos/$R/environments/testpypi/deployment-branch-policies -f name='v*' -f type=tag gh api -X PUT repos/$R/environments/pypi --input - <<EOF {"reviewers": [{"type": "User", "id": $(gh api users/SirCypkowskyy -q .id)}], "prevent_self_review": false, "deployment_branch_policy": {"protected_branches": false, "custom_branch_policies": true}} EOF gh api repos/$R/environments/pypi/deployment-branch-policies -f name='v*' -f type=tag -
Zarejestruj oczekującego publishera na TestPyPI. TestPyPI ma własne konta: zarejestruj się na test.pypi.org, potwierdź adres e-mail i włącz 2FA. Potem w Your account → Publishing dodaj oczekującego publishera GitHub:
Pole Wartość PyPI Project Name inwardsOwner SirCypkowskyyRepository name inwardsWorkflow name pypi.ymlEnvironment name testpypiOczekujący publisher nie rezerwuje nazwy; projekt tworzy pierwsza wysyłka. Wykonaj krok 3 wkrótce potem. 3. Próba na sucho na TestPyPI z opublikowaną wersją przedpremierową v0.1.0-rc.1, gdy
pypi.ymlbędzie nadevelop. Ten tag jest starszy niż workflow, więc uruchomienie startuje zdevelop, które akceptuje tylko środowiskotestpypi:gh workflow run pypi.yml --repo SirCypkowskyy/inwards --ref develop -f tag=v0.1.0-rc.1 -f index=testpypi gh run watch --repo SirCypkowskyy/inwards \ "$(gh run list --repo SirCypkowskyy/inwards --workflow pypi.yml -L 1 --json databaseId -q '.[0].databaseId')"test.pypi.org/project/inwards/0.1.0rc1 powinno wtedy pokazywać pięć wheeli i wyrenderowane README. Zainstaluj pakiet w świeżym projekcie, na tylu platformach, na ilu możesz:
cd "$(mktemp -d)" && uv init --bare --name testpypi-check uv add --dev "inwards==0.1.0rc1" --default-index https://test.pypi.org/simple/ uv run inwards --version # 0.1.0To zużywa nazwy plików 0.1.0rc1 tylko na TestPyPI; PyPI zostaje nietknięte. 4. Uruchomienie produkcyjne, gdy zbliża się pierwsze wydanie przeznaczone na PyPI: - Dodaj publishera do istniejącego projektu na PyPI.
inwardsjuż istnieje na pypi.org (rezerwacja w wersji 0.0.0), więc to nie jest oczekujący publisher: otwórz Manageinwards→ Publishing i dodaj publishera GitHub z właścicielemSirCypkowskyy, repozytoriuminwards, workflowpypi.ymli środowiskiempypi. - Włącz publikowanie:gh variable set PYPI_PUBLISH --body true --repo SirCypkowskyy/inwards.gh variable delete PYPI_PUBLISHznowu je wyłącza.
Opcjonalne utwardzenie: włącz niezmienne wydania (Settings → General → Releases → „Enable release immutability”; API zgłasza, że to ustawienie jest dostępne i wyłączone w tym repozytorium). Pliki i tag opublikowanego wydania nie mogą się wtedy już zmienić, więc późniejsze uruchomienie wysyła te bajty, które zostały opublikowane. Dotyczy to tylko wydań opublikowanych po włączeniu ustawienia i nie chroni szkicu.
Przy każdym wydaniu poczekaj, aż zadanie „Draft the GitHub Release” w cd.yml zakończy się sukcesem, a szkic będzie zawierał pięć plików .whl i SHA256SUMS. Dopiero wtedy opublikuj szkic (krok 4 wydania w AGENTS.md). cd.yml odmawia dołączania plików do opublikowanego wydania, więc zbyt wczesna publikacja zużywa wersję. Publikacja uruchamia pypi.yml: pełne wydanie trafia na TestPyPI, a potem czeka na twoje zatwierdzenie w „Review deployments” danego uruchomienia, zanim trafi na PyPI (gdy środowisko pypi ma recenzenta). Potem uv add --dev inwards w świeżym projekcie powinno je zainstalować. Po pierwszym wydaniu na PyPI wycofaj tam (yank) rezerwację 0.0.0 (Manage → Releases → 0.0.0 → Options → Yank).
Żeby umieścić na PyPI także wersję przedpremierową, uruchom workflow z jej tagu, czego wymaga środowisko pypi: gh workflow run pypi.yml --repo SirCypkowskyy/inwards --ref v0.2.0-rc.1 -f tag=v0.2.0-rc.1 -f index=pypi. PyPI nigdy nie przyjmuje tej samej nazwy pliku dwa razy, więc wysłana tam wersja jest ostateczna; ponowne uruchomienie tylko uzupełnia pliki, których zabrakło po nieudanym uruchomieniu.
Publikacja rozszerzenia VS Code¶
.github/workflows/vscode-publish.yml wysyła siedem plików VSIX opublikowanego pełnego wydania do Visual Studio Marketplace (vsce publish) i Open VSX (ovsx publish) (ADR-043). Tak jak pypi.yml, niczego nie buduje: pobiera pliki spakowane przez cd.yml, sprawdza je względem SHA256SUMS wydania i wersji z taga (a także ich pochodzenie, gdy repozytorium będzie publiczne) i wysyła je z --skip-duplicate, więc ponowne uruchomienie dokańcza przerwaną wysyłkę. Każde zadanie wysyłki czyta jeden token z własnego środowiska, vscode-marketplace albo open-vsx, i biegnie na runnerze hostowanym przez GitHub. Opublikowanie pełnego wydania uruchamia workflow, gdy VSCODE_PUBLISH ma wartość true; wersja przedpremierowa nigdy go nie uruchamia, bo oba rejestry przyjmują tylko X.Y.Z. Ręczne uruchomienie przyjmuje tag i both, marketplace albo open-vsx. Pull requesty nigdy go nie uruchamiają.
Jednorazowa konfiguracja, po stronie właściciela:
- Wydawca w Marketplace. Zaloguj się na stronie wydawców Marketplace i utwórz wydawcę
inwards, czylipublisherzsrc/vscode-extension/package.json. Jeśli ten identyfikator jest zajęty, wybierz inny i zmień tampublisher(oraz adres wvscode-publish.yml). Utwórz w Azure DevOps token PAT z zakresem Marketplace (Manage), jak opisuje przewodnik publikacji. Azure DevOps wycofuje tokeny globalne 1 grudnia 2026; dopóki workflow nie przejdzie na Microsoft Entra ID, odnów token przed tą datą. - Przestrzeń nazw w Open VSX. Zaloguj się na open-vsx.org przez GitHuba, połącz konto eclipse.org i zaakceptuj umowę wydawcy Eclipse na stronie profilu, utwórz token dostępu, a potem przestrzeń nazw:
npx ovsx create-namespace inwards -p <token>. -
Środowiska i sekrety. Utwórz środowiska
vscode-marketplaceiopen-vsx(Settings → Environments), każde ograniczone do wzorca tagówv*i gałęzidevelop, z tobą jako wymaganym recenzentem, jeśli GitHub to oferuje. Potem zapisz każdy token w jego środowisku: -
Uruchomienie:
gh variable set VSCODE_PUBLISH --body true --repo SirCypkowskyy/inwards. Następne opublikowane pełne wydanie trafi do obu rejestrów. Żeby wysłać wydanie opublikowane wcześniej:gh workflow run vscode-publish.yml --repo SirCypkowskyy/inwards --ref develop -f tag=vX.Y.Z -f registry=both.
Po pierwszej wysyłce sprawdź instalację na Linuksie, macOS i Windows: bun run src/vscode-extension/scripts/try-in-vscode.ts inwards.inwards-vscode instaluje rozszerzenie z Marketplace w jednorazowym profilu VS Code, otwiera mały projekt i czeka na jego diagnostykę INW007.
Znane ograniczenia¶
- Jeden
rootna konfigurację. W monorepo każdy pakiet Pythona ma własne[tool.inwards].inwards checkw katalogu głównym workspace'u uv sprawdza każdego członka jego własną konfiguracją, a wskazane ścieżki kieruje do najbliższej konfiguracji; hook i Stop gate wybierają najbliższą konfigurację dla każdego pliku (ADR-035). Wykrywane są tylko workspace'y uv: inne układy monorepo potrzebują osobnego uruchomienia na konfigurację albo wskazanych ścieżek. Globy członków dopasowują*tylko wewnątrz jednego segmentu, ainwards baselinebierze jedną konfigurację na uruchomienie. inwards serverpokazuje to, co zgłaszainwards check, gdy pliki są zapisane. Dopóki dokument ma niezapisane zmiany, pokazuje ten dokument sprawdzony osobno oraz to, co ostatni przebieg całego projektu znalazł w nim, a czego jeden plik nie pokaże (cykle, routery FastAPI, których nie dołącza żadna aplikacja); diagnostyki innych plików zależne od niezapisanego tekstu oraz niezapisane edycjepyproject.tomlalbo baseline'u liczą się od następnego zapisu. Dwa foldery obszaru roboczego, których konfiguracje obejmują ten sam plik, pokazują jego diagnostyki dwa razy. Każdy zapis kosztuje sprawdzenie całego projektu (154 ms dla 2100 plików po rozgrzaniu, ADR-041).- Niejawne pakiety przestrzeni nazw (bez
__init__.py) są nazywane, a ich importy względne rozwiązywane tak jak w Pythonie. Moduł, którego brakuje pod takim pakietem, przechodzi INW010, gdy ma go inny członek workspace'u uv (w swoimsrc/, a bez niego w katalogu członka), gdy ma go site-packages katalogu.venvprojektu albo gdy leży bezpośrednio w pakiecie wymienionym wnamespace-packages(#161). Środowisko wirtualne w innym miejscu nie jest widoczne. - Przynależność do warstwy wynika tylko z prefiksu modułu. Wzorce glob (
shop.*.domain) dla pionowych wycinków przyjdą ze schematem konfiguracji v2 (#51, ADR-018). - Dowiązania symboliczne w warstwie, które wskazują poza katalog
rootalbo do innej warstwy, zgłasza INW006 w miejscu dowiązania: robi toinwards checkcałego projektu, a dla dowiązań utworzonych w trakcie sesji także Stop gate (ADR-013).inwards checkze ścieżkami jako argumentami ich nie zgłasza, a katalog z danymi dowiązany do pakietu warstwy spozarootteż jest zgłaszany. - INW011 rozwiązuje import dynamiczny tylko wtedy, gdy celem jest stały napis. Każdy inny cel jest zgłaszany jako niesprawdzalny, i to tylko w warstwach, które mają warstwę zewnętrzną (ADR-026). Znane luki:
- stałe są zwijane tylko w granicach (#79): nazwa na poziomie modułu liczy się tylko wtedy, gdy cały plik wiąże ją raz, a plik nie ma
exec,eval, funkcji zapisującej przestrzeń nazw ani importu z gwiazdką, a zwijanie%,formati f-stringów zatrzymuje się na wszystkim poza%s,!si tekstową specyfikacją formatu. Wszystko inne jest niesprawdzalne; compilez niedosłownym kodem nie jest zgłaszane, bo uruchomienie jego obiektu kodu wymagaexecalboeval, które są zgłaszane; obiekt kodu uruchomiony inaczej (types.FunctionType) zostaje przeoczony;- w najbardziej zewnętrznej warstwie niesprawdzalne wywołanie nie jest zgłaszane, więc może niezauważenie sięgnąć do własnego kodu poza wszystkimi warstwami (INW006);
- loader zapisany w kontenerze, osiągnięty przez
operator.attrgetteralbo przekazany jako argument zostaje przeoczony, podobnie jakSourcelessFileLoader,ExtensionFileLoaderiimportlib.util.spec_from_loader. Loader plików jest czytany tylko ze względnej ścieżki do pliku.py, afunctools.partialtylko tam, gdzie wiąże argumenty; - fałszywy alarm luźnego traktowania zakresów: nazwa atrybutu, której gdziekolwiek w pliku przypisano loader (
self.load = importlib.import_module), liczy się na każdym obiekcie w tym pliku; - fałszywy alarm, zaakceptowany zamiast przeoczenia: przy dosłownym kodzie
exec,eval,compilei__import__zawsze są traktowane jak funkcje wbudowane, więc pofrom re import compilewywołaniecompile("from shop.infrastructure import x")zostaje zgłoszone. Bajty, które CPython odrzuca przed wykonaniem (UTF-8 BOM z innym zadeklarowanym kodowaniem, deklaracjautf-16alborot13), są nadal czytane albo zgłaszane jako niesprawdzone; - fałszywie negatywny wynik, zaakceptowany zamiast szumu: samo
execalboevalz wyliczanym kodem jest pomijane tylko wtedy, gdy kod na pewno ponownie wiąże tę nazwę przy wywołaniu. Wiązaniem musi byćdef,class, zwykłe przypisanie albo import umieszczone bezpośrednio w ciele modułu (przed instrukcją najwyższego poziomu, która zawiera wywołanie) lub w ciele funkcji otaczającej wywołanie, parametr funkcji albo lambdy, której ciało zawiera wywołanie, albo cel pętliforwewnątrz tej pętli. Wyjątek jest wyłączony dla całego pliku, gdy którekolwiek wiązanie tej nazwy może być funkcją wbudowaną: przypisanie, operator morsa, celfor,withalboexceptczy domyślna wartość parametru, które wspominają loader;defalboclass, których dekoratory lub argumenty klasy (klasy bazowe,metaclass=) go wspominają; import zbuiltins,importlib,runpy, modułu względnego albo własnego modułu projektu (każdy z nich może ponownie eksportować funkcję wbudowaną). Jest wyłączony także wtedy, gdy nazwa maglobal,nonlocalalbodelalbo gdy plik ma import z gwiazdką lub wspominaglobals,vars,locals,setattr,delattr,__dict__,__builtins__albosys.modules. Dlategodef eval(model, loader)w kodzie treningowym nie jest zgłaszane. Nadal przeoczona zostaje funkcja wbudowana przekazana jako argument (def run(exec, c): return exec(c)wywołane jakorun(exec, code)) oraz funkcja wbudowana osiągnięta przez obiekt, który nie nazywa loadera,__self__,__globals__ani funkcji zapisującej przestrzeń nazw. To samo sprawdzenie chroniexec(compile("<literal>", ...)), któremu ufamy tylko, dopókicompilenie jest ponownie związane; - zaakceptowane fałszywe alarmy tego ostrożnego sprawdzenia: zmienna wyrażenia listowego (
[eval(m) for eval in evaluators]), nazwa metody użyta w ciele jej własnej klasy i przechwyceniematcho nazwieevalalboexecsą nadal zgłaszane.
- stałe są zwijane tylko w granicach (#79): nazwa na poziomie modułu liczy się tylko wtedy, gdy cały plik wiąże ją raz, a plik nie ma
- INW010 sprawdza tylko tę część importu statycznego, która jest modułem (ADR-025):
from shop.domain import pricingprzechodzi, gdyshop/domainjest pakietem, bopricingmoże być nazwą zdefiniowaną w jego__init__.py, a importy dynamiczne nie są sprawdzane. Moduł generowany przy budowaniu przechodzi, gdy obejmuje gogenerated, a domyślnie obejmuje*_pb2i*_pb2_grpcz protoc oraz moduł_version(ADR-029); każdy inny jest zgłaszany, dopóki nie pojawi się w checkoucie, podobnie jak opcjonalny import zatry/except ImportError. Zmyślony import, którego nazwę obejmuje wzorzec zgenerated, też przechodzi. Pozostałe reguły nadal widzą moduł generowany, którego nie ma na dysku, jako brakujący: INW001 patrzy na nazwę i zgłasza import skierowany na zewnątrz tak czy inaczej, a INW006 ocenia import po najbliższym istniejącym pakiecie, więc moduł generowany leżący bezpośrednio w pakiecie nad warstwami (shop._version, importowany z warstwy) dostaje błąd INW006 o innej treści z plikiem i bez niego, a wpis baseline'u zrobiony w jednym checkoucie nie pasuje w drugim. Generowany pakiet najwyższego poziomu bez zacommitowanego__init__.pywygląda dla każdej reguły jak kod zewnętrzny. - Moduły, które nie należą do żadnej warstwy, nie podlegają żadnej regule warstw. INW006 to uwidacznia (ostrzeżenie na pakiet, błąd dla importu do takiego pakietu z warstwy i martwe prefiksy), ale importy wewnątrz nieprzypisanego pakietu nie są sprawdzane względem warstw, dopóki użytkownik go nie przypisze. Gdy zadeklarowano konteksty, importy każdego modułu są z nimi porównywane (INW002, INW003), a INW000 dotyczy każdego pliku. Moduły bez źródeł, głębokość
ignore, przemianowane pakiety najwyższego poziomu i sprawdzanie prefiksów w zakresie ścieżek są otwarte w #86.
Katalog reguł¶
Każda wdrożona reguła ma własną stronę w sekcji Reguły, z przykładami, poprawkami i konfiguracją; diagnostyki linkują do niej.
Reguły są pogrupowane według kategorii, w kolejności, której używa indeks reguł: co moduł importuje, gdzie leży kod, co robi kod roli, integralność samego sprawdzenia, diagramy architektury i rodzina FastAPI. Kody są numerowane w kolejności, w jakiej reguły trafiały do kodu, i nigdy się nie zmieniają, więc kody jednej kategorii nie idą po kolei. „Włącza ją preset” wymienia presety inwards init --style, które zostawiają regułę włączoną albo włączają regułę opt-in jako ostrzeżenie.
Importy¶
| Kod | Nazwa | Co wyłapuje | Włączona | Poziom | Włącza ją preset | Stan |
|---|---|---|---|---|---|---|
| INW001 | layer-dependency |
Warstwa wewnętrzna importująca zewnętrzną | domyślnie | błąd | wszystkie | |
| INW002 | context-independence |
Kontekst ograniczony albo pionowy wycinek importujący inny kontekst, którego jego depends-on nie deklaruje, statycznie albo dynamicznie, niezależnie od tego, czy plik należy do warstwy. Koniec poza wszystkimi kontekstami jest pomijany, a kontekst zagnieżdżony jest osobnym kontekstem. Zobacz konteksty |
domyślnie | błąd | wszystkie oprócz django, fastapi | |
| INW003 | public-api-only |
Import, spoza kontekstu (z innego kontekstu albo z żadnego), jednego z jego modułów, który nie leży na prefiksach public ani pod nimi, statyczny albo dynamiczny. Import zgłoszony już przez INW002 nie dostaje INW003. Poprawka wskazuje moduł publiczny, który udostępnia importowaną nazwę, jeśli taki istnieje |
domyślnie | błąd | wszystkie | |
| INW004 | import-cycles |
Moduły (cycles = ["modules"]) albo konteksty (domyślnie, ["contexts"]), które importują się nawzajem w cyklu, zgłaszane raz na silnie spójną grupę, z pełną ścieżką i rozmiarem grupy. Tylko przy sprawdzaniu całego projektu, z importów, które sprawdzenie i tak czyta; plik w cyklicznej grupie, którego szkielet mógł źle odczytać import, jest potwierdzany pełnym parsowaniem. Nie da się go wyciszyć w linii; istniejące cykle przyjmuje baseline. Zobacz ADR-032 |
domyślnie | błąd | wszystkie | |
| INW005 | pure-domain |
Warstwa importująca moduł zewnętrzny albo z biblioteki standardowej, na który nie pozwalają jej allow-libraries / deny-libraries / extend-deny-libraries, statycznie albo dynamicznie, także w funkcjach i pod TYPE_CHECKING. Najbardziej wewnętrzna z dwóch lub więcej warstw domyślnie zabrania frameworków, klientów baz danych i sieci oraz operacji wejścia-wyjścia z biblioteki standardowej (sqlalchemy, fastapi, requests, subprocess...); extend-deny-libraries dopisuje wpisy do tej listy, deny-libraries ją zastępuje, a [tool.inwards.rules.pure-domain].deny zakazuje bibliotek prefiksom modułów, które nie są całymi warstwami. Własny kod zostaje dla INW001 i INW006. Zobacz Biblioteki w warstwach |
domyślnie | błąd | wszystkie | |
| INW010 | unknown-first-party |
Import statyczny w warstwie, który wskazuje własny moduł, który nie istnieje, typowa halucynacja agenta (from shop.domain.pricing import X bez żadnego pricing), oraz import względny, który wychodzi ponad pakiet najwyższego poziomu, czego Python nigdy nie przyjmuje. Sprawdzana jest część będąca modułem: X w from X import name, a w pozostałych przypadkach cała nazwa. Istnienie jest sondowane na dysku, więc liczą się pakiety przestrzeni nazw, zaślepki i skompilowane moduły rozszerzeń (.so, .pyd, .pyx); poprawka wymienia trzy najbliższe moduły z tego samego pakietu. Taki import nie dostaje dodatkowo INW006, a import skierowany na zewnątrz, który zgłasza INW001, nie dostaje INW010. Pakiety, które rozszerzają swój __path__, są pomijane, podobnie jak moduły zapisywane przez krok budowania (generated, domyślnie *_pb2, *_pb2_grpc i _version, ADR-029). Zobacz ADR-025 |
domyślnie | błąd | wszystkie | |
| INW011 | dynamic-import |
Import dynamiczny z celem w postaci literału napisowego, który sięga do warstwy zewnętrznej: importlib.import_module, __import__ (także builtins. i importlib.), runpy.run_module oraz instrukcje importu wewnątrz dosłownego kodu dla exec / eval / compile (bajty, których zadeklarowanego kodowania Inwards nie umie czytać, są zgłaszane jako niesprawdzone). Śledzone są aliasy importów, przypisania name = loader, getattr(m, "name"), m.__dict__["name"] i vars(m)["name"]; + między literałami i f-stringi z dosłownymi polami są składane. W każdej warstwie poza najbardziej zewnętrzną cel, którego Inwards nie umie odczytać, jest zgłaszany jako niesprawdzalny: zmienna, pole f-stringa, sekwencja \N{...}, argument ukryty za *args albo **kwargs, względne import_module z nieznanym package, exec albo eval z niedosłownym kodem (ADR-026). Popularny sposób obejścia INW001. Znane luki są wymienione wyżej |
domyślnie | błąd | wszystkie | |
| INW021 | private-module |
Import, spoza pakietu, własnego modułu, którego nazwa poniżej pakietu najwyższego poziomu zaczyna się od jednego podkreślenia (nazwy dunder nie są prywatne): moduł jest prywatny dla rodzica najgłębszego takiego segmentu, więc __init__.py pakietu, jego moduły i podpakiety mogą go importować. Ten sam zbiór importów co w INW003, łącznie z TYPE_CHECKING i odczytywalnymi importami dynamicznymi; import, który INW003 zgłasza w tej samej linii, dostaje tylko INW003. Czyta też moduły poza warstwami, w tym skrypty; moduły testowe pomija, chyba że check-tests |
opt-in | błąd | vertical-slices |
Układ¶
| Kod | Nazwa | Co wyłapuje | Włączona | Poziom | Włącza ją preset | Stan |
|---|---|---|---|---|---|---|
| INW006 | unassigned-module |
Import z warstwy do własnego kodu, który nie należy do żadnej warstwy, w tym do pakietu nad warstwami (from shop import x uruchamia shop/__init__.py, który nie należy do żadnej warstwy), statyczny albo dynamiczny (błąd); kod warstwy przeniesiony w trakcie sesji poza wszystkie warstwy (błąd); pakiet poza wszystkimi warstwami i poza ignore (ostrzeżenie); prefiks warstwy, który nie pasuje do żadnego modułu (ostrzeżenie), warstwa bez żywego prefiksu albo prefiks opróżniony w trakcie sesji (błąd). Nieznane klucze i nakładające się prefiksy to błędy konfiguracji |
domyślnie | błąd, część diagnostyk to ostrzeżenia | wszystkie | |
| INW007 | package-shape |
Element pakietu, na który [[tool.inwards.shape]] nie pozwala (błąd albo ostrzeżenie przy extra = "warning") albo którego zabrania, taki jak nowy helpers.py obok service.py; nazwa elementu poza jej pakietami only-in z [[tool.inwards.names]], taka jak test_x.py w aplikacji (błąd); selektor kształtu, który nie pasuje do żadnego pakietu (ostrzeżenie, w pyproject.toml). Komunikat nigdy nie wypisuje dozwolonych elementów; poprawka podaje prawdopodobny cel. Zobacz Kształt pakietu |
domyślnie | błąd, część diagnostyk to ostrzeżenia | wszystkie | |
| INW008 | missing-member |
Brakuje elementu, którego wymaga kształt pakietu; zgłaszane w jego __init__.py. Uruchomienia dla całego projektu zgłaszają każdy taki brak; Stop gate blokuje tylko te, które pojawiły się od początku sesji |
domyślnie | błąd | wszystkie |
Role¶
| Kod | Nazwa | Co wyłapuje | Włączona | Poziom | Włącza ją preset | Stan |
|---|---|---|---|---|---|---|
| INW012 | thin-endpoint |
Endpoint HTTP (operacja ścieżki FastAPI, trasa Flaska, handler Litestara, widok albo akcja ViewSet Django lub DRF, handler rejestrowany przez add_api_route, add_url_rule, Route albo path, albo funkcja oznaczona skonfigurowanym dekoratorem lub metoda skonfigurowanej klasy bazowej), który sam wykonuje pracę, wliczając funkcje pomocnicze z tego samego modułu, które wywołuje: więcej instrukcji, gałęzi albo zagnieżdżeń niż jego limity (warunki ochronne, które rzucają błąd HTTP albo wywołują abort z Flaska, się nie liczą), pętle po danych, wywołania wskazane w deny-calls (httpx.*, sqlalchemy.*...), wywołania metod na parametrze otypowanym jako sesja bazy danych albo, z delegate-to, brak wywołania wskazanej warstwy. Jedno zgłoszenie na endpoint, na linii jego def (na rejestracji, gdy handler pochodzi z innego modułu), z każdym przekroczonym sygnałem i wliczonymi funkcjami pomocniczymi; poprawka nazywa warstwę docelową. Jedno parsowanie na plik, który wspomina rozpoznawany framework (albo te, które wymienia frameworks), skonfigurowany dekorator albo klasę bazową, bez względu na warstwę (poza modułami testowymi), plus moduł handlera, gdy wskazuje go rejestracja, i moduł klasy bazowej widoku, gdy klasa po niej dziedziczy |
opt-in | ostrzeżenie | fastapi | |
| INW013 | async-blocking |
Wewnątrz async def (bez ciał zagnieżdżonych def, lambda i class) wywołanie metody na blokującym odbiorcy (parametrze z adnotacją albo nazwie przypisanej z wywołania synchronicznego Session lub Engine SQLAlchemy, redis.Redis, klienta boto3 albo połączenia DB-API, plus extend-blocking-types) albo wywołanie, które samo blokuje (connect sterownika, plus extend-blocking-calls). Jedno zgłoszenie na wywołanie; poprawka podaje klienta asynchronicznego, wariant z def i wątek roboczy. Jedno parsowanie na plik, który ma async def i wymienia blokującą bibliotekę albo skonfigurowany wzorzec, niezależnie od warstwy (poza modułami testowymi) |
opt-in | błąd | fastapi | |
| INW014 | ports-abstract |
W module portu (takim, który ma segment ports, albo w modułach wybranych przez modules) klasa najwyższego poziomu, która nie jest ani ABC (abc.ABC, metaclass=ABCMeta), ani typing.Protocol, i nie rozszerza klasy z modułu portu; metoda ABC albo Protocol, której ciało zawiera coś więcej niż docstring, ..., pass albo raise NotImplementedError; oraz podklasa portu, która go implementuje. Wyjątki, modele Pydantic, named tuple, typed dict, enumy i dataclassy są zwolnione (allow-bases, extend-allow-bases, allow-decorators). Poprawka wskazuje warstwę adapterów. Jedno parsowanie na moduł portu, który zawiera class |
opt-in | błąd | clean, hexagonal | |
| INW015 | construct-only-in |
W module, który należy do warstwy, a nie do role ani allowed-in: import modułu z role, który się wykonuje (bez ciał if TYPE_CHECKING:), na importowanej nazwie, oraz wywołanie budujące klasę zdefiniowaną w roli, osiągniętą przez moduł spoza niej (reeksport albo atrybut modułu), na wywołaniu. Import, który INW001 zgłasza jako skierowany na zewnątrz, dostaje tylko INW001. Jedno parsowanie na taki plik z importem, plus moduły roli, allowed-in i pakiety nad rolą, gdy prowadzi tam wywołanie nazwy pisanej wielką literą |
opt-in | błąd | clean, hexagonal | |
| INW016 | orm-naming |
Nazwy tabel (__tablename__, Table("name", ...), nazwa, którą SQLModel wyprowadza dla klasy table=True, Meta.db_table w Django), które nie są w lower_case_snake albo, przy table-name = "snake_singular" (domyślnie), wyglądają na angielską liczbę mnogą; oraz kolumny daty i czasu (Mapped[datetime], mapped_column/Column z DateTime, TIMESTAMP albo Date, pola SQLModel, DateTimeField/DateField z Django), których nazwa w bazie nie kończy się na datetime-suffix (_at) albo date-suffix (_date). Jedno zgłoszenie na nazwę, z nazwą do użycia. Uruchomienie dla całego projektu dodaje jedno zgłoszenie, gdy projekt ma tabele SQLAlchemy i nigdzie nie ma MetaData(naming_convention=...). Jedno parsowanie na plik, którego tekst wymienia konstrukcję ORM, niezależnie od warstwy (poza modułami testowymi) |
opt-in | błąd | fastapi | |
| INW020 | adapter-conformance |
W module adaptera (z segmentem adapters albo wybranym przez modules) publiczna klasa najwyższego poziomu bez linii zgodności, czyli przypisania z adnotacją _: Port = Adapter(...) albo _: type[Port] = Adapter w bloku if TYPE_CHECKING: na poziomie modułu, zgłaszana na nazwie klasy; taka linia poza tym blokiem, zgłaszana na linii; oraz, gdy ustawiono ports, adnotacja, która nie jest klasą z tych modułów (Any i object zawsze), zgłaszana na adnotacji. Klasy Protocol, ABC, klasy prywatne i rekordy zwolnione jak w INW014 nie potrzebują linii. Jedno parsowanie na moduł adaptera, w którym jest class |
opt-in | błąd | clean, hexagonal |
Integralność¶
| Kod | Nazwa | Co wyłapuje | Włączona | Poziom | Włącza ją preset | Stan |
|---|---|---|---|---|---|---|
| INW000 | unsupported-encoding |
Plik w warstwie deklaruje kodowanie (PEP 263), takie jak unicode_escape albo utf-7, przy którym tekst, który Inwards czyta jako komentarz, może być dla CPythona prawdziwym importem. Plik jest zgłaszany, a nie pomijany |
domyślnie | błąd | wszystkie | |
| INW009 | suppression-comment |
Wyciszenie w linii, # inwards: ignore[INW001] reason="...", które niczego nie ukrywa: w złej postaci, bez powodu albo z kodem nieznanym lub takim, którego nie da się wyciszyć (INW000, INW004, INW007, INW008, INW009, INW017) (błąd); wyciszenie z kodem, który nie pasuje do żadnej diagnostyki w jego linii (ostrzeżenie). Zobacz ADR-028 |
domyślnie | błąd, nieużywane to ostrzeżenia | wszystkie |
Diagramy¶
| Kod | Nazwa | Co wyłapuje | Włączona | Poziom | Włącza ją preset | Stan |
|---|---|---|---|---|---|---|
| INW017 | diagram-unknown-name |
W plikach Markdown i .mmd, które wymienia diagrams, flowchart albo graph w Mermaid oznaczony %% inwards: layers lub %% inwards: contexts: identyfikator węzła, który nie nazywa żadnej zadeklarowanej warstwy (albo, w diagramie kontekstów, węzeł lub subgraph na najwyższym poziomie, który nie nazywa żadnego kontekstu), i etykieta w cudzysłowie, do której nie pasuje żaden moduł. Węzły :::external są pomijane. Przy sprawdzaniu całego projektu oraz dla wymienionego pliku, który jest edytowany (hook), otwarty (serwer języka) albo podany (check_files, inwards check FILE); CLI czyta pliki, a engine/diagrams.ts przekazuje je do ręcznie napisanego parsera linii w diagram/. Wpis diagrams, do którego nie pasuje żaden plik, jest zgłaszany w pyproject.toml. Zobacz ADR-045 |
opt-in | ostrzeżenie | żaden | |
| INW018 | diagram-forbidden-edge |
W tych samych oznaczonych diagramach ciągła albo pogrubiona strzałka w jedną stronę, która rysuje import zabroniony przez konfigurację: na zewnątrz albo między warstwami sąsiednimi w diagramie warstw; w diagramie kontekstów z jednego kontekstu do innego, którego nie wymienia jego depends-on, albo do węzła, którego etykieta w cudzysłowie nie jest jednym z modułów public tamtego kontekstu. Przerywane krawędzie i węzły :::external są pomijane; każda zabroniona para jest zgłaszana raz na diagram. inwards import-diagram zapisuje konfigurację, którą przedstawia diagram. Zob. ADR-045 |
opt-in | ostrzeżenie | żaden | |
| INW019 | diagram-missing-dependency |
W tych samych oznaczonych diagramach import w kodzie z jednej narysowanej warstwy albo jednego kontekstu do innego, dla którego nie ma strzałki: w diagramie warstw brak ścieżki strzałek, w diagramie kontekstów brak bezpośredniej strzałki. Importy pod if TYPE_CHECKING: się liczą. Uruchamia ją silnik na importach, które uruchomienie dla całego projektu zbiera dla INW004 (engine/diagrams.ts), i potwierdza plik zgłoszenia pełnym parsowaniem; każda brakująca para jest zgłaszana raz na diagram, przy węźle, który importuje. Zob. ADR-045 |
opt-in | ostrzeżenie | żaden |
FastAPI¶
| Kod | Nazwa | Co wyłapuje | Włączona | Poziom | Włącza ją preset | Stan |
|---|---|---|---|---|---|---|
| FAPI001 | endpoint-metadata |
Operacja ścieżki FastAPI w schemacie bez metadanych OpenAPI, których wymaga projekt: podsumowania albo docstringu, modelu odpowiedzi, jawnego kodu statusu dla POST i DELETE, pola description w każdym wpisie responses=, a opcjonalnie tagów i operation_id. Jedna diagnostyka na endpoint, na dekoratorze |
opt-in | błąd | fastapi | |
| FAPI002 | undocumented-error-response |
Operacja ścieżki FastAPI, która może zwrócić kod błędu niezadeklarowany w jej wpisie OpenAPI: rzucony albo zwrócony w endpoincie, w funkcjach pomocniczych i zależnościach z tego samego pliku albo importowanych, do max-depth wywołań, albo z własnego wyjątku, który handler aplikacji zamienia na kod. Liczą się deklaracje na dekoratorze, routerze, dołączeniach nad nim i aplikacji; wszystko, czego Inwards nie umie odczytać, ucisza regułę |
opt-in | błąd | fastapi | |
| FAPI003 | router-wiring |
APIRouter z trasami, do którego żadna aplikacja nie dochodzi przez include_router ani mount (ostrzeżenie, gdy któregoś include_router nie da się rozwiązać), routery dołączające się nawzajem w cyklu oraz include_router nad trasami dołączanego routera w jednym pliku. Nazwy są rozwiązywane między plikami przez model; graf aplikacji i routerów (rules/fastapi/model/_graph.ts) powstaje tylko wtedy, gdy sprawdzany plik zawiera router albo dołączenie. Hook edycji zgłasza tylko przypadki z jednego pliku; Stop gate zgłasza niepodpięte routery, które sesja utworzyła lub zmieniła |
opt-in | błąd | fastapi | |
| FAPI005 | route-shadowing |
Operacja ścieżki, na którą wcześniejsza z tą samą metodą już odpowiada: /users/{id} nad /users/me albo ta sama metoda i ścieżka dwa razy, na jednym routerze albo między routerami, które aplikacja dołącza z literalnymi prefiksami (pełne ścieżki, w kolejności, w jakiej FastAPI trzyma trasy). Wszystko, co nie jest literałem (ścieżka, lista metod, prefiks), jest pomijane. Hook edycji zgłasza tylko przypadki z jednego routera; te między routerami czekają na Stop gate |
opt-in | błąd | fastapi | |
| FAPI006 | lifespan-events |
Przestarzałe zdarzenie startu lub zamknięcia: @app.on_event(...), add_event_handler albo on_startup= / on_shutdown= (ostrzeżenie) oraz to samo na aplikacji, albo na routerze dołączanym przez aplikację, która ustawia lifespan=, gdzie FastAPI nigdy nie uruchomi handlera (błąd). Odbiorca jest rozwiązywany między plikami przez model. Hook edycji nie wchodzi w górę grafu routerów, więc handler routera zostaje tam ostrzeżeniem |
opt-in | błąd | fastapi | |
| FAPI007 | yield-dependency-swallows |
Funkcja generatora z kodu projektu, w której yield stoi w try z klauzulą except niezgłaszającą wyjątku na każdej ścieżce, więc błąd z endpointu jest ukryty przed serwerem. Jedna funkcja naraz, bez modelu i bez grafu wywołań; menedżery kontekstu i fixtures są pomijane. Zgłoszenie stoi na linii except |
opt-in | błąd | fastapi | |
| FAPI008 | duplicate-operation-id |
Operacja ścieżki, której literalne operation_id= ma już wcześniejsza operacja tej samej aplikacji, także między routerami, które aplikacja dołącza (te same listy tras czyta FAPI005). To samo id w dwóch aplikacjach jest w porządku; id, które nie są literałami, są pomijane. Hook edycji zgłasza tylko przypadki z jednego routera |
opt-in | błąd | fastapi | |
| FAPI009 | depends-called |
Depends(f(...)) albo Security(f(...)), w wartości domyślnej albo w Annotated[...], gdzie f jest generatorem z kodu projektu, funkcją async def albo funkcją zwracającą wyłącznie zwykłe wartości; fabryka zwracająca funkcję przechodzi. Wywoływana funkcja jest rozwiązywana w tym samym pliku albo o jeden skok przez indeks |
opt-in | błąd | fastapi |
Reguły FAPI (ADR-037) czytają jeden wspólny model, rules/fastapi/model/: aplikacje i routery, operacje ścieżek, krawędzie include_router i mount oraz handlery wyjątków, z jednego parsowania każdego pliku, który wspomina FastAPI, z nazwami rozwiązywanymi między plikami przez ProjectIndex. FAPI004 jest nieużywany: spike #185 wypadł na nie (indeks reguł).
Mapa kodu¶
src/
├── core/ # engine, no I/O; src/core/AGENTS.md explains the rules
│ ├── src/
│ │ ├── index.ts # the public API adapters import (pinned by test/api.test.ts)
│ │ ├── shared/ # the kernel every slice builds on; imports only flow down
│ │ │ ├── contracts/ # records.ts: SourceFile, ImportRef, Diagnostic, Fix, Span, Suppressed
│ │ │ ├── meta/ # product.ts (VERSION, DOCS_BASE), registry.ts (every rule's code,
│ │ │ │ # name, severity, docs link; diagnostic())
│ │ │ ├── python/ # parser.ts (tree-sitter, import extraction), module-names.ts,
│ │ │ │ # prescan.ts (import skeleton), encoding.ts (PEP 263),
│ │ │ │ # literals.ts, stdlib.ts, qualify.ts (names through imports)
│ │ │ │ # private-modules.ts (which module names are private)
│ │ │ ├── lookup/ # project-index.ts (the engine's project input), module-lookup.ts,
│ │ │ │ # directory-listing.ts (the ListDir and ListMembers ports)
│ │ ├── config/ # [tool.inwards]: index.ts (the face; the modules are _x.ts), _parse.ts, _rule-settings.ts ([tool.inwards.rules]),
│ │ │ # _shape.ts (shapes and names), _generated.ts, _glob.ts (fnmatch),
│ │ │ # _diagrams.ts (the diagrams key and its path globs),
│ │ │ # _source-span.ts (where a value sits in pyproject.toml), _toml.ts
│ │ ├── diagram/ # index.ts (the face; the modules are _x.ts), _blocks.ts (marked Mermaid blocks in Markdown and .mmd files),
│ │ │ # _mermaid.ts and _syntax.ts (the flowchart reader), _plantuml-blocks.ts,
│ │ │ # _plantuml.ts, _plantuml-syntax.ts and _plantuml-skip.ts (PlantUML),
│ │ │ # _read.ts (either reader), _model.ts, _names.ts (what a node names),
│ │ │ # _notation.ts (hints in the diagram's syntax), _draft.ts (import-diagram),
│ │ │ # _write.ts (the config as a diagram, for inwards diagram)
│ │ ├── rules/ # one folder per rule, named after it: index.ts its face, the logic in _ modules; they share only shared/
│ │ │ ├── shared/ # layer-ownership.ts (owning layer, outward imports, port steps),
│ │ │ │ # edit-distance.ts, words.ts (English lists), entry-matches.ts,
│ │ │ │ # annotations.ts, classes.ts (top-level classes, exempt bases)
│ │ │ │ # (parameter types), name-patterns.ts (fnmatch-style names)
│ │ │ ├── unsupported-encoding/ # INW000
│ │ │ ├── layer-dependency/ # INW001 + fix composer
│ │ │ ├── context-independence/ # INW002: depends-on between contexts
│ │ │ ├── public-api-only/ # INW003: a context's public modules
│ │ │ ├── private-module.ts # INW021: _private modules stay in their package
│ │ │ ├── import-cycles/ # INW004: graph.ts (Tarjan, shortest cycle), cycles.ts
│ │ │ ├── pure-domain/ # INW005: libraries per layer, default deny list
│ │ │ ├── unassigned-module/ # INW006: imports.ts (code outside every layer),
│ │ │ │ # layout.ts (dead prefixes, layer code moved away)
│ │ │ ├── package-shape/ # INW007 + INW008: shape.ts, fix.ts (wording, likely target)
│ │ │ ├── suppression-comment/# INW009: inline suppressions, what they hide
│ │ │ ├── unknown-first-party/# INW010: first-party modules that don't exist
│ │ │ ├── dynamic-import/ # INW011: imports.ts, callees.ts (loaders through aliases),
│ │ │ │ # loader-targets.ts, computed-source.ts
│ │ │ ├── thin-endpoint/ # INW012: endpoints.ts (decorators, registrations),
│ │ │ │ # frameworks.ts, classes.ts (view classes), view.ts,
│ │ │ │ # helpers.ts (same-module helpers), remote.ts (handlers
│ │ │ │ # and view bases in other modules), metrics.ts,
│ │ │ │ # signals.ts, wording.ts, check.ts
│ │ │ ├── adapter-conformance/ # INW020: lines.ts (adapter classes, conformance lines),
│ │ │ │ # wording.ts, check.ts
│ │ │ ├── async-blocking/ # INW013: catalog.ts (blocking families), receivers.ts,
│ │ │ │ # check.ts
│ │ │ ├── construct-only-in/ # INW015: check.ts (imports), calls.ts (constructions
│ │ │ │ # through re-exports), wording.ts
│ │ │ ├── orm-naming/ # INW016: models.ts (tables and date columns), types.ts,
│ │ │ │ # names.ts, check.ts
│ │ │ ├── ports-abstract/ # INW014: classes.ts (class kinds, method bodies),
│ │ │ │ # wording.ts, check.ts
│ │ │ ├── diagram-unknown-name/ # INW017: check.ts (names in marked diagrams)
│ │ │ ├── diagram-forbidden-edge/ # INW018: check.ts (arrows in marked diagrams)
│ │ │ ├── diagram-missing-dependency/ # INW019: check.ts (imports a diagram leaves out)
│ │ │ └── fastapi/ # FAPI family: index.ts (the face), model/ (the shared
│ │ │ # model: _model.ts (apps, routers, operations, wiring,
│ │ │ # handlers, events, dependencies, resolved across files),
│ │ │ # _graph.ts (app and router graph), _route-list.ts (routes
│ │ │ # in FastAPI's order) with _path-match.ts, _project.ts,
│ │ │ # _records.ts and the extraction it builds on), then
│ │ │ # _router-wiring.ts (FAPI003), _endpoint-metadata.ts
│ │ │ # (FAPI001), _undocumented-error-response.ts with
│ │ │ # _error-codes.ts and _placement.ts (FAPI002),
│ │ │ # _route-shadowing.ts (FAPI005), _lifespan-events.ts
│ │ │ # (FAPI006), _yield-dependency-swallows.ts (FAPI007),
│ │ │ # _duplicate-operation-id.ts (FAPI008),
│ │ │ # _depends-called.ts (FAPI009), _function-body.ts (a
│ │ │ # function's own yield, return and try statements)
│ │ ├── baseline/ # index.ts (the face), _accepted.ts: baseline keys, which findings a baseline accepts
│ │ ├── engine/ # engine.ts: the facade, rule precedence, the baseline shortcut;
│ │ │ # fastapi.ts runs the FAPI rules that are on, thin-endpoint.ts INW012,
│ │ │ # content-rules.ts INW013 to INW015 and INW020, orm-naming.ts INW016,
│ │ │ # diagrams.ts INW017 to INW019,
│ │ │ # package-boundaries.ts INW002, INW003 and INW021
│ │ └── report/ # index.ts (the face), _render.ts: text / concise / json / sarif; _github.ts
│ ├── scripts/ # prescan-diff.ts: the differential test
│ └── test/ # mirrors src/, plus api.test.ts and architecture.test.ts
├── cli/
│ ├── src/ # one folder per concern (#176); src/cli/AGENTS.md explains the rules
│ │ ├── main.ts # argv, then a command with the wired adapters
│ │ ├── compose.ts # wires the adapters into AppDeps (the composition root with main.ts)
│ │ ├── deps.ts # AppDeps and DaemonDeps
│ │ ├── usage.ts # the help text
│ │ ├── hook/ # inwards hook claude-code, a slice (ADR-047): index.ts its face, cli.ts
│ │ │ # the handler, contracts.ts (HookDeps, hookProject, ASK_USER and the
│ │ │ # session record's shapes; the eval reads it); claude-code/ the hook
│ │ │ # adapter: dispatch, SessionStart, the PreToolUse config guard (Bash
│ │ │ # reader, edit simulation) and shape guard, PostToolUse, the Stop gate
│ │ │ # and its changed-file checks, escalation, settings; session/ commands/
│ │ │ # (the session record, start record, copies, start identity) and
│ │ │ # queries/ (old errors, agent suppressions, layout changes, committed
│ │ │ # config, start content); daemon/ inwards daemon, a mini-domain: the wire format,
│ │ │ # the hook's side, request handling, the in-memory caches;
│ │ │ # adapters/ the state files and the daemon's socket, lock and record
│ │ ├── check/ # inwards check and inwards baseline, a slice (ADR-047): index.ts its
│ │ │ # face, cli.ts the two handlers, running a check (_check.ts), config
│ │ │ # discovery, routing (_routing.ts, _runs.ts), project snapshots, texts
│ │ │ # laid over the disk (_overlay.ts), the whole-project checks
│ │ │ # (_whole.ts), the diagram files `diagrams` lists (_diagrams.ts),
│ │ │ # violation fingerprints (_fingerprint.ts); baseline/ the baseline
│ │ │ # file; adapters/ the baseline writer, the extraction cache and the
│ │ │ # worker threads
│ │ ├── runlog/ # the opt-in run log, a slice (ADR-047): index.ts its face, cli.ts
│ │ │ # inwards stats, commands/ writes the log, queries/ reads it back
│ │ │ # (stats, --export), adapters/ the export key and file
│ │ ├── lsp/ # inwards server, a slice (ADR-047): index.ts re-exports the handler
│ │ │ # (cli.ts); when to check what (_session.ts), the whole pass and
│ │ │ # the one-document check (_checks.ts), what each file shows;
│ │ │ # adapters/connection.ts is the LSP connection over stdio
│ │ ├── mcp/ # inwards mcp, a slice (ADR-047): index.ts re-exports the handler
│ │ │ # (cli.ts); the three tools (_check-files.ts, _explain-rule.ts,
│ │ │ # _where.ts), queued one at a time (_tools.ts);
│ │ │ # adapters/connection.ts is the MCP connection over stdio
│ │ ├── rules/ # inwards rules and inwards rule, a slice (ADR-047): index.ts its face,
│ │ │ # cli.ts the handlers, _sources.ts the config key behind each rule,
│ │ │ # page/ the rule-page reader mcp/ shares; adapters/pages.ts the
│ │ │ # embedded rule pages
│ │ ├── init/ # inwards init, a slice (ADR-047): index.ts its face, cli.ts the init
│ │ │ # handler; agents, --style, the scaffold plan, the report, presets
│ │ │ # (_*.ts); brief/ (inwards context), diagram/ (inwards diagram and
│ │ │ # import-diagram), import-linter/ (inwards import-config);
│ │ │ # adapters/ the init file writer and the style picker
│ │ └── shared/ # the kernel every slice may use
│ │ ├── paths/ # lexical path text, the physical meaning of `..`, display paths
│ │ ├── platform/ # the contracts for everything outside the process, and print()
│ │ ├── json/ # type guards for parsed JSON and TOML
│ │ └── adapters/ # node:fs, git, the environment, stdio, TOML, the grammars,
│ │ # the file walk, the .d.ts typings
│ └── test/ # mirrors src/, plus integration/ (E2E, docs) and support/
└── vscode-extension/
├── src/client/ # extension.ts (settings, starts inwards server), binary.ts (which
│ # binary), selector.ts
├── scripts/ # package-target.ts (a VSIX per platform), try-in-vscode.ts
└── test/ # the binary lookup, boundaries, packaged.test.ts on the built VSIX
Poza src/: scripts/ buduje pliki binarne i wheele oraz sprawdza wersje i nawigację dokumentacji, packaging/ zawiera README wheela i rezerwacje nazw, eval/ to środowisko ewaluacji agentów (z eval/app, warstwowym sklepem, na którym budują jego fixture'y), bench/ generuje syntetyczne repozytorium do benchmarków i porównuje na nim dwa buildy dla bramki regresji w PR-ach, a examples/clean-app to aplikacja w pionowych wycinkach, którą CI sprawdza najnowszym wydaniem, Ruffem i Ty.