Modernizacja legacy z agentami: program, nie prompt
Modernizacja legacy z agentami kodującymi działa jako program z bramkami, a nie pojedynczy prompt: utrwal dzisiejsze zachowanie testami charakteryzacyjnymi, podziel system na plastry za szwami, migruj je w równoległych falach izolowanych uruchomień agentów, porównaj starą i nową ścieżkę na żywym ruchu, a potem przełącz na podstawie zapisanych dowodów. Agenci piszą kod, a utrwalone zachowanie decyduje, co zostanie scalone.
Twój zespół utrzymuje dwunastoletni monolit w PHP. Siedzą w nim rozliczenia, pokrycie testami jest cienkie, a dwie osoby, które rozumiały reguły podatkowe, odeszły z firmy. Zarząd chce, żeby do przyszłego roku rozliczenia działały jako serwis w TypeScripcie. Ktoś już poprosił agenta: „zmigruj billing/ do TypeScriptu” i dostał pull request na 900 plików, którego nikt nie przejrzy ani nie odważy się scalić.
Szybkość samej zmiany kodu przestała być ograniczeniem. Ogłoszenie Claude Opus 5.5 od Anthropic (22 września 2026) przytacza anonimowego testera, który ukończył „a 680,000-line code migration in less than a day”. To anegdota raportowana przez dostawcę i nie mówi, czy zmigrowany system nalicza ten sam podatek od tej samej faktury. Ta strona jest o tym, jak wytworzyć taki dowód.
Jest dla tech leada, który prowadzi program, dla CTO, który go finansuje i podpisuje przełączenie, oraz dla deweloperów, którzy uruchamiają fale.
Co zyskujesz z modernizacji prowadzonej przez agentów
Dział zatytułowany „Co zyskujesz z modernizacji prowadzonej przez agentów”- Program w pięciu fazach z bramką wyjścia i jej właścicielem dla każdej fazy oraz plan
slices.yaml, wspólny dla agentów i ludzi. - Zestaw nagrywający (harness) testów charakteryzacyjnych na szwie HTTP (przetestowany z Vitest 5.0.2 oraz z pytest i syrupy 6.1.1) i strażnika w CI, który nie pozwala pull requestom migracyjnym zmienić utrwalonego zachowania.
- Konfiguracje fal dla Claude Code, Codexa i Cursora, pomocnika do porównania w cieniu, checklistę przełączenia i cztery prompty do skopiowania.
Dlaczego „zmigruj to do TypeScriptu” nie działa jako jeden prompt?
Dział zatytułowany „Dlaczego „zmigruj to do TypeScriptu” nie działa jako jeden prompt?”Zawodzi z czterech powodów i żaden z nich nie dotyczy umiejętności programistycznych modelu.
- Nie ma wyroczni. Kod legacy rzadko ma testy. Testy, które agent pisze po nowym kodzie, opisują nowy kod razem z jego błędami i przechodzą z definicji (zob. siła wyroczni).
- Partia jest za duża, żeby ją zweryfikować. Wytyczne DORA do modelu AI capabilities (Nathen Harvey i Allison Park, Google Cloud, 10 grudnia 2025) mówią wprost: „AI can easily generate massive blocks of code, which are hard to review and test. Enforcing the discipline of small batches counteracts this risk.”
- Nieudokumentowane zachowanie jest nośne. Dziwne zaokrąglenia, pusty identyfikator klienta oznaczający „klienta z ulicy”, raport sortowany w kolejności wstawiania: od każdego z nich ktoś zależy, a przepisanie kodu na podstawie jego lektury je gubi.
- Intencje się mieszają. Przeniesienie kodu, podbicie wersji frameworka i „poprawka” wyliczenia w jednej zmianie nie pozwalają ustalić, co zepsuło produkcję.
Pięć faz i ich bramki wyjścia
Dział zatytułowany „Pięć faz i ich bramki wyjścia”Każda faza kończy się dowodem, który człowiek może sprawdzić bez czytania zmigrowanego kodu. Większość pisania wykonują agenci, a ludzie są właścicielami bramek.
| Faza | Co robią agenci | Za co odpowiadają ludzie | Bramka wyjścia (dowód) |
|---|---|---|---|
| 0. Mapa | Inwentaryzują punkty wejścia, wywołujących, magazyny danych i efekty uboczne | Zakres, docelowa architektura, granice plastrów | slices.yaml zatwierdzony przez tech leada |
| 1. Utrwalenie | Nagrywają testy charakteryzacyjne i pliki golden na systemie legacy | Które wejścia są reprezentatywne; które osobliwości są błędami | Pliki golden w repozytorium, wynik testów mutacyjnych na minimum lub wyżej, akceptacja właściciela wyroczni |
| 2. Szew | Wprowadzają punkt routingu (fasada, trasa w proxy albo flaga) bez zmiany zachowania | Gdzie przebiega szew | Zestaw charakteryzacyjny zielony przez szew, zero zmian w plikach golden |
| 3. Migracja w falach | Przenoszą każdy plaster w izolowanym worktree albo VM, jeden pull request na plaster | Kolejność fal, kolejka scalania, spory | Dla każdego plastra: pliki golden bez zmian i zielone na nowej ścieżce, funkcje dopasowania zielone, pakiet dowodów w pull requeście |
| 4. Dowód i przełączenie | Uruchamiają porównanie w cieniu, klasyfikują rozbieżności, szkicują zapis przełączenia | Decyzje o rozbieżnościach, wdrożenie, decyzja „jedziemy” | Zero niewyjaśnionych rozbieżności przez pełny cykl biznesowy, przećwiczone wycofanie, podpisany zapis przełączenia |
Poprowadź program krok po kroku
Dział zatytułowany „Poprowadź program krok po kroku”-
Zmapuj system i spisz plan plastrów. Uruchom pierwszy prompt niżej w trybie planowania (
/planw Claude Code i Codexie, Plan Mode w Cursorze), przejrzyj plan, a potem go zatwierdź, żeby agent zapisałslices.draft.yamlimap.md. Poprawiaj szkic, aż każdy plaster ma jeden szew, jednego właściciela i klasę ryzyka. Plaster ma dobry rozmiar, gdy jego zestaw wykonuje się w minuty, a migracja mieści się w jednym pull requeście, który da się przejrzeć. Przy bardzo dużych systemach zajrzyj do baz kodu na miliony linii. -
Utrwal zachowanie pierwszych plastrów. Nagraj pliki golden na działającym systemie legacy zestawem nagrywającym opisanym niżej, na prawdziwych wejściach: zanonimizowanych żądaniach z produkcji, nietypowych kontach od zespołu wsparcia, przypadkach z zamknięcia miesiąca. Plik golden, który wygląda na błąd, trafia na listę
quirksplastra, a decyzję podejmujesz później, w osobnej zmianie. Pełną technikę opisujemy na stronie testy charakteryzacyjne. -
Udowodnij, że pliki golden potrafią zawieść. Uruchom testy mutacyjne na plastrze legacy i trzymaj wynik na uzgodnionym minimum (narzędzia niżej). Mutant, który przeżył, to wejście, którego jeszcze nie nagrałeś: w naszym teście plik golden pustej faktury został zielony po zmianie stawki podatku, bo pusta faktura daje zero przy każdej stawce.
-
Zablokuj wyrocznię. Przypisz
tests/characterization/wCODEOWNERSwłaścicielowi wyroczni, odbierz agentom prawo do edycji (zakładki narzędzi niżej) i dodaj strażnika CI. Od tej chwili pull request migracyjny, który dotyka pliku golden, odrzuca reguła, a nie uwaga recenzenta. -
Przetnij szew. Poprowadź ruch plastra przez jeden punkt z flagą, która wysyła każde żądanie do ścieżki legacy albo do nowej. Scal szew, gdy ścieżka legacy nadal odpowiada na wszystko, i potwierdź, że zestaw jest przez niego zielony (wzorzec strangler fig).
-
Migruj w falach. Fala to zbiór niezależnych plastrów, z których każdy jest izolowanym zadaniem agenta kończącym się jednym pull requestem. Zacznij od jednego plastra niskiego ryzyka jako pilotażu, potem poszerzaj. Trzymaj plaster przy jednej intencji: przeniesienie, aktualizacja albo poprawka, nigdy dwie naraz.
-
Najpierw cień, potem przełączenie. Wdróż nową ścieżkę po cichu, porównaj ją ze ścieżką legacy na żywym ruchu i napraw albo jawnie zaakceptuj każdą rozbieżność. Potem przesuwaj ruch krokami za flagą, jak w stopniowym wdrażaniu (progressive delivery), i złóż zapis przełączenia.
-
Usuń ścieżkę legacy. Po uzgodnionym okresie na 100% usuń stary kod, flagę i okablowanie cienia w jednym pull requeście. Zestaw charakteryzacyjny zostaje jako zestaw regresyjny.
Testy mutacyjne plastra legacy
Dział zatytułowany „Testy mutacyjne plastra legacy”Zmutowany kod musi wykonać się pod testami charakteryzacyjnymi, inaczej wynik nic nie znaczy.
- JavaScript albo TypeScript. Runner
commandw Strykerze może uruchomić aplikację legacy z mutowanego sandboksu, a potem zestaw HTTP. UstawcoverageAnalysis: "off", athresholds.breakna minimum. Instalacja:npm i -D @stryker-mutator/core(10.0.0 w npm we wrześniu 2026). - PHP. Infection 0.35.4 (aktualne wydanie w Packagist 26 września 2026;
composer require --dev infection/infection) uruchamia w swoim procesie wyłącznie zestawy PHPUnit, Pest albo Codeception, więc nie oceni zestawu HTTP wołającego osobny serwer. Daj mu zestaw PHPUnit dla plastra, który odtwarza nagrane przypadki przez front controller i porównuje je z tymi samymi plikami golden.
Plan plastrów, z którego pracuje każdy agent
Dział zatytułowany „Plan plastrów, z którego pracuje każdy agent”Trzymaj plan w repozytorium jako dane: agenci mogą go czytać, CI może go sprawdzać, a stan programu jest o jeden grep stąd:
# modernization/slices.yaml: the program's single source of truthprogram: billing-php-to-tsoracle_owner: "@acme/billing-leads"mutation_score_floor: 80 # percent, per slice, measured on the legacy codeslices: - id: customer-search seam: "GET /api/customers" risk: low # the pilot: first wave, one low-risk slice depends_on: [] goldens: tests/characterization/golden/customer-search/ quirks: [] status: shadow # mapped | pinned | seamed | migrating | shadow | cut-over | deleted wave: 1 - id: invoice-total seam: "POST /api/invoices/total" risk: high # money: code owner review plus staged rollout depends_on: [] goldens: tests/characterization/golden/invoice-total/ quirks: - "Totals round half-down on line items (legacy). Keep until INV-311 decides." status: seamed wave: 2 - id: tax-lookup seam: "GET /api/tax/rate" # an HTTP seam, so the wave's done command can verify it risk: high depends_on: [] goldens: tests/characterization/golden/tax-lookup/ quirks: [] status: seamed wave: 2 - id: vat-id-check seam: "POST /api/vat-ids/validate" risk: medium depends_on: [] goldens: tests/characterization/golden/vat-id-check/ quirks: [] status: seamed wave: 2 - id: invoice-pdf seam: "GET /api/invoices/:id/pdf" risk: medium depends_on: [invoice-total] goldens: tests/characterization/golden/invoice-pdf/ quirks: [] status: mapped wave: 3 # ...one entry per sliceTrzy reguły utrzymują go w uczciwości: plaster wchodzi do fali dopiero ze statusem seamed; plaster czeka, aż wszystko z jego depends_on osiągnie cut-over; a quirks to jedyne miejsce, w którym może żyć znany błąd. Wybieraj szwy HTTP, bo polecenia fal niżej traktują zestaw HTTP jako definicję ukończenia. Szew w postaci sygnatury funkcji wymaga testów w tym samym procesie w każdym z języków i własnego polecenia sprawdzającego ukończenie.
Zestaw nagrywający testów charakteryzacyjnych na szwie HTTP
Dział zatytułowany „Zestaw nagrywający testów charakteryzacyjnych na szwie HTTP”Nagrywanie na szwie HTTP pozwala jednemu zestawowi utrwalić system w PHP i zweryfikować jego następcę w TypeScripcie, bo oba odpowiadają na te same żądania. Ustaw TARGET_URL na wdrożenie legacy, żeby nagrywać, i na nowe, żeby weryfikować.
import { readFileSync } from 'node:fs';import { describe, expect, it } from 'vitest';
// Point TARGET_URL at the legacy system to record, at the new one to verify.const BASE_URL = process.env.TARGET_URL ?? 'http://localhost:8080';const cases: { id: string; body: unknown }[] = JSON.parse( readFileSync(new URL('./invoice-total.cases.json', import.meta.url), 'utf8'),);
// Mask what differs on every run, so the golden file pins behavior, not noise.const scrub = (body: Record<string, unknown>) => ({ ...body, requestId: '<id>', generatedAt: '<ts>' });
describe('POST /api/invoices/total: pinned legacy behavior', () => { it.each(cases)('$id', async ({ id, body }) => { const res = await fetch(`${BASE_URL}/api/invoices/total`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const observed = { status: res.status, body: scrub(await res.json()) }; await expect(JSON.stringify(observed, null, 2) + '\n').toMatchFileSnapshot( `./golden/invoice-total/${id}.json`, ); });});Nagrywasz poleceniem TARGET_URL=https://legacy.internal npx vitest run tests/characterization. Vitest zapisuje brakujący plik golden na maszynie dewelopera, ale gdy ustawiona jest zmienna CI, oblewa test (przetestowane na Vitest 5.0.2), więc pusta wyrocznia nie przejdzie w pipeline. Nigdy nie uruchamiaj vitest -u na gałęzi migracyjnej: aktualizacja plików golden to zmiana zachowania.
import jsonimport osimport pathlib
import httpximport pytest
# Point TARGET_URL at the legacy system to record, at the new one to verify.BASE_URL = os.environ.get("TARGET_URL", "http://localhost:8080")CASES = json.loads(pathlib.Path(__file__).with_name("invoice_total_cases.json").read_text())
def scrub(body: dict) -> dict: """Mask what differs on every run, so the snapshot pins behavior, not noise.""" return {**body, "requestId": "<id>", "generatedAt": "<ts>"}
@pytest.mark.parametrize("case", CASES, ids=lambda c: c["id"])def test_invoice_total_pinned(case, snapshot): res = httpx.post(f"{BASE_URL}/api/invoices/total", json=case["body"]) assert {"status": res.status_code, "body": scrub(res.json())} == snapshotZainstaluj poleceniem pip install pytest syrupy httpx, a potem nagraj raz poleceniem pytest tests/characterization --snapshot-update na systemie legacy; syrupy 6.1.1 zapisuje do tests/characterization/__snapshots__/. Bez tej flagi brakujący snapshot oblewa test, więc CI nigdy nie nagrywa po cichu. Dla wyjść o swobodnej postaci, takich jak PDF, użyj przepływu approval opisanego w testach charakteryzacyjnych.
Funkcja scrub to miejsce, w którym ludzie najczęściej się mylą. Maskuj tylko wartości, które różnią się przy każdym uruchomieniu tego samego systemu, na przykład identyfikatory żądań i znaczniki czasu. Maskowanie pola, bo nowy system formatuje je inaczej, to decyzja, która należy do listy quirks plastra.
Trzymaj utrwalone zachowanie poza zasięgiem agenta
Dział zatytułowany „Trzymaj utrwalone zachowanie poza zasięgiem agenta”W trakcie fal agent sprawia, że pliki golden przechodzą, bez ich zmieniania. Egzekwuj to w sesji agenta i w CI. Ten strażnik działa dla każdego narzędzia, bo sprawdza pull request, a nie sesję:
name: oracle-guardon: pull_request: types: [opened, synchronize, reopened, labeled, unlabeled]
permissions: contents: read
jobs: goldens-unchanged: if: ${{ !contains(github.event.pull_request.labels.*.name, 'behavior-change') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - name: Fail if a migration PR changes pinned behavior env: BASE_REF: ${{ github.base_ref }} run: | changed=$(git diff --name-only "origin/$BASE_REF...HEAD" -- tests/characterization/) if [ -n "$changed" ]; then printf 'Pinned behavior changed:\n%s\n' "$changed" >&2 echo "Split it into its own PR labelled behavior-change, approved by the oracle owner." >&2 exit 1 fiEtykietę behavior-change mogą dodać tylko osoby z uprawnieniami triage lub zapisu, ale agent z tokenem GitHub z prawem zapisu (Cloud Agent albo gh w sesji) doda ją sam: nie dawaj tokenom agentów prawa do etykiet albo niech strażnik wymaga dodatkowo akceptacji właściciela wyroczni. Ustaw strażnika i przebieg testów charakteryzacyjnych jako wymagane checki. Workflow pull_request uruchamia kopię strażnika z samego pull requesta, więc agent mógłby go przerobić tak, żeby przechodził: przypisz w CODEOWNERS także .github/workflows/ i modernization/slices.yaml właścicielowi wyroczni i włącz Require review from Code Owners, żeby zmiana strażnika wymagała tej samej akceptacji co zmiana pliku golden.
Zablokuj edycję wyroczni we wspólnych ustawieniach projektu, .claude/settings.json, gdzie wiodący / jest liczony względem projektu, do którego należy plik ustawień (głównego katalogu roboczego):
{ "permissions": { "deny": [ "Edit(/tests/characterization/**)", "Edit(/modernization/slices.yaml)", "Edit(/.github/**)", "Edit(/.claude/settings.json)", "Edit(/.claude/settings.local.json)", "Edit(/.claude/hooks/**)" ] }, "sandbox": { "enabled": true, "filesystem": { "denyWrite": [ "./tests/characterization", "./modernization/slices.yaml", "./.github", "./.claude/settings.json", "./.claude/settings.local.json", "./.claude/hooks" ] }, "network": { "allowLocalBinding": true } }}Same reguły deny zatrzymują narzędzia edycji, a nie skrypt, który agent uruchomi. Przy włączonym sandboksie Claude Code stosuje reguły deny Edit(...) także do poleceń w sandboksie (v2.1.283); jawna lista denyWrite czyni to widocznym i obejmuje ścieżki, których nie blokujesz dla Edit. Wpisy dla .claude wskazują pliki, a nie cały .claude/, bo Claude Code tworzy swoje worktree w .claude/worktrees/ (v2.1.283), a reguła na cały katalog mogłaby zablokować edycje każdej jednostki /batch.
Na macOS sandbox domyślnie nie pozwala zająć lokalnego portu, więc jednostka fali nie uruchomiłaby serwera; pozwala na to sandbox.network.allowLocalBinding: true. Klucz działa tylko na macOS (sprawdzone na v2.1.283) i na Linuksie ani WSL2 niczego nie zmienia. Dodaj hook z ochrony wyroczni i sprawdź konfigurację na plastrze pilotażowym, zanim uruchomisz falę: poproś jednostkę /batch o edycję pliku golden w jej worktree (w .claude/worktrees/<nazwa>/) i potwierdź, że edycja zostaje odrzucona. Jeśli nie zostaje, reguły deny zakotwiczone w katalogu głównym nie sięgają tego worktree, więc polegaj na sandboksie i checku oracle-guard.
Użyj profilu uprawnień (beta, Codex CLI 0.138.0 lub nowszy), który w sandboksie ustawia wyrocznię jako tylko do odczytu: profilu przetestowanego w ochronie wyroczni z 0.157.1, zawężonego do tego programu, w ~/.codex/config.toml. Katalogi i dokładne ścieżki plików działają; globy tylko do odczytu są w 0.157.1 odrzucane:
[permissions.locked-oracle]extends = ":workspace"
[permissions.locked-oracle.filesystem.":project_roots"]"tests/characterization" = "read""modernization/slices.yaml" = "read"".github" = "read"".codex" = "read"
[permissions.locked-oracle.network]enabled = trueZostaw tabelę sieci: :workspace wyłącza sieć, więc job fali nie uruchomiłby serwera ani nie wywołał localhost. W naszym sprawdzeniu z Codex CLI 0.157.1 na Linuksie codex sandbox pod :workspace odmówił nawet utworzenia gniazda, a bind i połączenie z 127.0.0.1 zadziałały po dodaniu enabled = true. Ten przełącznik otwiera też sieć wychodzącą, więc dodaj wpisy per domena opisane w uprawnieniach i sandboksie. Żeby job został offline, usuń krok z serwerem i uruchamiaj sprawdzenie charakteryzacyjne po każdym jobie, poza sandboksem.
Wybierasz go dla pojedynczego uruchomienia przez -c default_permissions=locked-oracle, jak w poleceniach fal niżej.
Wpisz „never edit tests/characterization/ or modernization/slices.yaml; if a golden looks wrong, stop and report it” do reguły projektu (Rule). Reguła to instrukcja, a nie blokada, a Cloud Agents działają we własnych maszynach wirtualnych, więc Cursora wiążą job oracle-guard i CODEOWNERS. Hooki blokujące działania agenta opisujemy w ochronie wyroczni.
Jak uruchomić falę migracji w każdym narzędziu?
Dział zatytułowany „Jak uruchomić falę migracji w każdym narzędziu?”Kształt jest wszędzie ten sam: jeden plaster na izolowany checkout, jeden pull request na plaster, zestaw charakteryzacyjny jako definicja ukończenia.
Dla jednego plastra uruchom claude --worktree invoice-total (w skrócie -w) i wklej prompt migracyjny z sekcji niżej.
Dla całej fali wbudowany skill /batch „decomposes the work into 5 to 30 independent units, and presents a plan”; po twojej akceptacji „spawns one background subagent per unit in an isolated worktree”, a każdy z nich „implements its unit, runs tests, and publishes its change” (dokumentacja poleceń Claude Code, sprawdzona 26 września 2026 na v2.1.283). Nazwij plastry, żeby nie wymyślał własnego podziału:
/batch migrate wave 2 from modernization/slices.yaml (invoice-total, tax-lookup, vat-id-check). One unit per slice. Give unit i its own port, PORT=3001+i (3001, 3002, 3003). Each unit ports only its slice to services/billing-ts, leaves tests/characterization/ untouched, starts services/billing-ts from its own worktree on its PORT, and is done only when TARGET_URL=http://localhost:$PORT npx vitest run tests/characterization/<slice> passes.Dla większych fal albo orkiestracji, którą chcesz uruchamiać ponownie, użyj dynamicznych przepływów pracy (dynamic workflows): Anthropic wymienia wśród zastosowań „large migrations” i podaje skalę „Dozens to hundreds of agents per run”. W planie Pro najpierw włącz je w /config.
Uruchom każdy plaster jako nieinteraktywne zadanie we własnym zarządzanym worktree, z zablokowaną wyrocznią (Codex CLI 0.157.1):
# Terminal, repository root: one job per slice in the wave, one port per jobmkdir -p wave-runsi=0for slice in invoice-total tax-lookup vat-id-check; do port=$((3001 + i)); i=$((i + 1)) codex exec --worktree -c default_permissions=locked-oracle \ -o "$PWD/wave-runs/$slice.md" \ "Port slice $slice from modernization/slices.yaml to services/billing-ts. Start services/billing-ts from this worktree on port $port, then run TARGET_URL=http://localhost:$port npx vitest run tests/characterization/$slice. Done means that run passes. Do not edit tests/characterization/ or modernization/slices.yaml." &donewaitZadania działają jednocześnie, więc każde potrzebuje własnego portu: przy wspólnym porcie serwer, który pierwszy go zajmie, odpowiada na testy wszystkich zadań, a pozostałe plastry świecą na zielono na kodzie zbudowanym z innego checkoutu. -o (--output-last-message) zapisuje końcowy raport każdego zadania do wave-runs/ w głównym checkoucie (ścieżka bezwzględna przez $PWD, bo każde zadanie działa we własnym worktree). Dla opornego plastra codex cloud exec --env ENV_ID --attempts 3 "…" uruchamia best-of-N w Codex cloud (eksperymentalne w 0.157.1); codex cloud diff TASK_ID --attempt N pokazuje zmiany jednego podejścia, a codex cloud apply TASK_ID --attempt N sprowadza wybrane lokalnie. Więcej szczegółów Codexa znajdziesz w modernizacji bazy kodu z Codexem.
Worktrees w Cursorze „let Agent work in isolated Git checkouts”, więc z edytora uruchamiaj jeden plaster na worktree. Dla fali uruchom jednego Cloud Agenta na plaster, z Cursora albo przez Cloud Agents API (/v1/agents), każdego z promptem migracyjnym z zamrożoną wyrocznią (niżej), nazwą plastra i własnym portem. Każdy pull request przechodzi przez te same checki oracle-guard i charakteryzacyjne. Pojedynczy moduł legacy w edytorze opisujemy w refaktoryzacji kodu legacy z Cursorem.
Rozmiar fali dobieraj do przepustowości przeglądu i scalania, a nie do liczby agentów, które potrafisz uruchomić: 12 pull requestów na jednego recenzenta jednego popołudnia przesuwa wąskie gardło. Zobacz wzorcach orkiestracji.
Prompty do skopiowania: modernizacja legacy
Dział zatytułowany „Prompty do skopiowania: modernizacja legacy”Jak udowodnić nową ścieżkę na produkcji przed przełączeniem?
Dział zatytułowany „Jak udowodnić nową ścieżkę na produkcji przed przełączeniem?”Testy charakteryzacyjne pokrywają wejścia, o których pomyślałeś; równoległe uruchomienie pokrywa to, co wysyłają użytkownicy. Uruchamia starą ścieżkę jako kontrolną, a nową jako kandydata, zwraca wynik kontrolnej i raportuje każdą różnicę:
type Outcome = 'match' | 'mismatch' | 'error';
export interface ShadowOptions<I, O> { slice: string; legacy: (input: I) => Promise<O>; modern: (input: I) => Promise<O>; // must have no side effects while shadowing enabled: (input: I) => boolean; // your feature flag: per tenant or a percentage normalize?: (output: O) => unknown; // sort keys, drop fields that may differ record: (event: { slice: string; outcome: Outcome; detail?: unknown }) => void;}
export function shadow<I, O>(opts: ShadowOptions<I, O>): (input: I) => Promise<O> { const norm = opts.normalize ?? ((o: O) => o); return async (input) => { const control = await opts.legacy(input); // users still get the legacy answer if (opts.enabled(input)) { // Not awaited: the comparison never adds latency or errors to the request. // Recording failures are swallowed by the final catch, so they cannot surface either. void opts .modern(input) .then( (candidate) => { const same = JSON.stringify(norm(candidate)) === JSON.stringify(norm(control)); opts.record({ slice: opts.slice, outcome: same ? 'match' : 'mismatch', detail: same ? undefined : { control, candidate } }); }, (err: unknown) => opts.record({ slice: opts.slice, outcome: 'error', detail: String(err) }), ) .catch(() => {}); } return control; };}Dwie reguły czynią go bezpiecznym. Kandydat w cieniu musi tylko czytać (skieruj jego zapisy na magazyn-cień albo je zaślep), inaczej każde żądanie wydarzy się dwa razy. Poza tym record dostaje prawdziwe dane, więc najpierw usuń dane osobowe.
Zdefiniuj liczby, zanim na nie spojrzysz, żeby nikt nie negocjował progu po zobaczeniu wyniku:
| Metryka | Definicja | Bramka przełączenia, którą zalecamy |
|---|---|---|
| Porównane wywołania | Żądania, w których zadziałały obie ścieżki i wywołało się record | Pełny cykl biznesowy, na przykład zamknięcie miesiąca |
| Odsetek niewyjaśnionych rozbieżności | Rozbieżności niesklasyfikowane jako naprawiony port-bug ani zaakceptowany legacy-quirk, na porównane wywołanie | Zero na koniec okna |
| Odsetek błędów kandydata | Wyniki error na porównane wywołanie | Zero albo każdy błąd powiązany ze zgłoszeniem |
| Pokrycie plików golden | Udział identyfikatorów przypadków golden, których kształt wejścia pojawił się w żywym ruchu | Każdy przypadek golden widziany co najmniej raz albo luka wyjaśniona |
Checklista dowodów przełączenia
Dział zatytułowany „Checklista dowodów przełączenia”Przełączenie podpisuje CTO albo wskazany właściciel serwisu; zapis szkicuje agent. Trzymaj go w modernization/cutover/<slice>.md i podlinkuj z pull requesta, który przestawia flagę:
- Zestaw charakteryzacyjny zielony na nowej ścieżce, pliki golden bez zmian od utrwalenia (linki do przebiegów
oracle-guard). - Wynik testów mutacyjnych plastra legacy na poziomie
mutation_score_floorlub wyżej, zmierzony w chwili nagrywania plików golden. - Funkcje dopasowania i kontrola typów zielone w nowym serwisie.
- Daty okna cienia, liczba porównanych wywołań i zero niewyjaśnionych rozbieżności.
- Każdy
legacy-quirkalbo przeniesiony celowo, albo zmieniony we własnym, zatwierdzonym pull requeściebehavior-change. - Kroki wdrożenia i metryka, która zatrzymuje każdy krok, spisane przed pierwszym krokiem (zob. stopniowe wdrażanie (progressive delivery)).
- Przećwiczone wycofanie: flaga przestawiona z powrotem na stagingu, ścieżka legacy odpowiedziała poprawnie.
- Data usunięcia ścieżki legacy i jej flagi.
Zatwierdzający może powiedzieć „tak” bez czytania zmigrowanego kodu, jak w dowodach zamiast diffów.
Co się psuje w modernizacji prowadzonej przez agentów?
Dział zatytułowany „Co się psuje w modernizacji prowadzonej przez agentów?”Agent „poprawia” plik golden. Objaw: zielony pull request migracyjny dotyka tests/characterization/. Wyjście: oracle-guard go odrzuca; jeśli plik golden zmienił się już na gałęzi głównej, cofnij zmianę i nagraj go ponownie z legacy.
Pliki golden przechodzą, ale produkcja się nie zgadza. Objaw: rozbieżności w cieniu na wejściach, których zestaw nigdy nie nagrał. Wyjście: dodaj każde reprezentatywne wejście jako przypadek golden (czwarty prompt je szkicuje); wiele rozbieżności oznacza, że plaster utrwalono zbyt cienko.
Pull requesty z jednej fali wchodzą w konflikt. Objaw: plastry z jednej fali edytują ten sam router, manifest albo wspólny typ. Wyjście: scal współdzielone rusztowanie przed falą i zaostrz depends_on. Plastry, które ciągle się zderzają, były jednym plastrem.
Współdzielona baza danych to ukryte sprzężenie. Objaw: nowy serwis czyta tabelę, do której pisze kod legacy, więc plastrów nie da się przełączyć niezależnie. Wyjście: zrób ze szwu danych osobny plaster z plikami golden, a zmiany schematu prowadź wzorcem expand, migrate, contract z pracy z bazami danych.
Długi ogon staje w miejscu i pali budżet. Objaw: 80% plastrów jest przełączonych, a podejść agentów na pozostałych przybywa bez scaleń. Wyjście: daj każdemu pozostałemu plastrowi właściciela i datę usunięcia, po dwóch nieudanych uruchomieniach przekaż go człowiekowi po lepszy szew albo więcej plików golden, a na najtrudniejszych użyj podejść best-of-N (Codex cloud) albo pętli sterowanej celem. Mierz scalone plastry na tydzień, nie uruchomienia agentów. Modernizacja zatrzymana na 80% utrzymuje dwa systemy bez końca.