Mój pierwszy PR do open source — z pomocą Claude Code
Jak w dwie godziny otworzyłem trzy PR-y do GraphHoppera, używając Claude Code jako partnera do eksploracji nieznanego kodu.
Byłem na Warszawskich Dniach Informatyki. Na Stadionie Narodowym, co samo w sobie brzmi absurdalnie jeśli znasz moje marzenia raczej sportowe niż prelegenckie.
Dzień wcześniej odbyło się spotkanie prelegentów — polecam taki networking, bo właśnie tam można wyłapać fajne pomysły na inicjatywy. I tak się stało u mnie: między jedną rozmową a drugą jedna z osób rzuciła pomysł, który zapadł mi w pamięć: weź jakiś projekt open source, najlepiej coś realnego i dużego, i użyj go jako laboratorium do eksperymentowania z agentami AI. Nie własny projekt od zera, nie tutorial, lecz prawdziwy kod, prawdziwe standardy, prawdziwe code review.
Idea trafiła na podatny grunt. Lubię refaktoring. Od lat mam na liście “zacznij kontrybuować do open source”. A do tego: ta rewolucja AI w wytwarzaniu oprogramowania po prostu mnie wciąga i chcę ją zgłębiać na konkretnych przykładach. Trudno o lepsze połączenie trzech rzeczy naraz.
Dlaczego ten projekt?
Wybrałem GraphHopper — silnik routingu napisany w Javie. Dlaczego akurat ten? Przejrzałem listę najpopularniejszych repozytoriów w Javie na GitHubie i GraphHopper po prostu przykuł uwagę: routing, grafy, coś nietrywialnego domenowo. Nie wiedziałem nic o jego architekturze. I o to chodzi: chodziło mi o nieznany, prawdziwy kod, nie o projekt w którym czuję się bezpiecznie.
Pierwsze wejście w nieznany codebase
GraphHopper ma kilkanaście modułów. Skupiłem się na core, z dwóch powodów.
Pierwszy: domenowy.
Core to pewnie samo mięso, logika routingu bez warstw HTTP czy konfiguracji — to tam dzieje się coś ciekawego. Przynajmniej tak zakładam…
Drugi: bardziej pragmatyczny.
Nie chciałem wrzucać całego repo do kontekstu Claude’a i przepalać tokenów na kod, który być może mnie jednak nie zainteresuje. Wąski kontekst = lepsze odpowiedzi, mniejsze koszty.
Zamiast godzinami czytać kod, zadałem Claude Code proste pytanie: przeanalizuj strukturę modułu i opowiedz mi o tym co się tutaj dzieje, za co ten moduł odpowiada.
Agent opisał strukturę modułu, odpowiedzialności poszczególnych pakietów. Czytając to, sam zacząłem dostrzegać problemy. Klasa GraphHopper robi za dużo — czyta pliki z dysku, deserializuje JSON, ładuje custom areas i modele, a przy tym orkiestruje routing. Klasyczne naruszenie SRP. A to jeden z wielu dostrzeżonych problemów…
Trzy PR-y, dwie godziny
Tworzę kurs SSJ: Super Senior Java z Mateuszem Nowakiem i Arturem Laskowskim — powiedz nam, czego potrzebujesz. Więcej znajdziesz na SuperSeniorJava.pl!
Wypełnij ankietę →PR #3318 — walidacja headingów
W klasie Router znajdowała się logika walidacji, która operowała na danych należących do GHRequest: klasyczny Feature Envy. Router iterował po headingach z GHRequest i sam je walidował, zamiast zlecić to właścicielowi danych. I to wszystko bez żadnych testów pokrywających tę logikę.
// przed - Router sam grzebie w danych GHRequest i waliduje headingi
private void checkHeadings(GHRequest request) {
if (request.getHeadings().size() > 1 && request.getHeadings().size() != request.getPoints().size())
throw new IllegalArgumentException("The number of 'heading' parameters must be zero, one "
+ "or equal to the number of points (" + request.getPoints().size() + ")");
for (int i = 0; i < request.getHeadings().size(); i++)
if (!GHRequest.isAzimuthValue(request.getHeadings().get(i)))
throw new IllegalArgumentException("Heading for point " + i + " ...");
}
// po - logika wraca do GHRequest, Router deleguje jednym wywołaniem
request.checkHeadings();
// w GHRequest
public void checkHeadings() {
if (headings.size() > 1 && headings.size() != points.size())
throw new IllegalArgumentException("...");
for (int i = 0; i < headings.size(); i++)
if (!isHeadingInRange(i))
throw new IllegalArgumentException("Heading for point " + i + " must be in range [0,360) or NaN, but was: " + headings.get(i));
}
Finalnie to ja wskazałem problem. Agent napisał kod, testy do GHRequestTest, oznaczył starą metodę jako @Deprecated. Done!
PR #3322 — CustomModelResolver
Klasa GraphHopper zawierała statyczną metodę resolveCustomModelFiles, mieszającą I/O, deserializację JSON i logikę łączenia modeli bezpośrednio w silniku routingu. Testy wysokiego poziomu pewnie gdzieś to pokrywały — przynajmniej happy path, mam nadzieję — ale nie było żadnego jawnego wskazania, że ta konkretna logika jest dobrze przetestowana.
// przed - statyczna metoda schowana w GraphHopper
@Deprecated
public static List<Profile> resolveCustomModelFiles(...) { ... }
// po - dedykowana klasa z jedną odpowiedzialnością
CustomModelResolver resolver = new CustomModelResolver(customModelFolder, globalAreas);
List<Profile> profiles = resolver.resolveAll(ghConfig.getProfiles());
Podsumowując zmiany, została zachowana pełna kompatybilność wsteczna: stara metoda oznaczona jako @Deprecated. Deleguje logikę do nowej klasy. A ta zmiana pozwoliła na dodanie testów pokrywających wszystkie ścieżki ładowania modeli.
PR #3321 — CustomAreasProvider
Klasa GraphHopper sama czytała custom areas z dysku — czytała pliki GeoJSON, deserializowała je przez Jacksona i konwertowała do obiektów CustomArea. Wszystko w środku silnika routingu, jako prywatna metoda. Dwa konkretne problemy: po pierwsze, przetestowanie tego kawałka kodu nie było trywialne — trzeba było instancjonować dużą liczbę klas. Po drugie, ktoś chcący dostarczyć custom areas z innego źródła (baza danych, classpath, pamięć) nie miał żadnej opcji bez dziedziczenia po GraphHopper. A, no i po trzecie. Brak testów…
Tworzę kurs SSJ: Super Senior Java z Mateuszem Nowakiem i Arturem Laskowskim — powiedz nam, czego potrzebujesz. Więcej znajdziesz na SuperSeniorJava.pl!
Wypełnij ankietę →// przed - prywatna metoda wewnątrz GraphHopper, nie do ruszenia
private Collection<JsonFeatureCollection> readCustomAreas() { ... }
// po - interfejs z domyślną implementacją
public interface CustomAreasProvider {
List<CustomArea> loadAreas();
}
// GraphHopper przyjmuje provider z zewnątrz
public GraphHopper setCustomAreasProvider(CustomAreasProvider provider) {
this.customAreasProvider = provider;
return this;
}
Wynik? Logika została przeniesiona do DefaultCustomAreasProvider, przez co testy można było napisać w oderwaniu od GraphHopper — bezpośrednio na implementacji interfejsu.
Co z tego wynika
Otworzyłem trzy PR-y: #3318, #3321, #3322. Sumarycznie jakieś dwie godziny, łącznie z poprawkami następnego dnia.
Zmiany są małe. To świadome podejście, nie przypadek. Czy dwie godziny dały mi głębokie zrozumienie modułu? Nie. Żeby naprawdę ogarnąć domenę routingu, musiałbym tam wsiąść znacznie dłużej. Ale to właśnie fajny pomysł na kolejne wykorzystanie agentów AI: eksploracja domeny krok po kroku, z agentem jako przewodnikiem po nieznanym kodzie.
Wartość w tym “eksperymencie” była w czymś innym. Mogłem skupić się na tym, co znam: code smells, zasady projektowania. Agent zajął się nawigacją po nieznanym kodzie, wygenerował implementację, napisał testy. Bez niego pewnie też bym to zrobił — ale znacznie dłużej, a szansa na porzucenie całego pomysłu w połowie drogi byłaby zdecydowanie większa.
Czy będzie mi smutno jeśli któryś z tych PR-ów zostanie odrzucony? Szczerze — nie. To nie jest projekt, który piszę od lat i w który włożyłem serce. To jest eksperyment nauki pracy z agentami na prawdziwym kodzie. Code review maintainerów to przy okazji cenna lekcja, a merge będzie miłym dodatkiem.
Kontrybuowanie do open source nie jest dziś tak trudne jak kiedyś. Weź projekt w swojej domenie, znajdź coś co nie odpowiada zdrowemu rozsądkowi, zadaj agentowi kilka pytań. Reszta sama przyjdzie.
Edit: Czyli tak jak się spodziewałem… wszystko zostało odrzucone.
