Trzy prawdziwe bugi, w których kliknięcie nic nie robi — Claude mylił się za każdym razem, aż jedno dodatkowe zdanie w prompcie to unieruchamiało.
Bugi są dwóch rodzajów. Te, które rzucają błąd — dajesz Claude'owi stack trace i w 30 sekund masz odpowiedź. Te, które nie rzucają — przycisk, który nic nie robi, strona, która się nie rusza, formularz, który po cichu pada — na tych Claude myli się przy pierwszej próbie. Nie dlatego, że jest głupi. Bo nie widzi.
Ostatnio, składając flow płatności w how2claude, wpadłem na trzy takie bugi pod rząd. Oto post-mortem i wzorce promptów, których teraz używam do cichych bugów.
Podpiąłem płatności krypto x402. Lokalnie działało. Pierwszy klik na produkcji: invalid_string at payTo w konsoli. Signing flow nawet nie wystartował — schemat Zod facilitatora odrzucił request wcześniej.
Portfel to 42-znakowy adres 0x..., na oko nienaganny. Poprosiłem Claude'a, żeby sprawdził pole wallet w config/credentials/production.yml.enc:
w = Rails.application.credentials.dig(:x402, :wallet_address).to_s
puts "length: #{w.length}"
# => 43
43 znaki. Adresy EVM mają 42. 43. znak to chiński znak zapytania pełnej szerokości ? (U+FF1F), który wlazł podczas kopiuj-wklej z chińskiego input method.
Pierwszy skan Claude'a niczego nie zgłosił — dla niego to był string zaczynający się od 0x i wyglądający poprawnie. Nie policzył długości z własnej inicjatywy. Dodaj do promptu: „ten adres jest o jeden znak dłuższy niż oczekiwany — wypisz każdy codepoint osobno". Wyskakuje 0xFF1F, sprawa zamknięta.
Przycisk Subscribe na stronie cennika — klikasz, strona się nie rusza. Bez błędów. Zakładka network pokazywała, że POST idzie, Stripe zwraca 302 na checkout.stripe.com — a potem... nic.
Najpierw kazałem Claude'owi sprawdzić kontroler. Logika ok: redirect_to session.url, allow_other_host: true. JS — żaden relevantny listener.
W końcu zauważyłem nagłówek odpowiedzi: Content-Type: text/vnd.turbo-stream.html. Turbo przechwytywał submit button_to jako request Turbo Stream, a Turbo Stream nie podąża za cross-origin 302 — więc redirect był połykany, a strona cicho zostawała w miejscu.
Fix:
<%= button_to "Subscribe", ..., data: { turbo: false } %>
Ten sam bug trafił mnie ponownie miesiąc później przy przycisku Google OAuth. Interceptory na poziomie frameworka to żyzna gleba dla cichych bugów — Claude domyślnie rozumuje liniowo żądanie/odpowiedź i nie pójdzie szukać warstwy pośredniej, która przepisała semantykę. Dodaj do promptu: „przejdź przez każdy interceptor na poziomie frameworka, przez który idzie ten klik — wymień każdy middleware/warstwę JS, która przetwarza ten request po drodze browser → serwer → browser".
Stimulus controller przełącznika miesięcznego/rocznego na stronie cennika — klikasz przycisk, nic się nie przełącza. Metoda kontrolera odpalała (potwierdzone console.log), ale this.monthlyTarget był undefined.
Pierwszy strzał Claude'a: literówka w nazwie target. Nie była. data-pricing-target="monthly" był w DOM.
Problem tkwił w scope. data-controller="pricing" siedział na kontenerze przycisku przełącznika, a dwie sekcje grid leżały poza tym kontenerem. Stimulus szuka targetów tylko w poddrzewie elementu kontrolera; te na zewnątrz dla niego nie istnieją. Podniosłem data-controller na <section>, które opakowuje całość — naprawione.
Ten bug krzyczy „kod jest poprawny" — wszystkie nazwy się zgadzają, wszystkie atrybuty są, tylko funkcja nie działa. Claude domyślnie czyta kod linia po linii; nie pójdzie z własnej inicjatywy wizualizować struktury DOM. Dodaj do promptu: „narysuj drzewo przodków i potomków elementu z data-controller='pricing' — zaznacz, które data-pricing-target wpadają do poddrzewa, a które nie".
Wszystkie trzy bugi z zewnątrz wyglądały identycznie: klik, nic się nie dzieje, bez błędu. Claude za każdym razem mylił się przy pierwszym strzale, i za każdym razem jedno dodatkowe zdanie w prompcie go unieruchamiało. Wspólny wzorzec:
1. Powiedz mu ilościową różnicę między oczekiwanym a rzeczywistym — nie tylko „jest źle"
Nie „adres portfela ma problem", tylko „jest o jeden znak dłuższy niż oczekiwany".
Nie „przycisk nie działa", tylko „odpowiedź to 302, ale browser za nią nie poszedł".
Nie „przełącznik jest zepsuty", tylko „metoda kontrolera odpala, ale target jest undefined".
Im węższa delta, tym mniejsza przestrzeń poszukiwań Claude'a.
2. Skieruj go na niewidoczne warstwy — framework, browser, kodowanie
Ciche bugi prawie nigdy nie mieszkają w twoim kodzie biznesowym. Mieszkają w Turbo, w scope Stimulus, w kodowaniu znaków, w CSP, w CORS, w service worker'ach. Domyślnie Claude czyta twój kod. Powiedz mu wprost: idź i zajrzyj do tych warstw.
3. Proś o stan pośredni, nie o wnioski
„Wypisz każdy codepoint." „Wymień nagłówki odpowiedzi." „Zrób dump poddrzewa DOM." Materializuj stan pośredni zamiast prosić Claude'a, żeby rozumował do odpowiedzi. „Ciche" w cichym bugu to to, że jakiś krok w łańcuchu rozumowania opiera się na ukrytym założeniu, które nie jest spełnione. Materializacja stanu pośredniego to sposób, żeby wymusić wypłynięcie tego założenia.
Błędy sprawdzają, co Claude wie. Ciche bugi sprawdzają jakość sygnału, który mu dajesz. Im bardziej konkretnie, tym szybciej znajdzie odpowiedź.