Dostępny na nowe rolePiotr Czerwiński

Blog · 25 sierpnia 2026 · 10 min czytania

Zielony build to nie dowód: jak sprawdzam zmiany wygenerowane przez AI

agenci AI · Claude Code · code review · bezpieczeństwo · workflow

TL;DR: Automatyczny masowy refaktor, który przenosił zahardkodowane teksty do wywołań tłumaczeń, zostawił jedno wywołanie hooka Reacta na poziomie modułu, tuż za klamrą zamykającą komponent. Sprawdzanie typów przeszło, build produkcyjny przeszedł, zmiana poszła na produkcję, a cały panel administracyjny jednego z moich produktów przestał się otwierać. Linter wskazałby to w sekundę, ale nic go nie uruchomiło. Bramki, które dodałem: lint plików w staging w globalnym hooku pre-commit, wąski strażnik przed buildem dla reguł, których złamanie wywraca aplikację w runtime, przetestowany w obie strony, oraz nawyk uruchamiania aplikacji i otwierania każdego zmienionego ekranu po każdej masowej zmianie z automatu. To samo podejście obejmuje inne sposoby, w jakie psują się zmiany wygenerowane przez AI: nazwy paczek sprawdzam przed instalacją, „tego nie ma” wymaga dwóch niezależnych weryfikacji, a diff przegląda drugi agent ze świeżym kontekstem.

Zielony build, który położył panel administracyjny

Dużą część kodu w moich produktach piszą albo przekształcają dziś agenci kodujący, a sporo z tego przychodzi jako masowe zmiany: setki drobnych, mechanicznych edycji nakładanych przez skrypt. W tym agenci są najlepsi i właśnie tam najłatwiej ukrywa się jedno błędne wstawienie. Tym razem chodziło o internacjonalizację. Skrypt zamieniał dosłowne teksty na wywołania hooka tłumaczeń, a w jednym pliku wstawiona linia trafiła za klamrę zamykającą komponent zamiast do jego wnętrza:

// illustrative shape
export function DemandPanel() {
  const t = useTranslations();
  return <Section title={t.demand.title} />;
}
const labels = useTranslations().demand; // module level: builds fine, crashes on render

Wszystkie automaty mówiły „tak”. Sprawdzanie typów było czyste, build produkcyjny był czysty i deploy poszedł. A potem panel administracyjny w ogóle się nie otwierał, bo wywołanie hooka działało poza jakimkolwiek komponentem i rzucało wyjątek od razu przy ładowaniu modułu.

Dlaczego ani sprawdzanie typów, ani build tego nie złapały?

Bo nikt ich o to nie prosił. Sprawdzanie typów weryfikuje typy i składnię, a ta linia ma poprawne typy. Rules of hooks, czyli wymóg Reacta, żeby hooki wywoływać wyłącznie na najwyższym poziomie komponentu albo innego hooka, egzekwuje reguła lint, a nie kompilator. Build w tej konfiguracji nie uruchamia lintera. Wynik lintowania powiedziałby to wprost: hooka nie można wywołać na najwyższym poziomie modułu. Nikt go nie uruchomił.

To ogólna pułapka zmian wygenerowanych przez AI. Zielony build mówi, że kod ma poprawny kształt. Nie mówi nic o tym, czy działa poprawnie, a przez sito przechodzą właśnie te błędy, w których kształt i zachowanie się rozjeżdżają: reguły runtime, renderowanie, kolejność, wszystko, co istnieje dopiero wtedy, gdy kod działa.

Gdzie postawić bramkę?

Rozważyłem cztery miejsca, w których da się złapać tę klasę błędów.

  • W CI. Działa, ale płacisz minutami serwera i czekaniem na odpowiedź w sprawie, którą laptop rozstrzyga w sekundę, a deploy, który trzeba wycofać, może już być na produkcji.
  • Menedżer hooków osobny dla każdego repozytorium. Typowa odpowiedź. U mnie zastąpiłby globalną ścieżkę hooków, co po cichu wyłącza skaner sekretów, który uruchamiam przy każdym pushu. Odpada.
  • Pełny lint jako bramka buildu. Kuszące i błędne przy kodzie z istniejącym długiem lintowym. Przy mniej więcej siedemdziesięciu istniejących, niegroźnych błędach bramka byłaby czerwona od pierwszego dnia, a bramkę, która zawsze świeci na czerwono, wyłącza się w ciągu tygodnia. Czerwony musi znaczyć jedno: nie wdrażaj.
  • Lokalny hook pre-commit plus wąski strażnik przy buildzie. Szybko, za darmo, a do tego odpala się w momencie, w którym zepsuty plik trafia do commita.

Wybrałem ostatnią opcję, w dwóch warstwach. Pierwsza to globalny hook pre-commit, czyli skrypt, który git uruchamia przed zapisaniem commita. Lintuje tylko pliki w staging i przy błędach blokuje commit. Jest szybki, bo dotyka tylko tego, co się zmieniło, a błąd zawsze jest w pliku, nad którym właśnie pracujesz. W monorepo grupuje pliki według najbliższego configu lintera i uruchamia linter z tego katalogu, bo flat config jest wyszukiwany względem katalogu roboczego, a uruchomienie z roota po prostu nie znajduje configów aplikacji. Formatuje tylko w repozytoriach, które mają config formattera; narzucanie formatowania gdzie indziej przeformatowałoby cały projekt przy pierwszym commicie i zakopało właściwą zmianę w tysiącach linii diffa.

Druga warstwa to strażnik prebuild, który blokuje build przy krótkiej liście reguł, których złamanie kładzie stronę. Lista zaczęła się od jednej reguły, rules of hooks, a kolejna reguła trafia na nią tylko wtedy, gdy jej złamanie coś wywraca. Strażnik łapie commity, które ominęły hook, z innej maszyny albo z awaryjnym pominięciem. Jest przetestowany w obie strony: na czystym kodzie musi przejść, a na celowo zepsutym pliku musi paść. Nieprzetestowana bramka to rekwizyt. Ten sam test w obie strony stosuję do hooków bezpieczeństwa wokół mojego agenta kodującego.

Pierwsza wersja hooka pre-commit nie działała, a sposób, w jaki zawiodła, warto znać. Używała wbudowanego polecenia powłoki, które istnieje we współczesnym bashu, ale nie w starym bashu dostarczanym przez macOS jako powłoka systemowa, a to właśnie jego wywołuje git. Uruchomiony ręcznie z mojego terminala hook przechodził. Uruchomiony przez prawdziwy commit padał za każdym razem i blokował zupełnie czystą pracę. Hooki piszę teraz pod starą powłokę i testuję przez prawdziwy commit, nigdy przez bezpośrednie wywołanie skryptu, bo ręczne uruchomienie korzysta z innej powłoki, innej ścieżki i innego katalogu roboczego.

Dlaczego uruchamiać aplikację po masowym automatycznym refaktorze?

Lint łapie jedną regułę. Szersza lekcja jest taka, że skrypt wstawiający kod w setkach miejsc pomyli się gdzieś, czego nie przewidzisz, a build nie powie Ci, gdzie. Dlatego po każdej masowej zmianie z automatu uruchamiam aplikację i przed deployem otwieram każdy zmieniony ekran. To zajmuje kilka minut i jest jedynym sprawdzeniem, które pokazałoby zepsuty panel, zanim zobaczyli go użytkownicy.

To samo dotyczy twierdzeń o wyglądzie. Markup, który czyta się poprawnie, potrafi źle się renderować. Kiedyś HTML jednej funkcji wyglądał dobrze, a na ekranie pokazywał tylko mały ułamek treści, którą miał pokazywać, z przyciskiem nałożonym na nagłówek tabeli. Żadnego z tych problemów nie było widać w źródle. Działa to też w drugą stronę: zanim zgłoszę błąd wizualny na podstawie zrzutu ekranu, potwierdzam go wyliczonymi stylami i geometrią elementu, bo narzędzie do zrzutów, które przewija zawartość wewnątrz przyciętego kontenera, potrafi wyprodukować błąd, którego nie ma.

Jak sprawdzić paczkę zaproponowaną przez agenta AI?

Modele halucynują nazwy paczek, a atakujący rejestrują te wymyślone nazwy z wyprzedzeniem. To slopsquatting, odpowiednik z ery AI dla typosquattingu, w którym złośliwa paczka jest o jedną literówkę od popularnej. Oba to ataki na łańcuch dostaw: niebezpieczny kod przychodzi jako zależność, z pełnymi uprawnieniami Twojego procesu, na Twojej maszynie i na produkcji.

Moja zasada: żadna paczka nie wchodzi bez audytu, a audyt odbywa się przed instalacją, nie po niej. Zajmuje mniej więcej minutę: maintainerzy, link do repozytorium, licencja, status deprecated, data ostatniej publikacji i tygodniowa liczba pobrań z rejestru. Sygnały ostrzegawcze to nazwa podejrzanie podobna do znanej paczki, jeden anonimowy maintainer, brak repozytorium i skrypty instalacyjne, które uruchamiają kod, zanim cokolwiek zbudujesz. Pierwsze pytanie brzmi zawsze: czy ta paczka jest w ogóle potrzebna? Kilkanaście linii własnego kodu wygrywa z nową zależnością z dużym drzewem.

Tej kolejności nauczyłem się bezboleśnie. Dodałem kiedyś renderer markdownu z pluginem do tabel i sprawdziłem je dopiero, gdy ktoś o to zapytał. Wynik był czysty, ale kolejność była zła: przy podatnej albo porzuconej paczce dowiedziałbym się o tym, gdy już była w projekcie. Audyt musi też obejmować model bezpieczeństwa paczki, nie tylko jej CVE. Ten renderer domyślnie escapuje surowy HTML, a osobny plugin to wyłącza. Gdy na wejściu jest output z LLM, właśnie o to ustawienie domyślne chodzi, więc jest test, który padnie, jeśli ktoś je usunie. O drugiej połowie higieny zależności, czyli łataniu tego, co już masz, piszę w A CVE three levels deep: patching a transitive dependency with npm overrides (po angielsku).

Dlaczego „tego nie ma” to najdroższe twierdzenie?

Gdy agent mówi „to jeszcze nie jest zaimplementowane”, brzmi to niewinnie, a prowadzi prosto do planowania pracy, która jest już zrobiona. Przydarzyło mi się to: sesja szukała typu danych strukturalnych w podwójnym cudzysłowie, kod używał pojedynczego, wynik wyszukiwania był pusty, a wniosek brzmiał, że funkcję trzeba zbudować. Istniała na czterech poziomach stron i trafiła do commita dwa dni wcześniej.

Potwierdzenie, że czegoś nie ma, wymaga więcej niż jednego grepa. Najpierw szukaj gołego słowa kluczowego, bez cudzysłowów i interpunkcji, a dopiero potem zawężaj. Sprawdź historię gita, bo opisy commitów zwykle mówią wprost, kiedy coś dodano. A gdy człowiek mówi, że coś istnieje, a wyszukiwanie tego nie znajduje, załóż, że błędne jest wyszukiwanie, i szukaj dalej, zanim go poprawisz.

Drugi agent jako recenzent i co powiedziałbym Ci jutro

Agent, który napisał zmianę, ma te same martwe pola co ona: wie, co miał na myśli, więc czyta to, co miał na myśli. Bardziej ufam przeglądowi drugiego agenta w świeżej sesji, który widzi tylko diff i specyfikację, najlepiej na innym modelu, bo inny model popełnia inne błędy i zauważa inne. Jego checklista jest krótka: testy przechodzą w głównym drzewie roboczym, zakres zgadza się ze specyfikacją i nic poza nim się nie zmieniło, a konwencje projektu zostały zachowane. Ograniczenie jest takie, że przegląd to wciąż czytanie. Recenzent mógł zauważyć hook poza komponentem; pewność dałoby dopiero uruchomienie lintera albo aplikacji. Więcej o tym, jak to wpisuje się w codzienną pracę, w tekście o ośmiu miesiącach prowadzenia produktu z agentami jako współinżynierami.

Dlaczego wciąż sam czytam każdy pull request?

Agenci otwierają pull requesty; nigdy ich nie mergują. Ostatnią bramką jest człowiek, a w moich produktach tym człowiekiem jestem ja. To właśnie oznacza człowiek w pętli (human in the loop): agent wykonuje masę pracy, a osoba odpowiedzialna za wynik decyduje, co trafia na produkcję. Zielone checki to warunek wstępny mojego przeglądu, nigdy jego zamiennik.

Moje czytanie diffa to nie drugie podejście do literówek. Sprawdzam go względem spisanych standardów, które dostali agenci: czy route waliduje i deleguje, czy wyciekła do niego logika biznesowa; czy nowy helper nie jest trzecią kopią czegoś, co już istnieje; czy któraś funkcja nie przestała mieścić się na ekranie albo któryś plik nie urósł ponad kilkaset linii; czy reguły biznesowe są czystymi funkcjami, a efekty uboczne zostały zepchnięte na brzegi. Potem same testy: czy sprawdzają zachowanie, które zauważyłby użytkownik, i przypadki brzegowe psujące pieniądze albo dane, czy może są przywiązane do implementacji, więc następny refaktor zepsuje je bez powodu. Test, który sprawdza, że getter zwraca to, co ustawiono, to szum; test, który odtwarza błąd przed poprawką, to ten, którego chcę.

Testy mają warstwy i każda łapie inną klasę pomyłek: testy jednostkowe na reguły i przypadki brzegowe, testy integracyjne na ścieżki, które dotykają bazy danych i usług zewnętrznych, oraz testy end-to-end w Playwright na przepływy, na których polegają użytkownicy, czyli te, które psują się dopiero po złożeniu elementów w całość. Zestaw E2E to najbliższy odpowiednik użytkownika przeklikującego się przez produkt przed każdym wydaniem, a przepływ, który otwiera panel administracyjny, łapie dokładnie taki crash jak ten z początku tego tekstu. Merge robię dopiero wtedy, gdy diff czyta się dobrze, testy sprawdzają właściwe rzeczy, a zmienione ekrany działają w przeglądarce.

  • Nigdy nie pozwalaj agentowi robić merge. Zielone checki otwierają przegląd przez człowieka; nie zastępują go.
  • Traktuj zielony build jako sprawdzenie kształtu. Dowodzi poprawności typów i składni, a nie zachowania.
  • Postaw pierwszą bramkę tam, gdzie powstaje błąd. Lintuj pliki w staging przed commitem, a wąskiego strażnika przy buildzie trzymaj jako drugą linię.
  • Dbaj o to, żeby bramki coś znaczyły. Tylko reguły, których złamanie coś wywraca, żeby czerwony zawsze znaczył „stop”. Testuj każdą bramkę w obie strony, przez prawdziwy wyzwalacz.
  • Po masowych zmianach z automatu uruchom aplikację. Przed deployem otwórz każdy zmieniony ekran.
  • Sprawdź paczkę przed instalacją. Zwłaszcza taką, którą zaproponował model.
  • Wymagaj dwóch niezależnych weryfikacji dla „tego nie ma”. Fałszywie negatywny wynik kosztuje więcej niż minuta dodatkowego szukania.

Pytania, na które odpowiada ten wpis

Dlaczego build Next.js przechodzi, a strona pada w runtime?
Sprawdzanie typów i build produkcyjny weryfikują typy i składnię, a nie reguły działania Reacta w runtime. Hook wywołany poza komponentem kompiluje się i buduje bez problemu, a potem wywraca się przy ładowaniu modułu. Przed deployem łapie to tylko reguła lint rules-of-hooks.
Czym jest slopsquatting?
Slopsquatting to atak na łańcuch dostaw, w którym atakujący rejestrują nazwy paczek często halucynowane przez modele AI, więc programista instalujący zaproponowaną paczkę dostaje złośliwy kod. Obrona polega na potwierdzeniu, że paczka istnieje i jest tą właściwą, oraz na sprawdzeniu przed instalacją jej maintainerów, repozytorium, licencji, liczby pobrań i skryptów instalacyjnych.
Jak robić code review kodu napisanego przez agenta kodującego AI?
Traktuj zielony build jako sprawdzenie kształtu, a potem weryfikuj zachowanie: lintuj pliki w staging przed commitem, po masowych zmianach uruchom aplikację i otwórz każdy zmieniony ekran, a każdą nową zależność sprawdź przed instalacją. Drugi agent w świeżej sesji może porównać diff ze specyfikacją, a potem człowiek czyta pull request i robi merge dopiero wtedy, gdy jest pewny; agenci nigdy nie powinni sami mergować swojej pracy.