Přeskočit na obsah

Inteligence kódu

Control Center dává agentovi čtyři způsoby, jak kódu rozumět i jinak než čtením. Každý odpovídá na jinou otázku. Jsou čtyři právě proto, že žádný jeden mechanismus nezodpoví všechny.

Mechanismus Na co odpovídá Co stojí
Graf kódu Co tu je a co na tom závisí Průchod indexem; dotaz je levný
Jazykový server Přeloží se to opravdu a jaký je to typ Proces serveru na každý projekt
Strukturální hledání Kde se objevuje tento TVAR Rozbor na soubor
Debugger Co v tomto okamžiku a v tomto rámci platilo Zastavený proces

Mezera, kterou uzavírá jazykový server

Sekce “Mezera, kterou uzavírá jazykový server”

Graf kódu vzniká z tree-sitter: symboly, hrany volání, poloměr dopadu. Je rychlý, je levný a o typech neví nic. Neřekne ti, jestli se volání, které jsi právě napsal, přeloží, jestli to pole na objektu existuje, ani jestli přejmenování minulo re-export.

Bez jazykového serveru se to agent dozví až při testech, nebo se to nedozví vůbec. Napíše něco věrohodného, jde dál a chyba se ukáže o tři tahy později, přilepená k jiné změně.

Přínos proto není samotný nástroj lsp. write, edit a apply_patch jsou obalené: po každém zápisu soubor projde jazykovým serverem a k výsledku nástroje se připojí nově zavedené diagnostiky. Agent se dozví, že rozbil sestavení, v okamžiku, kdy ho rozbil, ať se zeptal, nebo ne.

Opakovat, co už agent slyšel, je horší než mlčet. Výsledek se zaplní šumem, který se naučí přeskakovat, a jediná nová chyba odejde s ním. Evidence daného běhu si vede, co už bylo nahlášeno, podle závažnosti, kódu a textu zprávy, ne podle pozice. Diagnostika, která jen sjela o čtyři řádky níž, se proto nehlásí znovu jako nová.

Líně a upřímně o tom, že chybí

Sekce “Líně a upřímně o tom, že chybí”

Při startu se nespouští žádný server. Spustí se až s prvním požadavkem, který ho potřebuje, skoro vždy při první úpravě souboru, o který se hlásí. Nečinné servery se odklízejí. Hostitel, který se Dartu nikdy nedotkne, nespustí analyzátor.

Detekce je průnik: v checkoutu musí ležet kořenová značka a zároveň se musí najít binárka. Značka sama říká „tohle je projekt v Pythonu“ a nic o tom, jestli je server nainstalovaný. Binárka, kterou jde najít, sama říká, že hostitel má toolchain, a nic o tomto checkoutu. Teprve obojí dohromady znamená, že dotaz může projít. Nástroj, který se nabízí a pokaždé selže, je horší než ten, který v nabídce není.

Proč strukturální hledání, když už je tu grep

Sekce “Proč strukturální hledání, když už je tu grep”

grep hledá znaky. Na jméno je to správný nástroj, na tvar špatný.

Strukturální vzor se parsuje stejnou gramatikou jako soubor. dispose($X) proto najde volání, ať je oddělené mezerami, zalomené, nebo zabalené, a tytéž znaky uvnitř řetězcového literálu nebo komentáře neshodí nikdy.

Od regulárního výrazu s pár kroky navíc se liší tím, že metaproměnná zopakovaná v jednom vzoru musí zachytit tentýž text:

if ($X != null) $X.dispose()

sedí na guard, který zavolá dispose na to, co právě otestoval, a nesedí na guard, který dispose zavolá na něco jiného. Taková třída chyb se textem vyjádřit nedá.

Běží na tree-sitter, který už je nalinkovaný

Sekce “Běží na tree-sitter, který už je nalinkovaný”

Druhý engine tu není. cc_natives už pro graf kódu načítá tree-sitter a jeho gramatiky, s cache zkompilovaných dotazů a tabulkou jazyků. Porovnávač je čistý Dart nad stromy, které se tak jako tak parsovaly. Druhý strukturální engine by znamenal druhý parser, druhou sadu gramatik, kterou je třeba sestavit a připravit na každé platformě, a dvě odpovědi na otázku „co je výraz volání“.

Fragment potřebuje, kam ho zasadit

Sekce “Fragment potřebuje, kam ho zasadit”

Gramatika říká jen to, čím je celý soubor. Když se dispose(x) parsuje holé na nejvyšší úrovni Dartu, je to úplně platná signatura funkce: deklarace pojmenovaná dispose s parametrem x. Nikde není chyba, která by to prozradila. Porovnávač postavený na takovém rozboru hledá deklarace, volání nenajde a u vzoru, který se nikdy nečetl tak, jak vypadá, ohlásí „žádné shody“.

Vzor se proto zkouší po žebříčku opor: příkaz, výraz, člen třídy, nejvyšší úroveň. První, která ho přijme, vyhrává. Fragment, který nepřijme žádný kontext, se nahlásí jako nerozparsovatelný. Nehledá se a pak tiše nenajde.

Proč se přepis připraví, místo aby se zapsal

Sekce “Proč se přepis připraví, místo aby se zapsal”

Strukturální přepis je nejširší úprava, kterou agent umí udělat: jeden vzor, čtyřicet souborů a výsledek řádek po řádku nečte nikdo. Ani model, ani člověk, který o přepis požádal. Zapsat ho a ohlásit „hotovo“ znamená, že první poctivý pohled na změnu je až git diff potom.

ast_edit proto nahlásí, co by udělal: počet, soubory, místa shody. Změna se zapíše, teprve když ji resolve potvrdí. Díky tomu může před člověkem nejdřív ležet diff a volba Přijmout/Zahodit, než cokoli dopadne na disk.

Zápis je všechno, nebo nic. Každý soubor se porovná s obsahem zachyceným při přípravě. Mezi přípravou a zápisem mohl agent některý soubor upravit ručně, průchod diagnostik ho mohl přepsat, nebo se ho dotkl formátovač. Zapsat to přesto by tu práci zahodilo a diff by vypadal jako záměr. Jakýkoli nesoulad odmítne celou změnu: částečně provedený strukturální přepis nechá strom, který se nepřeloží ani v jednom z obou tvarů.

Co debugger zodpoví a příkaz print ne

Sekce “Co debugger zodpoví a příkaz print ne”

Smyčka s příkazem print je: přidej print, spusť to celé znovu, přečti výstup, print smaž. Každá otázka stojí celý běh testů a odpoví jen na to, na co ses zeptal před během.

Zastavený rámec odpoví najednou na každou otázku o tom okamžiku: každá lokální proměnná, každý volající a výraz vyhodnocený ve vlastním rozsahu rámce. U zaseknutého procesu, nebo u chyby, která se ukáže jednou z deseti běhů, je to rozdíl mezi odpolednem a minutou.

Ohraničený jako izolované prostředí, a ze stejného důvodu

Sekce “Ohraničený jako izolované prostředí, a ze stejného důvodu”

Adaptér debuggeru vlastní zastavený proces, který drží všechno, co ten proces drží: port, zámek, spojení s databází. Relace, kterou nikdo neřídí, je únik, který přežije tah, v němž vznikla. Proto je tu tvrdá TTL, přesně jedna relace na konverzaci a druhý launch se odmítne, místo aby první tiše nahradil. Agent, který spustí druhou, aniž první ukončil, o té první skoro jistě ztratil přehled.

Jeden transport, dva protokoly

Sekce “Jeden transport, dva protokoly”

Debug Adapter Protocol sdílí s Language Server Protocol rámování Content-Length a nic nad ním. Korelační klíče jsou jiné, tvar selhání je jiný a třetí typ zprávy (události) nemá v LSP protějšek. Rámování je na jednom místě, protokoly na něm sedí každý zvlášť.

Napsat rámování dvakrát by znamenalo dvě odpovědi na „zpráva sahá přes dva chunky“, „chunk nese tři zprávy“ a „server zapsal banner na stdout ještě před prvním rámcem“. Opravu by dostala jen jedna z nich.