Cestou k temné továrně: zeď

2. díl z 5 v sérii „Towards the Dark Factory“ (Cestou k temné továrně)
V prvním dílu jsme si vypůjčili slovník Saumyi Tyagiho: mezi vrstvou, která generuje kód, a vrstvou, která ho validuje, stojí zeď. V jeho článku zeď chrání odložené scénáře (holdout scenarios), které přebírá ze softwarové továrny StrongDM: akceptační testy, které kódující agent nikdy nevidí, posuzované samostatným hodnotitelem proti dočasnému nasazení. Generátor nemůže ošidit test, který nikdy nečetl.
Naše zeď je postavená jinak a tenhle díl je o tom, z čeho je, proč drží a co nevidí. Krátká verze: nic v naší zdi nečte prózu. Je z llvm-cov, mutačního runneru, skriptu na přehrávání, testu na rozdíly v katalogu, typové kontroly a jednoho agenta, jehož jedinou prací je nevěřit jinému agentovi. Jedno místo, kde odložená data používáme, pojmenujeme.
Proč ne odložené scénáře
Nad odloženými scénáři jsme přemýšleli a nepřijali je, z důvodu, který je specifický pro tuhle aplikaci. Sestava je časovač pro iPhone. Jeho nejtěžší chování je engine tréninku s časovou sémantikou (tiky, dohánění po uspání, hodiny posunuté zpět), stavový automat řízený hlasovými pokyny a haptikou a dekodéry otevřeného formátu plánu. LLM hodnotitel, který šťouchá do API dočasného nasazení, se hodí na webovou službu; nehodí se na „krok odpočinku se nesmí posunout, když je now dřív než začátek kroku“. To jsou vlastnosti a vlastnosti chtějí deterministické testy.
Zeď je tedy white-box. Tentýž agent, který píše kód, píše i testy, a to je skutečná slabina, kterou je třeba říct nahlas: test napsaný autorem kódu dokazuje, že kód dělá to, co si autor myslel. To, co dává testům význam, je vrstva pod nimi.
Vrstva 1: pokrytí, 100 %, bez výjimek
scripts/coverage-gate.sh spustí testy balíčku s instrumentací, vyexportuje profil přes llvm-cov a selže, pokud řádky, funkce a regiony nejsou každé na 100 % pro Sources/. Žádný mechanismus výjimek neexistuje. Soubor se smlouvou říká „Pokrytí je 100 % bez výjimek“ a brána nemá přepínač, kterým by se nějaká přidala.
Vlastní přísnost brány našla chybu v bráně. Během vlny tréninkových formátů hlásila na balíčku jádra falešný řádek s nulovým počtem průchodů ve 2 z 5 běhů, bez změny kódu mezi nimi. S cílem 99 % by člověk pokrčil rameny a spustil to znovu; s cílem 100 % a bez výjimek nebylo kde krčit rameny, tak se to zkoumalo. Swift Testing spouští testovací funkce jako souběžné úlohy v jednom procesu a jeho LLVM čítače pokrytí jsou obyčejné neatomické inkrementace (výchozí chování popisuje volba -fprofile-update v dokumentaci Clangu); dva testy, které ve stejném okamžiku projdou stejným místem ve zdrojáku, mohou ztratit jediný inkrement, který ten řádek kdy dostane. Instrumentovaný průchod teď běží s --no-parallel (obyčejný swift test zůstává paralelní) a 20 po sobě jdoucích běhů na macOS plus 10 uvnitř linuxového kontejneru po opravě hlásilo 100 %. Cena jsou zhruba dvě sekundy. Komentář v bráně to všechno vysvětluje, aby to další agent „neoptimalizoval“ pryč.
Vrstva 2: mutační runner
Pokrytí říká, že řádek proběhl. Neříká, že by si test všiml, kdyby byl řádek špatně. Na to jsme napsali mutační runner, přímo v repozitáři, nad swift-syntax, první večer (commit má časové razítko 22:02). Mutuje Sources/ po jednom místě (obrácené relační operátory, prohozená aritmetika, && za ||, booleovské literály, inkrementy a dekrementy celých čísel, negované podmínky, jeden smazaný příkaz v těle funkce) a na každém mutantovi spustí testovací sadu.
Mutant, který přežije, znamená, že si změny nevšiml žádný test, a přeživší mutant shodí build. Jediná cesta ven je seznam výjimek, jedno id na řádek se zdůvodněním na témž řádku, pro mutanty, kteří prokazatelně nemohou změnit chování, a runner hlásí zastaralé položky, takže seznam nemůže zastarat. K 30. září má seznam jádra položky jako EngineReducer.swift:302:35:integerDecrement:1->0 s větou vysvětlující, proč smyčka vrátí stejný index tak i tak.
Čísla u pull requestu vlny typovaných chyb z 30. září: 1 730 mutantů na balíčku jádra, 1 706 zabitých, 8 vypršelo (počítají se jako zabití; obvykle nekonečná smyčka), 16 přežilo a je na seznamu výjimek; 1 622 na balíčku aplikační logiky, 1 615 zabitých, 4 přeživší na seznamu. Mutanti, kteří se nezkompilují, se nepočítají vůbec. Na pull requestech CI pouští runner jen nad změněnými soubory; na main a jednou týdně pouští všechno.
Runner má vlastní testy a dvě z jeho pravidel vznikla z pozorování, jak selhává. Nejdřív jednou spustí nezměněnou sadu jako základ a přeruší se, pokud tahle sada selže, vyprší nebo nahlásí nula testů, místo aby hodnotil mutanty proti rozbitému základu. A když běh testů mutanta narazí na timeout, runner zabije celý strom procesů a mutanta počítá jako vypršelého, jen když dokáže potvrdit, že běh skončil; jinak to zkusí znovu na čerstvé kopii, a když ani to nejde, shodí celou bránu s unconfirmedRun. To pravidlo existuje, protože ztracené oznámení o ukončení jednou nechalo osiřelého testovacího pomocníka tři hodiny zahálet, a protože jeden CI job na Linuxu čekal na rouru drženou sirotkem, dokud ho GitHub po 240 minutách nezrušil.
Vrstva 3: plány přehrané na čistý checkout
První díl říkal, že jednotka práce je specifikace a plán, každý s adversariálním review. Revidovat plán čtením je myšlení úrovně 2. Naše pravidlo od šestého dne: plán se reviduje jako soubor. Skript vytáhne jeho bloky kódu, přehraje je na čistý checkout a výsledek se porovná se stromem, který autor plánu ověřil, bajt po bajtu. Pravidlo vzniklo v den, kdy reviewer spustil vytažený kód a zjistil, že jeden blok v plánu se tak, jak byl napsaný, neparsuje, a dva z jeho diffů git apply odmítl; autorův ověřený strom byl v pořádku, dokument, který ho popisoval, ne.
Od té doby je „přehrání bajtově shodné“ řádek ve verdiktu review plánu a kód v plánu se počítá za ověřený, jen když proběhly jeho testy.
Vrstva 4: reviewer, který nevěří
Každý úkol v běhu dostane nezávislého reviewera: jiného agenta, worktree jen pro čtení, diff, zadání a hlášení implementátora. Pravidlo, na kterém záleží: reviewer znovu spustí implementátorovy reprodukce a nikdy mu nevěří na slovo. Hlášení implementátora říká „29 nových testů bylo na starém kódu červených“; reviewer si vyzvedne starý kód a počítá.
Tahle vrstva je místo, kde se naše zeď nejvíc podobá Tyagiho: reviewer kód nenapsal a mimo diff neví, jak vznikl. Je to také místo, kde se kola review zaplatí. Na pull requestu blogu našla čtyři kola review pokaždé to, co předchozí kolo přehlédlo (prázdný blog odkazovaný z každé stránky, tagy s diakritikou vypadlé z postranního panelu, feedy s anglickými výchozími hodnotami, pak jedna regrese, kterou zanesla oprava); na plánu pro Apple Watch udělala totéž dvě kola. Drobné nálezy reviewera merge neblokují; odkládají se jako issues se štítkem, což je čtvrtý díl.
Vrstva 5: typy tam, kde by se próza rozcházela s kódem
Dvě pravidla ze souboru se smlouvou jsou méně o testech a více o tom, aby se určité chyby nedaly ani vyjádřit.
Typované chyby nesou polohu jako data; UI nikdy nematchuje text chyby. Chyba dekódování formátu plánu je typovaný důvod s JSON cestou, ne věta. Ve vlně z 30. září se to protáhlo celou aplikací: každý anglický String s detailem chyby v balíčku aplikační logiky se stal typovanou hodnotou vykreslenou z klíče LocalizedText, po jednom vlastníkovi, aby český překlad byl položka katalogu, a ne druhý if nad řetězcem.
Všechny věty pro uživatele jsou LocalizedText z Copy; po přidání věty spusť mise run copy-catalog; CatalogTests selže na rozdílu. Nástroj přegeneruje katalog řetězců z definic Copy a test obojí porovná. Řádek brány z té vlny zní 0 added, 518 keys, 0 stale. Věta napsaná rovnou do view se proti typu nezkompiluje; klíč přidaný do Copy bez katalogu shodí test.
A ještě dvě menší cihly ve zdi: swift format lint --strict na každý pull request a push na main a schémová brána, která validuje ukázkové plány formátu proti JSON Schema a, což je stejně důležité, ověřuje, že složka neplatných fixtur je odmítnuta.
Jediné pravidlo s odloženými daty
Tady je místo, kde děláme to, co dělají scénáře StrongDM. Soubor se smlouvou má jedno pravidlo o datech, která ladič nikdy neviděl:
Změna laděná proti příkladům se měří na příkladech, které ladění nikdy nevidělo, commitnutých před začátkem ladění.
Vzešlo z funkce „Popsat trénink“, která žádá jazykový model v zařízení, aby z věty udělal plán, a z bugu: model četl opakování jako sekundy. Agent doladil instrukce proti 16 známým případům a zvedl úspěšnost z 24 % na 44 %. Na 8 případech, které nikdy neviděl, šla úspěšnost z 11 % na 13 %. Bez neviděných případů by změna odešla jako oprava. Další pokus přesunul kontroly mimo model (čísla se musí v popisu vyskytovat, opakované položky se odmítnou) a měřil se na druhé odložené sadě, kde špatných plánů ubylo z 61 % na 24 %. Ten odchází jako oprava.
Tohle je zeď přesně v Tyagiho smyslu a to, co StrongDM přirovnává k odloženým datům při trénování modelů: generátor nevidí hodnoticí data. Používáme ji přesně tam, kam patří: kde je měněná věc prompt a měřená věc chování modelu. Všude jinde jsou testy white-box a poctivé je drží mutační brána.
Tři věci, které brány chytily a review přehlédlo
Zeď si na sebe vydělá v dnech, kdy nesouhlasí s reviewerem.
Dvě vady přístupnosti napříč celým webem. První běh Lighthouse a axe nad webem zjistil, že bílý text na firemní červené měří 3,6 : 1, pod hranicí 4,5 : 1 z WCAG AA, a že odkazy v běžném textu se odlišují jen barvou. Tři designové handoffy a každé review, lidské i agentní, obojí pustily. Změnily se dva CSS tokeny a rozpočty teď běží na každou změnu webu.
Dvanáct přeživších mutantů po osmi zrevidovaných úkolech. Vlna tréninkových formátů na balíčku jádra prošla osmi úkoly, každý implementovaný, zrevidovaný, opravený a znovu zrevidovaný, a pak plnou mutační branou: 12 přeživších mimo seznam výjimek. Agent napsal testy, které deset z nich zabily, a dva zdůvodnil jako ekvivalentní; review toho diffu bylo samostatné kolo. Zrevidovaný kód se 100% pokrytím měl pořád tucet míst, kde obrácené porovnání nezměnilo nic, co by test viděl.
Každý koncept článku na blogu by web shodil. Smoke test přidaný kvůli jednomu issue kontroluje každou stránku v sitemapě na popis o 70 až 160 znacích a titulek nejvýš 60 znaků včetně přípony webu. Změřeno na deseti konceptech článků, které právě schválila čtyři kola review: všech deset popisů příliš dlouhých (166 až 235 znaků), pět titulků přes limit. Review články četla; nikdo je nezměřil.
Co zeď nevidí
Brány běží na Macu a na Linuxu. Nemohou cítit haptiku, slyšet pokyn přes podcast ani číst česky. Když se ukázalo, že vibrace při změně kroku je v kapse nepostřehnutelná, žádný test nebyl špatně; unit testy pokrývají volbu techniky a životní cyklus enginu, nikdy to, co je cítit, a pull request, který to opravil, odešel s 24řádkovým kontrolním seznamem pro člověka s iPhonem. Když koncept na blogu tvrdil funkci, kterou měl roadmap, ale aplikace ne, byl validátorem čtenář, který porovnával text s kódem, a to, co se počítá za pravdu při launchi, rozhodl člověk. To je zbývající práce člověka a je to pátý díl.
Předtím třetí díl: s takovou zdí se agentům dá věřit, že si sami určí tempo, a jak to vypadá na jednom Macu se dvěma swiftovými sloty a jedním simulátorem.
Toto je 2. díl série Towards the Dark Factory. 1. díl: Z úrovně 2 na úroveň 3 za týden. 3. díl: Tempo určuje agent (vyjde 16. října). 4. díl: Agent plní backlog (vyjde 23. října). 5. díl: Kde ještě zůstává člověk (vyjde 30. října).
Související: Architektonické testy jako pás proti agentnímu rozjezdu · Lintujeme promptovou vrstvu


