Pular para o conteúdo

Inteligência de código

O Control Center oferece a um agente quatro formas de entender o código além de simplesmente lê-lo. Elas respondem a perguntas genuinamente diferentes, e a razão de serem quatro em vez de uma é que nenhum mecanismo isolado responde a todas elas.

Mecanismo Responde Custos
Grafo de código O que existe aqui e do que isso depende Uma passagem de indexação; consulta barata
Servidor de linguagem Isso compila de fato, e que tipo é este Um processo de servidor por projeto
Busca estrutural Onde esta FORMA aparece Uma análise sintática por arquivo
Depurador O que era verdade neste momento, neste frame Um processo parado

O grafo de código é construído a partir do tree-sitter: símbolos, arestas de chamada, um raio de impacto. É rápido, é barato e não sabe nada sobre tipos. Ele não consegue dizer se a chamada que você acabou de escrever compila, se aquele campo existe no objeto ou se sua renomeação deixou passar um re-export.

Sem um servidor de linguagem, um agente descobre isso na hora do teste — ou nunca. Ele escreve algo plausível, segue em frente, e a falha aparece três turnos depois, atrelada a outra alteração.

Então o ganho não está propriamente na ferramenta lsp. Está no fato de que write, edit e apply_patch são envolvidos: depois de cada escrita, o arquivo passa pelo servidor de linguagem e os diagnósticos introduzidos recentemente são incorporados ao resultado da ferramenta. O agente descobre que quebrou a build no momento em que a quebrou, tenha ou não pensado em perguntar.

Repetir o que o agente já ouviu é pior do que não dizer nada: enche o resultado de ruído que o agente aprende a ignorar, e o único erro novo se perde junto. Um registro por execução acompanha o que já foi reportado, com chave em gravidade, código e mensagem — não em posição, de modo que um diagnóstico que apenas desceu quatro linhas não é reportado de novo como novo.

Nenhum servidor inicia no boot. Um deles inicia na primeira solicitação que precisa dele, que quase sempre é a primeira edição em um arquivo que ele cobre, e os ociosos são removidos. Um host que nunca toca em Dart nunca inicia um analisador.

A detecção é uma interseção: um marcador de raiz precisa existir no checkout e o binário precisa resolver. Um marcador sozinho diz “este projeto é Python” e nada sobre o servidor estar instalado; um binário que resolve sozinho diz que o host tem um toolchain e nada sobre este checkout. Só os dois juntos significam que uma consulta pode ter sucesso, e uma ferramenta oferecida que sempre falha é pior do que uma ausente.

O grep casa caracteres. Essa é a ferramenta certa para um nome e a errada para uma forma.

Um padrão estrutural é analisado pela mesma gramática do arquivo, então dispose($X) encontra a chamada de qualquer jeito que esteja espaçada, quebrada em várias linhas ou envolta, e nunca casa os mesmos caracteres dentro de um literal de string ou de um comentário.

A propriedade que faz dele mais do que uma regex com etapas extras é que uma metavariável repetida em um mesmo padrão precisa capturar o mesmo texto:

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

casa com uma guarda que descarta o que ela testou e não casa com uma que descarta outra coisa. Essa é uma classe de bug que você não consegue expressar textualmente de forma alguma.

Não existe um segundo mecanismo. O cc_natives já carrega o tree-sitter e suas gramáticas para o grafo de código, com um cache de consultas compiladas e uma tabela de linguagens. O comparador é Dart puro sobre árvores que já estavam sendo analisadas de qualquer forma. Um segundo mecanismo estrutural significaria um segundo parser, um segundo conjunto de gramáticas para compilar e distribuir em cada plataforma, e duas respostas para “o que é uma expressão de chamada”.

Uma gramática só define o que é um arquivo inteiro. Analisado isoladamente no nível superior do Dart, dispose(x) é uma assinatura de função perfeitamente válida — uma declaração chamada dispose que recebe um parâmetro x — sem nenhum erro em lugar algum que denuncie o contrário. Um comparador construído sobre essa análise procura declarações, não encontra chamadas e reporta “nenhuma correspondência” para um padrão que nunca estava sendo lido do jeito que parece.

Então um padrão é testado em uma escada de andaimes — instrução, expressão, membro de classe, nível superior — e o primeiro que o aceita vence. Um fragmento que nenhum contexto aceita é reportado como não analisável, em vez de ser procurado e silenciosamente não encontrado.

Por que uma reescrita passa por preparação em vez de escrever direto

Seção intitulada “Por que uma reescrita passa por preparação em vez de escrever direto”

Uma reescrita estrutural é a edição mais ampla que um agente pode fazer: um padrão, quarenta arquivos, e ninguém lê o resultado linha por linha — nem o modelo, nem a pessoa que pediu. Escrevê-la e reportar “concluído” significa que o primeiro olhar honesto para a alteração é um git diff depois.

Então o ast_edit reporta o que ele faria — a contagem, os arquivos, os trechos correspondentes — e a alteração só se efetiva quando o resolve a confirma. É isso também que permite que um diff e um Aceitar/Descartar fiquem diante de um humano antes que qualquer coisa chegue ao disco.

A confirmação é tudo ou nada, e cada arquivo é verificado em relação ao conteúdo capturado no momento da preparação. Entre a preparação e a confirmação, o agente pode ter editado à mão um desses arquivos, uma passagem de diagnósticos pode tê-lo reescrito ou um formatador pode tê-lo tocado — confirmar de qualquer forma descartaria esse trabalho e o diff pareceria intencional. Qualquer divergência recusa a alteração inteira, porque uma reescrita estrutural aplicada em parte deixa uma árvore que não compila em nenhuma das duas formas.

O que um depurador responde que uma instrução de print não consegue

Seção intitulada “O que um depurador responde que uma instrução de print não consegue”

O ciclo da instrução de print é: adicionar um print, rodar tudo de novo, ler a saída, apagar o print. Custa uma execução completa de testes por pergunta, e só responde à pergunta que você pensou em fazer antes de rodar.

Um frame parado responde a todas as perguntas sobre aquele momento de uma vez — cada variável local, cada chamador e uma expressão avaliada no próprio escopo do frame. Para um processo travado ou uma falha que se reproduz uma vez a cada dez execuções, essa é a diferença entre uma tarde inteira e um minuto.

Um adaptador de depuração é dono de um processo parado que segura o que quer que aquele processo segure — uma porta, um lock, uma conexão de banco de dados. Uma sessão que ninguém está conduzindo é um vazamento que sobrevive ao turno que a iniciou. Por isso existe um TTL rígido, exatamente uma sessão por conversa, e um segundo launch é recusado em vez de substituir silenciosamente o primeiro: um agente que inicia um segundo sem encerrar o primeiro quase certamente perdeu o rastro do primeiro.

O Debug Adapter Protocol compartilha o enquadramento Content-Length do Language Server Protocol e nada acima disso — chaves de correlação diferentes, um formato de falha diferente e um terceiro tipo de mensagem (eventos) sem equivalente no LSP. O enquadramento fica em um só lugar; os protocolos se apoiam nele separadamente.

Escrever o enquadramento duas vezes significaria duas respostas para “uma mensagem atravessa dois blocos”, “um bloco contém três mensagens” e “o servidor escreveu um banner em stdout antes do primeiro frame”, e só uma delas receberia a correção.