Wczorajszy audyt V0.2 na tym samym portfolio znalazł 3 realne findings. Static plus dynamic, zero LLM w pętli, deterministyczna podłoga robi swoje. Dziś puszczam V0.3 public na ten sam target, te same route’y, ten sam build Astro 5. V0.3 dodaje 5 AI specjalistów czytających source przez Read/Grep/Glob.

Dwa produktywne runy audytu, osiem godzin odstępu. 16 unique produkcyjnych findings złapanych między nimi. Trzeci run, po naprawieniu pierwszych dwóch batchy, znajduje zero nowych findings. To jest liczba, na której warto otworzyć: 16 złapanych, 0 zostających.

Dwie produktywne rundy, jedna runda konwergencji. Ta ostatnia runda to ten kawałek, który większość AI-audit case studies pomija, bo to ten kawałek, który wymaga puszczenia tego samego toola na ten sam projekt trzeci raz i przyznania, czy coś nowego się pojawi. Nic się nie pojawiło. Trzy niezależne runy, trzy różne powierzchnie skanu LLM, wszystkie zbiegły się na zero produkcyjnych issues.

To są najuczciwsze dane audytu accessibility, jakie do tej pory shippowałem. To też jest kawałek toolkit story, której wczoraj nie miałem jak opowiedzieć. Dzisiejsza warstwa czyta JSX, nie wyrenderowany HTML. Inna warstwa, inne zadania, inne findings.

Architektura V0.3

V0.3 dodaje pięciu specjalistów. Każdy czyta source. Każdy ma węższy zakres. Lecą równolegle.

Pięcioro to: semantic-structure (hierarchia nagłówków, pokrycie landmarków, atrybuty lang, ranga nagłówka modala), aria-patterns (ARIA misuse, politeness live regionów, taksonomia typów dialogu), keyboard-interaction (composite widgets, focus management, onClick bez onKeyDown, tabele klawiatury z APG), color-contrast-static (CSS contrast policzony z source, color-only indicators, prefers-* media queries) i forms-accessibility (labels, validation timing, autocomplete, payment review steps).

Każdy specjalista dostaje wąsko sformułowanego prompta i ma dostęp do Read, Grep, Glob i LS po projektowym source. Bez write access, bez shell access, bez internetu. Otwierają pliki, patrzą w kod, zwracają JSON findings z ruleId, file:line, severity, WCAG SC i sugerowanym fixem. Nie puszczają projektu, nie renderują niczego, nie liczą wartości pikseli. Czytają.

Lead orchestrator dispatch’uje wszystkich pięciu przez Task tool Claude Code w jednym równoległym batchu. Pięciu specjalistów leci concurrently. Orchestrator czeka, zbiera, merguje i deduplikuje po (ruleId, file:line, url). Findings, które matchują static albo dynamic backbone, zwijają się do jednej entry.

graph TB
    A[Project Source] --> B[Lead Orchestrator]
    B --> C[5 AI Specialists in parallel]
    C --> D1[semantic-structure]
    C --> D2[aria-patterns]
    C --> D3[keyboard-interaction]
    C --> D4[color-contrast-static]
    C --> D5[forms-accessibility]
    D1 --> E[Read/Grep/Glob source]
    D2 --> E
    D3 --> E
    D4 --> E
    D5 --> E
    A --> F[Static TS Analyzer]
    A --> G[Dynamic Playwright + axe]
    E --> H[Findings Merge + Dedupe]
    F --> H
    G --> H
    H --> I[Score + Grade A-F]
    I --> J[Reports: dev + exec]

Ten sam merge step co w V0.2. Ten sam kształt WcagFinding. Ta sama logika dedupe. Static i dynamic backbone nadal lecą, nadal emitują findings, nadal zasilają ten sam model kar i grade A-F. AI to trzecie źródło, nie zastępca.

Run 1 (rano), co AI złapał, czego V0.2 nie zauważył

Audyt poranny na tym samym portfolio. V0.2 znalazł 3 contrast findings. V0.3 znalazł 9, włączając te 3.

Te 9 findings dzieli się na trzy klastry. Pierwszy to misuse aria: trzy findings pod WCAG 4.1.2 Name, Role, Value. Hero.astro:17 miał aria-label="C64 boot" na elemencie <p>, przesłaniając widoczny tekst. ProjectCard.astro:45 miał aria-label="Tech stack" na <ul>. EcosystemCard.astro:40 miał aria-label="Key metrics" na <dl>.

Statyczny pominął wszystkie trzy, bo wartości aria-label to stringi. Regex matchuje atrybut, nie to, czy atrybut jest sensowny na paragrafie, który już ma implicit role i widoczny tekst. Dynamic też je pominął. Axe sprawdza obecność atrybutu, nie semantyczną sensowność, a wyrenderowany HTML nadal ma aria-label nietknięte. Specjalista aria-patterns przeczytał JSX, rozpoznał, że paragrafy i definition lists już mają semantykę, i oflagował override jako anti-pattern.

Drugi klaster to kontrast na szerszej powierzchni. Pięć findings, wszystkie WCAG 1.4.3. Trzy wróciły z dynamic V0.2 (linia .hero-load i trzy badge’y .license ze wczoraj). AI specjalista color-contrast też złapał wszystkie trzy, czytając CSS source bezpośrednio. Nowy był ArticleCard.astro:136 .meta przy 2.98:1 na ciemnym gradiencie. Dynamic axe go nie złapał, bo axe chodzi po odwiedzonych stronach i liczy kontrast względem faktycznego renderowanego stanu. Karty artykułów w grze nigdy nie wylądowały w sweepie axe z tą dokładną kombinacją gradientu. AI specjalista przeczytał CSS, zauważył --color-text-subtle na tle dark backgroundu i policzył ratio ręcznie.

Trzeci klaster to jeden minor finding: contact.astro:52 miał <ul role="list">. Redundantne ARIA. WCAG 4.1.2. Specjalista semantic-structure oflagował z jednolinijkowym fixem: zdjąć atrybut role, implicit role już tam jest.

Sześć issues, których V0.2 nie mógł zobaczyć, plus te trzy, które mógł. Inna warstwa, inne findings. Aria misuse jest niewidoczne dla wszystkiego, co nie czyta source. Nowy finding kontrastu jest niewidoczny dla wszystkiego, co nie idzie za indirectionem CSS variables. To jest discovery payoff dla agentów czytających source.

Round 1 fix

Naprawiłem wszystkie 9. Sześć commitów, dwadzieścia pięć minut. Krótkie szczegóły:

FixPlikWCAG SCCommit
Zdjąć aria-label z .hero-load <p>Hero.astro:174.1.20c0e19e
aria-label -> sr-only <h4> + aria-labelledby (tech stack)ProjectCard.astro:454.1.29bca4bf
Ten sam pattern (key metrics)EcosystemCard.astro:404.1.2b141c52
Zdjąć redundantne role="list"contact.astro:524.1.2356ae8c
.meta color: text-subtle -> text-mutedArticleCard.astro:1361.4.33498856
.license color: accent-muted -> accentProjectCard.astro1.4.34929186

Fix’y aria-label nie były wszystkie takie same. Na <p> zdjęcie atrybutu było słuszne: widoczny tekst już to mówi. Na <ul> i <dl> labelki były strukturalne (“tech stack”, “key metrics”), więc dodałem screen-reader-only <h4> plus aria-labelledby wskazujące na nie. Inna strategia podmiany per element, ten sam anti-pattern złapany.

Fix .license podmienił jeden design token (--color-accent-muted na --color-accent), co rozwiązało wszystkie trzy findings badge’y license w jednym commicie. Pierwszy smak token-level multiplicative impact.

Re-run audytu, oczekuję czystego grade’u. Dostaję 7 NOWYCH findings zamiast tego. I tu się robi ciekawie.

Run 2 (popołudniu), insight triangulacji

To samo portfolio, post-Round-1, drugi audyt. Siedem nowych findings. Żadne z nich to nie regresja. Wszystkie w miejscach, których Run 1 nie tknął.

Lista, w kolejności severity: Footer.astro:269 .footer-links .note przy 1.77:1 (najgorsze ratio, jakie shippowałem, dark theme). Footer.astro:259 .footer-links a przy 2.80:1. ArticleCard.astro:123 .description przy 2.80:1. Topbar.astro:194 .icon-button przy 2.94:1. Topbar.astro:167 .nav-desktop a przy 2.94:1. MatrixToggle.astro:69 .matrix-hint przy 3.15:1. I jedno spoza klastra kontrastu: articles/index.astro:47 filter pille bez aria-pressed (WCAG 3.3.2 plus 4.1.2).

Wzorzec: sześć z siedmiu to kontrast, wszystkie używające --color-text-subtle albo --color-text-muted z arkusza dark theme. Jeden root cause, sześć widocznych symptomów, sześć różnych plików. Siódme to issue forms, toggle button bez programmatic state.

Czemu Run 1 tego nie wyciągnął? Uczciwa odpowiedź jest taka, że specjaliści LLM skanują nieco inną powierzchnię w każdym runie. Inna aktywacja prompta, inna kolejność traversal plików, inna uwaga. Run 1 trafił w klaster aria-misuse, bo poranne prompty kierowały specjalistów na elementy semantyczne z jawnym ARIA. Run 2 trafił w skorupy layoutu (Footer, Topbar) i powierzchnie content (ArticleCard description, MatrixToggle hint), bo prompty w tym runie, pracujące na świeżo naprawionym codebase, otworzyły inny zestaw plików.

To nie jest bug. To jest design. AI specjaliści wymieniają determinizm na szerokość. Jeden run widzi jeden wycinek. Trzy runy widzą projekt.

Counter-narratywa, którą chcę odbić, to ta głośna: “AI auditorzy są zawodni, bo są niedeterministyczni”. To framing mierzy nie to, co trzeba. Słuszna miara to nie “czy wyprodukowałeś identyczny output dwa razy?”. To “czy wiele runów zbiega się do tego samego stanu końcowego?”. Konwergencja na zero findings przez wiele niezależnych runów to najsilniejszy sygnał jakości, jaki probabilistic auditor może Ci dać. Statyczne reguły nie mogą nawet wyprodukować takiego sygnału, bo sprawdzają tylko to, do czego były zaprogramowane.

Dwa komplementarne tryby, nie konkurenci. Static łapie to, co jest, w sposób deterministyczny i CI-friendly. AI łapie to, co jest intencją, przez source semantic understanding, z szerokością, która wymaga wielu runów dla pełnego harvestu. Nudna warstwa i warstwa AI robią różne rzeczy.

Mam wybór w tym punkcie: shippować z wiedzą tylko z Run 1 albo naprawić to, co Run 2 znalazł. Poszedłem głębiej.

Round 2 fix i multiplicative impact tokenu

Dwa commity. Dwadzieścia pięć minut. Siedem findings rozwiązanych. Jeden commit naprawił sześć findings czterema linijkami CSS.

:root {
  /* dark theme default, text on #141414 */
- --color-text-subtle: #737373;   /* 3.4:1, AA fail */
- --color-text-muted:  #a1a1a1;   /* 6.5:1, AA pass but tight */
+ --color-text-subtle: #a3a3a3;   /* 6.7:1, AA pass */
+ --color-text-muted:  #b3b3b3;   /* 8.8:1, AA pass with headroom */
}

Ten jeden commit (03ae229) tknął global.css i dokładnie nic więcej. Sześć plików zawierających symptomy (Footer x2, Topbar x2, ArticleCard, MatrixToggle) nie zostało otwartych. Nie potrzebowały. Każdy komponent używający --color-text-muted albo --color-text-subtle na ciemnym tle dostał nowe ratio za darmo.

Fix forms był osobnym commitem (ab4d716): dodałem aria-pressed plus mały JS toggle sync do filter pilli w articles/index.astro. Filter pille teraz announce’ują “pressed” albo “not pressed” do screen readerów, co oczekują WCAG 3.3.2 (Labels or Instructions) i toggle button pattern z APG.

Statyczne analizatory raportują 6 contrast violations. AI specjalista czyta source, rozpoznaje wzorzec: 6 widocznych symptomów, 2 design tokens, 1 root cause. Jeden commit, cztery linijki, sześć findings rozwiązane. To jest design-tokens-first architecture spotykająca AI semantic understanding. Więcej o tym wzorcu jutro.

Czas na re-audyt. Round 3.

Run 3, konwergencja

Round 3, ten sam skill, to samo portfolio, post-Round-1-i-2. Wszyscy 5 AI specjalistów zwraca puste tablice.

Liczby: AI specjaliści 0 findings each (5 z 5 z sukcesem). Statyczny analizator 2 findings, oba playwright-report/index.html (output Playwright HTML reportera, generowany, nigdy nie deployowany). Dynamic 4 raw findings dedupowane do 1 unique, wszystkie <astro-dev-toolbar> (Astro dev-mode injection, który nie shippuje na produkcję). Total realnych findings, po odsianiu udokumentowanej podłogi szumu: 0. Score: 100/100. Grade: A.

Trzy niezależne runy w 8 godzin. Trzy różne powierzchnie skanu LLM, zero shared context między runami. Wszystkie zbiegające się na zero produkcyjnych issues dla tego samego codebase. To jest sygnał konwergencji. To nie jest pojedynczy point measurement, czy LLM ma rację. To multi-run aggregate, który pyta, czy projekt ma rację.

Większość AI-audit case studies zatrzymuje się na run 1. “Tool znalazł N issues”. Jeden audyt, zero weryfikacji, zero testu konwergencji, zero uczciwego framingu, co tool widzi vs czego nie widzi. Dane tutaj idą dalej: 9 znalezionych w run 1, 7 znalezionych w run 2 (inna powierzchnia, nie regresja), 0 znalezionych w run 3 między niezależnymi specjalistami. 16 unique findings złapanych, 0 zostających. Wiele runów to jednostka, nie pojedyncze runy.

Konwergencja to design point. Wiele runów agreguje się do szerszego pokrycia, niż jakikolwiek pojedynczy run może dać. Round 3 znajdujący zero nowych to tak wygląda “done” dla audytu AI-driven. Nie “LLM powiedział że nie ma problemów”, ale “trzy niezależne przebiegi, trzy różne powierzchnie skanu, wszystkie powiedziały że nie ma problemów”.

Ale konwergencja na homepage to nie konwergencja na całym site

Trzy runy na homepage powiedziały konwergencję. Ta sama strona, każde przejście audytu, osiem godzin między nimi, trzy różne powierzchnie skanu LLM, zero nowych findings. Czysty sygnał - dla jednego URL.

Potem wylądowało V0.4. Multi-page audit z 4-strategy auto-discovery: sitemap, router-scan, AI agent czytający strukturę projektu, JSON config gdy żadne z tych nie działa. Ten sam toolkit, szersza powierzchnia. Puściłem V0.4 router-scan na tym samym portfolio.

Dziewięć nowych findings na trzech stronach, których audyt homepage nigdy nie tknął. /privacy. /numbers. /projects. Strukturalne problemy HTML - <ul> tam, gdzie powinno być <dl>, kontrast .archived badge przy 3.0:1, brak semantyki definition list na blokach statystyk. Żadne z nich niewidoczne z perspektywy homepage, bo żadnej z tych stron nie było w surface’ie audytu homepage.

Round 4 fix sprint: 35 minut, trzy atomic commits. Re-audit na router-scan: clean. Potem przełączyłem na sitemap strategy i odpaliłem audyt na całym opublikowanym site. 35 routes.

5,816 findings.

Długa pauza.

99.86% z tych 5,816 to JEDEN bug. Shiki light-theme tokens przeciekały na jasne tła w każdym code blocku na każdej stronie artykułu. Jedna zmiana konfiguracji w Astro Markdown integration, 5,808 findings wyczyszczone. Pozostałe 8 podzieliło się na 1 residual .archived badge token (one-line fix) i 7 keyboard-trap-runtime false positives na długich stronach artykułów - zgłoszone jako toolkit issue dla v0.5+ (naturalna konwergencja focus cycle błędnie klasyfikowana jako pułapka).

Wzorzec: single-page audit mierzy jeden URL. To w najlepszym razie rozszerzenie Lighthouse’a. Multi-page audit z 4-strategy auto-discovery mierzy realną produkcyjną powierzchnię. Dopiero to zarabia słowo “professional”.

Konwergencja i pokrycie to niezależne wymiary jakości. Trzy runy zbiegły się na homepage. Site potrzebował innego narzędzia, żeby znaleźć resztę.

Krótki uczciwy moment, dogfooding bug

Shippowałem V0.3 w piątek. W niedzielę dogfoodowałem na tym samym portfolio. Audyt zwrócił 10 findings, ale z przypisem: “Wszyscy 5 AI specjalistów zwróciło błędy”.

Bug był w skillu /wcag:audit, nie w kodzie toolkit. SKILL.md instruował Claude Code, żeby wywołać CLI przez Bash subprocess z --use-ai. Ten subprocess puszcza Node bezpośrednio, gdzie nie ma globalThis.Task, bo Task tool istnieje tylko w runtime JS Claude Code. Pięciu specjalistów silent-failowało przy dispatchu, a orchestrator spadł do static plus dynamic only. Design CLI był poprawny (graceful degradation, gdy Task jest niedostępny). Design skilla był zły (routowanie AI przez warstwę, gdzie Task nie sięga).

Pominąłem to w smoke teście z żenującego powodu. Smoke test leciał z wewnątrz sesji Claude Code, gdzie Task jest dostępny. Wywołania Task się powiodły, output wyglądał czysto, shippowałem. Produkcyjny user flow (clone repo, otwórz Claude Code, puść /wcag:audit) trafia w ścieżkę Bash subprocess, gdzie Task jest niedostępny. Inna ścieżka kodu, inny wynik.

Fix zajął 45 minut i jeden plik. Refaktoryzowałem skilla, żeby dispatch’ował wywołania Task bezpośrednio wewnątrz sesji Claude Code (5 równoległych wywołań), zostawiając CLI subprocess tylko dla static plus dynamic. Wzorzec: skill to dokument orkiestracji, CLI to deterministyczny silnik, nie mieszać warstw. v0.1 wcześniejszego skilla wcag-static-analyze toolkit miał to dobrze. v0.3 zregresjonował. Teraz nie. Jeden plik zmieniony, 134 insertions, 37 deletions, zero zmian w kodzie. Zweryfikowane na fixturze react-basic: 19 findings, matchuje baseline Pro alpha.2 19. Parytet potwierdzony.

Public toolkit shipped w piątek. Bug znaleziony w niedzielę. Fix w poniedziałek. Jeden plik zmieniony. Dlatego dogfooding przed publikacją ma znaczenie.

Smoke test w Claude Code to nie jest realny user flow przez skill. Złapałem. Naprawiłem. Liczby wyżej są z naprawionego skilla.

Jutro

Ostatni kawałek jutro. F do A w 8 commitach, 75 minut total Claude Code work. Jeden token edit, cztery linijki, sześć findings rozwiązane, w szczegółach. Pro tier na tym samym projekcie: multi-runtime (Claude Code, OpenCode subprocess, Ollama lokalnie dla wrażliwych client repo), auto-fix engine z deterministycznymi patcherami (image-alt, html-lang), niche specjaliści lądujący w alpha.4 (modal-specialist, ecommerce-journey). Live demo auto-fixu z grade’em before-and-after. Uczciwy commercial framing: public to edukacja, Pro to ekspertyza, możesz to zbudować od zera albo mnie zatrudnić. Series tease: tydzień następny, jarvis-brain.

Jeśli shippujesz robotę accessibility w 2026 i używasz AI w swoim stacku, Część 3 jest dla Ciebie. #FromTheField.