Skip to content

Naprawy WCAG 2.2 AA stwierdzone lekturą kodu (1.1.1, 3.1.1, 3.1.2) - #732

Open
mpasternak wants to merge 22 commits into
devfrom
fix-wcag-naprawy-stwierdzone
Open

Naprawy WCAG 2.2 AA stwierdzone lekturą kodu (1.1.1, 3.1.1, 3.1.2)#732
mpasternak wants to merge 22 commits into
devfrom
fix-wcag-naprawy-stwierdzone

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Naprawy naruszeń WCAG 2.2 AA, które specyfikacja z 2026-08-05 stwierdziła na podstawie lektury kodu — bez czekania na audyt, skan szeroki ani infrastrukturę testową. To krok 3 z jej sekcji „Kolejność prac i zależności", wykonany jako samodzielna iteracja.

Specyfikacja tej iteracji: 2026-08-06-wcag-naprawy-stwierdzone-design.md

Co naprawia

Kryterium Zmiana
3.1.2 Language of Parts (AA) Atrybut lang na tytułach oryginalnych publikacji — w szablonach stron szczegółów oraz w generatorze opisu bibliograficznego
1.1.1 Non-text Content (A) alt="" na ikonie dekoracyjnej w 504.html; usunięcie martwego szablonu user_navigation_autocomplete.html
3.1.1 Language of Page (A) <html lang="pl"> zamiast lang="en" w 504.html — treść strony jest polska, więc czytnik odczytywał ją całą angielską fonetyką

Bez lang czytnik ekranu odczytuje angielski tytuł polską fonetyką. Dane były już w modelu (Jezyk.kod_bcp47, dodane pod eksport CERIF) — brakowało wyłącznie wykorzystania ich w warstwie prezentacji.

Dwie warstwy, dwa czasy działania

Szablony stron szczegółów — efekt natychmiast po wdrożeniu, bo renderują się z pól modelu przy każdym żądaniu.

Generator opisu bibliograficznego — opis jest cache'owany w bazie jako gotowy HTML i używany w 31 miejscach (listy, wyniki multiseek, wydawnictwa powiązane, REST API). Znacznik musi trafić do środka tego blobu, więc wstawia go generator. Istniejące wiersze złapią go przy najbliższym nocnym denorm_rebuild (22:00 z harmonogramu Ofelii) — bez ręcznej interwencji i bez okna serwisowego ponad to, które już istnieje.

Trzy rzeczy, które wyszły w trakcie

Sanityzator wyciąłby znacznik. safe_opis_bibliograficzny_html przepuszcza opis przez nh3 z wąską allowlistą, w której span nie był dozwolony, a atrybuty miał wyłącznie <a>. Bez rozszerzenia listy o span[lang] poprawka generatora cicho by nie zadziałała — testy szablonu przechodziłyby, a opis w bazie byłby bez atrybutu. Rozszerzenie jest wąskie (lang nie wykonuje kodu, span bez style/class nie pozwala nadpisać wyglądu) i ma osobny test regresji bezpieczeństwa.

Osierocony wiersz dbtemplates (migracja 0488). Migracja 0295 zainstalowała browse/praca_tabela.html do bazy jako drugi wariant generatora opisu; migracja 0473 sprzątała dbtemplates, ale tylko dla nazw wskazywanych przez SzablonDlaOpisuBibliograficznego.nazwa_szablonu — ta nie była wskazywana, więc wiersz przetrwał. Loader dbtemplates stoi przed plikowym, więc edycja tego szablonu na dysku nie miałaby efektu na wdrożeniu, które by się na niego przełączyło. Błąd byłby cichy: bez wyjątku, ze starą treścią sprzed lat.

Regresja w wyszukiwarce rekordów powiązanych. Filtr i podświetlanie działały na surowym HTML. Po dodaniu <span lang="en"> fraza span/lang dopasowywała każdy rekord z tytułem obcojęzycznym, a podświetlanie wstawiało <mark> w środek atrybutu. Logika została wydzielona do modułu related-records-highlight.js (tokenizer dzielący na znaczniki/encje/tekst), przetestowana jednostkowo i naprawiona w obie strony.

Świadomie odroczone

Wpisy z uzasadnieniem i datą w sekcji „Odroczone niezgodności" obu specyfikacji:

  • 2.1.4 — skrót / wiązany na document. Wszystkie trzy dopuszczone przez kryterium wyjścia mają koszt produktowy; brak nacisku regulacyjnego i odbiorcy raportu.
  • 2.5.7 — nawigacja po grafie powiązań wyłącznie przez przeciąganie. Koszt nieproporcjonalny, funkcja opcjonalna i w części wdrożeń wyłączona.
  • 3.1.2 dla tytułu przełożonego — model nie zawiera danych o jego języku. jezyk_alt to odwzorowanie atrybutu z API PBN oznaczające drugi język pracy, nie język przekładu; użycie go dałoby oznaczenie błędne, a to gorsze niż brak.
  • Warunki po stronie wdrożenia — niewypełniony kod_bcp47 w słowniku języków, własny szablon opisu, override OPIS_BIBLIOGRAFICZNY_ALLOWED_TAGS, oraz wiersz opis_bibliograficzny.html zapisany w dbtemplates (admin wprost zachęca do jego edycji, a loader bazy ma pierwszeństwo — wtedy znacznik nie dotrze mimo poprawnego kodu).

Weryfikacja

Lokalnie, na finalnym drzewie:

  • make tests-without-playwright9350 passed, 4 skipped, 1 xfailed
  • make js-tests107 passed
  • make tests-only-playwright157 passed, 1 skipped
  • pre-commit (bez argumentów) → wszystkie hooki Passed

Dryf zgłaszany przez makemigrations --check dotyczy trzech pakietów zewnętrznych (favicon, flexible_reports, siteblog) — identycznie na czystym dev, więc zastany, nie wprowadzony tą gałęzią.

Znane drobiazgi do follow-upu

  • Filtr i podświetlanie rozjeżdżają się dla frazy dokładnie przeciętej granicą znacznika (<i>): rekord trafia na listę bez podświetlenia. Rzadkie; nie odtwarza naprawionej regresji.
  • test_znacznik_przezywa_post_processing nie ćwiczy gałęzi .replace(".</span></b>[", …) — scenariusz nie ma autora z typem odpowiedzialności. Poprawka zweryfikowana ręcznie, brak pokrycia regresyjnego.

Uwaga przy scalaniu

baseline-sql/baseline.sql był regenerowany na Apple Silicon (postgres_version zmienia x86_64aarch64) i dociągnął niezwiązany dryf formdefaults (15 wierszy, dane domyślne). Zgodnie z regułą z CLAUDE.md warto odświeżyć baseline raz, przy scalaniu, najlepiej przez workflow refresh-baseline.yml.

🤖 Generated with Claude Code

mpasternak and others added 19 commits August 6, 2026 11:15
Wydziela z programu WCAG 2.2 AA (spec z 2026-08-05) te naprawy, które nie
zależą od skanu szerokiego, audytu ani infrastruktury testowej: 1.1.1
(obraz bez alt, martwy szablon) oraz 3.1.2 (atrybuty lang na tytułach
obcojęzycznych, oba wektory).

Dokument koryguje dwa ustalenia poprzednika:

- wektor 2 (opis_bibliograficzny_cache) NIE wymaga liveops per wdrożenie —
  nocny denorm_rebuild z Ofelii przelicza całą bazę niezależnie od tej
  zmiany, a szablon opisu wrócił na dysk migracją 0473. Rzeczywistym
  warunkiem koniecznym jest rozszerzenie allowlisty nh3 o span/lang,
  którego poprzednik nie wymieniał;
- user_navigation_autocomplete.html jest martwy (widok autocomplete zwraca
  JSON, listę rysuje Select2) — usuwamy plik zamiast łatać alt.

Bramka CI, baseline, audyt ręczny i raport zgodności pozostają poza
zakresem do czasu pojawienia się odbiorcy raportu. 2.1.4 (skrót /) i 2.5.7
(graf powiązań) odroczone świadomie, z wpisem w wykazie niezgodności.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8RjSZWMR6L7fuRDJJZW4L
Recenzja podważyła cztery ustalenia pierwszej wersji; wszystkie
zweryfikowane w kodzie i przyjęte.

- browse/praca_tabela.html NIE jest stroną szczegółów, tylko alternatywnym
  szablonem opisu bibliograficznego (instalowany przez migrację 0295 obok
  opis_bibliograficzny.html; praca.html:54 włącza wyłącznie wariant _mono).
  Przechodzi więc ścieżką wektora 2, nie 1. Pierwsza wersja popełniła
  dokładnie ten błąd, który wytykała poprzednikowi: uznała plik za żywy
  widok bez sprawdzenia, kto go renderuje.
- Argument, że założenie o semantyce jezyk_alt jest "bezpieczne", był
  fałszywy: przy WYPEŁNIONYM polu o innym znaczeniu tytuł dostałby BŁĘDNY
  lang, co dokument sam nazywa gorszym niż brak. Zawężamy do tytułu
  oryginalnego (jezyk); przekład trafia do wykazu jako niezgodność
  częściowa. Fakt osłabiający hipotezę: eksport CERIF nie używa jezyk_alt
  do oznaczania tytułów.
- Tabela konsumentów opisu miała 5 pozycji zamiast ~15 i błędnie
  etykietowała eksport oświadczeń jako PDF/weasyprint (to XLSX z surowym
  HTML w komórce). Uzupełniona i podzielona wg mechanizmu.
- Wyszukiwanie po podłańcuchu w surowym HTML występuje w czterech
  miejscach, nie jednym — w tym w PUBLICZNEJ wyszukiwarce rekordów
  powiązanych (praca_tabela_mono.html:676,914), czyli w zakresie audytu.
  Gałąź w list_views.py to elif, aktywny tylko dla rekordów bez
  tytul_oryginalny — test musi ten warunek odtworzyć.

Drobne: denorm nie zależy od bpp.Jezyk (uzupełnienie kod_bcp47 nie brudzi
cache), zastrzeżenie dla wdrożeń bez harmonogramu Ofelii, wpis w wykazie
dla instalacji z własnym szablonem opisu, testy breadcrumbu ograniczone do
przypadków faktycznie tam występujących.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8RjSZWMR6L7fuRDJJZW4L
Autor modelu potwierdza: jezyk_alt to odwzorowanie atrybutu z API PBN
(pbn.nauka.gov.pl/api/v1/), skopiowane "na zapas", oznaczajace drugi jezyk
PRACY (publikacja dwujezyczna) — nie jezyk tytulu przelozonego.

Potwierdzenia w kodzie: adapter PBN wysyla mainLanguage z jezyk i
originalLanguage z jezyk_orig, a jezyk_alt mimo pochodzenia z PBN nie jest
tam eksportowane; zaden importer go nie wypelnia (wchodzi wylacznie recznie
z admina); CERIF oznacza tytuly przez jezyk i dodatkowe_tytuly.jezyk.

Decyzja bez zmian (oznaczamy tylko tytul_oryginalny), ale uzasadnienie sie
zmienia: to nie ostroznosc wobec niepewnej semantyki, tylko brak danych.
Zmienia to tez charakter wpisu w wykazie — domkniecie 3.1.2 dla przekladu
wymaga NOWEGO pola w modelu (migracja + uzupelnianie danych przez uczelnie),
a nie wykorzystania istniejacego.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8RjSZWMR6L7fuRDJJZW4L
Dziewiec zadan w kolejnosci zaleznosci: filtr oznacz_jezyk → allowlista nh3
→ wektor 1 (strony) → wektor 2 (generator opisu) → naprawa podswietlania →
testy substring-search → 1.1.1 → korekty spec i newsfragmenty → weryfikacja.

Dwa ustalenia dopisane do specyfikacji przy rozpisywaniu planu:

- Podswietlanie w wyszukiwarce rekordow powiazanych
  (praca_tabela_mono.html:1140-1200) wymaga NAPRAWY, nie tylko testu.
  Regex podswietla na surowym HTML-u; dzis blob ma tylko <b>/<i>, wiec
  problem jest utajony, ale <span lang="en"> wnosi podciagi "en", "an",
  "lang" — pospolite w tytulach. Wpisanie "en" wstawia <mark> w srodek
  atrybutu i psuje markup przy .html(). Logika podswietlania zostaje
  wyekstrahowana do modulu JS (testowalnego vitestem) i poprawiona tak,
  by omijac zawartosc znacznikow.
- 504.html:2 deklaruje <html lang="en"> przy polskiej tresci — WCAG 3.1.1
  (poziom A), czytnik odczytuje CALA strone angielska fonetyka. Poprawka
  jednowyrazowa, w pliku i tak edytowanym w tej iteracji. Jedyny szablon
  z wlasnym <html lang>; reszta dziedziczy po base.html.

Plan zweryfikowany wobec kodu: fixture jezyki juz ustawia kod_bcp47
(pl/en), _get_record_title czyta opis w gałęzi elif, praca_tabela_mono.html
nie ma bloku extra_js ani {% load static %}, style .praca-mono__related-*
siedza w praca_detail.scss:1001.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8RjSZWMR6L7fuRDJJZW4L
Pre-flight scan przed wykonaniem planu: parametryzowany test asertowal
'fraza in OPIS_ZE_ZNACZNIKIEM.lower()', czyli sprawdzal wlasna stala, nie
kod BPP. Recenzent slusznie zglosilby to jako test bez wartosci.

Zastapione dwoma testami wykonujacymi realny kod: _get_record_title
w obu galeziach (tytul / opis) oraz przebieg widoku listy kolejki PBN
z parametrem wyszukiwania na rekordzie bez tytulu oryginalnego.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8RjSZWMR6L7fuRDJJZW4L
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Migracja 0295 zainstalowala browse/praca_tabela.html do dbtemplates jako
drugi wariant generatora opisu. Migracja 0473 sprzatala dbtemplates tylko
dla nazw wskazywanych przez SzablonDlaOpisuBibliograficznego, a ta nie byla
wskazywana — wiersz przetrwal jako sierota.

Loader dbtemplates stoi przed plikowym, wiec dopoki wiersz istnieje, kazda
edycja tego szablonu na dysku (w tym znacznik lang z WCAG 3.1.2) nie ma
zadnego efektu na instalacji, ktora przelaczy sie na ten wariant. Blad jest
cichy: bez wyjatku, bez ostrzezenia, ze stara trescia sprzed lat.

Kasowanie przez usun_dbtemplate_i_przebuduj — ten sam mechanizm co 0473,
z guardem dysk-existence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ata-records

Recenzja Task 5 wykazala dwa braki:

1. Regex segmentujacy rozpoznawal wylacznie znaczniki <tag>, wiec
   sekwencje encji HTML (np. podwojnie zescape'owany '&amp;lt;'
   powstajacy z _escape_bare_angle_brackets + escapowania ampersandu
   w data-records) byly traktowane jak zwykly tekst. Fraza 'amp' albo
   'lt' (pospolita w tekstach medycznych) rozbijala encje identycznie
   jak wczesniej span[lang]. Regex rozszerzony o trzecia alternatywe
   dla lancuchow encji (&(?:[#a-zA-Z0-9]+;)+), sprawdzana przed galezia
   tekstowa.

2. Test Pythona asertowal 'lang=' na calej stronie, co przechodzilo
   nawet gdyby znacznik trafil tylko do widocznego linku, a nie do
   data-records. Test teraz parsuje HTML przez lxml, wyciaga wartosc
   atrybutu data-records i dekoduje ja jak json.loads (dokladnie tak,
   jak robi to JS przez JSON.parse po stronie klienta, odwracajac
   escapowanie znakow przez filtr escapejs), zeby dowiesc, ze znacznik
   jezyka dotarl do konkretnego atrybutu, na ktorym operuje
   wyszukiwarka.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rozszerzono o korektę sekcji "Rollout wektora 2" w spec 08-06: wyjątek
dla osieroconego dbtemplate browse/praca_tabela.html (migracja 0488).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recenzja Task 8 wykazala, ze wstawione korekty (wektor 2 wykonany,
martwy szablon usuniety) stoja obok oryginalnych, juz nieaktualnych
twierdzen w tym samym dokumencie - klasyczna sprzecznosc faktograficzna.

- Otwarte decyzje / Zakres 3.1.2: dopisano ze rekomendacja odroczenia
  wektora 2 nie zostala przyjeta.
- Ryzyka / 3.1.2 moze nie zamknac sie w tej iteracji: dopisano ze
  ryzyko sie nie zmaterializowalo.
- 3.1.2 Language of Parts: dopisano trzecia korekte przy zdaniu ktore
  wciaz przedstawialo domkniecie wektora 2 jako otwarte.
- Kolejnosc prac i zaleznosci, krok 3: dopisano ze krok objal tez
  wektor 2, a 2.1.4/2.5.7 zostaly odroczone, nie naprawione.
- Punkt wyjscia: dopisano ze user_navigation_autocomplete.html byl
  martwy i zostal usuniety - w zakresie zostaje jeden obraz, nie dwa.
- Poprawiono bledny numer linii browse/praca.html:54 -> :55 w obu
  dokumentach (08-05 i zrodlowym 08-06).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ione)

Re-recenzja Task 8 znalazla te sama klase sprzecznosci jak poprzednio,
tym razem dla 2.1.4 (skrot /) i 2.5.7 (graf powiazan) - oba swiadomie
odroczone w tej iteracji, ale trzy miejsca dokumentu 08-05 nadal
przedstawialy je jako naprawione albo zmierzajace do naprawy.

- Tabela "Przypisanie 55 kryteriow": nota pod tabela Funkcjonalnosc
  przy wierszach 2.1.4 i 2.5.7 ("naprawa stwierdzona" bylo faszywe);
  wiersz 3.1.2 zostawiony bez zmian - tam naprawa faktycznie zaszla.
- Naglowek sekcji "Naruszenia stwierdzone": korekta rozgraniczajaca
  naprawione (1.1.1, 3.1.2) od odroczonych (2.1.4, 2.5.7), plus dopiski
  przy samych podsekcjach 2.1.4 i 2.5.7 (jedyne bez adnotacji).
- "Otwarte decyzje" / Skrot / (2.1.4): korekta ze rekomendacja
  zawezenia do focusa nie zostala przyjeta, skrot dziala bez zmian.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Naprawiliśmy PODŚWIETLANIE w wyszukiwarce rekordów powiązanych, ale
FILTROWANIE zostało nietknięte — record.text.indexOf(fraza) na surowym
HTML dopasowywał KAŻDY rekord z <span lang="…"> do frazy "span"/"lang",
bez żadnego podświetlenia na liście (regresja tej gałęzi, publiczna
strona szczegółów).

- related-records-highlight.js: drugi eksport bppStripTags(html), ten
  sam tokenizer co podświetlanie, zwraca tekst bez znaczników z
  zdekodowanymi encjami (&amp; &lt; &gt; &quot; &#39; + numeryczne).
- praca_tabela_mono.html: filtr dopasowuje teraz
  bppStripTags(record.text), nie surowy record.text.
- Efekt uboczny na plus: znika też zastany przypadek frazy przeciętej
  znacznikiem (dawniej niedopasowywanej przez indexOf na HTML-u).
- tests/js: 26 testów (było 17) — bppStripTags, filtr end-to-end, test
  kontrastowy dla frazy przeciętej znacznikiem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-M6, M10)

Reszta znalezisk z całościowej recenzji gałęzi (poza I1, osobny commit):

I2 — testy okruszka nawigacyjnego celowały w cały res.content, nie w
węzeł okruszka; przechodziły niezależnie od tego, czy filtr lang tam
w ogóle działał. Teraz lxml.html + xpath na //li[@Class="current"].

I3 — dopisano wpis do wykazu odroczonych niezgodności (oba dokumenty
spec) o override'ach opis_bibliograficzny.html w dbtemplates: loader
dbtemplates stoi przed plikowym, admin zachęca do edycji tego
konkretnego szablonu, a migracja 0488 czyści wyłącznie
browse/praca_tabela.html. Plus zdanie w newsfragmencie.

M1 — usunięto martwe (post-0488) drop_dbtemplate() i nieprawdziwy
komentarz w test_lang_opis.py.

M2 — util.py: dopisano .replace(".</span></b>[", ".</span></b> [")
obok istniejącego wariantu bez <span>; poprawiono komentarz testu,
który opisywał nieistniejące twierdzenie.

M3 — usunięto martwy lokalny import format_html w autor_nazwa()
(templatetags/prace.py) — modułowy import już to pokrywa.

M4 — dopisano test na action_views._get_ids_matching_title
(ten sam wzorzec fraza-w-HTML co list_views, dotąd bez testu).

M5 — dopisano asercję, że related-records-highlight.js jest
faktycznie podpięty do strony (script src).

M6 — test_mono_bez_atrybutu_gdy_kod_pusty: dodano asercję
wykluczającą gołe <span> bez atrybutu lang.

M10 — dopisano w spec 2026-08-05 wzmiankę o poprawce lang="en" ->
lang="pl" w 504.html przy korekcie 1.1.1 (kryterium 3.1.1, poza
pierwotną inwentaryzacją).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines +65 to +76
return html.replace(
TAG_LUB_ENCJA_LUB_TEKST,
function (_, znacznik, encja, tekst) {
if (znacznik || encja) {
return znacznik || encja;
}
return tekst.replace(
regex,
'<mark class="bpp-highlight">$1</mark>'
);
}
);
Comment on lines +135 to +145
html.replace(TAG_LUB_ENCJA_LUB_TEKST, function (_, znacznik, encja, tekst) {
if (znacznik) {
return "";
}
if (encja) {
segmenty.push(decodeEncje(encja));
return "";
}
segmenty.push(tekst);
return "";
});
mpasternak and others added 3 commits August 7, 2026 13:23
…ds-highlight.js

Alerty CodeQL js/incomplete-multi-character-sanitization (155, 156) w
bppHighlightOutsideTags i bppStripTags przeanalizowane i uznane za
falszywie dodatnie — pelne uzasadnienie w
.superpowers/sdd/2026-08-06-wcag-naprawy-stwierdzone/codeql-155-156-report.md
(nietrackowane, .gitignore).

- bppHighlightOutsideTags: jedyne nowe '<' w wyniku to staly literal
  <mark>/</mark>; znaczniki/encje wejscia przechodza bez zmian, wiec
  <script> nie moze powstac inaczej niz z wejscia (a wejscie jest juz
  przepuszczone przez nh3.clean po stronie serwera).
- bppStripTags: dekodowanie MOZE odtworzyc tekst '<script>' z podwojnie
  zescape'owanej encji (test to potwierdza), ale wynik ma jedyne miejsce
  uzycia w calym repo (praca_tabela_mono.html:1185, .indexOf() w filtrze)
  — nigdy nie trafia do DOM.

Dopisano komentarze przy obu funkcjach + 5 testow vitest dokumentujacych
oba zalozenia bezpieczenstwa. Logika funkcji bez zmian.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants