Inteligencia de código
Control Center ofrece a un agente cuatro formas de entender el código más allá de leerlo. Responden a preguntas genuinamente distintas, y la razón de que haya cuatro en lugar de una es que ningún mecanismo por sí solo responde a todas.
| Mecanismo | Responde a | Coste |
|---|---|---|
| Grafo de código | Qué hay aquí y qué depende de ello | Una pasada de indexación; consultas baratas |
| Servidor de lenguaje | Si esto compila de verdad y qué es este tipo | Un proceso de servidor por proyecto |
| Búsqueda estructural | Dónde aparece esta FORMA | Un análisis por archivo |
| Depurador | Qué era cierto en este momento, en este frame | Un proceso detenido |
El hueco que cierra el servidor de lenguaje
Sección titulada «El hueco que cierra el servidor de lenguaje»El grafo de código se construye a partir de tree-sitter: símbolos, aristas de llamada, un radio de impacto. Es rápido, es barato y no sabe nada de tipos. No puede decirte si la llamada que acabas de escribir compila, si ese campo existe en el objeto o si tu renombrado se ha dejado una reexportación.
Sin un servidor de lenguaje, un agente se entera al ejecutar los tests, o nunca. Escribe algo plausible, sigue adelante y el fallo aparece tres turnos después, asociado a un cambio distinto.
Así que el beneficio real no es la herramienta lsp. Es que write, edit y apply_patch están envueltas: tras cada escritura, el archivo pasa por el servidor de lenguaje y los diagnósticos recién introducidos se incorporan al resultado de la herramienta. El agente se entera de que ha roto la compilación en el mismo momento en que la rompe, tanto si se le ocurrió preguntar como si no.
Solo los diagnósticos nuevos
Sección titulada «Solo los diagnósticos nuevos»Repetir lo que ya se le ha dicho al agente es peor que no decir nada: llena el resultado de ruido que el agente aprende a pasar por alto, y el único error nuevo se va con él. Un registro por ejecución hace seguimiento de lo ya notificado, con la clave puesta en la severidad, el código y el mensaje — no en la posición, de modo que un diagnóstico que solo ha bajado cuatro líneas no se vuelve a notificar como nuevo.
Perezoso y sincero cuando no está
Sección titulada «Perezoso y sincero cuando no está»Ningún servidor arranca al iniciar. Uno arranca en la primera petición que lo necesita, que casi siempre es la primera edición de un archivo que reclama como suyo, y los inactivos se limpian. Un host que nunca toca Dart nunca arranca un analizador.
La detección es una intersección: debe existir un marcador de raíz en el checkout y el binario debe poder resolverse. Un marcador por sí solo dice «este proyecto es Python» y nada sobre si el servidor está instalado; un binario que se resuelve por sí solo dice que el host tiene una cadena de herramientas y nada sobre este checkout. Solo ambos a la vez significan que una consulta puede tener éxito, y una herramienta que se ofrece y siempre falla es peor que una que no está.
Por qué existe la búsqueda estructural si ya está grep
Sección titulada «Por qué existe la búsqueda estructural si ya está grep»grep compara caracteres. Es la herramienta adecuada para un nombre y la equivocada para una forma.
Un patrón estructural se analiza con la misma gramática que el archivo, así que dispose($X) encuentra la llamada sin importar cómo esté espaciada, envuelta o partida en varias líneas, y nunca coincide con los mismos caracteres dentro de un literal de cadena o un comentario.
La propiedad que lo hace algo más que una regex con pasos extra es que una metavariable repetida en un patrón debe capturar el mismo texto:
if ($X != null) $X.dispose()coincide con una guarda que libera lo que comprobó y no coincide con una que libera otra cosa. Es una clase de error que no puedes expresar de forma textual en absoluto.
Se ejecuta sobre el tree-sitter que ya está enlazado
Sección titulada «Se ejecuta sobre el tree-sitter que ya está enlazado»No hay un segundo motor. cc_natives ya carga tree-sitter y sus gramáticas para el grafo de código, con una caché de consultas compiladas y una tabla de lenguajes. El buscador es Dart puro sobre árboles que de todos modos se estaban analizando. Un segundo motor estructural significaría un segundo analizador, un segundo conjunto de gramáticas que compilar y desplegar en cada plataforma, y dos respuestas a «qué es una expresión de llamada».
Un fragmento necesita un sitio donde vivir
Sección titulada «Un fragmento necesita un sitio donde vivir»Una gramática solo define qué es un archivo entero. Analizado en crudo en el nivel superior de Dart, dispose(x) es una firma de función perfectamente válida —una declaración llamada dispose que recibe un parámetro x— sin ningún error en ninguna parte que lo delate. Un buscador construido sobre ese análisis busca declaraciones, no encuentra llamadas e informa de «ninguna coincidencia» para un patrón que nunca se estaba leyendo como parece.
Así que un patrón se prueba en una escalera de andamiajes —sentencia, expresión, miembro de clase, nivel superior— y gana el primero que lo acepta. Un fragmento que ningún contexto acepta se notifica como no analizable en lugar de buscarse y no encontrarse en silencio.
Por qué una reescritura se prepara en lugar de escribirse directamente
Sección titulada «Por qué una reescritura se prepara en lugar de escribirse directamente»Una reescritura estructural es la edición más amplia que puede hacer un agente: un patrón, cuarenta archivos y nadie lee el resultado línea por línea, ni el modelo ni la persona que lo pidió. Escribirla e informar de que está «hecho» significa que la primera mirada honesta al cambio es un git diff a posteriori.
Así que ast_edit informa de lo que haría —el recuento, los archivos, los sitios coincidentes— y el cambio solo se aplica cuando resolve lo confirma. Eso es también lo que permite que un diff y un Aceptar/Descartar se pongan delante de una persona antes de que nada llegue al disco.
La confirmación es todo o nada y cada archivo se comprueba contra el contenido capturado en el momento de la preparación. Entre la preparación y la confirmación, puede que el agente haya editado a mano uno de esos archivos, que una pasada de diagnósticos lo haya reescrito o que un formateador lo haya tocado; confirmar de todos modos descartaría ese trabajo y el diff parecería intencionado. Cualquier discrepancia rechaza todo el cambio, porque una reescritura estructural aplicada a medias deja un árbol que no compila con ninguna de las dos formas.
Qué responde un depurador que no puede responder una instrucción print
Sección titulada «Qué responde un depurador que no puede responder una instrucción print»El bucle de la instrucción print es: añadir un print, volver a ejecutarlo todo, leer la salida y borrar el print. Cuesta una ejecución completa de tests por cada pregunta y solo responde a la pregunta que se te ocurrió hacer antes de ejecutar.
Un frame detenido responde de una vez a todas las preguntas sobre ese momento: cada variable local, cada invocador y una expresión evaluada en el propio ámbito del frame. Para un proceso colgado o un fallo que se reproduce una vez de cada diez ejecuciones, esa es la diferencia entre una tarde entera y un minuto.
Acotada como un recinto, y por el mismo motivo
Sección titulada «Acotada como un recinto, y por el mismo motivo»Un adaptador de depuración posee un proceso detenido que retiene todo lo que retiene ese proceso: un puerto, un bloqueo, una conexión a una base de datos. Una sesión que nadie dirige es una fuga que sobrevive al turno que la inició. Así que hay un TTL estricto, exactamente una sesión por conversación, y un segundo launch se rechaza en lugar de reemplazar silenciosamente al primero: un agente que inicia un segundo sin cerrar el primero casi con total seguridad ha perdido la pista del primero.
Un transporte, dos protocolos
Sección titulada «Un transporte, dos protocolos»El Debug Adapter Protocol comparte el framing de Content-Length del Language Server Protocol y nada por encima de él: claves de correlación distintas, una forma de fallo distinta y un tercer tipo de mensaje (eventos) sin equivalente en LSP. El framing vive en un único lugar; los protocolos se asientan sobre él por separado.
Escribir el framing dos veces significaría dos respuestas a «un mensaje abarca dos trozos», «un trozo contiene tres mensajes» y «el servidor escribió un banner en stdout antes de su primer frame», y solo una de ellas recibiría la corrección.
Relacionado
Sección titulada «Relacionado»- Buscar código con el grafo de código — el lado de los símbolos y el impacto
- Depurar un test fallido con un agente — el depurador, de principio a fin
- Refactorizar código de forma estructural —
ast_grepyast_edit - Herramientas de agente integradas — todas las operaciones y sus argumentos