Przejdź do głównej zawartości

Refaktoryzacja na dużą skalę w Claude Code: zmień strukturę, udowodnij zachowanie

Refaktoryzacja na dużą skalę z Claude Code zmienia strukturę dziesiątek lub setek plików, na przykład wydziela moduł albo konwertuje JavaScript na TypeScript, a zachowanie systemu pozostaje identyczne. Działa, gdy zachowanie jest najpierw przypięte testami, docelowa struktura zapisana jako reguła architektury, a każdą partię dowodzą bramki, nie recenzenci.

Ta strona jest dla programistów, którzy prowadzą refaktoryzację obejmującą całą bazę kodu, i dla tech leadów, którzy muszą ją zmergować. Sytuacja: katalog utils ma 47 plików, połowa kodu używa callbacków, druga połowa async/await, a logika uwierzytelniania jest rozrzucona po sześciu plikach. Ostatnia ręczna zmiana nazwy pominęła jedno dynamiczne wywołanie i zgłoszenie błędu przyszło w piątek po południu.

Wszystko poniżej sprawdzono na Claude Code v2.1.283 (kanał latest) 26 września 2026 roku.

Co daje refaktoryzacja na dużą skalę zabezpieczona bramkami

Dział zatytułowany „Co daje refaktoryzacja na dużą skalę zabezpieczona bramkami”
  • Tabelę decyzyjną, która kieruje każdy rodzaj zmiany do właściwego mechanizmu i właściwej strony.
  • Trzy wyrocznie (ang. oracles), których agent nie może edytować: testy charakteryzacyjne, regułę architektury i, w bibliotekach, raport API.
  • Hook Stop, który nie pozwala Claude zakończyć pracy, dopóki sprawdzanie typów i testy nie przechodzą.
  • Prompty do skopiowania dla refaktoryzacji odkładanych najdłużej: wydzielenia modułu, przejścia z callbacków na async/await i konwersji JavaScript na TypeScript.
  • Skrypt weryfikacyjny, który dowodzi zakresu, struktury i zachowania bez czytania każdej linii.
  • Sposoby naprawy po pominiętych wywołaniach, edytowanych testach, zapętlonych hookach i codemodach, których /rewind nie cofnie.

Refaktoryzacja zmienia strukturę i zachowuje działanie. Kilka sąsiednich stron opisuje zmiany, które wyglądają podobnie, ale wymagają innego przepływu pracy, więc przed startem sprawdź tabelę.

Twoja zmianaWykonanieStrona kanoniczna
Przebudowa kodu w wielu plikach bez zmiany zachowania: wydzielenie modułu, wymuszenie warstw, callbacki na async/await, JavaScript na TypeScriptJedna sesja, partia po partii, w kolejności zależnościTa strona
Jedna mechaniczna edycja powtarzana w niezależnych plikach/batch, pętla claude -p albo codemodOperacje wsadowe
Zmiana nazwy, przy której wywołujący muszą się zmienić w tym samym commicieJedna zaplanowana sesjaZmiany w wielu plikach
Aktualizacja biblioteki, frameworka lub schematuMigracja w fazachMigracje bazy danych i kodu
Jeden wzorzec w jednym pliku, na przykład extract methodPętla test-firstWzorce refaktoryzacji
Cały system legacy, przez miesiąceProgram modernizacjiModernizacja legacy

Dlaczego to rozróżnienie ma znaczenie: w operacji wsadowej jednostki są niezależne i mogą działać równolegle. W refaktoryzacji strukturalnej partie zwykle zależą od siebie, bo przeniesienie typu psuje każdy plik, który go importuje. Domyślnie wykonujesz więc partie szeregowo, w kolejności zależności, a równoległość jest wyjątkiem.

Dlaczego refaktoryzacja strukturalna potrzebuje własnych wyroczni?

Dział zatytułowany „Dlaczego refaktoryzacja strukturalna potrzebuje własnych wyroczni?”

Testy dowodzą zachowania, ale nic nie mówią o strukturze. Po wydzieleniu modułu zestaw testów może być zielony, choć połowa kodu nadal importuje src/modules/auth/service.ts bezpośrednio i omija publiczny punkt wejścia. Agent ma też prosty sposób, żeby zamienić czerwone testy w zielone: edytuje test.

Dlatego duża refaktoryzacja działa na trzech wyroczniach, napisanych przed zmianą pierwszego pliku produkcyjnego:

  1. Testy charakteryzacyjne zapisują, co kod robi dziś, łącznie z jego dziwactwami. Jak je wygenerować, nie utrwalając błędów, opisuje strona o testach charakteryzacyjnych.
  2. Reguła architektury opisuje docelową strukturę i nie przechodzi, dopóki refaktoryzacja się nie skończy. Liczba jej naruszeń jest twoim wskaźnikiem postępu. Ogólną technikę opisują funkcje przystosowania architektury; ta strona używa dependency-cruiser (pakiet npm dependency-cruiser, v18.4.0 według stanu z 26 września 2026 roku, z poleceniem depcruise).
  3. Raport API dla pakietów, które importują inne zespoły. API Extractor (@microsoft/api-extractor, v7.59.2) zapisuje eksportowaną powierzchnię do pliku .api.md. Zainstaluj go przez npm install --save-dev @microsoft/api-extractor, a potem uruchom npx api-extractor init, żeby utworzyć api-extractor.json. Pakiet api-extractor bez zakresu w npm jest pustym placeholderem, więc npx api-extractor bez tej zależności deweloperskiej nie robi nic użytecznego. W CI (bez --local) zmieniona powierzchnia kończy przebieg błędem zamiast po cichu aktualizować raport. W kodzie aplikacji tę samą pracę wykonuje npx tsc --noEmit na wszystkich konsumentach.

Wprowadź wyrocznie osobnym pull requestem i zmerguj go, zanim wystartuje gałąź refaktoryzacji. Wtedy gałąź refaktoryzacji ma jedną prostą zasadę: nie zmienia treści żadnego testu, snapshotu ani pliku reguł.

Przygotuj refaktoryzację: od zielonej bazy do sesji z bramkami

Dział zatytułowany „Przygotuj refaktoryzację: od zielonej bazy do sesji z bramkami”
  1. Zacznij od zieleni. Uruchom sprawdzanie typów, linter i pełen zestaw testów na czystej gałęzi. Jeśli coś już jest czerwone, napraw to albo zapisz przed startem. Refaktoryzacja mierzona względem zepsutej bazy niczego nie dowodzi.

  2. Zrób inwentaryzację w trybie planowania. Uruchom Claude poleceniem claude --permission-mode plan albo wpisz /plan w działającej sesji (lub naciskaj Shift+Tab, aż wskaźnik trybu pokaże plan), żeby mógł czytać i szukać, ale nie edytować. Wklej poniższy prompt planujący i zapisz zatwierdzony plan w docs/refactor/auth-module.md, żeby przetrwał /clear.

  3. Napisz regułę architektury. Zainstaluj narzędzie przez npm install --save-dev dependency-cruiser, a potem, jeśli repozytorium nie ma konfiguracji, uruchom raz npx depcruise --init. W pakiecie CommonJS init zapisuje .dependency-cruiser.js, a w pakiecie ESM .dependency-cruiser.cjs. Ta strona używa nazwy .cjs, więc jeśli init zapisał plik .js, podawaj tę nazwę w regule i w każdej fladze --config poniżej. Najpierw instalacja: bez tej zależności npx depcruise pobiera niepowiązany pakiet depcruise, placeholder chroniący przed dependency confusion, który wypisuje ostrzeżenie, kończy się kodem 0 i niczego nie tworzy. Dodaj docelową strukturę do pliku konfiguracji. Ta reguła pozwala sięgać do modułu auth tylko przez jego publiczny punkt wejścia:

    .dependency-cruiser.cjs
    module.exports = {
    forbidden: [
    {
    name: 'auth-public-api-only',
    comment: 'Import the auth module through src/modules/auth/index.ts only.',
    severity: 'error',
    from: { pathNot: '^src/modules/auth/' },
    to: { path: '^src/modules/auth/', pathNot: '^src/modules/auth/index\\.ts$' },
    },
    ],
    options: { tsConfig: { fileName: 'tsconfig.json' } },
    };

    Uruchom npx depcruise src --config .dependency-cruiser.cjs i zanotuj liczbę naruszeń. Spada do zera, gdy wydzielenie jest gotowe.

  4. Dodaj testy charakteryzacyjne tam, gdzie pokrycie jest słabe. Inwentaryzacja z planu wymienia pliki bez testów. Przypnij ich obecny wynik, zanim dotknie ich jakakolwiek partia. W tym samym pull requeście przepnij każdy test, który importuje wnętrze modułu auth, na import przez src/modules/auth/index.ts, żeby refaktoryzacja nie musiała dotykać testów, gdy pliki się przenoszą. Na czas refaktoryzacji zostaw pliki testów na miejscu, a potem zmerguj pull request z wyroczniami.

  5. Odetnij agentowi dostęp do wyroczni. Na gałęzi refaktoryzacji dodaj reguły deny do .claude/settings.local.json (albo do współdzielonego .claude/settings.json, jeśli nad refaktoryzacją pracuje cały zespół):

    {
    "permissions": {
    "deny": [
    "Edit(**/*.test.ts)",
    "Edit(**/__snapshots__/**)",
    "Edit(/.dependency-cruiser.cjs)",
    "Edit(**/*.api.md)"
    ]
    }
    }

    Dla ścieżek używaj reguł Edit(...): w v2.1.283 sprawdzanie uprawnień do plików dopasowuje tylko reguły Edit(ścieżka) i Read(ścieżka), a reguła Write(ścieżka) nigdy nie zostaje dopasowana. Reguła deny dla Edit obejmuje też polecenia plikowe, które Claude Code rozpoznaje w Bashu, takie jak sed i tee, oraz przekierowania w rodzaju > plik. Nie obejmuje skryptu, który sam otwiera pliki, na przykład codemoda w Node albo jednolinijkowca python -c. W trybie auto (tryb startowy w v2.1.283) klasyfikator również próbuje blokować takie obejścia, ale działa probabilistycznie, więc egzekwowanie zostaw sprawdzeniom 1a i 1b skryptu weryfikacyjnego oraz sandboxowi. Ten przypadek wyłapie później skrypt weryfikacyjny, a sandbox egzekwuje ścieżkę na poziomie systemu operacyjnego. Jeśli niektóre testy importują wnętrze modułu, którego nie udało się przepiąć w pull requeście z wyroczniami, usuń regułę dla *.test.ts i polegaj na sprawdzeniu 1b skryptu weryfikacyjnego, które przepuszcza w testach tylko zmiany linii importu.

  6. Dodaj bramkę na zakończenie. Skonfiguruj hook z następnej sekcji, żeby Claude nie mógł zakończyć tury, dopóki sprawdzanie typów albo testy są czerwone.

Pozwól Claude skończyć dopiero przy zielonych bramkach

Dział zatytułowany „Pozwól Claude skończyć dopiero przy zielonych bramkach”

Hook Stop uruchamia się, gdy Claude kończy odpowiedź. Kod wyjścia 2 blokuje zakończenie i pokazuje Claude twój stderr, więc agent dalej pracuje nad błędem. Dwa szczegóły z dokumentacji hooków dla v2.1.283 decydują o tym, czy masz bramkę, czy dekorację:

  • W zdarzeniach Stop i PostToolUse stdout hooka, który kończy się kodem 0, trafia do logu debugowania i Claude go nie widzi. Hook w rodzaju npx tsc --noEmit | head -20 zawsze kończy się kodem 0 (zwraca go head), więc jego błędy nigdy nie docierają do agenta. Wypisuj błędy na stderr i kończ kodem 2.
  • ${CLAUDE_PROJECT_DIR} nadal wskazuje katalog główny projektu, w którym sesja wystartowała, czyli główny checkout, gdy sesja wchodzi do worktree (dokumentacja hooków). Katalog roboczy sesji czytaj z wejścia hooka (cwd). cwd podąża też za każdym cd, które wykona Claude, więc wyznacz z niego katalog główny repozytorium przez git rev-parse --show-toplevel.
#!/usr/bin/env bash
# .claude/hooks/refactor-gate.sh: hook Stop. Claude nie zakończy tury, dopóki bramka jest czerwona.
input=$(cat)
# Już raz zablokowany w tej sekwencji: pozwól Claude zakończyć i zaraportować zamiast zapętlać.
[ "$(jq -r '.stop_hook_active' <<<"$input")" = "true" ] && exit 0
cd "$(git -C "$(jq -r '.cwd' <<<"$input")" rev-parse --show-toplevel)" || exit 0
if ! out=$( { npx tsc --noEmit && npx jest --ci --changedSince=origin/main; } 2>&1 ); then
printf '%s\n' "$out" | tail -n 40 >&2
echo "Refactor gate is red. Fix the production code, not the tests, then finish." >&2
exit 2
fi

Przykłady na tej stronie używają Jesta. Przy Vitest zamień polecenie Jesta na npx vitest run --changed origin/main.

Zarejestruj go na gałęzi refaktoryzacji i nadaj mu prawo wykonywania (chmod +x .claude/hooks/refactor-gate.sh):

{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/refactor-gate.sh" }
]
}
]
}
}

${CLAUDE_PROJECT_DIR} wskazuje główny checkout, więc skrypt musi istnieć także na gałęzi, która jest tam wybrana. Jeśli refaktoryzacja działa w worktree, a główny checkout zostaje na main, najpierw zacommituj skrypt hooka do main. Inaczej ścieżka nie istnieje, hook kończy się błędem nieblokującym i bramka po cichu znika.

Bramka celowo pomija regułę architektury. Reguła jest czerwona przez całą refaktoryzację, aż do ostatniej partii, więc należy do raportu postępu i końcowej weryfikacji, a nie do sprawdzenia po każdej turze. Automatyzacja hookami opisuje pozostałe zdarzenia, w tym sprawdzanie typów w PostToolUse po każdej edycji. W dużym repozytorium takie sprawdzenie może kosztować więcej czasu, niż oszczędza.

Wykonuj jedną partię na turę, commituj ją i zaczynaj następną od zapisanego planu. Bramka Stop pilnuje uczciwości każdej tury, a jeden commit na partię sprawia, że każdą partię da się osobno cofnąć i przeszukać przez git bisect.

Po dwóch lub trzech partiach uruchom /clear i zacznij od pliku planu z oznaczonymi gotowymi partiami. Kompaktowanie kontekstu (compaction) w długiej sesji streszcza wcześniejsze decyzje, a plik planu jest ich trwałym zapisem.

Gdy partia jest czysto składniowa, na przykład przepisanie 200 ścieżek importu po przeniesieniu, poproś Claude o napisanie codemoda zamiast edycji każdego pliku. Przeglądasz wtedy 30 linii reguły ast-grep albo transformacji jscodeshift, a nie 200 zmienionych plików. Codemod działa przez powłokę, więc /rewind go nie cofnie: commituj przed każdym uruchomieniem. Gdy partie naprawdę są niezależne, na przykład jeden katalog na jednostkę i żadnych wspólnych plików, przekaż je do /batch, tak jak opisują operacje wsadowe.

Prompty do refaktoryzacji, które zespoły odkładają najdłużej

Dział zatytułowany „Prompty do refaktoryzacji, które zespoły odkładają najdłużej”

Każdy prompt zakłada przygotowanie opisane wyżej: zatwierdzony plan, zmergowane wyrocznie, aktywne reguły deny i bramkę Stop.

Dla wzorca legacy, którego nie da się wymienić w jednej gałęzi, na przykład własnej walidacji rozsianej po 30 plikach, użyj wzorca strangler fig. Zbuduj nową ścieżkę obok starej, uruchamiaj obie i loguj każdą rozbieżność, przełącz wywołujących, gdy log jest pusty, i usuń starą ścieżkę w ostatnim pull requeście. Log rozbieżności jest wyrocznią zachowania, więc przeglądaj jego zawartość, a nie diff, który go wytworzył.

Jak udowodnić, że refaktoryzacja niczego nie zmieniła, bez czytania każdego diffa?

Dział zatytułowany „Jak udowodnić, że refaktoryzacja niczego nie zmieniła, bez czytania każdego diffa?”

Refaktoryzacja na 150 plików jest daleko poza granicą, przy której przegląd linia po linii jest uczciwy. Zamień czytanie na sprawdzenia, które głośno padają, i czytaj tylko tam, gdzie wskażą. Uruchom ten skrypt na gałęzi refaktoryzacji, lokalnie albo jako zadanie CI:

#!/usr/bin/env bash
# verify-refactor.sh: uruchom z katalogu głównego repozytorium na gałęzi refaktoryzacji
set -euo pipefail
BASE=${BASE:-origin/main}
# 1a. Wyrocznia nietknięta: żaden snapshot, reguła ani raport API nie został dodany, zmieniony, przeniesiony ani usunięty
# (--no-renames pokazuje przeniesienie jako usunięcie plus dodanie, więc przeniesiony snapshot też wychodzi)
if git diff --name-only --no-renames "$BASE"...HEAD -- '*__snapshots__/*' .dependency-cruiser.cjs '*.api.md' | grep .; then
echo "Oracle files changed on the refactor branch: review these by hand"; exit 1
fi
# 1b. Testy: przeniesiony lub edytowany test może różnić się tylko liniami importu
if git diff -U0 -M "$BASE"...HEAD -- '*.test.ts' | grep -E '^[+-][^+-]' | grep -vE '^[+-]\s*(import |\} from )'; then
echo "Test bodies changed on the refactor branch: review these by hand"; exit 1
fi
# 2. Zachowanie: typy i pełen zestaw testów, łącznie z testami charakteryzacyjnymi
npx tsc --noEmit
npx jest --ci
# 3. Struktura: docelowa architektura obowiązuje, zero naruszeń
npx depcruise src --config .dependency-cruiser.cjs
# 4. Kompletność: nic nie sięga już do wnętrza modułu
if git grep -nE "from ['\"][./]*.*modules/auth/(service|middleware|types|constants)['\"]" -- src ':!src/modules/auth'; then
echo "Deep imports into src/modules/auth remain"; exit 1
fi
# 5. Publiczna powierzchnia (tylko pakiety biblioteczne): pada, jeśli zmieniło się eksportowane API
# Wymaga zależności deweloperskiej @microsoft/api-extractor; nazwa bez zakresu w npx to placeholder
if [ -f api-extractor.json ]; then npx api-extractor run --print-api-report-diff; fi

Potem skieruj jednego agenta recenzującego na poprawność. Uruchom /code-review na gałęzi. Przegląda diff pod kątem błędów poprawności, nie stylu. Automatyzacja code review ustawia to samo sprawdzenie na każdym pull requeście.

Akceptacja dzieli się według ról:

KtoCo akceptuje
Programista, który prowadził refaktoryzacjęPlan, wykluczoną z planu listę zmian zachowania i każde zatrzymanie zgłoszone przez agenta
CISkrypt weryfikacyjny i pełen zestaw testów na każdym pull requeście
RecenzentPull request z wyroczniami linia po linii; w pull requestach refaktoryzacji tylko oznaczone miejsca i wynik sprawdzeń
Tech leadWielkość partii i kolejność mergowania, uzgodnione przed pierwszą partią, żeby zespół nie rebase’ował na ruchomy cel

Tech lead mierzy refaktoryzację tym, co usunęła, a nie liczbą zmienionych linii. Przydatne liczby to spadek naruszeń reguły do zera, liczba pozostałych typów any lub sygnatur callbacków i wielkość publicznego punktu wejścia modułu. Strona o tym, jak utrzymać w zdrowiu kod pisany przez agentów, zamienia je w trendy. Dla kontekstu: raport GitClear z czerwca 2026 roku „The Maintainability Gap” (źródło wtórne) podaje, że kod przeniesiony lub zrefaktoryzowany spadł z 13% zmienionych linii w 2023 roku do 3,8% w 2026 roku. Badanie jest korelacyjne, ale zgadza się z dryfem, który sprawia, że takie refaktoryzacje są potrzebne.

Co psuje się w refaktoryzacji na dużą skalę i jak to naprawić?

Dział zatytułowany „Co psuje się w refaktoryzacji na dużą skalę i jak to naprawić?”

Zmiana nazwy lub przeniesienie pominęły dynamiczne wywołania

Dział zatytułowany „Zmiana nazwy lub przeniesienie pominęły dynamiczne wywołania”

Objaw: wszystko jest zielone, a produkcja pada z undefined is not a function na ścieżce, której testy nie wykonują. Przyczyna: dostęp przez obj[name], napis w pliku konfiguracyjnym, token DI albo szablon. Sprawdzanie typów tego nie widzi. Naprawa: cofnij partię przez git revert, a potem poproś Claude o wyszukanie starej nazwy jako napisu we wszystkich typach plików, łącznie z YAML, JSON, SQL i szablonami. Zanim powtórzysz partię, dodaj test charakteryzacyjny dla tej ścieżki.

Objaw: sprawdzenie 1a lub 1b skryptu weryfikacyjnego pada albo w diffie pojawia się plik snapshotu. Przyczyna: agent trafił na czerwony test i zmienił oczekiwanie, często przez skrypt, który sam zapisuje pliki, a tego reguła deny dla Edit nie obejmuje. Naprawa: przywróć test z gałęzi głównej (git checkout origin/main -- src/auth/session.test.ts), uruchom go ponownie i potraktuj błąd jako prawdziwą zmianę zachowania: napraw kod albo wyłącz tę zmianę z refaktoryzacji. Mocniejsze egzekwowanie opisuje strona o ochronie wyroczni.

Bramka Stop się zapętla albo nie pozwala Claude skończyć

Dział zatytułowany „Bramka Stop się zapętla albo nie pozwala Claude skończyć”

Objaw: Claude powtarza tę samą poprawkę albo tura się nie kończy. Przyczyna: błąd, którego agent nie może naprawić w ramach partii, na przykład niestabilny test albo brakująca zależność. Naprawa: sprawdzenie stop_hook_active pozwala Claude zakończyć po jednej zablokowanej próbie i zaraportować. Bez tego sprawdzenia Claude Code i tak kończy turę po ośmiu kolejnych kontynuacjach wymuszonych przez hook Stop (v2.1.283), ale to osiem zmarnowanych prób naprawy, a nie zabezpieczenie, na którym warto polegać. Napraw niestabilny test na gałęzi głównej albo zmniejsz partię. Jeśli hook blokuje każde zakończenie, sprawdź, czy kończy się kodem 2 tylko przy prawdziwych błędach i czy jq jest zainstalowany.

Objaw: cofasz się do checkpointu, a pliki zmienione przez codemod zostają zmienione. Przyczyna: checkpointy śledzą edycje plików wykonane przez Claude, a nie plików zmienionych przez polecenia Bash. Naprawa: git restore . albo git reset --hard do commita ostatniej partii. Commituj przed każdym uruchomieniem codemoda. Co checkpointy przywracają, a czego nie, opisuje strona o wychodzeniu z błędów.

Przejście na async zmieniło zachowanie, którego testy nie przypięły

Dział zatytułowany „Przejście na async zmieniło zachowanie, którego testy nie przypięły”

Objaw: testy przechodzą, ale logi pokazują połknięte błędy albo operacje kończące się w złej kolejności. Przyczyna: await w pętli, która wcześniej uruchamiała callbacki równolegle, albo catch, który loguje zamiast rzucić błąd dalej. Naprawa: dodaj testy charakteryzacyjne na granicy serwisu, które sprawdzają kolejność i propagację błędów, a potem powtórz partię. Prompt dla async zabrania obu zmian, więc porównaj partię z zasadami 2 i 3.

Zespół ciągle merguje do plików, które przenosisz

Dział zatytułowany „Zespół ciągle merguje do plików, które przenosisz”

Objaw: każdy rebase ma konflikty na tych samych plikach barrel i routera. Naprawa: merguj małe partie często zamiast jednej długo żyjącej gałęzi, a wspólne pliki, takie jak index.ts i router, wprowadź w pierwszej partii. Przy pracy dłuższej niż tydzień uzgodnij właściciela katalogów objętych przenosinami albo użyj wzorca strangler fig, żeby stary i nowy kod współistniały.