Hydra leerlijn — Deel 3: Quality gates
Wat zijn de mechanische quality gates van Hydra, waarom werken ze juist NIET met AI-oordeel, en wat doe je als een gate onterecht alarm slaat? Derde van zeven korte modules.
In deel 2 zag je dat de build-lane bij elke build de hydra-gates-suite draait, en dat een reviewstage waarvan het verdict niet laat zien dat die gates draaiden niet wordt geaccepteerd. Dat zijn mechanische quality gates — checks die deterministisch slagen of falen, zonder AI ertussen. Dit deel legt uit welke gates we hebben, waarom ze mechanisch zijn, en hoe je omgaat met de uitzondering: het vals-positief.
Waarom mechanische gates?
AI-review schaalt mee, maar is niet voorspelbaar. Twee runs op precies dezelfde diff kunnen verschillende bevindingen opleveren. En AI is juist zwak in het saaie nakijkwerk dat geen oordeel vraagt — bijvoorbeeld: "heeft elke nieuwe PHP-file bovenaan een SPDX-licentieheader?". Zo'n check doe je veel sneller en goedkoper met een script.
De stelregel binnen Hydra:
Wat objectief te controleren is, doet een mechanische gate — één script dat per check slaagt of faalt. Pas waar oordeel nodig is, schakelen we een reviewer in.
Concreet: voordat Juan Claude of Clyde dure modeltijd aan een PR besteden, heeft de build-lane de gates al gedraaid en hun output in de PR-body gezet. De reviewers richten zich daarna op wat een tool níet kan vangen.
Categorie 1: generieke code-quality-tools
Elke Conduction-app heeft een eigen quality-suite (composer check:strict aan de PHP-kant, plus de frontend-linters). Voor een lokale check draait hydra's scripts/run-quality.sh die in een Docker-container php:X.Y-cli:
| Tool | Wat het vangt |
|---|---|
lint | Syntaxfouten in PHP. |
phpcs | Coding standard (PSR-12 + Nextcloud-conventie). |
phpmd | Code-mess detector (te lange methodes, te diepe nesting, dode code). |
psalm | Statische type-analyse, zoals in de app geconfigureerd. |
phpstan | Tweede statische type-analyser (vangt dingen die Psalm mist en omgekeerd). |
phpmetrics | Complexiteitsmetrieken (cyclomatic, maintainability-index). |
composer audit | CVE-check op de dependencies in composer.lock. |
eslint | JS-/TS-lint. |
stylelint | CSS-/SCSS-lint. |
npm audit | CVE-check op package-lock.json. |
PHPUnit | Unit- en integratietests. |
Newman | API-tests. |
Waar ze draaien, maakt uit. De build-lane draait ze niet: die draait alleen de hydra-gates-suite, en geen enkele flow roept scripts/run-quality.sh aan. De reviewer-brief vraagt Juan Claude de suite van de app te draaien en alleen bevindingen mee te tellen op bestanden die de PR raakt. Een build:pass betekent dus "de gates zijn groen", niet "PHPUnit is groen".
Categorie 2: Hydra-specifieke gates
Naast de generieke tools heeft Hydra een eigen gate-suite voor zaken die Conduction-specifiek zijn. De gate-logica zit in het publieke pakket conduction/hydra-gates, in de map hydra-gates/ van ConductionNL/.github. Hydra's scripts/run-hydra-gates.sh is een doorgeefluik: het zoekt dat pakket op en geeft het werk door, met dezelfde flags en exitcodes. Zelf bevat het geen gate-logica, dus een gate-fix hoort in het pakket, niet in hydra.
De hydra-gate-*-skills in hydra/.claude/skills/ (zo'n 60) beschrijven afzonderlijke gates voor de agents; de hydra-gates-skill draait de hele suite en vat de failures samen.
Hoeveel gates zijn er? De runner declareert er zo'n 96, genummerd tot 116 met gaten ertussen, en dat aantal verandert zodra er gates bijkomen. Vertrouw geen totaal dat ergens is opgeschreven, ook niet hier: elke run print zijn eigen telling.
[hydra-gates] COVERAGE: N of M declared gates reported a result (K not applicable to this repo/diff; N of L applicable gates ran).
Een schone run eindigt met ALL L APPLICABLE GATES GREEN — and all L of them ran. Lees de COVERAGE-regel: een gate die is overgeslagen (bijvoorbeeld omdat een tool ontbrak) is geen pass. De exitcode is het aantal failures; 0 is de enige pass, en 99 betekent dat de gates helemaal niet konden draaien.
De eerste 22 gates geven een goed beeld van wat de suite controleert:
| # | Gate | Wat het controleert |
|---|---|---|
| 1 | spdx-headers | Elk lib/**/*.php-bestand heeft een EUPL-1.2 SPDX-licentieheader. |
| 2 | forbidden-patterns | Geen achtergebleven var_dump / die / exit / error_log / print_r / dd / dump in lib/. |
| 3 | stub-scan | Geen "In a complete implementation"-stubs, lege run()-bodies, of auth-methodes die een caller-identiteit als argument krijgen maar die nooit gebruiken. |
| 4 | composer-audit | Geen bekende CVE's of advisories in de dependency-boom van composer.lock. |
| 5 | route-auth | Elke controller-methode in appinfo/routes.php declareert zijn auth-houding (#[PublicPage] / #[NoAdminRequired] / #[NoCSRFRequired] / #[AuthorizedAdminSetting]). |
| 6 | orphan-auth | Publieke is*/requires*/validate*/authorize*/check*/ensure*/verify*/assert*-methodes hebben minstens één aanroeper — geen dode auth-code. |
| 7 | no-admin-idor | Elke #[NoAdminRequired]-methode heeft een autorisatiecheck per object of voor admins in de body (blokkeert IDOR). |
| 8 | unsafe-auth-resolver | Geen fail-open-patroon catch (\Throwable) { return null; } in auth-, permissie- of rol-resolvers. |
| 9 | semantic-auth | De auth-annotatie klopt met wat de methode-body werkelijk aan autorisatie vereist (niet alleen syntactisch aanwezig). |
| 10 | initial-state | De frontend gebruikt loadState() uit @nextcloud/initial-state, nooit getElementById(...).dataset.*. |
| 11 | admin-router | Vue-componenten voor admin-instellingen staan NIET als route in de vue-router van de app (ze renderen via AdminSettings.php). |
| 12 | nc-input-labels | Elke <NcSelect> declareert een inputLabel of ariaLabelCombobox (WCAG 2.1 AA). |
| 13 | modal-isolation | <NcModal>- / <NcDialog>-markup staat in een eigen bestand onder src/modals/ of src/dialogs/, nooit inline in een parent. |
| 14 | route-reachability | Elke controller-methode die een Response teruggeeft staat in appinfo/routes.php, en elke route verwijst naar een bestaande methode. |
| 15 | dashboard-antipattern | Geen dashboard-in-dashboard (<CnDashboardPage>) nesting. |
| 16 | spec-coverage | Elke gewijzigde public/protected- en frontend-methode heeft @spec openspec/... of @spec exclude <reden> (diff-scoped, ADR-020). |
| 17 | redundant-controller | Geen doorgeef-CRUD-methodes die alleen OpenRegisters ObjectService inpakken (ADR-022 — apps gebruiken de abstracties). |
| 18 | notification-dialect | Register-bestanden gebruiken het canonieke x-openregister-notifications-dialect (ADR-031); het oude dialect FAILt, imperatieve dispatch WARNt. |
| 19 | e2e-coverage | Elk toegevoegd of gewijzigd Scenario in een openspec-spec wordt via @e2e door een Playwright-test gerefereerd, of heeft @e2e exclude <reden> (diff-scoped, ADR-020). |
| 20 | or-objectservice-api | Geen aanroepen van verzonnen ObjectService-methodes — alleen find / findAll / saveObject / createObject / updateObject / deleteObject bestaan. |
| 21 | conflict-markers | Geen onopgeloste git-merge-markers (<<<<<<<, =======, >>>>>>>) gecommit. |
| 22 | manifest-validation | src/manifest.json valideert tegen het meegeleverde app-manifest-schema. |
De latere gates dekken toegankelijkheid (WCAG 2.2 AA), kruisverwijzingen in manifest en register, supply-chain-configuratie en meer. Veel gates zijn uit incidenten geboren: een gate ontstaat als reactie op een bug die door alle eerdere checks heen glipte. Neem de caller-identiteitsregel in stub-scan (gate-3). In het decidesk-44-45-retrospectief kwam een builder langs gate-7 door een autorisatiemethode aan te roepen en die methode vervolgens als lege stub aan te maken. Gate-7 keek alleen of de aanroep bestond; de security reviewer zag de lege body. stub-scan markeert nu een methode die een caller-identiteit ontvangt en die nooit gebruikt.
Gate-19: waar specs en browser elkaar raken
Twee gates in de lijst hierboven controleren geen stijl of security — ze controleren traceability, en ze vormen het mechanische hart van spec-driven development:
spec-coverage(gate-16) dwingt de link terug af: elke methode die je toevoegt of wijzigt, moet met een@spec openspec/...-annotatie verwijzen naar het requirement dat hij implementeert.e2e-coverage(gate-19) dwingt de link vooruit af: elk Scenario dat je in een OpenSpec-spec toevoegt of wijzigt, moet door een Playwright-test worden uitgevoerd, teruggekoppeld met een@e2e-annotatie — of expliciet uitgezonderd met@e2e exclude <reden>(voor puur backend-gedrag, dat in Newman of PHPUnit thuishoort).
Samen maken ze de lus in beide richtingen controleerbaar: scenario → @e2e → Playwright-test, en code → @spec → requirement. Een builder kan niet stilletjes een feature opleveren die niet gespecificeerd is, en kan een gespecificeerd UI-scenario niet stilletjes ongetest laten. Beide gates zijn diff-scoped (ADR-020), dus oude schuld in onaangeraakte bestanden blokkeert nooit een PR.
De precieze annotatiemechaniek — hoe een #### Scenario:-kop een slug wordt, de lange en korte @e2e-vormen, en de drie uitzonderingsscopes — staat in de OpenSpec-leerlijn: Van scenario naar Playwright-test. Lees die pagina als je wilt begrijpen waarom dit een veilige zandbak voor AI is in plaats van vrij baan voor "vibe coding".
ADR-020: gate-scope is de PR-diff
Een belangrijke regel die je in deel 2 al tegenkwam: gates draaien op de PR-diff, niet op de hele repo. Dat staat in ADR-020.
Waarom? Omdat veel van onze repo's een achterstand aan technische schuld meedragen. Laat je een gate op de hele repo los, dan krijg je honderden bevindingen die niets met de huidige PR te maken hebben — en wordt elke PR rood. Door de scope te beperken tot de PR-diff, slaat de gate alleen aan op wat de huidige build net heeft toegevoegd of gewijzigd.
De flows leggen dit vast: de build-lane roept de runner aan met --scope-to-diff --base origin/<base>. De override HYDRA_REVIEW_SCOPE=full werkt alleen bij lokale en handmatige runs (scripts/dev-run.sh, scripts/manual-review.sh), waar hij de scope van de reviewer naar de hele repo zet. Gebruik dat als je een nieuwe repo onboardt of een gerichte tech-debt-sweep doet. Verwacht veel rood.
Vals-positieven herkennen
Mechanische gates zijn deterministisch, maar niet altijd correct. Een echt voorbeeld uit gate-2 (forbidden-patterns): die bestond eerst uit een reeks tekstzoekopdrachten naar \bdd\(, \bvar_dump\( en verwanten over de ruwe bytes van lib/. Daardoor telde een commentaarregel die waarschuwt tegen var_dump( als gebruik, en een string-literal als "select dd(x)" ook. Echte aanroepen werden juist gemist, zoals var_dump ($value) met een spatie, of die; zonder haakjes. Eén detectiekeuze, een herhaalbare fout in beide richtingen.
Zo herken je een vals-positief:
- De gate slaat bij elke build aan op dezelfde regel, terwijl die regel zelf onschuldig oogt.
- De gate lokaal draaien (
scripts/run-hydra-gates.sh, plus de log van de gate) bevestigt het: ja, het patroon matcht, maar niet om de bedoelde reden. - Niemand in het team kan uitleggen waarom juist deze regel een klacht verdient.
De fix is dan niet de build opnieuw in de wachtrij zetten en hopen. De fix is de detectie repareren, in het pakket conduction/hydra-gates (ConductionNL/.github, hydra-gates/). Voor gate-2 was die fix: beoordeel code, geen tekst. Het bestand wordt nu gelezen met de inhoud van commentaar en strings weggepoetst, voordat de patronen worden gematcht.
Waar de gates opnieuw draaien
De gates draaien in de build-lane, over de gepushte branch. Falen ze, dan volgt één fix-ronde, en daarna draaien de gates nog één keer over de gerepareerde tree. Die tweede run is de enige automatische herhaling van de gates.
Een "quality-recheck" na review bestaat niet in de flows. De pipeline draait de gates niet opnieuw na een begrensde fix van een reviewer; in plaats daarvan moet elke reviewstage de gates zelf draaien en hydra-gates noemen in de checks_run van zijn verdict, anders eindigt de stage in needs-input.
Test jezelf
Vier korte vragen om te checken of je dit deel begrepen hebt. Vastgelopen? Klik Hint. Benieuwd naar het antwoord? Klik Antwoord.
1. Waarom heeft Hydra mechanische gates en AI-reviewers, en niet alleen één van de twee?
Hint
Eén soort check is voorspelbaar en goedkoop, de andere is duur maar kan oordelen. Wat is de sterke en zwakke kant van elk?
Antwoord
Ze vullen elkaar precies aan op de plekken waar de ander zwak is.
- Mechanische gates zijn deterministisch: zelfde input → zelfde uitkomst, slagen of falen. Perfect voor saai, objectief nakijkwerk — bijvoorbeeld "heeft elke nieuwe PHP-file een SPDX-header?". Goedkoop en herhaalbaar.
- AI-reviewers leveren oordeel: "klopt deze autorisatielogica inhoudelijk met het doel van de route", "is dit in deze context een veiligheidsrisico". Niet voorspelbaar en duurder, dus zet je ze in waar oordeel nodig is.
Alleen mechanisch mist fouten die van de context afhangen; alleen AI is duur, niet voorspelbaar, en verspilt modeltijd aan dingen die een script kan.
2. Wat zegt ADR-020 over de gate-scope, en waar heeft HYDRA_REVIEW_SCOPE=full effect?
Hint
Denk aan wat er gebeurt als je een gate loslaat op een repo met veel oude technische schuld. En lezen de flows die variabele?
Antwoord
ADR-020 zegt: gates draaien op de PR-diff, niet op de hele repo.
Reden: veel repo's slepen technische schuld mee. Een gate over alles geeft honderden bevindingen die niets met de huidige PR te maken hebben — elke PR zou rood worden. Door alleen de diff te controleren, slaat de gate aan op wat de build net heeft toegevoegd of gewijzigd.
De flows leggen --scope-to-diff vast. HYDRA_REVIEW_SCOPE=full verandert alleen lokale en handmatige runs (dev-run.sh, manual-review.sh), waar de review dan de hele repo omvat. Gebruik dat bij:
- Het onboarden van een nieuwe repo in Hydra.
- Een gerichte tech-debt-sweep.
NIET voor gewone PR's — verwacht veel rood.
3. Hoe herken je een vals-positieve gate, en wat is de juiste fix? Wat NIET?
Hint
Drie signalen samen wijzen op een vals-positief. En de "verleidelijke maar verkeerde" reflex is hetzelfde nog een keer doen.
Antwoord
Herkenning — drie signalen samen:
- De gate slaat bij elke build aan op dezelfde regel, terwijl die regel onschuldig oogt.
- De gate lokaal draaien bevestigt: het patroon matcht, maar niet om de bedoelde reden (bijvoorbeeld een tekstzoekopdracht die ook commentaar of een string-literal raakt).
- Niemand in het team kan uitleggen waarom juist deze regel een klacht verdient.
Juiste fix: repareer de gate-detectie in het pakket conduction/hydra-gates (ConductionNL/.github, hydra-gates/). Hydra's scripts/run-hydra-gates.sh geeft alleen door.
NIET: de build opnieuw in de wachtrij zetten. Dat levert hetzelfde vals-positief op en verspilt een run.
4. Waar draait Hydra de gates automatisch opnieuw, en waar NIET?
Hint
Denk aan de fix-ronde in de build-lane, en aan wat er gebeurt na een begrensde fix van een reviewer.
Antwoord
- Wel: in de build-lane. Faalt de eerste gate-run, dan volgt één fix-ronde, en daarna draaien de gates nog één keer over de gerepareerde tree. Die uitslag bepaalt
build:passofbuild:fail. - Niet: na review. Een "quality-recheck" na review bestaat niet in de flows. Elke reviewstage moet de gates zelf draaien en
hydra-gatesopnemen inchecks_run; een verdict dat dat niet doet, wordt niet geaccepteerd en het issue komt opneeds-input.
Volgende stap
In deel 4 kijken we naar de skills en commands die de personas tijdens hun werk gebruiken — inclusief hoe je zelf een nieuwe skill kunt toevoegen.