Ga naar inhoud

Grip houden op je architectuur bij AI-gegenereerde code

Albert Skibinski
Grip houden op je architectuur bij Ai gegenereerde code

Het bewaken van de architectuur in een applicatie is niet altijd makkelijk. Bij menselijke code is dat al broos, en met Ai is dat niet anders (of lastiger).

Een LLM levert makkelijk code op die compileert, de tests haalt, en er in de diff schoon uitziet, maar stiekem een laag overslaat of twee layers aan elkaar knoopt. De fout is lokaal onzichtbaar; de schade is globaal en structureel.

Hieronder de strategie en de tools die ik momenteel gebruik om de architectuur zoveel mogelijk te bewaken: een landkaart met grenzen en handhaving.

De landkaart

Ik gebruik de secties van arc42 als inspiratie voor de structuur van mijn architectuur documentatie. Dit geeft me een gestructureerde documentatie zodat ik zeker weet dat alle aspecten gecovered zijn. Ik gebruik dus niet het hele arc42 framework, dat is mij veel te groot en te log. 

Voorbeeld dat in docs/architecture in de repo leeft:

| Bestand                  | arc42 § | Scope                                  |
|--------------------------|---------|----------------------------------------|
| principles.md            | §2, §4  | dragende principes                     |
| service-contracts.md     | §8, §12 | contracten tussen services + glossary  |
| config-cascade.md        | §8      | config-assemblage                      |
| api.md                   | §5      | building block — API                   |
| data-model.md            | §8      | schema-conventies                      |
| worker.md                | §5      | building block — achtergrond-worker    |
| gateway.md               | §5      | building block — gateway / router      |
| web-client.md            | §5      | building block — web-client (SPA)      |
| plugins.md               | §5      | building block — plugin-framework      |
| billing.md               | §5      | building block — facturatie            |
| external-integration.md  | §3, §5  | context + building block — koppeling   |
| deployment.md            | §7      | deployment view                        |
| runtime-views.md         | §6      | runtime choreografie                   |
| decisions/               | §9      | ADR-laag                               |

De kaart bestaat dus uit een verzameling markdown files. Een aantal van deze bevatten ook mermaid diagrammen die vooral handig zijn om dingen te visualiseren voor mensen.

Daarnaast:

  • Consistente structuur en naamgeving. Een voorspelbare indeling (lagen, mappen, namespaces) maakt de kaart leesbaar uit de paden zelf. Hoe consequenter de conventie, hoe minder je hoeft op te zoeken.
  • Een gespecialiseerde architectuur-agent met rules. Die kent de kaart en houdt elke wijziging ertegenaan, niet alleen aan het eind, maar al tijdens refinement/planning.
  • On-demand verkenning. Voor de actuele, concrete details laat ik mijn agents gewoon grep/Explore draaien; ze reconstrueren per taak precies het stukje kaart dat ze nodig hebben. Geen vooraf berekende graph die kan verouderen - de structuur is gedisciplineerd genoeg om er direct doorheen te navigeren.

De architectuur agent wordt tijdens plannen, refinements en reviews gebruikt. Deze agent heeft een set regels en principles en gebruikt bovenstaande documentatie.

De grenzen en handhaving

Een kaart is een ding, maar grenzen zonder handhaving zullen ervoor zorgen dat code wegglipt naar plekken waar ze niks te zoeken heeft. Om dat te enforcen heb ik gekozen om deptrac te gebruiken. Maar je zou ook phpstan + custom rules kunnen gebruiken.

Hieronder de 4 stappen hoe dit werkt met deptrac. Zie ook deptrac.yaml.

1. Layers. Je deelt je codebase op in lagen. 

- name: Controller   → ^App\Controller\
- name: Service      → ^App\Service\
- name: App_x   → ^App\Apps\x\
- name: App_y → ^App\Apps\y\

2. De dependency-graph
Deptrac "leest" je architectuur niet uit een tekening — het bouwt 'm op uit je code. Elke use, elke typehint, elke new en elke extends is een pijl in een dependency-graph. Die graph staat nergens als bestand; deptrac bouwt 'm elke run opnieuw op uit de AST. Maar je kunt 'm wél bevragen, met debug:dependencies:

$ vendor/bin/deptrac debug:dependencies App_x

Dit is dus vergelijkbaar met wat tools zoals graphify doen, maar ik gebruik het enkel bij het bewaken van de grenzen.

3. De ruleset = een allow-list. Per laag som je op welke lagen die laag mag aanroepen. Alles wat niet expliciet is toegestaan, is verboden.

Controller:  [Service, Repository, Entity, Enum, Security, Validator, Webhook, AppFramework]
App_x:  [AppFramework, Service, Repository, Entity, Enum, Security, Webhook, Validator, Doctrine]
AppFramework:[Entity, Enum]
Enum:        []

Uitleg:

  • Controller mag een Service, Repository enz. aanroepen — maar App_x staat er niet in. Een controller die App\Apps\x\… importeert = overtreding. 
  •  App_x mag de gedeelde kern + de AppFramework-naad gebruiken — maar App_y en App_z staan er niet in. X mag dus niet naar Y reiken, en andersom ook niet. (geen cross-app bleed)
  •  AppFramework mag alleen Entity en Enum kennen. De gedeelde app-infrastructuur móét app-agnostisch blijven; zodra iemand 'm naar een concrete app laat verwijzen, valt de check om.
  • Enum is een leaf: [], mag van niemand afhankelijk zijn.

4. De violations check

Deptrac mapt elke edge uit de graph naar de layers en toetst die aan de ruleset. Geen match = violation. Omdat het een allow-list is en geen blocklist, wordt ook een verboden koppeling die je nog nooit hebt gezien automatisch geblokkeerd. Dát maakt het toekomstbestendig.

  Report
 -------------------- ------
  Violations           0
  Skipped violations   15
  Uncovered            2219
  Allowed              1055
  Warnings             0
  Errors               0
 -------------------- ------

Uncovered zijn hier delen uit vendor, bijvoorbeeld Symfony zelf.

Violations zijn leesbaar, dus agents kunnen er meteen mee aan de slag:

{
  "Report": { "Violations": 15, "Skipped violations": 0, "Allowed": 1055, ... },
  "files": {
    "src/Apps/x/Action/ListHandler.php": {
      "messages": [
        { "message": "… must not depend on … (App_x on App_y)", "line": 58, "type": "error" }
      ],
      "violations": 2
  }
}


Maar kan een agent niet gewoon stiekem je deptrac.yaml aanpassen om een "uitzondering" toe te voegen? In theorie wel, vandaar dat deptrac gecommit is en deze diff dus ook opvalt in de review en een bewuste stap is. Hier is een mooie rol neergelegd voor je architect agent, die tijdens de review hier kritisch naar kijkt (1 van de regels) maar ook voor jezelf. 

In mijn eigen opzet kijkt mijn architect-agent tijdens de refinement/planningfase al mee om dit zo vroeg mogelijk af te vangen overigens.

En Javascript/TypeScript?

Deptrac werkt voor PHP projecten, maar een stack bevat vaak ook Javscript/Typescript applicaties. Daarvoor kan je kijken naar de ESLint plugin JS Boundaries. Het werkt vergelijkbaar met deptrac.

Als je die niet wil gebruiken kun je ook kijken naar dependency-cruiser eventueel in combinatie met ArchUnitTS. Met dependency cruiser kan je rules instellen om te voorkomen dat bepaalde npm dependencies worden toegevoegd (de harde "dit mag nooit" verboden). En met ArchUnitTS kun je interne layering regels toevoegen ("laag X mag alleen Y") op een semantische manier. Maar het meeste kan je vergelijkbaar configureren met eslint boundaries.

Vang ik hiermee alles af? Helaas, het kan nog steeds zo zijn dat een oplossing wordt geïmplementeerd binnen de huidige regels, terwijl een nieuwe abstractielaag beter was geweest. Maar de kans wordt wel kleiner.

 

Albert Skibinski

Over de auteur

  • Albert Skibinski is een zelfstandig ontwikkelaar en co-founder van Jafix
  • Ik schrijf over web development, lange fietstochten en lekker eten!