Refaktoryzacja kodu legacy w Cursorze
Refaktoryzacja kodu legacy w Cursorze to uporządkowany proces: mapujesz kod w Plan Mode, utrwalasz obecne zachowanie testami charakteryzacyjnymi, a potem agent zmienia jedną funkcję lub jeden moduł na commit. Każdy krok jest przyjmowany tylko wtedy, gdy utrwalone testy przechodzą bez zmian, a diff nie wychodzi poza zakres, więc nikt nie czyta każdej linii.
Odziedziczyłeś pięcioletnie API w Expressie: około 50 plików JavaScript, callbacki zagnieżdżone na cztery poziomy, reguły biznesowe w handlerach tras i zero testów. Prawdziwi użytkownicy polegają na każdym dziwactwie, także na tych nieudokumentowanych. Prośba do agenta „zmodernizuj to” kończy się diffem na 3000 linii, którego nikt nie jest w stanie przejrzeć i który zmienia zachowanie w sposób zauważony dopiero na produkcji. Ta strona jest dla programistów, którzy robią taką refaktoryzację w Cursorze, oraz dla tech leadów, którzy muszą zatwierdzić jej wynik.
Co daje bezpieczna refaktoryzacja legacy w Cursorze
Dział zatytułowany „Co daje bezpieczna refaktoryzacja legacy w Cursorze”- Prompt mapujący kod w trybie tylko do odczytu i śledzenie krytycznych ścieżek, które pokazują, gdzie jest ryzyko, zanim zmieni się jakikolwiek plik.
- Prompt do testów charakteryzacyjnych, który utrwala to, co kod robi dziś, łącznie z błędami, oraz dwa sprawdzenia dowodzące, że te testy wykryją zmianę.
- Prompty z twardym zakresem dla trzech mechanicznych ruchów: callbacki na
async/await, wydzielenie serwisów i stopniowe wprowadzanie TypeScriptu. - Bramka uruchamiana po każdym kroku (zablokowane testy, zakres diffu, sprawdzenie typów, wykrywanie cykli) oraz prompt zbierający dowody przed PR, które recenzent zatwierdza bez czytania całego diffu.
- Sposoby wyjścia z sześciu typowych awarii takiej refaktoryzacji.
Ta strona opisuje implementację w Cursorze. Proces niezależny od narzędzia (podział na wycinki, równoległe fale i dowody gotowości do przełączenia, czyli cut-over) opisuje strona modernizacja legacy z agentami, a odpowiednik dla Codexa to modernizacja kodu z Codexem, a w Claude Code zobacz refaktoryzację z Claude Code.
Dlaczego kolejność kroków w refaktoryzacji legacy ma znaczenie?
Dział zatytułowany „Dlaczego kolejność kroków w refaktoryzacji legacy ma znaczenie?”Każdy krok zależy od poprzedniego. Testy przed refaktoryzacją, bo bez nich „zachowanie się nie zmieniło” to tylko opinia. Zmiany mechaniczne przed strukturalnymi, bo czystą konwersję na async łatwo zweryfikować, a późniejsze wydzielanie jest wtedy mniejsze. Poprawki błędów na końcu i w osobnych commitach, bo commitu refaktoryzacyjnego, który przy okazji naprawia błąd, nie da się zweryfikować testami utrwalającymi stare zachowanie.
| Krok | Tryb lub narzędzie Cursora | Co agent może zmienić | Bramka, która go przyjmuje |
|---|---|---|---|
| 1. Mapa kodu | Plan Mode | Nic | Zacommitowana mapa, w której każde ustalenie wskazuje plik albo ma oznaczenie „inferred” |
| 2. Śledzenie krytycznych ścieżek | Plan Mode | Nic | Potrafisz wskazać trzy najbardziej ryzykowne ścieżki i przypadki brzegowe każdej z nich |
| 3. Utrwalenie zachowania | Agent | Tylko nowe pliki testów | Testy przechodzą na nietkniętym kodzie dwa razy i padają, gdy celowo coś zepsujesz |
| 4. Konwersja callbacków | Edycja inline, jedna funkcja | Jedną funkcję | Zablokowane testy przechodzą, diff dotyczy tylko tego pliku |
| 5. Wydzielenie serwisów | Agent, jeden moduł | Wskazany moduł, jego nowy plik serwisu i plik błędów | Zablokowane testy przechodzą, git status --short pokazuje tylko te pliki, brak nowego cyklu importów |
| 6. Dodanie typów | Agent, jedna warstwa | Przemianowane pliki i pliki, które je importują, po commicie z samą konfiguracją | tsc --noEmit bez błędów, zablokowane testy przechodzą |
| 7. Błędy i zapytania | Agent | Kod i odpowiadające mu oczekiwanie w teście | Zmiana testu i poprawka trafiają razem do jednego commitu fix: |
Jak krok po kroku zrefaktoryzować kod legacy w Cursorze?
Dział zatytułowany „Jak krok po kroku zrefaktoryzować kod legacy w Cursorze?”Przed pierwszym promptem przygotuj siatkę bezpieczeństwa. Utwórz gałąź, zacommituj obecny stan i zapisz jedno polecenie, które uruchamia testy. Dodaj regułę projektu, na przykład: „Podczas tej refaktoryzacji zmieniaj tylko pliki wskazane w prompcie. Nigdy nie edytuj plików w tests/characterization/. Nigdy nie zmieniaj treści odpowiedzi, kodu statusu ani komunikatu błędu”. Reguła to instrukcja, nie ogrodzenie. Ogrodzeniem jest bramka uruchamiana po każdym kroku (pierwszy raz pokazana w kroku 4), której agent nie obejdzie argumentami.
Krok 1: zmapuj kod w Plan Mode
Dział zatytułowany „Krok 1: zmapuj kod w Plan Mode”Plan Mode „tworzy szczegółowe plany implementacji przed napisaniem jakiegokolwiek kodu”, więc to bezpieczne miejsce do czytania nieznanego kodu. Jeśli twoja wersja Cursora oferuje tryb czatu tylko do odczytu, możesz użyć go zamiast tego. Jak podać agentowi duże repozytorium, nie zalewając jego kontekstu, opisuje strona zarządzanie kontekstem.
Zapisz raport jako docs/refactor/map.md i zacommituj go. Oznaczenia „inferred” mówią, które ustalenia musisz sprawdzić sam, zanim na nich oprzesz decyzję.
Krok 2: prześledź krytyczne ścieżki
Dział zatytułowany „Krok 2: prześledź krytyczne ścieżki”Wybierz dwa lub trzy przepływy, których awaria bolałaby najbardziej, na przykład logowanie, koszyk i płatność. Prześledź każdy od początku do końca, zanim powstanie jakikolwiek test, bo to śledzenie mówi, które przypadki brzegowe testy muszą utrwalić.
Krok 3: utrwal obecne zachowanie testami charakteryzacyjnymi
Dział zatytułowany „Krok 3: utrwal obecne zachowanie testami charakteryzacyjnymi”Test charakteryzacyjny zapisuje to, co kod robi dziś, a nie to, co powinien robić. Jeśli refaktoryzacja zmieni wynik, test zaczyna padać i wiesz, że zachowanie się zmieniło. Ogólną technikę (golden master, testy zatwierdzające (approval tests), dobór punktów styku) opisuje strona testy charakteryzacyjne; poniżej prompt dla jednego modułu w Cursorze.
Czytaj testy, nie implementację. Sprawdzasz, czy każda asercja jest dokładna i czy są w nich przypadki brzegowe ze śledzenia. Potem udowodnij, że zestaw jest wyrocznią, a nie dekoracją:
-
Uruchom zestaw dwa razy z rzędu. Test, który raz przechodzi, a raz pada, jest niestabilny; napraw go albo usuń teraz, bo inaczej będzie podnosił fałszywe alarmy na każdym kolejnym kroku.
-
Celowo zepsuj kod. Zmień w
src/jeden kod statusu albo jedno wyliczenie ceny, uruchom zestaw i potwierdź, że co najmniej jeden test pada. Cofnij zmianę poleceniemgit checkout -- src/. -
Dla mocniejszego sygnału na modułach, których dotkniesz najpierw, uruchom zawężone testy mutacyjne. Stryker 10.0.0 (npm, sprawdzone 2026-09-26) wprowadza w kodzie drobne zmiany i raportuje każdą, której testy nie wykryły:
Okno terminala # Terminal, katalog główny repozytoriumnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runnernpx stryker run --testRunner jest --mutate src/controllers/orders.js --concurrency 1--concurrency 1uruchamia jeden proces testów naraz. Domyślnie Stryker uruchamia kilka równolegle, a równoległe mutanty zapisujące do jednej wspólnej bazy testowej dają ocalałe i zabite mutanty, które nie mają nic wspólnego z twoimi testami. Jeśli chcesz przebiegów równoległych, daj każdemu procesowi Strykera własną bazę.Jeśli w kodzie, który zamierzasz zmieniać, przeżyły mutanty, testy nie zauważą takiej zmiany. Napisz jeden dodatkowy test na każdego ocalałego mutanta w
tests/characterization/, uruchom zestaw ponownie dwa razy i dopiero wtedy zacommituj w następnym kroku. Jeśli mutant nie może zmienić obserwowalnego zachowania (mutant równoważny), odnotuj to w raporcie Strykera zamiast pisać test; żaden test go nie zabije.Dodaj
reports/,stryker.logi.stryker-tmp/do.gitignoreprzed commitem w następnym kroku. -
Zacommituj same testy:
git commit -m "test: pin current order behaviour". Zanotuj hash commitu; toBASELINEw każdej bramce poniżej.
Jaki wynik jest wystarczający, wyjaśnia strona jak silna jest twoja wyrocznia.
Krok 4: przepisuj callbacki po jednej funkcji
Dział zatytułowany „Krok 4: przepisuj callbacki po jednej funkcji”Używaj edycji inline na jednej zaznaczonej funkcji, żeby zmiana była wycelowana w to zaznaczenie; bramka z git status --short poniżej potwierdza, że nie wyszła poza nie. Zaznacz jedną funkcję opartą na callbackach i wydaj takie polecenie:
Jeśli funkcja opiera się na stanie modułu albo na funkcjach pomocniczych spoza zaznaczenia, edycja inline może je przeoczyć. Wtedy przełącz się na agenta i wskaż cały plik przez @. Po każdej funkcji uruchom bramkę:
# Terminal, katalog główny repozytorium. BASELINE to commit z testami charakteryzacyjnymi.git diff --exit-code BASELINE -- tests/characterization/ # testy nie zostały ruszonenpx jest tests/characterization # zachowanie się nie zmieniłogit status --short # zmienił się tylko zamierzony plik i nie ma nowych nieśledzonych plikówgit status --short zamiast git diff --stat, bo diff pomija pliki nieśledzone, a nowy plik to właśnie to, co zostawia po sobie nadgorliwy agent. Wszystkie trzy przechodzą: commit. Którekolwiek pada: git restore . && git clean -n, co cofa zmiany i wypisuje nieśledzone pliki, a po sprawdzeniu tej listy git clean -f. Powtórz tę funkcję z węższym zaznaczeniem albo bardziej precyzyjnym poleceniem. Jedna funkcja na commit wydaje się powolna, ale każdy taki commit przejrzysz w chwilę, a git bisect od razu wskaże winny. Ogólne wzorce stojące za tymi ruchami opisuje strona strategie refaktoryzacji.
Krok 5: wydziel warstwę serwisów, jeden moduł na przebieg
Dział zatytułowany „Krok 5: wydziel warstwę serwisów, jeden moduł na przebieg”Gdy moduł jest już na async, przenieś logikę biznesową z handlera trasy. To zmiana w wielu plikach, więc użyj agenta i ogrodź zakres w prompcie.
Uruchom tę samą bramkę co w kroku 4 (git status --short ma pokazać tylko trzy wskazane pliki) i dodaj sprawdzenie cykli, bo przy wydzielaniu pojawiają się cykliczne importy:
# madge 8.0.0 (npm, sprawdzone 2026-09-26): kończy się kodem 1 i wypisuje 'Found … circular dependency', gdy istnieje cyklnpx madge --circular src/Krok 6: dodawaj TypeScript od dołu
Dział zatytułowany „Krok 6: dodawaj TypeScript od dołu”Konwertuj najpierw modele, potem serwisy, potem kontrolery, na końcu trasy. Każda warstwa zależy od tej pod nią, więc otypowanie fundamentu daje wyższym warstwom prawdziwe typy do sprawdzania.
Przygotuj narzędzia przed pierwszą zmianą nazwy pliku, bo inaczej bramka padnie z powodu konfiguracji, a nie zachowania: Jest nie załaduje modułu .ts, a Node nie zrobi na nim require.
# Terminal, katalog główny repozytorium. ts-jest 29.4.14 akceptuje TypeScript >=4.3 <7,# a najnowszy TypeScript w npm to 7.0.2 (oba sprawdzone 2026-09-26), więc przypnij TypeScript 6.npm install --save-dev typescript@6 @types/node @types/express @types/jest ts-jest tsxnpx tsc --initnpx ts-jest config:initPotem popraw tsconfig.json:
- Ustaw
allowJs: true, żeby pliki.jsi.tswspółistniały,checkJs: false, żeby nieruszony JavaScript nie był jeszcze sprawdzany, oraznoEmit: true, żebytsctylko sprawdzał. Zostawstrict: true. tsc --initw TypeScripcie 6 zapisuje"types": []iverbatimModuleSyntax: true. Zmień je na"types": ["node", "jest"]oraz, w kodzie CommonJS opartym narequire, naverbatimModuleSyntax: false.- Dodaj
"rootDir": "."; bez tego ts-jest zatrzymuje się z błędem TS5011.
ts-jest config:init zapisuje jest.config.js z transformacją tylko dla plików .ts, więc istniejące pliki .js ładują się jak dotąd. Jeśli projekt ma już konfigurację Jesta, dodaj do niej transformację ts-jest. Aplikację uruchamiaj przez tsx (na przykład npx tsx src/server.js w skrypcie start), który rozwiązuje require('./models/order') bez rozszerzenia do przemianowanego pliku .ts; krok budowania tsc z emisją do dist/ też się sprawdzi.
Uruchom npx jest tests/characterization i npx tsc --noEmit. Oba muszą przejść, zanim zmienisz nazwę jakiegokolwiek pliku; zacommituj samą konfigurację (chore: add TypeScript toolchain), żeby późniejszy czerwony test wskazywał na konwersję, a nie na narzędzia.
Komentarze TYPE-FINDING to twoja lista błędów do naprawy. Trafiają do kroku 7, nie do tego commitu.
Krok 7: naprawiaj błędy i wolne zapytania w osobnych commitach
Dział zatytułowany „Krok 7: naprawiaj błędy i wolne zapytania w osobnych commitach”Teraz zmieniasz zachowanie celowo. Bierz po jednym teście PINNED: looks like a bug albo jednym TYPE-FINDING: najpierw zmień oczekiwanie w teście i uruchom zestaw, żeby zobaczyć, jak pada; wtedy wiesz, że test widzi błąd. Potem poproś agenta, żeby test przeszedł, i zacommituj zmianę testu razem z poprawką, z komunikatem fix:, który nazywa stare i nowe zachowanie. Nie commituj samego czerwonego testu: łamie to obietnicę z kroku 4, że każdy commit nadaje się do git bisect, i powoduje czerwone CI na gałęzi. Recenzent i tak czyta zmianę testu jako pierwszą, bo dowody przed PR opisane niżej wymieniają każdy commit, który zmienił oczekiwanie w teście, a ta zmiana to jednolinijkowy diff w tests/characterization/ opisujący nowe zachowanie. Po każdej poprawce: commit, a potem przestaw BASELINE na ten commit (na przykład git tag -f refactor-baseline), żeby bramka po każdym kroku pilnowała nowego oczekiwania; prompt z dowodami przed PR nadal porównuje z pierwotnym commitem z testami charakteryzacyjnymi.
Praca nad zapytaniami dzieli się tak samo. Zapytanie N+1 albo odczyt całej tabeli można poprawić bez zmiany tego, co zwraca funkcja, więc testy charakteryzacyjne nadal go pilnują. Brakujące WHERE w UPDATE to poprawka błędu i idzie ścieżką czerwonego testu opisaną wyżej. Profilowanie zapytań po refaktoryzacji opisuje strona optymalizacja wydajności.
Jak udowodnić poprawność refaktoryzacji bez czytania każdej linii?
Dział zatytułowany „Jak udowodnić poprawność refaktoryzacji bez czytania każdej linii?”Zadaniem recenzenta staje się sprawdzanie dowodów, nie diffów. Poproś agenta, żeby je zebrał, zanim otworzysz pull request.
Wklej tabelę do opisu pull requesta. Recenzent zatwierdza, gdy każdy wiersz przechodzi, czyta tylko commity, które celowo zmieniły test, i wyrywkowo sprawdza pliki oznaczone w punkcie 4. Programista odpowiada za testy charakteryzacyjne z kroku 3, tech lead zatwierdza tabelę dowodów. Automatyczny recenzent, taki jak Bugbot, może dodać drugie przejście po diffie, ale nie zastępuje utrwalonych testów. Pełny format takiego zapisu to pakiet dowodów.
Testy obejmują tylko ścieżki, które utrwaliłeś. W usłudze o dużym ruchu wypuść zrefaktoryzowaną ścieżkę za flagą funkcji w trybie cienia (shadow): zwracaj starą odpowiedź, licz nową, loguj każdą różnicę i przełącz się dopiero po okresie bez różnic, których nie umiesz wyjaśnić. Do tego czasu zostaw starą ścieżkę w kodzie, żeby wycofanie było zmianą flagi, a nie wdrożeniem.
Co psuje się przy refaktoryzacji legacy w Cursorze i jak z tego wyjść?
Dział zatytułowany „Co psuje się przy refaktoryzacji legacy w Cursorze i jak z tego wyjść?”Agent „ulepsza” kod podczas kroku mechanicznego. Dodaje walidację, przepisuje komunikat błędu albo poprawia przypadek brzegowy przy konwersji callbacka. Wyjście: testy charakteryzacyjne wychwytują zmienione odpowiedzi, a git status --short zmienione i nowe pliki. Uruchom git restore . && git clean -n, po sprawdzeniu listy git clean -f, i powtórz prompt z listą zakazanych zmian („bez walidacji, bez nowych komunikatów”).
Agent edytuje test, żeby dojść do zielonego. Wyjście: git diff --exit-code BASELINE -- tests/characterization/ kończy się kodem niezerowym. Przywróć testy poleceniem git checkout BASELINE -- tests/characterization/ i uruchom prompt ponownie z dopiskiem „katalog testów jest tylko do odczytu”. Żeby to się w ogóle nie zdarzało, dodaj w CI zadanie, które blokuje pull request, gdy pliki w tests/characterization/ zmieniają się bez commitu test: albo fix:; wzorce pokazuje strona ochrona wyroczni.
Agent przepisuje pół repozytorium. Prompt, który do zmiany (a nie do pytania) wskazuje @src, zaprasza do rozległej edycji. Wyjście: git restore . i git clean -n, a po sprawdzeniu listy git clean -f. Uruchom prompt ponownie z jawną listą plików, które agent może zmienić.
Testy przechodzą, produkcja pada. Wejście, które wywołuje różnicę, nigdy nie zostało utrwalone. Wyjście: przełącz flagę z powrotem, wyciągnij z logów żądanie, które zawiodło, dodaj je jako test charakteryzacyjny na starym kodzie i powtórz krok.
Po wydzieleniu pojawia się cykliczny import. Dwa serwisy importują się nawzajem. Wyjście: npx madge --circular src/ nazywa cykl. Poproś agenta w Plan Mode o propozycję, gdzie powinny trafić wspólne typy lub funkcje, i przenieś je do osobnego modułu w osobnym commicie.
Testy charakteryzacyjne są niestabilne. Czas, losowe identyfikatory albo zależność od kolejności testów sprawiają, że wyrocznia kłamie. Wyjście: zamroź zegar i ustaw ziarno generatora losowego w fixturze, daj każdemu testowi własne dane i dwukrotnie uruchom zestaw, zanim znów mu zaufasz.