# Widoczność stron WIG20 i mWIG40 dla asystentów AI — sierpień 2026

Badanie pierwotne: pomiar techniczny stron korporacyjnych 60 największych spółek
notowanych na GPW. Sprawdza warunki wstępne bycia zacytowanym przez asystenta AI,
nie samo cytowanie.

Artykuł z wynikami: `/blog/widocznosc-ai-wig20-mwig40-pomiar`

## Data i próba

- Pomiar: 10–11 sierpnia 2026 (pomiar główny 10.08; przelicz po naprawie
  artefaktu `textContent` — 11.08)
- Próba: WIG20 (20) + mWIG40 (40) = 60 spółek, zbiory rozłączne
- Skład indeksów: portfele GPW Benchmark z sesji 7 sierpnia 2026, potwierdzone
  niezależnie w BiznesRadarze i StockWatchu (zgodność 60/60)
- Domeny: strona korporacyjna emitenta, nie serwis konsumencki marki. Cztery
  przypadki wymagały rozstrzygnięcia i są opisane w `probka.json` w polu `uwaga`
  (Allegro, Cyfrowy Polsat, Diagnostyka, Modivo)

Skład indeksów zmienia się kwartalnie; najbliższa korekta po trzecim piątku
września 2026. Po tej dacie próbę trzeba zbudować od nowa.

## Metoda

Każda strona główna otwierana dwa razy w tym samym Chromium, przy jednej różnicy:
raz z wyłączonym JavaScriptem, raz z włączonym.

**Miara główna (`lukaBota`)**: tekst całego dokumentu bez skryptów i styli, razem
z węzłami ukrytymi przez CSS, ale bez okien zgód — liczony tak samo po obu
stronach. Obie poprawki są konieczne i idą w przeciwne strony:

- Bez liczenia węzłów ukrytych miara krzywdzi strony zwijające treść w zakładki.
  modivoplatform.com pokazuje 426 słów widocznych bez JavaScriptu, a ma w kodzie
  serwerowym 7896 — bot widzi całość, człowiek nie.
- Bez odjęcia okien zgód miara krzywdzi wszystkich pozostałych. Panel zgód
  z listą dostawców TCF dokłada na cdprojekt.com ponad sto tysięcy słów po
  stronie z JavaScriptem i sam tworzy pozorną lukę poniżej 0,01 zamiast
  rzeczywistych 0,94.

Miary pomocnicze zostają w danych: `lukaNetto` liczy tekst widoczny na ekranie
(perspektywa człowieka), `slowaSuroweHtml` — surowy odczyt HTML-a bez korekty.

`robots.txt` i `llms.txt` pobierane osobnym skryptem, z hosta **końcowego** (po
przekierowaniach), z ponowieniami i dwoma nagłówkami `User-Agent`.

```
node pomiar.mjs probka.json > surowe.json      # wymaga playwright-core z Chromium
node pliki-hosta.mjs surowe.json > wyniki.json
node analiza.mjs wyniki.json
```

Pola `robotsBajty` i `llmsBajty` to rozmiar treści w bajtach UTF-8, bez
znacznika BOM — stąd różnica dokładnie 3 bajtów wobec `wc -c` na plikach
zapisanych z BOM.

Katalog `surowe/` zawiera zrzuty plików robots.txt i llms.txt spółek
wymienianych w artykule z nazwy, z datą pobrania w nagłówku i sumami
kontrolnymi w `SUMY-SHA256.txt`. Sprawdzalne bez odwiedzania badanych serwisów
są w ten sposób imienne twierdzenia **o plikach robots.txt i llms.txt**;
twierdzenia o treści stron (liczby słów, luki) weryfikuje się ponownym
uruchomieniem `pomiar.mjs`, póki strony się nie zmieniły.

Z plików llms.txt większych niż kilkanaście kilobajtów publikujemy fragment
(z reguły pierwsze 40 linii; dla BNP Paribas 200, bo plik nie jest w formacie
llms.txt) z rozmiarem i sumą SHA-256 całości — pełne kopie to cudze
utwory, a do weryfikacji twierdzeń wystarczy charakter pliku, rozmiar i suma.

**Licencja CC BY 4.0 obejmuje wyłącznie pliki własne badania**: wyniki.json,
probka.json, powtarzalnosc.json, metoda.md i skrypty. Zawartość katalogu `surowe/` to cudze treści
przytoczone w zakresie uzasadnionym celem weryfikacji twierdzeń (prawo
cytatu) — nie podlegają relicencjonowaniu.

## Kategorie botów

Za dokumentacją dostawców, w trzech grupach — dwie nie wystarczają:

- **odpowiadające**: OAI-SearchBot, Claude-SearchBot, Claude-User, ChatGPT-User,
  PerplexityBot, Perplexity-User. Blokada wyklucza z cytowań.
- **treningowe**: GPTBot, ClaudeBot, anthropic-ai, CCBot, Applebot-Extended,
  Bytespider, Meta-ExternalAgent, Amazonbot. Blokada nie wyklucza z odpowiedzi.
- **o podwójnej funkcji**: Google-Extended. Dokumentacja Google mówi, że steruje
  użyciem treści do trenowania modeli Gemini *oraz* do groundingu, czyli
  podawania treści modelowi w chwili zapytania. Blokada wycina także z odpowiedzi.

Osobno odczytywana jest linia `Content-Signal` — deklaracja przeznaczenia treści
stawiana w robots.txt. Bez niej pomiar pomija spółki, które decyzję podjęły, nie
wymieniając żadnego bota z nazwy (Text).

## Błędy narzędzia znalezione w walidacji

Każdy dawał wynik wyglądający wiarygodnie. Opisane, bo kto powtórzy pomiar,
trafi na te same pułapki.

1. **Zliczanie tekstu z HTML-a bez korekty.** Na cdprojekt.com dało 109 228
   „słów" po renderze przy 538 przed — nadmiar to ukryty panel zgód z listą
   dostawców TCF.
2. **Baner ciasteczek jako treść.** Okno zgód wstrzykuje JavaScript, więc przy
   wyłączonym JS go nie ma. Bez odjęcia po obu stronach regulamin ciasteczek
   liczy się jako treść niedostępna maszynie.
3. **`<body>` uznany za okno zgód.** Systemy zgód oznaczają korzeń dokumentu
   klasą (`eu-cookie-compliance` na kghm.com, `dp--cookie-consent` na jsw.pl),
   więc selektor łapał całą stronę i pięć spółek wypadało z pomiaru.
4. **Próg procentowy zamiast strukturalnego.** Panel zgód potrafi być większy
   niż reszta strony, więc odsiewanie „kandydatów powyżej połowy tekstu" go
   przepuszczało. Działa dopiero warunek strukturalny: okno zgód nie zawiera
   nagłówka `h1` ani sekcji `main`.
5. **`isConnected` na drzewie odłączonym.** `cloneNode` zwraca drzewo poza
   dokumentem, więc warunek oparty na `isConnected` wygaszał całą pętlę
   usuwającą i nie usuwał niczego.
6. **„Nie udało się pobrać" policzone jako „nie ma".** Osiem spółek trafiło do
   worka „bez robots.txt", a plik ma pięć z nich. Do tego gkpge.pl oddaje pod
   adresem pliku kod 200 i stronę HTML z komunikatem zapory.
7. **Pytanie o pliki pod złym hostem.** `robots.txt` i `llms.txt` należą do
   hosta: allegro.eu przekierowuje na about.allegro.eu i dopiero tam ma llms.txt.
8. **`textContent` skleja sąsiednie węzły bez spacji.** W zminifikowanym HTML
   `<li>Konta</li><li>Karty</li>` liczyło się jako jedno słowo; na pkobp.pl
   zaniżało to wynik o 22% (271 słów zamiast 346). Iloraz luki był na artefakt
   odporny, bo działa po obu stronach porównania, ale wartości bezwzględne
   były systematycznie zaniżone. Znalezione przez recenzenta w rundzie 2b,
   naprawione przed pomiarem końcowym: węzły tekstowe łączone są spacją.

## Powtarzalność

Pomiar główny wobec przebiegu kontrolnego z 11.08.2026 (kilka godzin później,
to samo narzędzie i próbka): **58 z 60 spółek dało identyczną lukę**, dwie
różnicę co najwyżej 0,03, mediana różnic 0,00. Zestawienie obu przebiegów dla
wszystkich 60 spółek jest opublikowane w `powtarzalnosc.json`. Wcześniejsze
przebiegi robocze z 10.08 (miara sprzed naprawy artefaktu `textContent`) dały
zbieżność 54 z 55 — tamtych danych nie publikujemy, bo dotyczyły wycofanej
wersji miary.

## Ograniczenia

- **Jedna domena na spółkę.** Polityka dla botów bywa ustawiona tam, gdzie stoi
  treść, a nie na wizytówce holdingu: holding.wp.pl nie ma robots.txt, a wp.pl
  ma rozpisaną politykę AI. Podobnie cdprojekt.com wobec cdprojektred.com.
- **Nagłówek GPTBota z adresu spoza OpenAI.** Serwis weryfikujący bota po
  odwrotnym DNS potraktuje to jak podszywanie, więc odczyt „bot dostaje 403"
  mierzy reakcję na sam nagłówek.
- **Strona główna, nie cały serwis.** Bywa uboższa w treść niż podstrony, ale to
  ona jest wizytówką encji i pierwszym adresem, po który sięga bot.
- **Trzy stany zamiast dwóch.** „Nie zmierzono" nigdy nie wchodzi do mianownika:
  2 pliki llms.txt (Cyfrowy Polsat, AmRest) pozostały niesprawdzone — hosty
  odrzucają żądania o pliki tekstowe kodem 403.
- **Jeden rekord scalony z dwóch dni.** Pliki hosta i odczyt nagłówka bota dla
  pl.asseco.com pochodzą z 10.08: 11.08 zapora Cloudflare objęła adres pomiarowy
  pełnym challenge po naszym własnym powtarzanym ruchu, więc świeży odczyt
  mierzyłby reakcję na nasz adres, nie politykę serwisu. Zaznaczone w polu
  `uwagaPomiaru` rekordu.
- Parser `robots.txt` wykrywa blokadę pełną (`Disallow: /`, a po ewaluacji
  wildcardów z RFC 9309 §2.2.3 także `/*` i `*`); blokady na innych ścieżkach
  sprawdza osobno `robots.mjs`.
- **Podtypy `Organization` liczą się do wyniku.** W próbie dotyczy to jednego
  przypadku: text.com deklaruje `Corporation`, podtyp `Organization`
  w schema.org — stąd 23 spółki przy 22 literalnych wystąpieniach typu.
- **Iloraz luki może przekroczyć 1.** Znaczy to, że strona z JavaScriptem
  pokazuje mniej tekstu niż stoi w kodzie serwerowym (aplikacja kliencka
  podmienia treść, karuzele chowają slajdy). Takie strony liczą się do pasma
  „komplet treści". Błąd mianownika jest przy tym asymetryczny: widać go tylko
  powyżej 1, a poniżej 1 działa na korzyść strony i nie jest kwantyfikowany.
  Kontrola wrażliwości: po obcięciu ilorazów do 1,0 mediana pozostaje 0,98.
- **Heurystyka okien zgód ma znany tryb błędu.** Warunek strukturalny („okno
  zgód nie zawiera h1 ani main") zawiedzie na panelu zgód, który sam używa
  nagłówka h1, oraz na kontenerze treści z tokenem cookie/consent w klasie bez
  h1/main. W tej próbie żaden z tych przypadków nie wystąpił, ale kto powtórzy
  pomiar na innych domenach, powinien to sprawdzić.
- **Test nagłówka bota ma trzy stany.** Odmowa (403/429), brak odmowy (2xx/3xx)
  i „nie zmierzono" (zerwane połączenie, kody 5xx) — kodBot = -1 to wyjątek
  klienta HTTP, nie odpowiedź serwera, i nie wchodzi do mianownika.
- **Atrybucję 403 do nagłówka rozstrzyga dopiero kontrola A/B** tym samym
  klientem HTTP z nagłówkiem przeglądarki — narzędzie jej nie wykonuje
  automatycznie; wykonana ręcznie 11.08.2026 pokazała, że u Modivo i Asbisu
  403 to reakcja na nagłówek (przeglądarka dostaje 200), a Inter Cars odcina
  klientów automatycznych niezależnie od nagłówka (403 na oba). Zaznaczone
  w polu `uwagaPomiaru` rekordu.

## Słownik pól rekordu (wyniki.json)

Po jednym rekordzie na spółkę; `null` = nie dotyczy albo nie zmierzono.

| pole | znaczenie |
|---|---|
| `ticker`, `nazwa`, `domena`, `indeks`, `pewnosc`, `uwaga` | identyfikacja spółki z próbki (probka.json) |
| `hostKoncowy`, `finalny` | host i adres po przekierowaniach — to jego dotyczą pliki hosta |
| `kod` | kod HTTP strony głównej dla przeglądarki |
| `kodBot` | kod HTTP na nagłówek GPTBota; -1 = zerwane połączenie (nie zmierzono) |
| `sciana`, `zmierzone` | wykryty challenge anty-botowy; czy strona weszła do pomiaru |
| `botBezJs`, `botRender`, `lukaBota` | **miara główna**: słowa dostępne parserowi przed/po wykonaniu skryptów i ich iloraz |
| `slowaSurowe`, `slowaRender`, `lukaNetto` | miara pomocnicza: tekst widoczny na ekranie (innerText) przed/po JS |
| `lukaRenderu` | iloraz tekstu widocznego przed/po JS bez odjęcia okien zgód (brutto) |
| `slowaSuroweNetto`, `slowaRenderNetto` | jw., po odjęciu okien zgód |
| `slowaSuroweHtml`, `slowaRenderHtml` | miara kontrolna: surowy odczyt HTML bez korekt |
| `zgodyBezJs`, `zgodyRender` | słowa w wykrytych oknach zgód (odejmowane w miarach netto i botowej) |
| `tytul`, `bladRenderu` | tytuł strony; błąd przeglądarki, jeśli render się nie powiódł |
| `jsonldSurowe`, `jsonldRender` | liczba bloków JSON-LD przed/po wykonaniu skryptów |
| `typySurowo`, `typyRender` | typy `@type` z tych bloków |
| `organizacjaSurowo`, `organizacjaPoRenderze` | czy wśród typów jest Organization lub jego podtyp |
| `robotsStan`, `robotsKod`, `robotsBajty`, `robotsUwaga`, `robotsJest` | robots.txt: jest / brak / niezmierzone, kod HTTP, rozmiar (bajty UTF-8 bez BOM) |
| `blokujeOdpowiedzi`, `blokujeTrening`, `blokujeDwufunkcyjne` | boty zablokowane pełną blokadą, wg trzech kategorii |
| `wymieniaBoty`, `blokadaGlobalna`, `contentSignal` | boty AI wymienione z nazwy; blokada w grupie `*`; linia Content-Signal |
| `llmsStan`, `llmsKod`, `llmsBajty`, `llmsTxt` | llms.txt: stan, kod, rozmiar, czy plik realnie istnieje |
| `uwagaPomiaru` | adnotacja o odstępstwie pomiaru dla tego rekordu |

## Wyniki (10–11.08.2026)

| miara | wynik |
|---|---|
| strony zmierzone | 60 / 60 |
| mediana treści dostępnej botowi bez JavaScriptu | 0,98 |
| komplet treści bez JavaScriptu (≥0,95) | 36 / 60 |
| poniżej połowy treści | 2 / 60 |
| mediana słów dostępnych botowi na stronie głównej | 879,5 |
| poniżej 300 słów | 3 / 60 |
| JSON-LD `Organization` bez JS | 23 / 60 |
| żadnego bloku JSON-LD bez JS | 26 / 60 |
| `Organization` dopiero po wykonaniu JS | 0 |
| `robots.txt` pobrany / brak / niezmierzony | 56 / 4 / 0 |
| blokuje boty odpowiadające | 0 / 56 |
| blokuje Google-Extended (podwójna funkcja) | 2 / 56 |
| blokuje boty treningowe | 2 / 56 |
| ślad decyzji o botach AI (nazwany bot lub Content-Signal) | 7 / 56 |
| odmowa 403 na żądanie z nagłówkiem GPTBota | 3 / 56 zmierzonych (4 niezmierzone); mechanizm rozstrzyga kontrola A/B — patrz ograniczenia |
| ma `llms.txt` | 12 / 58 zmierzonych (2 niezmierzone) |
| komplet warunków naraz | 12 / 60 |

Komplet warunków = pięć progów naraz: `lukaBota` ≥ 0,95 · `botBezJs` ≥ 300 ·
`Organization` w kodzie bez JS · zero blokad botów odpowiadających · zero blokad
Google-Extended. Reakcja na nagłówek GPTBota celowo nie jest szóstym progiem:
ten pomiar jest nierozstrzygający (nagłówek szedł z adresu spoza sieci OpenAI)
i dla czterech spółek niezmierzony, a miara nierozstrzygająca nie może być
twardą bramką wyniku zbiorczego — pozostaje miarą osobną. Usunięcie progu nie
zmienia wyniku: trzy spółki z 403 odpadają i tak na braku `Organization`.
Definicja wykonywalna: `analiza.mjs`, sekcja „Komplet warunków".
