Instrukcja to nie kontrakt

Reguły są zapisane, agent czyta je przy każdym promptowaniu, a i tak nie trafiają do kodu. Nic nie jest zepsute, bot nie ma czego zgłosić. Bo plik z regułami nie jest konfiguracją, tylko sugestią o nieznanej skuteczności.

Aug 18, 2026~6 min czytania
Instrukcja to nie kontrakt

Instrukcja to nie kontrakt

W commitach pojawia się wzmianka co-authored by, z nazwą modelu. Nikt tego nie ukrywa, wszyscy w zespole wiedzą, który fragment wyszedł z modelu, a który ktoś napisał sam.

Wydawałoby się, że to wszystko jest oczywiste. Wiem, co przeglądam, mam bota do code review, mam plik z regułami projektu, do którego agent ma dostęp przy każdym promptowaniu. Konfiguracja jest, reguły są, automatyzacja jest. Wszystko powinno przejść bez problemu.

I najlepsze, że przechodzi. Otwieram merge request, pipeline świeci się na zielono.

A potem zaczynam czytać kod i widzę, że żadna z tych reguł się nie zmaterializowała.

Trzy przykłady, w których reguła była zapisana

Testy jednostkowe. Ściana bloków describe, jeden pod drugim, testujących dokładnie to samo z inną wartością wejściową. W regułach projektu jest wprost: używaj it.each. Nie chodzi o żaden zysk czasowy przy uruchomieniu, testy pójdą tak samo szybko. Chodzi o to, że dodanie kolejnego przypadku ma być jedną linijką w tabeli, a nie kolejnym skopiowanym blokiem, który za pół roku ktoś będzie musiał przeczytać w całości, żeby się zorientować, że to wariant tego samego.

Iteracje po danych. Pracuję na widokach, gdzie zbiory danych idą w setki tysięcy rekordów, a na jednym ekranie żyje kilka tabel i wykresów naraz. Każda iteracja ma być możliwie jak najwydajniejsza. Algorytmika i performance nie są tu tematem z książek i kursów, tylko różnicą między płynnym a szarpiącym interfejsem.

W wygenerowanym kodzie dostaję łańcuch iteracji z wyszukiwaniem w środku. Wygląda mniej więcej tak:

ts
// Przykład napisany na potrzeby tego wpisu.
const enriched = orders.map(order => ({
  ...order,
  customerName:
    customers.find(customer => customer.id === order.customerId)?.name ?? '',
}));

Jest poprawny. Przechodzi testy, przechodzi typy, przechodzi review. I każdy z nas napisał to kiedyś ręcznie, bo to jest pierwsza rzecz, która przychodzi do głowy, gdy trzeba połączyć dwie listy.

Problem w tym, że find w środku map to wyszukiwanie liniowe wewnątrz pętli, czyli O(n·m). Przy dwóch kolekcjach po tysiąc pozycji robi się z tego milion porównań zamiast tysiąca. Przy rozmiarach danych, na których pracuję, to nie jest różnica, którą trzeba mierzyć, żeby zobaczyć.

Wystarczy zbudować indeks raz:

ts
const customerNames = new Map(
  customers.map(customer => [customer.id, customer.name]),
);

const enriched = orders.map(order => ({
  ...order,
  customerName: customerNames.get(order.customerId) ?? '',
}));

Ta sama logika, o linijkę więcej, inna klasa złożoności. Nie jest to żadna sztuczka ani wiedza tajemna, tylko decyzja, którą trzeba świadomie podjąć, znając skalę danych. A skali danych w diffie nie widać.

Potrójny operator warunkowy. W regułach jest zapisane, żeby go nie używać. Wraca. Działa, jest zwięzły, wygląda nawet nieźle, tylko że po trzech poziomach nikt już nie czyta warunku, tylko go odgaduje.

To nie jest tak, że model jest głupi

Za każdym razem kod przechodzi. Testy są zielone, typy się zgadzają, linter milczy, bot w review nie ma czego zgłosić. Nic nie jest zepsute i nikt nie złamał żadnej reguły w sensie, w jakim rozumie ją narzędzie.

Zwróć uwagę, co łączy wszystkie trzy przykłady. Blok describe to najczęściej pisany kształt testu. Wyszukiwanie w pętli to najbardziej naturalna rzecz, jaką się pisze, gdy trzeba połączyć dwie listy. Zagnieżdżony ternary to najczęściej spotykany zapis warunku w JSX.

Model nie wybrał źle. Model wybrał to, co najczęściej występuje, bo dokładnie tym jest. it.each, Map zamiast wyszukiwania i rozbicie warunku na osobne gałęzie wymagają czegoś więcej niż poprawności: trzeba wiedzieć, że alternatywa istnieje, i zdecydować, że akurat tutaj pasuje. Trzeba znać skalę danych, której w diffie nie widać, i wiedzieć, że ten plik ktoś będzie czytał za rok.

I tu jest rzecz, którą sam sobie ustawiłem w głowie źle. Wpisałem te reguły do pliku konfiguracyjnego i zacząłem je traktować jak konfigurację, czyli coś, co albo działa, albo się wywala z błędem. A to nie jest kontrakt. To jest obciążenie rozkładu. Podnosi prawdopodobieństwo, że dostanę to, o co proszę, i nic poza tym. Zakaz w tym pliku nie jest zakazem, tylko sugestią z pewną skutecznością, o której nikt ci nie powie, jaka jest.

To zmienia sens code review bardziej, niż mi się wcześniej wydawało. Jeżeli reguła jest gwarancją, to review sprawdza wyjątki. Jeżeli reguła jest tylko sugestią, to review jest jedynym miejscem, w którym ona się faktycznie egzekwuje.

Co z tym robię

Puszczam pełnego bota do code review jeszcze raz, ręcznie. Jak coś znajdzie, wrzucam raport do merge requesta. A jak merge request jest naprawdę duży, to po przejściu bota czytam go dodatkowo linijka po linijce.

Zielony bot przestał być dla mnie zgodą na merge. Stał się warunkiem wstępnym czytania. Bot mówi mi, że nie ma naruszeń, a nie że kod jest dobry, i przy dużym merge requeście to jest moment, w którym zaczynam czytać, a nie kończę.

Nie będę udawał, że to elegancki proces. To jest babysitting modelu i sam się na tym łapię, że myślę, czy nie powinienem tego czasu poświęcić na coś innego. Rzeczy do zrobienia zawsze jest więcej niż czasu, a ja siedzę i czytam kod, który maszyna wypluła w trzydzieści sekund.

Czy to nie jest over engineering

Długo nie miałem na to dobrej odpowiedzi. Miałem taką grzeczną, o odpowiedzialności za jakość, i sam w nią nie do końca wierzyłem.

Odpowiedź przyszła z zupełnie innej strony. Narzędzia AI mają downtime. I zamiast reakcji w stylu no trudno, nie działa, siadam i piszę sam, bo przecież umiem, pojawia się coś innego. Panika albo przestój. Nie działa od pół godziny, to poczekam, aż wróci.

Pół godziny. Nie dwa dni, nie tydzień. Pół godziny wystarczyło.

Sam się na tym złapałem i to było nieprzyjemne odkrycie.

Bo to jest ten rachunek. Branie tego, co najczęstsze, jest darmowe dopóty, dopóki narzędzie działa. Kod przechodzi, feature jedzie, nikt nie płaci. Umiejętność wyboru czegoś innego nie jest do niczego potrzebna, więc przestaje być używana. A downtime tylko pokazuje, że jej już nie ma.

Więc nie, nie uważam już, że to over engineering. Czytanie tego kodu linijka po linijce to na razie jedyny moment, w którym ten mięsień w ogóle pracuje.

Czy ten wpis był pomocny?

CLAUDE.md to nie kontrakt. Dlaczego reguły projektu nie działają | Code Nomad