Aller au contenu

Intelligence du code

Control Center donne à un agent quatre façons de comprendre le code autrement qu’en le lisant. Elles répondent à des questions réellement différentes, et s’il y en a quatre plutôt qu’une, c’est qu’aucun mécanisme pris seul ne répond à toutes.

Mécanisme Répond à Coûts
Graphe de code Ce qui se trouve ici, et ce qui en dépend Une passe d’indexation ; interrogation peu coûteuse
Serveur de langage Est-ce que ceci compile réellement, et quel est ce type Un processus de serveur par projet
Recherche structurelle Où cette FORME apparaît-elle Une analyse syntaxique par fichier
Débogueur Ce qui était vrai à ce moment, dans cette trame Un processus arrêté

Le graphe de code est construit à partir de tree-sitter : des symboles, des arêtes d’appel, un rayon d’impact. Il est rapide, il est peu coûteux, et il ne sait rien des types. Il ne peut pas vous dire si l’appel que vous venez d’écrire compile, si ce champ existe sur l’objet, ni si votre renommage a raté un réexport.

Sans serveur de langage, un agent l’apprend au moment des tests — ou jamais. Il écrit quelque chose de plausible, passe à la suite, et la défaillance refait surface trois tours plus tard, rattachée à un autre changement.

Le gain n’est donc pas vraiment l’outil lsp. Il vient du fait que write, edit et apply_patch sont enveloppés : après chaque écriture, le fichier passe par le serveur de langage et les diagnostics nouvellement introduits sont intégrés au résultat de l’outil. L’agent apprend qu’il a cassé la compilation au moment même où il l’a cassée, qu’il ait pensé à le demander ou non.

Répéter à l’agent ce qu’on lui a déjà dit est pire que de ne rien dire : cela remplit le résultat de bruit que l’agent apprend à survoler, et la seule erreur nouvelle passe avec. Un registre par exécution garde la trace de ce qui a été signalé, indexé sur la gravité, le code et le message — pas sur la position, de sorte qu’un diagnostic simplement descendu de quatre lignes n’est pas signalé de nouveau.

Aucun serveur ne démarre au lancement. Un serveur démarre à la première requête qui en a besoin, c’est-à-dire presque toujours à la première modification d’un fichier qu’il prend en charge, et les serveurs inactifs sont balayés. Un hôte qui ne touche jamais à Dart ne démarre jamais d’analyseur.

La détection est une intersection : un marqueur racine doit exister dans le checkout et le binaire doit être résolu. Un marqueur seul dit « ce projet est en Python » et rien sur l’installation ou non du serveur ; un binaire résolu seul dit que l’hôte possède une chaîne d’outils et rien sur ce checkout. Seuls les deux ensemble signifient qu’une requête peut réussir, et un outil offert qui échoue toujours est pire qu’un outil absent.

Pourquoi la recherche structurelle, alors que grep existe

Section intitulée “Pourquoi la recherche structurelle, alors que grep existe”

grep fait correspondre des caractères. C’est le bon outil pour un nom et le mauvais pour une forme.

Un motif structurel est analysé par la même grammaire que le fichier, de sorte que dispose($X) trouve l’appel quelle que soit sa disposition, son découpage ou ses retours à la ligne, et ne correspond jamais aux mêmes caractères à l’intérieur d’un littéral de chaîne ou d’un commentaire.

Ce qui en fait plus qu’une expression régulière avec des étapes en plus, c’est qu’une métavariable répétée dans un même motif doit capturer le même texte :

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

correspond à une garde qui dispose ce qu’elle a testé, et ne correspond pas à une garde qui dispose autre chose. C’est une catégorie de bogue que vous ne pouvez pas exprimer du tout sous forme textuelle.

Il n’y a pas de second moteur. cc_natives charge déjà tree-sitter et ses grammaires pour le graphe de code, avec un cache de requêtes compilées et une table des langages. Le moteur de correspondance est du Dart pur sur des arbres qui étaient de toute façon analysés. Un second moteur structurel impliquerait un second analyseur, un second ensemble de grammaires à compiler et à installer sur chaque plateforme, et deux réponses à la question « qu’est-ce qu’une expression d’appel ».

Une grammaire ne définit que ce qu’est un fichier entier. Analysé tel quel au niveau supérieur de Dart, dispose(x) est une signature de fonction parfaitement valide — une déclaration nommée dispose prenant un paramètre x — sans la moindre erreur pour le trahir. Un moteur de correspondance bâti sur cette analyse cherche des déclarations, ne trouve aucun appel et rapporte « aucune correspondance » pour un motif qui n’était pas lu de la façon dont il en a l’air.

Le motif est donc essayé dans une échelle d’échafaudages — instruction, expression, membre de classe, niveau supérieur — et le premier qui l’accepte l’emporte. Un fragment qu’aucun contexte n’accepte est signalé comme non analysable plutôt que recherché puis discrètement introuvable.

Pourquoi une réécriture est préparée plutôt qu’écrite d’emblée

Section intitulée “Pourquoi une réécriture est préparée plutôt qu’écrite d’emblée”

Une réécriture structurelle est la modification la plus large qu’un agent puisse faire : un motif, quarante fichiers, et personne ne lit le résultat ligne par ligne — ni le modèle, ni la personne qui l’a demandé. L’écrire et annoncer « terminé » signifie que le premier vrai coup d’œil sur le changement est un git diff par la suite.

Ainsi, ast_edit rapporte ce qu’il ferait — le nombre, les fichiers, les sites correspondants — et le changement n’atterrit que lorsque resolve le valide. C’est aussi ce qui permet de présenter un diff et un choix Accepter/Abandonner à une personne avant que quoi que ce soit n’atteigne le disque.

La validation est tout ou rien, et chaque fichier est comparé au contenu capturé au moment de la préparation. Entre la préparation et la validation, l’agent a pu modifier un de ces fichiers à la main, une passe de diagnostics a pu le réécrire, ou un formateur a pu y toucher — valider quand même écarterait ce travail et le diff aurait l’air voulu. Tout écart fait refuser l’ensemble du changement, parce qu’une réécriture structurelle partiellement appliquée laisse un arbre qui ne compile sous aucune des deux formes.

Ce qu’un débogueur répond et qu’une instruction d’affichage ne peut pas

Section intitulée “Ce qu’un débogueur répond et qu’une instruction d’affichage ne peut pas”

La boucle de l’instruction d’affichage, c’est : ajouter un affichage, tout relancer, lire la sortie, supprimer l’affichage. Elle coûte une exécution complète des tests par question, et elle ne répond qu’à la question que vous avez pensé à poser avant de lancer.

Une trame arrêtée répond d’un coup à toutes les questions sur ce moment — chaque variable locale, chaque appelant et une expression évaluée dans la portée propre à la trame. Pour un processus bloqué ou une défaillance qui ne se reproduit qu’une fois sur dix exécutions, c’est la différence entre un après-midi et une minute.

Délimité comme une enceinte, et pour la même raison

Section intitulée “Délimité comme une enceinte, et pour la même raison”

Un adaptateur de débogage possède un processus arrêté qui retient tout ce que ce processus retient — un port, un verrou, une connexion à une base de données. Une session que personne ne pilote est une fuite qui survit au tour qui l’a lancée. Il y a donc un TTL strict, exactement une session par conversation, et un second launch est refusé plutôt que de remplacer silencieusement le premier : un agent qui en lance un deuxième sans avoir terminé le premier a presque certainement perdu la trace du premier.

Le Debug Adapter Protocol partage le cadrage Content-Length du Language Server Protocol et rien au-dessus — des clés de corrélation différentes, une forme d’échec différente et un troisième type de message (les événements) sans équivalent dans le LSP. Le cadrage vit à un seul endroit ; les protocoles s’y appuient séparément.

Écrire le cadrage deux fois signifierait deux réponses à « un message s’étend sur deux segments », « un segment contient trois messages » et « le serveur a écrit une bannière sur stdout avant sa première trame », et une seule de ces réponses recevrait la correction.