To część 2 serii o przygotowaniu notACMS do wydania jako open-source. Część 1 opisuje suite testowy. Oryginalna seria o migracji z WordPressa opisuje samą migrację.


Kod przeszedł PHPStan level 6. PHP CS Fixer nie miał nic do zarzucenia. Rector dry-run czysty. Przejrzałem każdy plik osobiście. Byłem gotowy na open-source.

Potem poprosiłem AI o audyt na podstawie 200-linijkowego pliku instrukcji. Znalazł 79 problemów.

Setup: AGENTS.md jako instrukcja audytu

Nie "przejrzyj mój kod" — to daje generyczne uwagi o obsłudze błędów i przypadkach brzegowych. Zamiast tego szczegółowa checklista w AGENTS.md:

  • Zasady architektury — tylko klasy final, segregacja interfejsów, value objects zamiast tablic asocjacyjnych
  • Konwencje nazewniczeXxxInterfaceXxx, public const w interfejsach
  • Wzorce bezpieczeństwa — guardy przed path traversal, walidacja przekierowań, sanitizacja danych
  • Zasady stylu kodu — warunki Yody, pusta linia przed return, strict types
  • Synchronizacja dokumentacji — tabele serwisów zgodne z src/, tabele zmiennych zgodne z _variables.scss

AI nie zgadywało czego szukać. Wykonywało instrukcje. Różnica jest taka sama jak między przeglądem kodu od kogoś kto zna projekt a przeglądem od kogoś kto go nie zna.

Co AI złapało, a ja przeoczyłem

Mutowalny stan w cacheContentTree::setIncludeDrafts() mutował obiekt z cache. Ta sama instancja drzewa była współdzielona między requestami, więc przełączenie draftów w jednym requeście wpływało na następny. Race condition czekający na okazję. Rozwiązanie: uczynić ContentTree niemutowalnym i przenieść filtrowanie draftów do warstwy serwisu.

Naruszenia SOLIDSiteConfigServiceInterface miał 16 metod. Zasada projektu to max 5. ContentTree miał 20+ metod robiących strukturę danych, silnik zapytań i scoring rekomendacji — trzy zadania w jednej klasie. Rozwiązanie: wyciągnąć RelatedPostsService (73-linijkowy algorytm scoringu → osobny serwis z własnym interfejsem).

Zduplikowana logikareadingTime() i excerpt() obie robiły strip_tags + str_word_count na tej samej zawartości HTML. Wyciągnięte do wspólnej metody getPlainText().

Luki w dostępności — Zagnieżdżone elementy <label> w formularzu kontaktowym (niepoprawny HTML), brakujące role="alert" na spanach błędów, aria-current="true" zamiast "page". Każde poprawka na jedną linię, ale niewidoczne bez checklisty.

Gnicie dokumentacjidocs/STYLEGUIDE.md podający Bootstrap blue (#0d6efd) zamiast faktycznego zielonego (#2d8a4e). docs/ARCHITECTURE.md odwołujący się do fantomowych plików: tagline.js, contact_widget.html.twig, _header.scss — żaden nie istniał.

Co AI źle oceniło (false positives)

CSRF na formularzu kontaktowym — oznaczone jako luka, ale celowe. Statyczne, pre-renderowane strony nie mogą używać tokenów CSRF powiązanych z sesją — token jest wypalony w HTML w czasie buildu, powiązany z sesją serwera buildowego. Każdy odwiedzający dostaje ten sam token. Turnstile + nagłówek X-Requested-With zapewniają rzeczywistą ochronę.

Dane użytkownika w komunikatach błędów 404 — oznaczone jako ryzyko enumeracji, ale te komunikaty pojawiają się tylko podczas statycznego buildu, nie dla odwiedzających. Produkcyjne strony błędów Symfony nie ujawniają komunikatów wyjątków.

"Niejodyczne" porównania — oznaczone wzorce zmienna-vs-metoda jak $page->directoryKey() === $directoryKey, ale prawdziwa reguła Yody dotyczy literałów (null, false, stringi) po lewej. Porównania zmienna-vs-zmienna nie wymagają odwracania.

Co zmieniłem vs co zaakceptowałem

Około 60 problemów naprawionych. Około 19 zaakceptowanych:

  • Zduplikowane serwisy podgląduDraftPreviewService i ScheduledPreviewService są prawie identyczne, ale każdy ma 3 linie. Sparametryzowanie wymagałoby ServiceLocatora, wstrzykiwania enuma i customowej serializacji na data collectorze. Więcej złożoności niż oryginał. Zaakceptowane.
  • Szeroki SiteConfigServiceInterface — 16 metod, ale to obiekt konfiguracyjny, nie serwis biznesowy. Podział dotyka 20 plików dla porządku anotacji tylko. Zaakceptowane.

Naprawki, które się liczyły:

// Przed: mutowalne drzewo w cache
$tree->setIncludeDrafts(true);  // mutuje współdzieloną instancję

// Po: niemutowalne drzewo, filtrowanie w warstwie serwisu
$posts = $this->contentService->getPosts($locale, includeDrafts: true);
// Przed: 73-linijkowy algorytm w ContentTree
public function getRelatedPosts(ContentItem $post, int $limit = 3): array
{
    // ... 73 linie scoringu tagów/kategorii/serii
}

// Po: osobny serwis
final class RelatedPostsService implements RelatedPostsServiceInterface
{
    public function findRelated(ContentItem $post, ContentTree $tree, int $limit = 3): array
    {
        // ... ta sama logika, ale izolowana
    }
}

Wniosek

"Przejrzyj mój kod" daje generyczne uwagi. "Audytuj według tych 200 linii standardów projektu" daje konkretne, akcjonowalne wyniki. AGENTS.md nie służy tylko do budowania — służy też do przeglądów. AI złapało rzeczy, na które patrzyłem dziesiątki razy, bo sprawdzało według reguł, a nie polegało na znajomości.

W następnym poście opisuję luki bezpieczeństwa wśród tych 79 problemów: XSS w wyszukiwarce, open redirecty i path traversal na "statycznej" stronie.