Wat een dependency-graph vindt dat je coding agent mist
7m leestijd

Wat een dependency-graph vindt dat je coding agent mist

Ik liet een scanner los op een interne applicatie waar agents al maanden in werkten. Hij deed er nog geen twee seconden over en leverde vier tickets op. Geen van die defecten zat in een bestand, en precies daarom vond niemand ze door bestanden te lezen.

Gisteren liet ik een scanner los op een interne applicatie die ik onderhoud. Coding agents werken er al maanden in: twee talen, een kleine zeventig endpoints, een testsuite die groen is.

De scan duurde nog geen twee seconden. Hij las 588 bestanden in, goed voor ongeveer 6.700 componenten en 17.700 relaties, en aan het eind van de middag had ik er vier tickets uit gehaald.

Geen van die defecten zat in een bestand.

Daar draait deze hele post om, dus laat ik precies zeggen wat het betekent.

Vier van de vijf exports van de resilience-module bereiken alleen het testbestand, één bereikt de draaiende code

De agent leest bestanden, en dat doet hij goed

Geef een coding agent één bestand en hij levert echt goed werk af. Hij ziet de off-by-one, de niet-afgevangen rejection, de omgedraaide vergelijking. Vraag hem wat er stukgaat als je deze functie aanpast en hij volgt de aanroepen die hij kan vinden, meestal tot het juiste antwoord.

Dat is geen schouderklopje bij gebrek aan beter. De meeste defecten zitten binnen één bestand, en daar is dit gereedschap sterk. De grens ligt ergens anders: een agent in een grote repository heeft die repository nooit in zijn hoofd, hij heeft alleen wat hij besloot te openen. Hij kent één buurt en doet uitspraken over de hele stad.

Sommige eigenschappen van een codebase bestaan alleen tússen bestanden. Dat zijn de eigenschappen die niemand zou vinden door zorgvuldig te lezen.

De vier tickets

Een circuit breaker die niemand importeert. De resilience-module exporteert vijf dingen: een retry-helper, een timeout-wrapper, de breaker-klasse en twee geconfigureerde breakers, één per upstream, elk met een eigen drempel en cooldown. Daar heeft iemand over nagedacht. Iemand koos vijf mislukte pogingen en zestig seconden voor het CRM, drie pogingen en dertig seconden voor de model-API.

De productiecode importeert precies één van de vijf. De retry. De timeout-wrapper, de breaker-klasse en beide instanties zijn alleen bereikbaar vanuit hun eigen testbestand.

De integratie blijft het dus proberen bij een hangende upstream, zonder timeout eromheen, en de breaker die voor die upstream is gebouwd en er ook naar heet, wordt nooit aangeroepen. Retry zonder timeout is de slechtste van de twee volgordes: één trage afhankelijkheid groeit daarmee uit tot een handvol hangende verzoeken.

Een outputvalidatie die nergens aan hangt. De sanitize-module exporteert twee functies. De ene schoont gebruikerstekst op voordat die de prompt in gaat, en die zit netjes in het chat-endpoint. De andere controleert wat het model terugstuurt, en die is nergens op aangesloten. Wat het model in gaat wordt opgeschoond. Wat eruit komt wordt geloofd.

Een migratierunner die alleen tests draaien. De module bestaat, hij werkt, zijn tests zijn grondig, en de enige verwijzingen ernaar komen uit dat testbestand. Migraties worden met de hand gedraaid, en dat is altijd zo geweest. De code wekt de indruk dat dat al een jaar geregeld is.

Acht API-wrappers zonder aanroepers, plus een tweede databasepool-module waar nergens naar wordt verwezen. Een integratieclient die ruim is opgezet en nauwelijks is aangesloten.

Wat me het langst bijbleef, was het getal eronder: 76 componenten in deze repository zijn alleen bereikbaar vanuit testcode. De coverage ziet er gezond uit. Een deel daarvan test code die nooit draait.

Het defect is een ontbrekende verbinding

Open de resilience-module en lees hem. Er is niks mis mee. Sterker nog, hij is zorgvuldig geschreven, en in de commentaarblokken staat netjes uitgelegd hoe je de breaker gebruikt.

Grep op de breaker en elke treffer stelt je gerust: de klasse, de twee instanties, de exportregel, de test die bewijst dat hij na vijf mislukkingen opengaat. Een agent die precies doet waar hij goed in is, concludeert terecht dat die functionaliteit er is.

De bug is de import die nooit geschreven is. Een verbinding die niet bestaat staat in geen enkel bestand, dus hoeveel bestanden je ook leest, je vindt hem niet. Je kunt niet greppen op iets wat er niet is.

Daarom is het antwoord een graph en geen betere prompt. Zodra de repository een verzameling knopen en verbindingen is, wordt "welke geëxporteerde functies hebben geen inkomende verbinding buiten de tests" een query van milliseconden. Het is dezelfde aanpak die fitness functions op boundaries loslaten, nu gericht op bereikbaarheid: stop met hopen dat iemand het opmerkt en reken het uit.

Diezelfde query beantwoordt de vraag die ik vóór een refactor echt wil stellen. Van de databasepool hangen 149 componenten af. Van de authenticatiecheck 137. Als een agent aanbiedt om "even de auth-helper op te schonen", is dat getal de reden dat het antwoord nee is tot ik de diff twee keer heb gelezen.

Het confidence-label is waar het om draait

Er was een vijfde melding, en die is de reden dat ik de andere vier vertrouw.

De scan meldde een cycle in de auth-service van de frontend, met confidence certain: het ophalen van een token plant een proactieve refresh in, en die refresh haalt weer een token op. Twee functies die elkaar aanroepen, een cycle uit het boekje.

Ik heb hem erbij gepakt en het is opzet. Het token ververst zichzelf op 75% van zijn levensduur, de vorige timer wordt gewist voordat er een nieuwe wordt gezet, en uitloggen annuleert hem. De cycle ís het ontwerp.

Een tool die dat als defect had gerapporteerd, had de geloofwaardigheid van de andere vier in zijn val meegesleept. Deze rapporteert wat hij kan bewijzen en labelt de rest: certain voor een AST-verbinding die hij heeft kunnen herleiden, probable voor de dode-codekandidaten, met de waarschuwing ernaast dat reflectie, dispatch tables en frameworkconventies iets kunnen aanroepen zonder een zichtbare statische verbinding achter te laten. Die vier tickets bleven staan omdat ik ze met de hand heb nagelopen en het bewijs naar de exacte regel wees.

Zelfverzekerd en fout is de faalmodus van al het andere in deze stack. Een tool die een agent architectuurfeiten aanreikt, is precies het onderdeel dat dat niet mag doen.

Zelf doen

Hier is mijn scanner niet voor nodig. Knossos-MCP is wat ik heb gebouwd omdat ik die antwoorden bij de agent wilde hebben in plaats van in een CI-rapport, en het is pre-release, dus zelf bouwen vanuit de broncode, of het gewoon laten. De onderliggende aanpak is veel ouder dan dit alles en zit al in het gereedschap dat je draait.

Stel je codebase drie vragen waar lezen geen antwoord op geeft:

  • Wat wordt geëxporteerd en buiten de eigen tests nooit geïmporteerd? knip beantwoordt precies deze vraag: hij rapporteert ongebruikte exports, en met knip --production vallen de testbestanden uit de graph, zodat een export die alleen zijn eigen test bereikt zichtbaar wordt in plaats van te verdwijnen achter een groene suite. Met dependency-cruiser kom je er via de reachable-regel, gericht op het entry point van je productiecode, plus een pathNot op je testglob. Dat entry point doet het werk, dus een pathNot in zijn eentje meldt niets, en niets melden is niet te onderscheiden van een schone codebase. Niet via no-orphans, want die stelt een strengere vraag op moduleniveau, geen inkomende en geen uitgaande verbindingen, en die had over de module uit dit stuk niets gezegd. Vulture vindt dode Python-code, maar maakt het onderscheid met testcode niet, dus op die stack bouw je dat zelf.
  • Waar hangt het meeste van af? Niet het grootste bestand. Het symbool met de meeste inkomende verbindingen, en dat is zelden hetzelfde. Dat getal hoort te bepalen hoe streng een wijziging daarin wordt gereviewd.
  • Wat raakt deze diff werkelijk? Niet de bestanden die hij aanpast. Alles wat vanuit die bestanden bereikbaar is, zodat een reviewer weet waar hij moet kijken.

Draai die eerste vandaag op iets waar een agent al een tijd in werkt. Niet om de agent te betrappen. Die vier dingen zaten er al lang voordat er een agent bij kwam, en geen agent heeft ze erin gezet. De agents zagen ze alleen net zo min als de mensen voor hen, en inmiddels komen er steeds sneller bestanden bij voor een lezer die er altijd maar een paar tegelijk vasthoudt.

Het vangnet was geschreven, getest en nooit aangesloten. Iedereen las dat bestand. Niemand las de ruimte eromheen.

(7 van 7)