Przejdź do głównej zawartości

Modernizacja legacy na dużą skalę z Codexem

Codex modernizuje legacy falami, a nie jednym przepisaniem. Ocena w trybie tylko do odczytu szereguje moduły według ryzyka, testy charakteryzacyjne utrwalają obecne zachowanie, warstwy niskiego ryzyka migrują w równoległych worktree, a najtrudniejsze moduły trafiają do zadań Codex cloud z dwiema–czterema próbami. Każda fala trafia na produkcję z tymi samymi dowodami: przechodzące, niezmienione testy, bramki typów i zapadka liczby plików.

Twój zespół utrzymuje 80 000 linii JavaScriptu z 2018 roku: callbacki zamiast async/await, komponenty klasowe Reacta i testy pokrywające ułamek kodu. Zarząd chce TypeScriptu „bez regresji na produkcji”, a ostatnie przepisanie big-bang trwało pół roku i nic nie dostarczyło. Ta strona jest dla developera, który prowadzi taką migrację w Codexie, i dla tech leada, który musi udowodnić bezpieczeństwo każdej fali bez czytania każdego zmigrowanego pliku.

Sama metoda (cięcie na kawałki, fale, dowody przed przełączeniem) nie zależy od narzędzia i opisuje ją strona modernizacja legacy z agentami. Ta strona to jej implementacja w Codexie: która powierzchnia Codexa robi które zadanie, dokładne komendy i prompty.

  • Mapę zadań na powierzchnie: które części migracji idą w CLI, w worktree, w chmurze, a które według harmonogramu.
  • Siedem promptów do skopiowania: ocena, testy charakteryzacyjne, infrastruktura migracji, migracja partii, zadanie best-of-N dla trudnego modułu, klasy na hooki i cotygodniowy raport z następną falą.
  • Skrypt postępu i zapadkę w CI, które nie pozwalają liczbie plików JavaScript znów rosnąć.
  • Listę dowodów, którą tech lead zatwierdza dla każdej fali, oraz sposoby wyjścia z typowych awarii.

Która powierzchnia Codexa robi które zadanie modernizacji?

Dział zatytułowany „Która powierzchnia Codexa robi które zadanie modernizacji?”

Dobierz powierzchnię do ryzyka i do tego, ile w zadaniu jest osądu. Komendy poniżej sprawdzono na codex-cli 0.157.1.

ZadaniePowierzchniaDlaczego tam
Ocena bazy koducodex exec z profilem uprawnień tylko do odczytuNa tym etapie nic nie powinno się zmienić, a raport ląduje w pliku
Testy charakteryzacyjne i infrastruktura migracjiInteraktywny codex na osobnej gałęziWspółdzielona konfiguracja (tsconfig.json, lint, CI) potrzebuje jednego właściciela i twojej uwagi
Partie mechaniczne (moduły pomocnicze, dostęp do danych, trasy)Jeden worktree Gita na partię, jedna sesja Codexa w każdymPartie idą równolegle i nie dotykają nawzajem swoich plików
Trudne moduły (głębokie callbacki, ukryte zmienne globalne)codex cloud exec --attempts 2–4Istnieje kilka sensownych projektów, więc porównujesz diffy zamiast ufać jednemu
Śledzenie postępu i wybór kolejnej partiiSkrypt powłoki plus cotygodniowa automatyzacja CodexaLiczenie jest deterministyczne; model jest potrzebny tylko do rekomendacji

GPT-6 Astra, wbudowany domyślny model Codexa, pracuje standardowo z niskim poziomem rozumowania (effort low). Podnieś effort przy ocenie i przy trudnych modułach migrowanych lokalnie, zanim rozważysz inny model: dodaj -c model_reasoning_effort="high" do uruchomienia w CLI, jak w kroku 1, albo wybierz poziom przez /model w sesji interaktywnej. Aktualną listę modeli znajdziesz w przeglądzie modeli.

Uruchom ocenę nieinteraktywnie, żeby nie mogła niczego edytować, a jej raport trafił do pliku, który możesz scommitować. Zapisz poniższy prompt jako assess.md i uruchom w terminalu w katalogu głównym repozytorium:

Okno terminala
mkdir -p docs/modernization &&
codex exec -c default_permissions=":read-only" -c model_reasoning_effort="high" \
-o docs/modernization/assessment.md - < assess.md

-c default_permissions=":read-only" wybiera wbudowany profil uprawnień tylko do odczytu (beta w 0.157.1), -c model_reasoning_effort="high" podnosi effort tylko dla tego uruchomienia, a -o zapisuje ostatnią wiadomość agenta do pliku. mkdir -p tworzy katalog wyjściowy, którego w świeżym repozytorium jeszcze nie ma.

Czytaj ranking ryzyka, nie cały raport. Jeśli moduł, o którym wiesz, że jest kruchy, trafił do „low”, ocena przeoczyła ukryty stan; dopisz go ręcznie do listy wysokiego ryzyka.

Krok 2: utrwal obecne zachowanie, zanim zmieni się jakikolwiek plik

Dział zatytułowany „Krok 2: utrwal obecne zachowanie, zanim zmieni się jakikolwiek plik”

Migracja przy słabym pokryciu testami nie ma wyroczni: agent może zmienić zachowanie, a wszystkie sprawdzenia dalej będą zielone. Zanim katalog wejdzie do fali, daj jego publicznemu interfejsowi testy charakteryzacyjne, które zapisują, co kod robi dziś, łącznie z błędami. Technikę niezależną od narzędzia opisuje strona testy charakteryzacyjne dla legacy.

Zanim zaufasz testom, sprawdź, czy potrafią wykryć błąd. Zepsuj ręcznie jedną linię modułu, uruchom ponownie testy charakteryzacyjne i upewnij się, że przynajmniej jeden test jest czerwony; potem cofnij zmianę. Zestaw, który zostaje zielony mimo celowego błędu, nie jest wyrocznią. Scommituj testy na gałęzi bazowej, żeby każda kolejna fala działała na tych samych plikach.

Krok 3: zbuduj infrastrukturę migracji na jednej gałęzi

Dział zatytułowany „Krok 3: zbuduj infrastrukturę migracji na jednej gałęzi”

Współdzielona konfiguracja to plik, którego inaczej dotykałaby każda równoległa partia, więc należy do jednej sesji na jednej gałęzi. Uruchom sesję interaktywną na gałęzi w rodzaju ts-migration-infra, daj jej poniższy prompt i scal wynik, zanim ruszy jakakolwiek partia.

Skrypt postępu liczy, a zapadka pilnuje, żeby liczba nie rosła. Oba są deterministyczne, więc żaden nie potrzebuje modelu:

#!/usr/bin/env bash
# scripts/migration-progress.sh: JS vs TS files per top-level directory under src/
set -euo pipefail
printf '%-28s %6s %6s\n' directory js ts
for d in src/*/; do
js=$(find "$d" -type f \( -name '*.js' -o -name '*.jsx' \) | wc -l)
ts=$(find "$d" -type f \( -name '*.ts' -o -name '*.tsx' \) | wc -l)
printf '%-28s %6d %6d\n' "${d%/}" "$js" "$ts"
done
Okno terminala
# CI step: the ratchet. Lower .migration-baseline on main after a batch merges, never inside a batch PR.
js_now=$(find src -type f \( -name '*.js' -o -name '*.jsx' \) | wc -l)
test "$js_now" -le "$(cat .migration-baseline)" || { echo "JS file count rose to $js_now"; exit 1; }

Krok 4: migruj warstwy niskiego ryzyka w równoległych worktree

Dział zatytułowany „Krok 4: migruj warstwy niskiego ryzyka w równoległych worktree”

Weź pierwszą falę z oceny: zwykle najpierw moduły pomocnicze bez wewnętrznych zależności (liście grafu importów), potem dostęp do danych, dalej serwisy, a trasy i UI na końcu. Każda partia dostaje własny worktree i gałąź, żeby dwie sesje nigdy nie edytowały tego samego pliku. Uruchom to w terminalu raz na partię:

Okno terminala
git fetch origin
git worktree add -b ts/utils ../app-ts-utils origin/main
cd ../app-ts-utils && npm ci && codex

Jeśli nie musisz wybierać nazwy gałęzi, codex --worktree utworzy zarządzany worktree; przewodnik po worktree w Codexie opisuje obie formy i sprzątanie. Wklej ten sam prompt do każdej sesji i zmieniaj tylko katalog:

Scalaj po jednej partii. Po każdym scaleniu obniż .migration-baseline na main do nowej liczby plików JS jednolinijkowym commitem. To, że linia bazowa nie trafia do pull requestów partii, chroni równoległe partie przed kolizją: gdyby każda partia ją obniżała, każde scalenie po pierwszym kończyłoby się konfliktem na tym jednym pliku. Potem przebazuj pozostałe otwarte partie na nową bazę i ponownie przepuść je przez bramki, bo typ dodany w jednej partii potrafi ujawnić błędy w innej.

Krok 5: wyślij trudne moduły do zadań w chmurze z best-of-N

Dział zatytułowany „Krok 5: wyślij trudne moduły do zadań w chmurze z best-of-N”

Niektóre moduły mają kilka rozsądnych projektów: sześć poziomów zagnieżdżonych callbacków, hierarchia klas oparta na globalnym obiekcie konfiguracji, zero testów. Dla nich codex cloud exec --attempts N uruchamia do czterech niezależnych prób na tym samym prompcie, a ty porównujesz diffy. Każda próba to pełny przebieg agenta, więc zostaw to dla modułów, w których zły pierwszy projekt drogo kosztuje.

Kontener w chmurze klonuje repozytorium z remote’a, więc infrastruktura z kroku 3 i testy charakteryzacyjne z kroku 2 muszą już być na zdalnym main. Wypchnij go, a potem wskaż gałąź jawnie (w terminalu; ENV_ID to ID lub etykieta twojego środowiska Codex cloud):

Okno terminala
git push origin main
codex cloud exec --env ENV_ID --branch main --attempts 3 - < hard-module.md

Nazwanie projektów w prompcie nie pozwala próbom zbiec się do tego samego diffa. Gdy zadanie jest gotowe, porównaj próby i zastosuj wybraną w czystym worktree na nowej gałęzi, nie w swojej kopii roboczej, żeby zastosowany diff był od razu gotowy do pull requestu:

Okno terminala
codex cloud diff TASK_ID --attempt 2
git fetch origin
git worktree add -b ts/payments ../verify-payments origin/main && cd ../verify-payments
codex cloud apply TASK_ID --attempt 2
npm ci && npm run typecheck && npm test

TASK_ID to ID lub URL zadania, który wypisuje codex cloud exec. Prompt porównujący wszystkie próby obok siebie znajdziesz w sekcji kiedy best-of-N jest wart swojej ceny.

Przejście z klas na hooki to migracja, w której zachowanie zmienia się najciszej: efekty uruchamiają się po odmalowaniu, a nieaktualne domknięcie czyta stary prop. Traktuj ją jak falę średniego ryzyka z własnym promptem:

Krok 6: śledź postęp i wybieraj kolejną falę według harmonogramu

Dział zatytułowany „Krok 6: śledź postęp i wybieraj kolejną falę według harmonogramu”

Uruchamiaj scripts/migration-progress.sh w CI przy każdym scaleniu, żeby liczby pochodziły ze skryptu, a nie z szacunku agenta. Potem niech cotygodniowa automatyzacja Codexa zamieni te liczby w rekomendację. Automatyzacje działają w aplikacji desktopowej Codex, dostępnej na macOS i Windows; na Linuksie zaplanuj codex exec z tym samym promptem w cronie albo w CI. Zanim zaplanujesz automatyzację, uruchom prompt ręcznie dwa razy, tak jak opisuje przewodnik po automatyzacjach. Zadanie dopisuje tabelę do docs/modernization/progress.md, więc uruchamiaj je w worktree i scalaj tę zmianę jak każdą inną gałąź; każde uruchomienie w worktree startuje od świeżej kopii, więc niescalona tabela za tydzień zniknie.

Jak zweryfikować falę modernizacji bez czytania każdej linii?

Dział zatytułowany „Jak zweryfikować falę modernizacji bez czytania każdej linii?”

Zmigrowany plik jest poprawny, gdy robi dalej to, co robił. Ustal, co każda fala musi udowodnić, zanim powstanie jakikolwiek diff, i pozwól bramkom to sprawdzić:

  1. Wyrocznia się nie ruszyła. Testy charakteryzacyjne i istniejące przechodzą, a fala ich nie edytowała. git diff --stat origin/main...HEAD -- tests/ musi być pusty (albo pokazywać tylko dozwolone zamiany API klasowego).

  2. Bramki typów i lintera przechodzą w CI, nie tylko w podsumowaniu agenta: npm run typecheck, linter i pełny zestaw testów.

  3. Zapadka ruszyła się we właściwą stronę. Liczba plików JS w raporcie agenta spadła, pull request nie dotknął .migration-baseline, a linię bazową obniżono na main zaraz po scaleniu.

  4. Automatyczny recenzent przeczytał diff jako pierwszy. W worktree partii najpierw uruchom git fetch origin, żeby recenzja i sprawdzenie tests/ z punktu 1 porównywały się z tą samą bazą, a potem wbudowaną recenzję poleceniem codex exec review --base origin/main albo podaj własne instrukcje bez --base: codex exec review "Review the changes on this branch against origin/main. Flag any behavior change: altered error handling, callback-to-promise timing, changed export shapes, or edits to tests." W codex-cli 0.157.1 nie da się połączyć --base z własnymi instrukcjami; polecenie kończy się błędem. Możesz też poprosić o @codex review w pull requeście. Zobacz code review w Codexie.

  5. Zatwierdza konkretna osoba. Właściciel partii scala fale niskiego ryzyka na podstawie powyższych dowodów. Właściciel modułu czyta sam kod tylko w klasach zmian, w których ludzie nadal czytają diffy (uwierzytelnianie, pieniądze, schemat i migracje danych), zgodnie z zasadą czytania dowodów zamiast kodu.

  6. Produkcja potrafi to cofnąć. Wdrażaj każdą falę osobno, żeby regresja cofała jedną falę, a nie całą migrację.

ObjawPrzyczynaJak wyjść
Po scaleniu partii niezmigrowany plik .js kończy się błędem „is not a function”Partia zamieniła module.exports = fn na eksporty nazwane, a wywołania require() oczekują starego kształtuCofnij zmianę eksportu, a potem zachowaj stary kształt, dopóki wszystkie importujące go moduły nie zostaną zmigrowane. Lista importujących plików z raportu partii pokazuje, których plików to dotyczy
Testy przechodzą, ale callback odpala się później niż wcześniejasync/await przeniosło pracę z synchronicznego callbacku do mikrozadaniaDodaj test charakteryzacyjny sprawdzający kolejność i powtórz partię
Partia jest zielona tylko dlatego, że zmieniono testyAgent poprawił asercje lub snapshoty pod nowe zachowanieSprawdzenie pustego diffa w tests/ blokuje scalenie; powtórz z „fix the migration, not the test” na początku promptu
Zadanie w chmurze nie widzi tsconfig.json ani nowych testówScalona infrastruktura i testy nie zostały wypchnięte na zdalny main albo zadanie ruszyło na innej gałęziNajpierw wypchnij i podaj --branch jawnie
Wszystkie trzy próby dają ten sam projektPrompt zostawił tylko jedną oczywistą drogęNazwij alternatywne projekty w prompcie, jak w prompcie dla trudnego modułu
Błędy typów pojawiają się w plikach, których nikt nie dotykałWspółdzielony typ albo tsconfig.json zmienił się w ramach partiiWspółdzieloną konfigurację trzymaj tylko w gałęzi infrastruktury; partie nie mogą jej edytować
Pull requesty partii mają konflikt na .migration-baselineKażda partia sama obniżała linię bazowąUsuń zmianę linii bazowej z partii; obniżaj ją na main po każdym scaleniu, a gdy dwa scalenia się ścigają, weź niższą liczbę
Dwie partie mają konflikt przy scalaniuIch katalogi współdzielą moduł albo obie edytowały plik indeksuPrzebazuj późniejszą partię po scaleniu pierwszej; w planie fali przypisz współdzielone pliki indeksu jednej partii
Cotygodniowy raport powtarza rekomendację z zeszłego tygodniaAutomatyzacja nie pamięta, co już się wydarzyłoTrzymaj jej stan w docs/modernization/progress.md i każ promptowi porównywać się z tym plikiem

Ta sama migracja w innych narzędziach: refaktoryzacja legacy w Cursorze i migracje z Claude Code.