Funkcje dopasowania architektury: utrzymywalność bez czytania kodu
Funkcje dopasowania architektury (architecture fitness functions) to automatyczne kontrole, które przerywają build, gdy kod łamie regułę strukturalną: niedozwolony import między warstwami, cykl zależności, funkcję ponad budżetem złożoności, nową duplikację albo niezatwierdzoną zmianę publicznego API. Uruchamiane jako wymagane bramki CI z zapadką utrzymują kod pisany przez agentów w dobrym stanie bez czytania każdego diffa.
Jeśli prowadzisz zespół, który scala 40 pull requestów od agentów tygodniowo, ta strona jest dla ciebie i twoich deweloperów, a CTO znajdzie tu widok trendu. Każdy z tych pull requestów przechodzi testy, a każdy diff z osobna wygląda rozsądnie. Pół roku później pakiet domenowy importuje ORM, serwis zamówień i serwis rozliczeń importują się nawzajem, a w repozytorium żyją trzy nieco różne funkcje formatMoney. Nie zrobił tego żaden pojedynczy pull request, więc żadne pojedyncze review nie mogło tego wyłapać. Testy sprawdzają zachowanie, a kształtu kodu nie sprawdzało nic. Ta strona zamienia ten kształt w kontrole.
Co zyskujesz dzięki funkcjom dopasowania architektury
Dział zatytułowany „Co zyskujesz dzięki funkcjom dopasowania architektury”- Pięć rodzin funkcji dopasowania, które warto zamienić w bramki, z narzędziem egzekwującym dla TypeScriptu, Pythona i Javy.
- Konfiguracje dla trzech stosów, przetestowane 26 września 2026: dependency-cruiser 18.4.0, import-linter 2.15, ArchUnit 1.5.1, ESLint 10, Ruff 0.16.9, Checkstyle, jscpd 5.3.2, API Extractor 7.59.2, Griffe 2.3.0 i japicmp 0.26.2.
- Wzorzec zapadki (ratchet) do wdrożenia tego w istniejącym kodzie.
- Job GitHub Actions, kanarka, który dowodzi, że bramka wciąż potrafi zgłosić błąd, oraz hook
Stop, który każe agentowi naprawić naruszenia, zanim zgłosi koniec pracy. - Trzy prompty do skopiowania: wyprowadź reguły, napraw naruszenie bez ruszania ich i zaproponuj kolejną zapadkę.
Dlaczego kod pisany przez agentów eroduje, gdy nikt nie czyta każdego diffa?
Dział zatytułowany „Dlaczego kod pisany przez agentów eroduje, gdy nikt nie czyta każdego diffa?”Agent optymalizuje pod zadanie, które ma przed sobą. Najkrótsza droga często przecina granicę (repozytorium importowane wprost do handlera) albo kopiuje funkcję pomocniczą, zamiast ją znaleźć. Każdy taki skrót jest lokalnie rozsądny, a review wyrywkowe przeoczy większość z nich.
Badania potwierdzają, że to efekt strukturalny, a nie kwestia lepszego promptowania:
- SlopCodeBench (Orlanski i in., arXiv, wersja 2 z 7 maja 2026) kazał agentom wielokrotnie rozbudowywać własne rozwiązania w 196 punktach kontrolnych. Najlepszy zaliczył 14,8% z nich, a benchmark nazywa degradację „structural erosion (concentrated complexity) and verbosity (redundant code)”.
- DORA (Google Cloud, raport 2025, 23 września 2025) wykazał dodatni związek adopcji AI z przepustowością i ujemny ze stabilnością, i wskazał mechanizm: „Teams working in loosely coupled architectures with fast feedback loops see gains, while those constrained by tightly coupled systems and slow processes see little or no benefit.”
- GitClear („The Maintainability Gap: AI Code Quality in 2026”, czerwiec 2026, 623 mln zmian w kodzie; przytaczamy za wtórnym wyciągiem z wyszukiwarki) zmierzył wzrost duplikacji bloków o 81% względem 2023 i spadek udziału przenoszonego (refaktoryzowanego) kodu z 13% do 3,8% zmienionych linii. GitClear przedstawia te dane jako korelację, nie dowód przyczynowości.
Funkcje dopasowania to deterministyczna odpowiedź. Termin pochodzi z książki Building Evolutionary Architectures Neala Forda, Rebekki Parsons i Patricka Kua (O’Reilly): każda obiektywna, automatyczna kontrola jakiejś cechy architektury.
Które funkcje dopasowania zamienić w bramki?
Dział zatytułowany „Które funkcje dopasowania zamienić w bramki?”Zacznij od pięciu rodzin z tabeli. Są tanie w uruchomieniu, kończą się komunikatem, na podstawie którego agent może działać, i każda łapie rodzaj erozji, którego testy nie widzą.
| Rodzina | Co łapie | TypeScript | Python | Java |
|---|---|---|---|---|
| Kierunek zależności i warstwy | Domena importuje infrastrukturę, handlery omijają serwisy | dependency-cruiser | import-linter (layers, forbidden) | ArchUnit layeredArchitecture() |
| Cykle | Dwa moduły, których nie da się już zmieniać niezależnie | dependency-cruiser circular | import-linter layers lub independence | ArchUnit slices().beFreeOfCycles() |
| Budżet złożoności | Funkcje, które z każdą funkcjonalnością dostają nową gałąź | ESLint complexity, max-depth, max-params | Ruff C901, PLR0912, PLR0913 | Checkstyle CyclomaticComplexity, ParameterNumber |
| Budżet duplikacji | Skopiowane helpery, wklejona walidacja | jscpd | jscpd | jscpd |
| Powierzchnia publicznego API | Niezatwierdzone zmiany łamiące eksporty | API Extractor | Griffe check | japicmp |
Dla API HTTP dodaj porównanie kontraktu na dokumencie OpenAPI: oasdiff breaking --fail-on ERR base.yaml head.yaml kończy się kodem 1, gdy znajdzie zmiany łamiące na poziomie błędu, niezależnie od stosu.
Dwóch rodzin celowo tu brakuje. Pokrycie i wynik mutacyjny mierzą testy, nie architekturę: zobacz, jak silna jest twoja wyrocznia. Skanowanie bezpieczeństwa to osobna bramka z własną stroną, testy bezpieczeństwa.
Dlaczego zapadka wygrywa z progiem w istniejącym kodzie
Dział zatytułowany „Dlaczego zapadka wygrywa z progiem w istniejącym kodzie”Próg mówi „najwyżej 3% duplikacji” albo „zero naruszeń warstw”. W kodzie, który już ma 212 naruszeń, próg jest albo wiecznie czerwony, albo tak luźny, że nic nie łapie. Zapadka zapisuje dzisiejsze naruszenia w pliku bazowym (baseline), przerywa build tylko przy nowych i pozwala bazie maleć, ale nigdy rosnąć.
Każde narzędzie z tej strony ma zapadkę wbudowaną:
| Narzędzie | Mechanizm bazy |
|---|---|
| dependency-cruiser | --baseline zapisuje .dependency-cruiser-known-violations.json; --ignore-known je pomija i przerywa build przy nowych. Żeby zacisnąć bazę, uruchom ponownie z --baseline --baseline-mode shrink-only, co tylko usuwa naprawione wpisy |
| import-linter | ignore_imports wylicza znane naruszenia w każdym kontrakcie |
| ArchUnit | FreezingArchRule.freeze(rule) zapisuje bieżące naruszenia i zgłasza tylko nowe |
| jscpd | --baseline-from-ref origin/main --fail-on-new-clones porównuje z gałęzią bazową, bez pliku do edycji |
| ESLint, Ruff, Checkstyle | Ustaw budżet na wartość twojej najgorszej funkcji i obniżaj go według harmonogramu |
Zapadka daje agentowi zadanie, które może spełnić w każdym pull requeście: „nie dodawaj naruszenia”. Naprawa tych 212 to osobno zaplanowana praca.
Wdróż funkcje dopasowania krok po kroku
Dział zatytułowany „Wdróż funkcje dopasowania krok po kroku”-
Zapisz architekturę w pięciu zdaniach. Nazwij warstwy, dozwolony kierunek między nimi i dwie, trzy reguły, za których złamanie odrzuciłbyś pull request. Jeśli prowadzisz ADR-y, to jest ich część sprawdzalna maszynowo; zobacz decyzje architektoniczne, których agenci przestrzegają.
-
Poproś agenta o szkic reguł na podstawie kodu w obecnym stanie. Użyj pierwszego promptu poniżej. Agent odwzoruje prawdziwy graf importów, zaproponuje reguły zgodne z twoimi pięcioma zdaniami i poda, ile naruszeń każda reguła zgłosiłaby dziś. Przeglądasz zestaw reguł, nie kod.
-
Uruchom reguły i zapisz bazę. Dodaj konfigurację i plik bazowy do repozytorium w jednym pull requeście i wpisz rozmiar bazy do jego opisu. Od tej liczby startuje zapadka.
-
Dodaj kanarka, a potem ustaw job jako wymagany. Kanarek (poniżej) podkłada znane naruszenie i przerywa build, jeśli bramka go nie złapie. Wymagaj joba fitness w rulesecie gałęzi, żeby czerwony wynik blokował merge ludziom i agentom tak samo.
-
Odbierz agentowi reguły. Konfigurację reguł, bazy,
package.json(w nim jest skryptfitness), kanarka, workflow i pliki hooków w.claude/i.codex/oddaj podCODEOWNERStech leada albo zespołu platformowego. Zablokuj agentowi edycję tych samych ścieżek w sesji (zobacz zakładki narzędzi poniżej) z wyjątkiempackage.json: ten plik zostaje tylko podCODEOWNERS, bo agent potrzebuje go do zmian zależności, a hook i CI wywołują narzędzia bezpośrednio, nie przez jego skrypty. W Pythonie i Javie odpowiednikami sąpyproject.toml,config/fitness-checkstyle.xml,ArchitectureTest.javaisrc/test/resources/archunit_store/**. Reguła, którą agent może poluzować, albo hook, który może wyłączyć, jest tylko sugestią. -
Zwracaj naruszenia agentowi, zanim zrobi to CI. Podepnij szybkie kontrole do pętli samego agenta, żeby naprawił złamaną warstwę w tej samej sesji, a nie w kolejnej rundzie przez CI.
-
Zaciskaj zapadkę według harmonogramu. Raz w miesiącu obniż każdy budżet do bieżącej najgorszej wartości i usuń nieaktualne wpisy bazy; w dependency-cruiserze
--baseline --baseline-mode shrink-onlyusuwa naprawione wpisy i nigdy nie dodaje nowych. Trzeci prompt poniżej przygotowuje tę zmianę jako łatkę, więc może go uruchamiać zaplanowana automatyzacja Codeksa albo Automation Cursora przy zablokowanych regułach; łatkę nakłada tech lead.
Konfiguracje funkcji dopasowania dla TypeScriptu, Pythona i Javy
Dział zatytułowany „Konfiguracje funkcji dopasowania dla TypeScriptu, Pythona i Javy”Każda zakładka zawiera kompletny, przetestowany zestaw dla jednego stosu: reguły zależności, budżet złożoności, duplikację i kontrolę publicznego API. Nazwy warstw odpowiadają typowemu układowi czterowarstwowemu (web lub api, app/application lub services, domain, infra/infrastructure lub adapters); zmień je na swoje.
Reguły zależności w dependency-cruiser 18.4.0. Zainstaluj go razem z pozostałymi narzędziami TypeScriptu z tej zakładki, łącznie z przypięciem TypeScriptu, które wyjaśnia ostrzeżenie niżej:
npm i -D dependency-cruiser eslint typescript-eslint jscpd @microsoft/api-extractor typescript@~6.0/** @type {import('dependency-cruiser').IConfiguration} */module.exports = { forbidden: [ { name: 'no-circular', severity: 'error', comment: 'Cycles make every module in the loop depend on every other one.', from: {}, to: { circular: true }, }, { name: 'domain-stays-pure', severity: 'error', comment: 'src/domain holds business rules; it must not import app, infra or web code.', from: { path: '^src/domain/' }, to: { path: '^src/(app|infra|web)/' }, }, { name: 'web-goes-through-app', severity: 'error', comment: 'Route handlers call use cases in src/app, never the database layer directly.', from: { path: '^src/web/' }, to: { path: '^src/infra/' }, }, ], options: { doNotFollow: { path: 'node_modules' }, tsConfig: { fileName: 'tsconfig.json' }, },};Pole comment to tekst, który agent czyta, gdy reguła zawiedzie, więc pisz je jako polecenie. Zapisz bazę raz, a potem przerywaj build tylko przy nowych naruszeniach. npx --no-install uruchamia wyłącznie lokalnie zainstalowaną zależność deweloperską; bez tej flagi brakujący pakiet zostaje pobrany z rejestru po nazwie, i tak w CI mógłby się uruchomić zajęty przez kogoś innego pakiet, na przykład nieoznaczony zakresem api-extractor:
# Terminal, jednorazowo: zapisz dzisiejsze naruszenianpx --no-install depcruise src --config .dependency-cruiser.cjs --baseline
# CI i pętla agenta: błąd przy wszystkim, czego nie ma w bazienpx --no-install depcruise src --config .dependency-cruiser.cjs --ignore-known --output-type err-longBudżet złożoności z regułami rdzenia ESLint 10 i parserem typescript-eslint:
import { defineConfig } from 'eslint/config';import tseslint from 'typescript-eslint';
export default defineConfig({ files: ['src/**/*.ts'], languageOptions: { parser: tseslint.parser }, rules: { complexity: ['error', { max: 10 }], 'max-depth': ['error', 3], 'max-lines-per-function': ['error', { max: 60, skipBlankLines: true, skipComments: true }], 'max-params': ['error', 4], },});Duplikacja w jscpd 5.3.2, z bramką na nowe klony względem gałęzi bazowej:
npx --no-install jscpd src --baseline-from-ref origin/main --fail-on-new-clones --fail-on-emptyPubliczne API w API Extractor 7.59.2 dla pakietu importowanego przez innych. Uruchom raz npx --no-install api-extractor init, zbuduj deklaracje i dodaj wygenerowany raport etc/*.api.md do repozytorium. Bez --local polecenie api-extractor run kończy się błędem, gdy publiczne API nie zgadza się już z raportem w repozytorium; --print-api-report-diff wypisuje, co się zmieniło:
npx --no-install tsc -p tsconfig.build.json && npx --no-install api-extractor run --print-api-report-diffDeweloper, który zmienia API celowo, uruchamia npx --no-install api-extractor run --local, co przepisuje raport, a diff raportu trafia do właściciela API do zatwierdzenia.
Jedno polecenie dla agenta i CI. Zbierz szybkie kontrole w jeden skrypt, żeby instrukcja dla agenta poniżej mogła mówić npm run fitness; hook Stop i CI wywołują narzędzia bezpośrednio, z powodu opisanego przy hooku:
{ "scripts": { "fitness": "npm run fitness:deps && npm run fitness:complexity && npm run fitness:dupes", "fitness:deps": "depcruise src --config .dependency-cruiser.cjs --ignore-known --output-type err-long", "fitness:complexity": "eslint src --no-inline-config", "fitness:dupes": "jscpd src --baseline-from-ref origin/main --fail-on-new-clones --fail-on-empty" }}--no-inline-config sprawia, że komentarze eslint-disable nie wyłączą budżetu. --baseline-from-ref origin/main wymaga lokalnej kopii gałęzi bazowej, więc pobierz ją (git fetch origin main), zanim agent zacznie pracę. W Pythonie i Javie tę samą rolę pełni cel w Makefile albo profil Mavena.
Reguły zależności w import-linter 2.15 (pip install import-linter), w pyproject.toml:
[tool.importlinter]root_package = "shop"include_external_packages = true
[[tool.importlinter.contracts]]name = "Layered architecture"type = "layers"layers = [ "shop.api", "shop.services", "shop.domain",]
[[tool.importlinter.contracts]]name = "Domain does not touch infrastructure"type = "forbidden"source_modules = ["shop.domain"]forbidden_modules = ["shop.adapters", "sqlalchemy", "httpx"]# Known violations, each with an owner and a ticket. This list only shrinks.ignore_imports = [ "shop.domain.order -> shop.adapters.db", # PAY-812]
[[tool.importlinter.contracts]]name = "API goes through services"type = "forbidden"source_modules = ["shop.api"]forbidden_modules = ["shop.adapters"]Kontrakt layers zgłasza każdy import w górę, co wyklucza też cykle między warstwami. Bez include_external_packages = true zakazany pakiet zewnętrzny sprawia, że lint-imports kończy się błędem, zanim cokolwiek sprawdzi. Uruchamiaj narzędzie z katalogiem źródeł na ścieżce:
PYTHONPATH=src lint-importsBudżet złożoności w Ruff 0.16.9, w tym samym pyproject.toml:
[tool.ruff.lint]extend-select = ["C901", "PLR0912", "PLR0913", "PLR0915"]
[tool.ruff.lint.mccabe]max-complexity = 10
[tool.ruff.lint.pylint]max-args = 5max-branches = 12max-statements = 50Duplikacja w jscpd, dostępnym także na PyPI (pip install jscpd) jako ten sam samodzielny plik binarny:
jscpd src --baseline-from-ref origin/main --fail-on-new-clones --fail-on-emptyPubliczne API w Griffe 2.3.0, które porównuje pakiet z referencją Gita i zgłasza zmiany łamiące:
griffe check shop --search src --against origin/main --format githubReguły zależności w ArchUnit 1.5.1, jako test (com.tngtech.archunit:archunit-junit5:1.5.1, zakres test):
package com.acme.shop;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;import static com.tngtech.archunit.library.Architectures.layeredArchitecture;import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
import com.tngtech.archunit.core.importer.ImportOption;import com.tngtech.archunit.junit.AnalyzeClasses;import com.tngtech.archunit.junit.ArchTest;import com.tngtech.archunit.lang.ArchRule;import com.tngtech.archunit.library.freeze.FreezingArchRule;
@AnalyzeClasses(packages = "com.acme.shop", importOptions = ImportOption.DoNotIncludeTests.class)class ArchitectureTest {
@ArchTest static final ArchRule layers = FreezingArchRule.freeze( layeredArchitecture() .consideringOnlyDependenciesInLayers() .layer("Web").definedBy("..web..") .layer("Application").definedBy("..application..") .layer("Domain").definedBy("..domain..") .layer("Infrastructure").definedBy("..infrastructure..") .whereLayer("Web").mayNotBeAccessedByAnyLayer() .whereLayer("Application").mayOnlyBeAccessedByLayers("Web") .whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("Application"));
@ArchTest static final ArchRule domainIsPure = FreezingArchRule.freeze( noClasses().that().resideInAPackage("..domain..") .should().dependOnClassesThat() .resideInAnyPackage("..infrastructure..", "org.springframework..", "jakarta.persistence.."));
@ArchTest static final ArchRule noCycles = slices().matching("com.acme.shop.(*)..").should().beFreeOfCycles();}Zamroź każdą regułę, która pada na starym kodzie, także layers: zgłasza już istniejące importy z domeny do infrastruktury, więc niezamrożona od pierwszego dnia czerwieni build w istniejącym systemie. Jeśli kod ma dziś cykle, zamroź też noCycles. Pozwól utworzyć magazyn raz, lokalnie, i dodaj jego katalog do repozytorium:
freeze.store.default.path=src/test/resources/archunit_storefreeze.store.default.allowStoreCreation=truePo pierwszym uruchomieniu usuń allowStoreCreation, żeby CI zgłaszało błąd, zamiast po cichu tworzyć pusty magazyn.
Budżet złożoności w Checkstyle przez maven-checkstyle-plugin 3.6.0 (cel check, podpięty pod verify):
<module name="Checker"> <property name="severity" value="error"/> <module name="TreeWalker"> <module name="CyclomaticComplexity"><property name="max" value="10"/></module> <module name="NestedIfDepth"><property name="max" value="2"/></module> <module name="MethodLength"><property name="max" value="60"/></module> <module name="ParameterNumber"><property name="max" value="5"/></module> </module></module>Duplikacja poleceniem jscpd z zakładki TypeScript.
Publiczne API biblioteki w japicmp-maven-plugin 0.26.2 (cel cmp): wskaż w oldVersion ostatni wydany artefakt i ustaw breakBuildOnBinaryIncompatibleModifications oraz breakBuildOnSourceIncompatibleModifications na true.
Uruchom bramki funkcji dopasowania w CI
Dział zatytułowany „Uruchom bramki funkcji dopasowania w CI”Bramki to zwykłe polecenia, więc job w CI nie potrzebuje agenta ani klucza API. Ten job GitHub Actions uruchamia zestaw dla TypeScriptu; joby dla Pythona i Javy podmieniają polecenia na te z ich zakładek.
name: fitnesson: pull_request
permissions: contents: read
jobs: fitness: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 # jscpd builds its baseline from origin/main persist-credentials: false - uses: actions/setup-node@v7 with: node-version: 22 - run: npm ci - run: ./scripts/fitness-canary.sh - run: npx --no-install depcruise src --config .dependency-cruiser.cjs --ignore-known --output-type err-long - run: npx --no-install eslint src --no-inline-config - run: npx --no-install jscpd src --baseline-from-ref origin/main --fail-on-new-clones --fail-on-empty # API surface: only for packages others import. In a monorepo, run it per # published package the pull request touched. - run: npx --no-install tsc -p tsconfig.build.json && npx --no-install api-extractor run --print-api-report-diffPierwsze cztery kontrole trwają sekundy. Krok z raportem API to jedyna kontrola, którą hook agenta zostawia dla CI; usuń go w aplikacji, której nikt nie importuje.
Jak zwracać naruszenia agentowi przed CI?
Dział zatytułowany „Jak zwracać naruszenia agentowi przed CI?”CI jest granicą, a pętla agenta szybką ścieżką. Instrukcja jest taka sama we wszystkich trzech narzędziach, więc umieść ją w CLAUDE.md albo AGENTS.md:
## Architecture fitness functions- Before you report a task done, run `npm run fitness`. It must pass.- A failure message is an instruction. Move the code to the right layer, reuse the existing helper jscpd points to, or split the function. Do not edit `.dependency-cruiser.cjs`, `eslint.config.js`, `.dependency-cruiser-known-violations.json`, `.jscpd.json`, the `fitness` scripts in `package.json` or `scripts/fitness-canary.sh`, and do not add `eslint-disable` comments.- If a rule blocks a change you believe is right, stop and write the case to `ARCH_DISPUTE.md` for the tech lead.Narzędzia różnią się egzekwowaniem: tym, czy kontrola ruszy także wtedy, gdy agent zapomni o instrukcji.
Hook Stop uruchamia bramki za każdym razem, gdy Claude próbuje zakończyć pracę. Kod wyjścia 2 „prevents Claude from stopping, continues the conversation”, a Claude czyta stderr jako powód. Sprawdzone z Claude Code 2.1.283 i dokumentacją hooków 26 września 2026.
W .claude/settings.json, czyli we współdzielonych ustawieniach projektu w repozytorium, w których wiodący / oznacza katalog główny projektu:
{ "permissions": { "deny": [ "Edit(/.dependency-cruiser.cjs)", "Edit(/.dependency-cruiser-known-violations.json)", "Edit(/eslint.config.js)", "Edit(/.jscpd.json)", "Edit(/scripts/fitness-canary.sh)", "Edit(/.github/**)", "Edit(/.claude/**)", "Edit(/.codex/**)" ] }, "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/fitness-on-stop.sh" } ] } ] }}#!/usr/bin/env bash# .claude/hooks/fitness-on-stop.sh: no "done" while a fitness function is redinput=$(cat)# Already continuing because of this hook: block once, then hand over.if [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)" = "true" ]; then echo 'Fitness functions still failing after one retry; leaving it to a human and CI.' >&2 exit 0ficd "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel)}" || exit 0# The same three commands as the CI job, not `npm run fitness`: a script in# package.json is something the agent could rewrite to a no-op.if ! out=$( { npx --no-install depcruise src --config .dependency-cruiser.cjs --ignore-known --output-type err-long \ && npx --no-install eslint src --no-inline-config \ && npx --no-install jscpd src --baseline-from-ref origin/main --fail-on-new-clones --fail-on-empty; } 2>&1); then printf 'Fitness functions failed. Fix the code, not the rules or baselines:\n%s\n' "$out" \ | tail -n 60 >&2 exit 2fiexit 0Nadaj skryptowi prawo wykonywania; wymaga jq. Uruchamia tylko szybkie kontrole i wywołuje narzędzia bezpośrednio, więc edycja package.json go nie wyłączy, a reguły deny dla .claude/** i .codex/** chronią sam hook. Blokuje tylko raz: gdy stop_hook_active pokazuje, że agent już kontynuuje z powodu tego hooka, pozwala zakończyć turę, a merge i tak zablokuje CI. Claude Code ignoruje też blokadę po ośmiu kolejnych kontynuacjach wymuszonych przez hook stop (limit zmienia CLAUDE_CODE_STOP_HOOK_BLOCK_CAP). Reguły deny nie zatrzymają skryptu, który sam zapisuje pliki, więc jeśli agent ma dostęp do powłoki, włącz sandbox z ochrony wyroczni.
Codex czyta instrukcję z AGENTS.md powyżej. Do egzekwowania Codex CLI 0.157.1 ma zdarzenie hooka Stop wśród swoich 12 zdarzeń, a jego .codex/hooks.json ma ten sam kształt JSON co w Claude Code. Codex uruchamia polecenie w katalogu roboczym sesji, więc katalog główny repozytorium wyznacz w samym poleceniu; skrypt sam przełącza się na git rev-parse --show-toplevel, gdy CLAUDE_PROJECT_DIR nie jest ustawione:
{ "hooks": { "Stop": [ { "hooks": [{ "type": "command", "command": "\"$(git rev-parse --show-toplevel)/.claude/hooks/fitness-on-stop.sh\"", "timeout": 600 }] } ] }}Codex również przekazuje stop_hook_active i to sprawdzenie ogranicza pętlę; nie licz na limit taki jak w Claude Code. Hooki projektu nie działają, dopóki nie zaufasz im przez /hooks, i to ponownie po każdej zmianie hooks.json. Konfigurację dla wszystkich narzędzi, z fiksturami dowodzącymi, że każdy hook blokuje, opisuje strona hooki agentów.
Żeby agent nie luzował reguł ani hooka, dodaj pliki konfiguracji i bazy, .codex/hooks.json i skrypt hooka jako dokładne ścieżki read do profilu uprawnień z ochrony wyroczni. Dokładne ścieżki tam działają; globy tylko do odczytu są w 0.157.1 odrzucane.
Dodaj instrukcję jako regułę projektu (Rule), żeby Agent stosował ją w każdym zadaniu. Cursor ma też hooki, które „run before or after defined stages of the agent loop and can observe, block, or modify behavior” (cursor.com/docs/hooks, sprawdzone 28 sierpnia 2026). Nazw zdarzeń nie mogliśmy ponownie sprawdzić 26 września 2026, więc weź je z dokumentacji hooków Cursora, gdy będziesz przenosić skrypt. Strona hooki agentów opisuje podejście z adapterem i to, jak trzymać hook Cursora w trybie doradczym, dopóki nie udowodnisz, że działa.
Cloud Agents działają we własnych maszynach wirtualnych, więc hook z twojego laptopa z nimi nie podróżuje: wiąże je wymagana kontrola w CI. Bugbot może przeglądać pull requesty zmieniające reguły, ale jest recenzentem, nie bramką.
Prompty do skopiowania: funkcje dopasowania
Dział zatytułowany „Prompty do skopiowania: funkcje dopasowania”Skąd wiesz, że funkcje dopasowania naprawdę działają?
Dział zatytułowany „Skąd wiesz, że funkcje dopasowania naprawdę działają?”Bramka, która nigdy nie zgłasza błędu, jest gorsza niż jej brak, bo zielony status daje zaufanie, na które nie zasłużyła. Dwa narzędzia z tej strony zrobiły dokładnie to w naszych testach: dependency-cruiser pod TypeScriptem 7 i ArchUnit pod starszym surefire. Udowadniaj przy każdym uruchomieniu, że bramka wciąż wykrywa naruszenia:
#!/usr/bin/env bash# scripts/fitness-canary.sh: proves the dependency gate can still failset -ucanary=src/domain/__fitness_canary__.tsecho "import { db } from '../infra/db.js'; export const c = db;" > "$canary"trap 'rm -f "$canary"' EXITout=$(npx --no-install depcruise src --config .dependency-cruiser.cjs --ignore-known --output-type err 2>&1)# Require the rule name: a crash (bad config, missing tsconfig) also exits non-zero.if ! printf '%s' "$out" | grep -q 'domain-stays-pure'; then echo "The known-bad import was not reported: the gate is blind or broken." >&2 printf '%s\n' "$out" | tail -n 20 >&2 exit 1fiecho "Canary caught: the dependency gate is live."Import w kanarku skieruj na moduł, który naprawdę istnieje w twojej warstwie infrastruktury: dependency-cruiser nie rozwiąże brakującego pliku, więc reguła nigdy nie zadziała, a kanarek podniesie fałszywy alarm.
Uruchomiliśmy tego kanarka 26 września 2026. Z TypeScriptem 6 zgłosił, że bramka działa; z zainstalowanym TypeScriptem 7 przerwał job jako ślepą bramkę, czyli dokładnie tym alarmem, na którym ci zależy. W innych stosach zastosuj tę samą zasadę: moduł-fikstura importujący przez zakazaną granicę dla import-lintera, klasa testowa ArchUnit sprawdzająca, że layers zawodzi na pakiecie-fiksturze, oraz --fail-on-empty dla jscpd.
Odpowiedzialność za zatwierdzanie dzieli się według ról:
| Kto | Za co odpowiada | Co zatwierdza |
|---|---|---|
| Tech lead | Zestaw reguł, budżety i kanarek | Każdą zmianę reguły, budżetu lub bazy, przez CODEOWNERS |
| Właściciel API | Raport API w repozytorium albo punkt odniesienia dla Griffe i japicmp | Każdą celową zmianę łamiącą API |
| Deweloper lub agent | Zielony job fitness | Nic w regułach; spory trafiają do ARCH_DISPUTE.md |
| CTO | Trend | Comiesięczne liczby opisane niżej |
CTO wystarczą trzy liczby na repozytorium, prosto z narzędzi: rozmiar bazy (JSON z dependency-cruisera podaje summary.baselineSize; w import-linterze to długość ignore_imports; w ArchUnit zawartość magazynu), który powinien tylko maleć; liczba zmian reguł lub budżetów scalonych w miesiącu, każda z zatwierdzeniem; oraz liczba naruszeń złapanych tygodniowo w pull requestach agentów. Malejąca baza przy stałej liczbie złapanych naruszeń oznacza, że architektura się trzyma, choć kod piszą agenci; wyniki dla pojedynczego PR-a należą do pakietu dowodów.
Co się psuje, gdy funkcje dopasowania są bramkami?
Dział zatytułowany „Co się psuje, gdy funkcje dopasowania są bramkami?”Bramka jest zielona, bo niczego nie przeanalizowała. Nieobsługiwana wersja TypeScriptu, zły root_package, stary surefire albo błędna ścieżka zamieniają kontrolę w pustą operację kończącą się kodem 0. Jak naprawić: uruchamiaj kanarka jako pierwszy krok CI, dodaj --fail-on-empty do jscpd i po każdej aktualizacji narzędzi sprawdzaj w logu liczbę przeanalizowanych modułów.
Agent luzuje regułę zamiast naprawić kod. Dopisuje wpis do bazy, podnosi max albo rozsiewa eslint-disable. Jak naprawić: krok 5 oraz --no-inline-config w ESLint, żeby komentarz wyłączający nie miał skutku.
Agent spełnia regułę gorszym projektem. Dzieli złożoną funkcję na trzy prywatne helpery, z których każdy przyjmuje siedem parametrów, albo przenosi kod infrastruktury do katalogu domeny, żeby import stał się „legalny”. Jak naprawić: łącz budżet złożoności z max-params, opieraj reguły warstw na ścieżkach i sprawdzaj przeniesienia plików w triażu code review PR-a agenta, gdzie przeniesione pliki są flagą ryzyka.
Baza zamienia się w drugi kod, którego nikt nie czyta. Setki linii ignore_imports bez właściciela. Jak naprawić: wymagaj numeru zgłoszenia przy każdym wpisie, co miesiąc uruchamiaj prompt zapadki i śledź rozmiar bazy jako liczbę.
Bramka duplikacji strzela w kod generowany. Stuby protobuf, migracje ORM i pliki snapshotów są zduplikowane z założenia. Jak naprawić: wyklucz ścieżki generowane ustawieniem ignore w .jscpd.json, a nie podnoszeniem budżetu.
Reguły kodują architekturę, na którą nikt się nie umówił. Agent je naszkicował, nikt nie zaprotestował, a teraz blokują dobre zmiany. Jak naprawić: reguły wynikają z pięciu zdań z kroku 1 i z ADR-u, który zespół przejrzał. Najpierw zmień ADR, potem regułę, w jednym pull requeście zatwierdzonym przez zespół.
Dokąd dalej po funkcjach dopasowania
Dział zatytułowany „Dokąd dalej po funkcjach dopasowania”Na ścieżce tech leada następnym krokiem jest code review pull requestów agentów.