Przejdź do głównej zawartości

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.

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ą.

RodzinaCo łapieTypeScriptPythonJava
Kierunek zależności i warstwyDomena importuje infrastrukturę, handlery omijają serwisydependency-cruiserimport-linter (layers, forbidden)ArchUnit layeredArchitecture()
CykleDwa moduły, których nie da się już zmieniać niezależniedependency-cruiser circularimport-linter layers lub independenceArchUnit slices().beFreeOfCycles()
Budżet złożonościFunkcje, które z każdą funkcjonalnością dostają nową gałąźESLint complexity, max-depth, max-paramsRuff C901, PLR0912, PLR0913Checkstyle CyclomaticComplexity, ParameterNumber
Budżet duplikacjiSkopiowane helpery, wklejona walidacjajscpdjscpdjscpd
Powierzchnia publicznego APINiezatwierdzone zmiany łamiące eksportyAPI ExtractorGriffe checkjapicmp

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ędzieMechanizm 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-linterignore_imports wylicza znane naruszenia w każdym kontrakcie
ArchUnitFreezingArchRule.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, CheckstyleUstaw 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.

  1. 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ą.

  2. 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.

  3. 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.

  4. 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.

  5. Odbierz agentowi reguły. Konfigurację reguł, bazy, package.json (w nim jest skrypt fitness), kanarka, workflow i pliki hooków w .claude/ i .codex/ oddaj pod CODEOWNERS tech leada albo zespołu platformowego. Zablokuj agentowi edycję tych samych ścieżek w sesji (zobacz zakładki narzędzi poniżej) z wyjątkiem package.json: ten plik zostaje tylko pod CODEOWNERS, 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.java i src/test/resources/archunit_store/**. Reguła, którą agent może poluzować, albo hook, który może wyłączyć, jest tylko sugestią.

  6. 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.

  7. 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-only usuwa 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:

Okno terminala
npm i -D dependency-cruiser eslint typescript-eslint jscpd @microsoft/api-extractor typescript@~6.0
.dependency-cruiser.cjs
/** @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:

Okno terminala
# Terminal, jednorazowo: zapisz dzisiejsze naruszenia
npx --no-install depcruise src --config .dependency-cruiser.cjs --baseline
# CI i pętla agenta: błąd przy wszystkim, czego nie ma w bazie
npx --no-install depcruise src --config .dependency-cruiser.cjs --ignore-known --output-type err-long

Budżet złożoności z regułami rdzenia ESLint 10 i parserem typescript-eslint:

eslint.config.js
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:

Okno terminala
npx --no-install jscpd src --baseline-from-ref origin/main --fail-on-new-clones --fail-on-empty

Publiczne 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:

Okno terminala
npx --no-install tsc -p tsconfig.build.json && npx --no-install api-extractor run --print-api-report-diff

Deweloper, 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.

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.

.github/workflows/fitness.yml
name: fitness
on: 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-diff

Pierwsze 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.

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 red
input=$(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 0
fi
cd "${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 2
fi
exit 0

Nadaj 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.

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 fail
set -u
canary=src/domain/__fitness_canary__.ts
echo "import { db } from '../infra/db.js'; export const c = db;" > "$canary"
trap 'rm -f "$canary"' EXIT
out=$(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 1
fi
echo "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:

KtoZa co odpowiadaCo zatwierdza
Tech leadZestaw reguł, budżety i kanarekKażdą zmianę reguły, budżetu lub bazy, przez CODEOWNERS
Właściciel APIRaport API w repozytorium albo punkt odniesienia dla Griffe i japicmpKażdą celową zmianę łamiącą API
Deweloper lub agentZielony job fitnessNic w regułach; spory trafiają do ARCH_DISPUTE.md
CTOTrendComiesię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.

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ół.

Na ścieżce tech leada następnym krokiem jest code review pull requestów agentów.