Skip to content

Changelog

Format: sekcja Unreleased na bieżące zmiany; przy wydaniu zamieniana na ## X.Y.Z - RRRR-MM-DD.

Unreleased

  • Cennik v4 2026-10-v4 od 2026-10-15 dla wszystkich planów (#484, ADR 0031): nowe ceny (pełny audyt $0.065, Premium $0.070, Lite $0.020, Spam/Quality $0.030, pobranie $0.0015, render $0.0060, Intent Basic $0.009, Premium $0.075), nowa pozycja stealth $0.0125 (tylko na żądanie, zamiast renderu; credits_per_page(stealth=True)); strona www, docs/site, legal billing 2026-10-15.1 (przykład ceny). v3 w docs/pricing/v3/.
  • evidence/report/audits (#482): dowody z kodu dla trafień (Evidence.facts: linki z anchorem/href/rel, ukryte fragmenty, skrypty; facts_missing) liczone zawsze i bez kosztu; krok 2 (EVIDENCE_PROMPT_VER e2) dostaje fakty i pisze FIX: w report_lang (Evidence.fix, fix_lang); raport pokazuje „Co zrobić na tej stronie” (RuleView.action, ActionPage.action) i poziom + siłę sygnału 0-100 zamiast 0.995/„probability” (także noty werdyktu i disclaimery). Kontrakt: nowe pola Evidence, Fact, RuleView.level/level_label/signal/facts*/model_fix*/action*, ActionPage.action; bez nowych zależności.
  • costs, ingest (#485): przed betą - budżet bety 300 USD/mies. (preset prod, .env staging i prod), limit max_cost_usd_per_page spider.cloud 0,005 liczony od sumy prób u dostawcy na stronę, cost_ledger.fetch_provider i engine (migracja 0033, expand); kontrakt: CostEntry.fetch_provider, CostEntry.engine, core.meter.add_fetch(..., provider).
  • Kontrakt API (#479): opcjonalne report_lang (en|pl, inne = 422) w POST /v1/judge, /v1/judge/batch, /v1/jobs, /v1/audits/site, /v1/intent, /v1/intent/batch(/csv); brak = Accept-Language, inaczej en. Nowe pola wyjścia: JudgeReport.report_lang, AuditRead.report_lang, IntentSummary.report_lang. Zapisany język decyduje o raporcie, eksporcie HTML/PDF i share (?lang= eksportów tylko dla starych raportów); panel: pole „Język raportu” w formularzach. Migracja 0032 (intent_reports.report_lang, expand). Lista języków: core/langs.py.
  • Kontrakt API (#400): POST /v1/auth/register zawsze 202 RegisterAccepted{status: pending, message} (było 201 UserRead/400 registration_failed); konto aktywne po linku z e-maila (POST /v1/auth/confirm, panel /confirm/{token}), istniejący adres dostaje mail informacyjny; zaproszenie aktywuje rejestrację oczekującą.
  • audits: raport witryny (panel, share, HTML/PDF) - sekcja „Najważniejsze do zrobienia” (reguły wg liczby stron i wagi, „Co zrobić” z katalogu, lista stron), „Co zrobić” pod każdym trafieniem, klikalne URL-e stron (nofollow/noopener); kontrakt: AuditSummary.actions (ActionItem, ActionPage) (#473).
  • docs: wewnętrzna wiki docs/wiki - indeks z kodu bez LLM (scripts/wiki.sh, --release vX.Y.Z = migawka + CHANGELOG wiki), strażnik w deploy/docs.sh, pierwsza migawka v0.1.7; bez nowych zależności (#459).
  • Dokumentacja (#450): audyt i porządki docs (PR #451-#456, #460, #461), runbook DR z kopią T7, neutralne opisy rynku w publicznym OpenAPI (#462).

0.1.7 - 2026-10-15

Rozliczenia (recenzje Codexa r1-r5, werdykt r5: WYDAĆ): #411-#418, #427-#431, #437-#439, #443-#444. Migracje 0029 (settled_at), 0030 (jobs.org_webhook_queued_at), 0031 (events.once_key) - tylko expand.

  • analytics, storage: emit(once=True) deduplikowane atomowo także między procesami - kolumna events.once_key z unikalnym indeksem (migracja 0031) i INSERT ON CONFLICT DO NOTHING (#444).
  • worker, jobs, storage: finalizacja, scrub i webhook orgu w osobnych świeżych sesjach (błąd SQL finalizacji nie blokuje scrub ani webhooka); trwały znacznik jobs.org_webhook_queued_at (migracja 0030) - webhook orgu wysyłany, gdy NULL, także przy ponowieniu; at-least-once z WebhookEvent.id (job_…:job.finished) do deduplikacji (#443).
  • analytics: emit(..., once=True) zapisuje najwyżej jeden wiersz na (event, request_id); worker używa go dla audit.finished/failed, więc ponowienie rozliczenia nie duplikuje zdarzeń (#439).
  • intent: unsettled_refs sprawdza „cena wymaga korekty” w SQL przed LIMIT 200 - ponad 200 rozliczonych fraz nie blokuje już odkrycia należnego zwrotu (#437).
  • worker, jobs: błąd rozliczenia zadania nie blokuje już scrub prywatnej treści ani webhooków orgu (wyjątek wraca po nich, settled_at zostaje NULL); kontrakt OnDone dostaje retry - ponowienie nie wysyła drugi raz webhooka orgu (#438).
  • audits/tiers (kilka feature'ów): Intent w jobs/audycie witryny i dopłatach rozliczany wg faktycznej płatnej pracy - per unikalna fraza max intent.charged_credits z udanych stron, nie więcej niż rezerwacja; różnica wraca z holda (#431).
  • intent: sweeper odkrywa też holdy z frazami done, których cena jest wyższa niż faktyczny cache (proces padł przed settle), i zwraca różnicę; cena wiersza pozostaje trwałym znacznikiem rozliczenia (#430).
  • tiers: sweeper dopłat przejmuje przebieg warunkowym UPDATE (status, stary heartbeat, niezmienione used), więc wyścig z zapisem postępu workera nie zwraca kwoty z nieaktualnego used (#429).
  • audits: błąd zwrotu fraz (refund_failed_intents) w finalize_job_audits propaguje zamiast być połykany - zadanie nie dostaje settled_at przy aktywnym holdzie intent:<job> i sweep je ponowi; błąd zapisu skrótu nadal niekrytyczny (#428).
  • storage: backfill migracji 0029 nie oznacza jako rozliczonego zadania z osieroconym holdem intent:<id> (wcześniej sprawdzał tylko job:<id>); 0029 nie była wdrożona, poprawka w samej migracji (#427).
  • api/audits (kilka feature'ów): POST /v1/jobs z targets wycenia i rezerwuje frazy (hold intent:<job>) wspólną funkcją audits.reserve_intents, jak audyt witryny; mapa fraz w audycie zadania, zwrot nieudanych w finalize; zły rynek frazy = 422 invalid_target (#414).

  • intent/api/panel (kilka feature'ów): Intent rozliczany wg faktycznego stanu cache po pracy, nie więcej niż zarezerwowano - narzędzie (wiersz done zapisuje faktyczny cache, settle zwraca różnicę i obniża price) i pojedyncza ocena (intent.charged_credits) (#413).

  • jobs/tiers (kontrakt bazy, migracja 0029): jobs.settled_at i upgrade_runs.settled_at - trwały stan rozliczenia holda; zakończone bez rozliczenia (awaria zwrotu, restart) ponawia run_job/jobs.sweep_unsettled i tiers.sweep_stale_runs (cron workera co 10 min); nowe jobs.sweep_unsettled(on_done) (#418).

  • judge/pipeline/api (zmiana kontraktu, kilka feature'ów): zero udanych reguł = błąd strony - JudgeReport.scored, pipeline.analyze rzuca JudgeFailed, API 502 inference_failed; pojedyncza ocena, batch, jobs i panel zwracają rezerwację (free: error) (#412).

  • intent: hold wsadu rozliczany raz z wszystkich pozycji, gdy żadna nie jest w toku (settle.py); sweeper nie zwalnia już wspólnego holda po części fraz, a nierozliczone zwroty (failed z ceną > 0) ponawia (#417).

  • intent: samodzielne narzędzie Intent liczy koszt każdej frazy w metering() i zapisuje go record_cost (dzienny/miesięczny limit widzi SERP i etykietowanie); limit sprawdzany przed każdą frazą wsadu (#416).

  • tiers: price() stosuje limit min(Σ trybów, pełny audyt) także dla dwóch kategorii (Spam+Quality Basic $0.0450 zamiast $0.0500), spójnie z usage.credits_per_page; dopłaty liczą deltę od tej ceny; pricing_check sprawdza pary trybów (#411).

  • intent: koszt DataForSEO trafia do cost_ledger z pola cost odpowiedzi task_post/live (add_fetch(cost)); polling nie jest liczony jako pobranie (#415).

0.1.6 - 2026-10-14

  • inference: redact_text maskuje URL, którego urlsplit nie parsuje (np. nazwa projektu http://[), zamiast rzucać ValueError (500 przy liście projektów) (#397).
  • core/rawstore: backend local przy błędzie zapisu obcina segment do rozmiaru sprzed próby - ponowienie nie zostawia uszkodzonego członu gzip (#398).
  • storage: upgrade_db trzyma jedną blokadę procesu i pamięta zmigrowane URL-e; równoległa inicjalizacja storage.get_engine() i accounts.get_sessionmaker() nie uruchamia dwóch migracji alembica (#399).

  • API (zmiana kontraktu): błędy /v1/auth/* i PATCH /v1/users/me mają stabilne kody (registration_failed, email_unavailable, invalid_password, invalid_credentials, account_not_verified, invalid_token) i komunikaty Ani EN/PL zamiast ErrorCode.*; zajęty e-mail przy rejestracji nie zdradza istnienia konta; panel przy nieudanej dopłacie nie pokazuje treści wyjątku (#387).

  • storage: get_engine() tworzy silnik i migruje SQLite pod blokadą wątków (zamiast lru_cache); pierwsze zadanie workera nie gubi już strony z KeyError: 'script' (#389).
  • panel/platform: przycisk „Send request” w playgroundzie blokowany na czas żądania (hx-disabled-elt + hx-disinherit, jak #358) - podwójny klik nie nalicza dwóch opłat; przypadek w tests/e2e/test_submit_lock.py (#385).
  • API (zmiana kontraktu): GET /v1/rules wymaga logowania lub klucza z zakresem judge; zwykły użytkownik dostaje RuleBrief (id, tytuł, warstwa, tryb), pełna treść reguł tylko superadmin; katalog reguł w dokumentacji bez pytań do modelu (#386).
  • accounts: limit prób logowania, rejestracji i resetu hasła (per IP i per e-mail, 10/min, narastająca blokada do 1 h, Redis lub pamięć), 429 rate_limited z komunikatem Ani EN/PL; panel pokazuje go przy logowaniu; ustawienia GSPAMJUDGE_AUTH_RATE_LIMIT, GSPAMJUDGE_AUTH_RATE_BLOCK_MAX (#68).
  • tests/e2e (#303): testy E2E przebiegu testera bety (zaproszenie, logowanie, saldo USD, ocena strony, audyt witryny, eksporty, zwrot za nieudane strony) i blokady przycisków wysyłki (#358); lokalny stos bez Dockera, GSJ_E2E=1, Playwright przez uv run --with (bez nowej zależności).
  • security (#302): lokalne testy bezpieczeństwa; inference.redact_text maskuje URL-e dostawcy z base_url profili (błąd 401 dostawcy ujawniał host w error reguły), core.netguard.resolve odrzuca adresy NAT64/IPv4-mapped z prywatnym IPv4.
  • core/rawstore: ponawianie zapisu segmentu z backoffem dla 5xx/429 i błędów sieci (bez retry dla innych 4xx), log z kodem statusu, licznik rawstore.stats(); nieudane segmenty nie trafiają do indeksu; bez nowej zależności (#307).
  • docs/design/VOICE.md: przewodnik głosu Ani (zasady, słownik, wzorce komunikatów, przed/po); panel: status „upstream”, błąd stron w raporcie witryny i pusty stan stron audytu w głosie Ani, bez wzmianki o dostawcy i modelu (#349).
  • www/pricing: strona cennika nie wspomina już o tańszym batchu 24 h (flaga batch_public w docs/pricing/pricing.yaml) (#372).

0.1.5 - 2026-10-14

Wydanie produkcyjne (#369, ADR 0029); obejmuje też zmiany z tagów v0.1.1-v0.1.4 wydanych bez osobnych sekcji.

  • panel (#358): formularze z hx-disabled-elt="find button…" (intent, nowe sprawdzenie, kreator audytu, dopłaty, feedback) mają hx-disinherit="hx-disabled-elt" - wewnętrzne żądania HTMX (np. świeżość wyników w /intent) nie dziedziczą selektora, który dla nich nic nie znajdował; przycisk wysyłki jest blokowany podczas żądania (bez podwójnego zlecenia).
  • status (#365): adapter telemetrii http-health dla API rozliczanego per token (GET health_url z profilu, 2xx = gotowy, błąd = niedostępny; bez workerów i strażnika); profil openrouter-gemma4 z blokiem telemetry; na serwerach GSPAMJUDGE_ENGINE_PROFILE puste, więc kontrolka śledzi profil, który faktycznie liczy oceny (staging pokazywał „Degraded” od wstrzymanego endpointu). Bez nowych zależności.
  • legal (#360): informacja o cookies 2026-10-14.2 bez wzmianek o CDN (statyki panelu i strony z własnego serwera, #331), nowy wiersz gsj_notices (zamknięte komunikaty o stanie usługi, 30 dni); polityka rozliczeń 2026-10-14.2 z przykładem ceny wg cennika v3 ($0.0250 za stronę, Spam); zgody RODO w formularzach strony (beta, strona główna, kontakt) wskazują administratora IBB MEDIA ROMAN ROZENBERGER (NIP 9291796680) zamiast marki Zwinnie; strona przegenerowana sync_content.py. Zatwierdzone przez właściciela 2026-10-14.
  • a11y (#337): poprawki WCAG 2.1 AA z audytu - tokeny panelu --gsj-link (jasny #0052ef, ciemny #55beff: hover linków, menu, obrys fokusu przez --gsj-focus), --gsj-field-line (ramki pól #8a8a8a/#797979), ciemny --gsj-faint #8a8a8a; nieaktywne radio-taby w --gsj-muted bez opacity; przełącznik języka aria-current zamiast aria-pressed, przełącznik motywu z aria-pressed; radio-taby w role=radiogroup (logowanie, nowe sprawdzenie, audyt); <main> w logowaniu/rejestracji, aria-invalid po błędnym logowaniu; lang w /docs i /redoc, 404 HTML z <title> dla przeglądarki; SQLAdmin: kontrast Logout; www (Grav) ciemny: --link dla nav/hover/FAQ/fokusu, --faint #8a8a8a, --field-line dla pól.
  • docs (#363): dokumentacja użytkownika i beta testera (EN/PL) opublikowana na https://docs.askaniela.com - statycznie przez Caddy edge na VPS (scripts/deploy/docs.sh), DNS A/AAAA w Bunny DNS; indeksowane start + user/, reszta noindex; Inter i Scalar 1.74.0 serwowane z własnego hosta (docs/site/assets/, bez Google Fonts i jsDelivr), kolory z docs/design/STYLES.md; usunięta wzmianka o logprobs ze strony startowej i Concepts.
  • intent (#346, zmiana kontraktu): poziomy Intent basic (dawny lite, „Ani's quick look / Ani zerknie”, $0.0070), premium (dawny Full, „Ani takes a closer look”, intent_full, wkrótce), compare („Ani compares your page”, cena przy starcie, wkrótce); lite/full przyjmowane jako aliasy, inne wartości 422 validation_error, niedostępny poziom 422 tier_unavailable, compare_url tylko z tier=compare; stare raporty lite zwracane jako basic. Nowe pola freshness (auto - wyniki nie starsze niż max_age_days, domyślnie 3 dni, GSPAMJUDGE_INTENT_REUSE_DAYS; fresh - zawsze nowe, pełna cena) w /v1/intent, /batch, /batch/csv; IntentSummary.snapshot_at; IntentTier (/v1/tiers): id basic/premium/compare + name_pl, label, summary, summary_pl; PriceItem.name/label. Panel: karty poziomów (radio_card z tag/meta/disabled), wybór świeżości z podglądem („Ani has results from 2 days ago - $0.0015”), kolumna „Results from”, lista wcześniejszych wyników frazy w raporcie; docs EN/PL, wzór nazw w docs/design/brand.md.
  • panel (#331): CSS budowany przy buildzie zamiast Tailwind browser runtime + daisyUI z CDN - scripts/build_css.sh (Tailwind CLI standalone 4.1.11 + plugin daisyUI 5.0.43, sha256 przypięte, etap css w Dockerfile) z modułów panel/css/ (tokens, base, components, report, public) do jednego /panel/static/app.css; htmx 2.0.4, json-enc 2.0.1 i Inter (v20, OFL) z własnych statyków, htmx z defer; linki ?v=<sha> + Cache-Control: public, max-age=31536000, immutable (także /report.css, /audit.css, /intent.css); CSP /share/* tylko self. Nowa zależność budowania (binarka Tailwind + daisyui.mjs, bez Node): zero obcych originów i ~110 KB mniej blokującego JS na każdej stronie. theme.css pusty (treść w panel/css/).
  • website (#350): CTA bety „Ask Ani” / „Zapytaj Ani”, przyciski formularza „Ask Ani for an invite” / „Poproś Ani o zaproszenie”; Ani z dymkiem w hero strony głównej; CSS/JS z ?v=<hash> w Gravie (funkcja Twig asset_v, cache 30 dni nie trzyma starych stylów); poprawione autocomplete/maxlength w formularzu (bez dosłownych cudzysłowów).
  • panel/audits (#345, rozszerzenie kontraktu, zgodne wstecz): kreator /audits/new w 4 krokach prostym językiem EN/PL - źródło (sitemap URL/indeks, wklejony XML sitemapy, wklejone adresy, plik TXT/CSV/ZIP), wybór stron (sekcje z liczbami, zaznacz wszystkie/żadne, filtry „zaczyna się od / zawiera / wzorzec”, typy stron, zrównoważona/losowa próba albo wszystkie, limit, lista pasujących stron na żywo), tryby Spam/Quality/Helpful z cenami v3, podsumowanie z ceną, środkami i potwierdzeniem; SiteSource.sitemap_xml, Scope.sections, SitePreview.sections, select_site/Selection, rule_of; bez nowych zależności.
  • status/panel/console/costs/inference (#342, nowy kontrakt): feature status - telemetria silnika analizy co 60 s (cron arq, adapter queue-serverless wybierany blokiem telemetry profilu, adresy i id endpointu z env; tylko bezpłatne odczyty health/endpoint/billing) w tabeli engine_samples (migracja 0028, także announcements, status_marks), alerty bezczynnych workerów i kosztu/h (costs.send_alert: webhook/e-mail), strażnik maks. workerów 0 (GSPAMJUDGE_ENGINE_GUARD, preset staging: włączony), automatyczny komunikat po > 5 min niedostępności; publiczny GET /v1/status (up/warming/busy/degraded, komunikat w stylu Ani EN/PL, eta_s, aktywne komunikaty, cache 5 s) i nagłówek X-Service-Notice; panel: kontrolka stanu w pasku i baner komunikatów (zamykany, zakres per moduł); konsola /console/engine (wykresy 24 h, pauza/wznowienie) i /console/announcements (CRUD, szablony Ani); Profile.telemetry; CLI gspamjudge engine-status; komponenty §32-34 (status pill, announcement banner, bar chart); docs użytkownika „Service status” EN/PL. Bez nowych zależności.
  • usage/tiers/panel/website/docs (#212, zmiana cennika i kontraktu): cennik 2026-10-v3 - cost = planowany koszt rzeczywisty (max referencji Vertex, trasy per token, RunPod wsad od progu), reference_cost informacyjnie; ceny tryby audytu Spam $0.025 / Quality $0.025 / Helpful $0.015 za stronę (nowe pozycje mode_*), pełny audyt (3 tryby, judge_basic) $0.045 zamiast sumy $0.065, kilka trybów = min(suma, pełny); Lite $0.015, Premium $0.05, pobranie $0.001, Intent Full $0.065, porównanie URL $0.0015 (render i Intent Lite bez zmian); kolumny pakietów/doładowań z mnożników planów, podłoga ×2,0 sprawdzona na każdym poziomie; karty trybów i podsumowanie w panelu pokazują rabat pakietu; scripts/pricing_check.py (yaml == aplikacja == www == docs); strona /pricing: „Questions about invoices or volume pricing? Contact us.” (/contact-sales) zamiast kontaktu przez Zwinnie, tak samo na stronie bety.
  • infra (#319): askaniela.com przełączona na Grav na VPS (DNS apex/www .com i .pl -> VPS, Let's Encrypt w Caddy); www -> apex, askaniela.pl -> /pl, CSP z hashem skryptu motywu, 301 ze starych adresów eksportu (.html, /index.html, /home); rollback w docs/infra/website.md.
  • rules/judge/panel/api/report (#334, zmiana kontraktu): audyty w trzech trybach Spam / Quality / Helpful content zamiast listy reguł - każda reguła ma jawny mode w YAML (alias Rule.category, poza promptem, bez podbicia ver; C4-PURPOSE helpful -> quality, konteksty = Quality); /judge/new i kreator audytu: karty trybów (checkboxy, wszystkie = pełny audyt) z opisem jednym zdaniem i ceną za stronę z cennika (obecna logika credits_per_page: Spam/Quality/pełny $0.0378, Helpful $0.0115); presety, polityki z kodami SQRG, id reguł i intencja frazy tylko dla superadmina (follow-up intencji #340); API: modes: [spam|quality|helpful] w /v1/judge, /v1/judge/batch, /v1/jobs, /v1/audits/site (alias categories), zgodne z rules/presetami, OpenAPI zaktualizowane; raport HTML/PDF/share i raport witryny: sekcje Spam / Quality / Helpful content z opisem, tylko uruchomione tryby (ReportView.modes); makro check_card; docs użytkownika EN/PL.
  • website (#328): baner udostępniania (og:image/twitter:image) i nagłówek coming-soon mówią „Beta starts November 17, 2026” / „Beta startuje 17 listopada 2026”; nowe pliki og-image-{en,pl}-beta1117.png (odświeżenie cache Facebook/X), alt OG zaktualizowany.
  • infra (#326): strefy DNS askaniela.com i askaniela.pl odtworzone w Hetzner DNS (rekordy 1:1 z Bunny, apex/www jako A/AAAA na VPS), runbook przełączenia NS, rollback, DNSSEC i zamknięcia Bunny docs/infra/dns-hetzner.md.

  • fix (#335): gspamjudge serve ufa X-Forwarded-Proto/For/Host tylko od GSPAMJUDGE_TRUSTED_PROXIES (preset: Caddy w sieci edge) - /admin i linki z request.base_url po HTTPS zamiast http:// (mixed content, admin bez stylów).

  • infra (#332): CI nie spowalnia prod - ci.slice (CPUWeight 20, 1 vCPU, 2G) dla runnerów Forgejo, kontenerów jobów (--cpus=1 --memory=1g --cpu-shares=256) i buildów (--cgroup-parent ci.slice); staging cpu_shares 256 i limity CPU, status/observability małe limity CPU; scripts/deploy/cleanup.sh po deploy i co noc (timer); zasada: agenci nie budują obrazów lokalnie; ADR 0030 (proponowany): runner poza serwerem prod.
  • docs (#320): sekcja „User guide” w MkDocs (docs/site/user/, EN + PL): przewodnik beta testera, moduły, konto i projekty, raporty i linki udostępniania, klucze API, saldo i cennik, szybki start API; strony z noindex do akceptacji właściciela.
  • deploy (#319): strona www na Grav na VPS - deploy/compose.www.yml (obraz jak lokalnie, 256m / 0,5 CPU), scripts/deploy/www.sh (sync user/ z commita, Sendy id, kopia do S3), Caddy www-staging.askaniela.com (basic auth, noindex, admin/API 404), sieć askaniela-www-edge; runbook przełączenia docs/infra/website.md. askaniela.com bez zmian (Bunny).
  • website (#327): strona główna Grav (EN/PL) z formularzem zapisu na betę w hero i w sekcji końcowej (ten sam flow Sendy co /beta: double opt-in, zgoda RODO, honeypot, /beta/check-email), terminy: zaproszeni od 2026-10-20, zapisani stopniowo od 2026-11-17; strony prawne zsynchronizowane z wersjami zaakceptowanymi 2026-10-14 (aup, beta-terms, balance-refunds, cookies, dpa, disclaimer: draft: false).
  • api/panel/report (#314, zmiana kontraktu): użytkownik nie wybiera ani nie widzi profilu/modelu - formularze panelu (Run check, kreator audytu) bez wyboru profilu i bez ostrzeżenia „Some profiles are not calibrated”; serwer bierze GSPAMJUDGE_PROFILE; profile w /v1/judge|judge/batch|jobs|audits/site tylko dla superadmina, innym ignorowany po cichu i usunięty z OpenAPI; odpowiedzi JSON (RedactedJSONResponse, inference.redact) bez profile.name/model/quant, ReportSummary.profile, FeedbackRead.profile, kluczy provider; nazwy z profiles/*.yaml w błędach -> [internal]; raport HTML/PDF/share: „Method ” zamiast profilu, bez ostrzeżenia o nieskalibrowanym profilu; docs-site i openapi.json zaktualizowane.
  • intent (#315): rynek = kraj + język z list DataForSEO (features/intent/data/markets.json, odświeżanie scripts/refresh_markets.py); panel: dwa pola z wyszukiwaniem (komponent search_select, pamięta ostatni wybór) w nowej kontroli, Search Intent i playgroundzie; formularz Intent pokazuje tylko pole wybranego trybu; API: market walidowany wobec list (422), nowe pola location_code + language_code (JudgeRequest, IntentRequest/IntentItem, UpgradeRequest); DataForSEO obsługuje wszystkie kraje z listy.
  • infra (#301): zaszyfrowana kopia .secret/ w secrets/ (SOPS + age, klucz w Passbolt), skrypty scripts/secrets/, strażnik jawnych sekretów w CI. Nowe narzędzia lokalne: sops, age (standard szyfrowania plików w git, bez serwera).

  • legal (#311): aup, beta-terms, billing, cookies, dpa, disclaimer bez oznaczenia Draft (draft: false, wersja i data 2026-10-14, treść bez zmian; zaakceptowane przez właściciela jako robocze na betę). Nowa wersja beta-terms = ponowna akceptacja tam, gdzie jest wymagana.

  • docs (#300): HANDOFF na 2026-10-14 (infra, RunPod, blokery właściciela), stan prod i RunPod w docs/infra, prefiks gałęzi infra/.
  • deploy (#295): agenci obserwowalności wdrożeni na askaniela-app-1 (NetBird 100.122.108.8, Beszel up, Dozzle remote agent + pełny Dozzle na VPS 64 MB z simple auth); fix bindu agenta Dozzle (DOZZLE_AGENT_ADDR).
  • deploy (#293): prod uruchomiony (v0.1.0, cap 2 USD/dobę); hosty app/api/platform: X-Robots-Tag noindex, nofollow, noarchive, robots.txt Disallow: /, blokada botów po IP Google i User-Agent (scripts/deploy/google_ranges.py, deploy/caddy/); edge.sh kopiuje deploy/caddy/*.caddy.
  • deploy (#295): deploy/compose.observability.yml - agenci Beszel i Dozzle (128 MB, bind tylko na IP NetBird) dla huba na dgx-p2p; runbook docs/infra/observability.md. Wdrożenie czeka na NetBird setup key.
  • deploy (#293): Hetzner Object Storage - raw store staging/prod (nbg1), kopie off-site askaniela-backups (hel1, prefiks per środowisko), reguły cyklu życia; sekrety Actions DEPLOY_/STAGING_ACCESS_TOKEN/BUNNY_ ustawione; prod wstrzymany (szkice prawne).
  • status (#294): status.askaniela.com - Uptime Kuma 2 na askaniela-app-1 (deploy/compose.status.yml, sieć askaniela-status-edge, blok w Caddyfile, panel tylko przez tunel), docker-socket-proxy (tylko odczyt kontenerów zamiast gniazda Dockera), scripts/deploy/status.sh + status_setup.py (uptime-kuma-api uruchamiane przez uv run --with, nie zależność projektu; obejście niezgodności z Kuma 2).
  • deploy (#291): pierwsze wdrożenie - edge Caddy (LE, 6 hostów, 503 maintenance dla prod), staging 6/6, runner Forgejo (ci/docker/docker-host); BACKUP_LOCAL_ONLY=1 (tylko staging), S3 backupu walidowane w entrypoint, STAGING_BASIC_HASH jako base64, dc() bierze bieżący tag stosu, blok .pl/.ai wyłączony.
  • docs/env (#285): .env.example - sekcje Hetzner Cloud, Object Storage, RunPod, Bunny, Sendy, Umami, Forgejo Actions, Stripe, SMTP, DataForSEO z instrukcją „skąd wziąć”; tabela w docs/infra/secrets.md.
  • website (#282): tło hero coming-soon - desktop tylko w prawej kolumnie za formularzem (maska ku tekstowi, akcenty ptaszek/cudzysłów), mobile/tablet pasek kafelków nad nagłówkiem (56 px); SVG na <symbol>/<use> (mniejszy HTML).
  • website/emails, marketing-kit (#283): wspólna stopka maili (logo AskAniela + "made by" z logo Zwinnie, szara linia operatora, Privacy, Unsubscribe; PNG @2x w images/email/), szkice autoresponderów dzień 3 i 7; stopki www z logo Zwinnie (partials/made-by); build statyczny kopiuje obrazki maili.
  • website/emails (#280): mail powitalny - start od 20 października 2026 (było 19).
  • marketing (#172): marketing/ - media kit (boilerplates 25-250 słów EN/PL, fact sheet z facts.yaml, logo placeholder SVG/PNG + zasady, kolory/typografia, zrzuty z danymi demo, kontakt prasowy), marketing kit (persony, claimy, słownik, szablony LinkedIn/X/newsletter Sendy/ogłoszenie testów/cold e-mail/one-pager PDF/banery SVG), agents/ + skill marketing-kit; strona /media-kit (EN/PL, pojedyncze pliki do pobrania, bez ZIP) w buildzie produkcyjnym coming-soon i link „Media kit” w stopce; favicon.ico + favicon.svg w katalogu głównym dist (static_post.py). Bez nowych zależności (WeasyPrint/Playwright tylko jako narzędzia lokalne).
  • intent (#194, zmiana kontraktu): narzędzie Search Intent - pozycja w nawigacji panelu, /intent (fraza, lista albo CSV do 100 fraz, poziomy Lite/Basic/Premium z cennika, Compare my URL „wkrótce”, historia), raport (udziały intencji, zalecana forma, długość top wyników, elementy, pytania czytelników, data zrzutu wyników), eksport HTML/PDF/CSV, linki publiczne (ShareKind.intent), API POST /v1/intent (+/batch, /batch/csv), GET /v1/intent[/{id}], eksporty; tabela intent_reports i share_links.intent_report_id (migracja 0027), id itr_/itb_; cena intent_lite za frazę, zwrot za nieudane; DataForSEO tylko w trybie zadań (GSPAMJUDGE_SERP_MODE, _SERP_POLL_S, _SERP_TASK_TIMEOUT_S); GSPAMJUDGE_INTENT_LABELER=fixture (etykiety z fixtures upstream, bez modelu); IntentProfile + pytania i tematy (klucz profilu s2); review: intent_reports.started_at, warunkowe zmiany stanu (bez failed -> done), w projekcie prywatnym brak linków publicznych do raportu intencji (403 private_project), CHECK ck_share_links_one_target także w modelu, CSV > 1 MB = invalid_csv; migracja sprawdzona na Postgres 16.
  • website (#276): Open Graph + Twitter Card (summary_large_image, obrazki 1200x630 EN/PL z scripts/og_images.py, nadpisania og_* w front matter), JSON-LD Organization/WebSite/SoftwareApplication, apple-touch-icon; kontakt w polityce prywatności, regulaminach i stopce = e-mail operatora (bez formularza zwinnie.com, wersje dokumentów bez zmian), opis stron prawnych "Operator: IBB MEDIA Roman Rozenberger".
  • website (#273): daty zatwierdzone przez właściciela (zamknięte testy od 20 października, dostęp dla zapisanych stopniowo od 17 listopada 2026) w hero, sekcji bety i FAQ; HTML z Cache-Control: max-age=300 w _headers.
  • website (#273): coming-soon = pełna strona pre-launch (problem/rozwiązanie, 3 kroki, nadchodzące funkcje FAB ze statusami, persony, teaser przykładowego raportu, zamknięta beta, porównanie z chatbotem, FAQ, drugi zapis do newslettera), nawigacja kotwicami + menu mobilne bez JS; build dodaje ?v=<hash> do CSS/JS; SVG z fill/stroke=none na wypadek braku CSS; lead-form obsługuje wiele formularzy na stronie (with {lf: 'b'}).
  • Zatwierdzone przez właściciela (#261, #226, #255): privacy 2026-10-13.3 (zapis na betę: dane, cel, zgoda, double opt-in, wypisanie w każdym mailu, retencja; formularz kontaktu; archiwum odpowiedzi dostawców i zdarzenia produktu; podmioty Sendy na serwerze LH.pl (PL), Amazon SES eu-west-1, Bunny.net, Hetzner, Stripe później) - draft: false, ponowna akceptacja użytkowników; cookies i beta-terms nadal projekty (poza produkcją).
  • Dokumenty prawne (#261, zmiana kontraktu legal.PAGES + beta-terms): operator i administrator danych = IBB MEDIA Roman Rozenberger (Kargowska 12, 66-133 Klenica, NIP, REGON; „Zwinnie” tylko marka) w company.yaml, terms/privacy, stopce e-maili i strony; terms i privacy zatwierdzone przez właściciela (draft: false, bez zmiany wersji - korekta danych operatora); nowe projekty beta-terms i cookies 2026-10-13.2 (strona bez cookies, GA4/GTM tylko planowane i za zgodą); strona: stopka coming-soon linkuje tylko zatwierdzone dokumenty + linia operatora/administratora; produkcja nie publikuje projektów: build_static.sh --mode coming-soon buduje tylko strony draft: false i przerywa, gdy baner projektu trafi do dist/ (sitemap bez projektów).
  • Strona: rozbudowany przykładowy raport #sample na stronie głównej (zakładki bez JS: werdykt Spam/Quality/Helpful, trafienia z cytatem i action items, Page Quality, E-E-A-T, YMYL, intencja wyszukiwania, taksonomie, audyt witryny; dane w front matter home.{en,pl}.md; tylko pełna strona/staging) (#268).
  • Newsletter (zapisy) przez Sendy, nowa strona coming-soon i Bunny edge rules (#260, #259, zmiana kontraktów leads (LeadRead.sendy_status|sendy_at, SendyStatus, migracja 0026_leads_sendy), core (ustawienia sendy_url, sendy_api_key, sendy_list_beta - także bez prefiksu SENDY_* - i sendy_sync)): coming-soon z formularzem newslettera w hero (Ani z dymkiem, grafika SVG z tokenów, docs/design/brand-ani/COMING-SOON.md), formularz beta/newsletter wysyła od razu do Sendy /subscribe (site.yaml signup.target: sendy|app, fetch no-cors, stan „sprawdź skrzynkę”, CSP z originem Sendy, lista z SENDY_LIST_BETA przy buildzie), strony /beta/check-email i /beta/confirmed EN/PL (noindex), mail potwierdzający i powitalny w website/emails/ (szkice do akceptacji); app: leady beta ze zgodą -> Sendy API (poza dev), status w /console/leads; build_static.sh robi kopie x/index.html (czyste URL-e na Bunny), scripts/deploy/bunny_edge.py (nagłówki i przekierowania z _headers/_redirects jako edge rules, www/.pl -> apex, aliasy domen z certyfikatem); tytuł bez podwójnej marki. Bez nowych zależności.
  • Środowiska dev/staging/prod i pipeline wdrożeń (#250, ADR 0029, zmiana kontraktu core: GSPAMJUDGE_ENV = dev|staging|prod z presetem core/envpresets.py, nowe ustawienia api_host, site_url, docs_url, stripe_mode, auth_cookie_name (dev gsj_session, prod ani_session, staging ani_session_stg; accounts.COOKIE_NAME z ustawień); błąd startu: nieznane ENV, STRIPE_MODE=live poza prod, raw store s3 bez endpointu/bucketu poza dev): host api.askaniela.com tylko z /v1, /openapi.json, /docs, /redoc, /health (api/apihost.py); Dockerfile dwuetapowy (git tylko w builderze - naprawia build, kopiuje fetchers/, HEALTHCHECK, bez roota); docker-entrypoint.sh: GSPAMJUDGE_SKIP_MIGRATE=1 (stosy VPS migrują jednorazowym jobem w deploy.sh, rollback obrazu bez błędu alembica); deploy/ (stosy compose prod/staging z osobnymi katalogami na serwerze, Caddy z Let's Encrypt, basic auth / token staging fail-closed, 301 dla .pl/.ai, kopie pg_dump | age do Hetzner Object Storage z retencją), scripts/deploy/ (deploy, rollback, smoke, backup, restore, restore_test, edge, publish_static.py dla Bunny CDN), .forgejo/workflows/ (ci z migracjami na Postgres/SQLite, deploy-staging, deploy-prod z ręcznym uruchomieniem, static-bunny); docs/infra: DNS Bunny, runbook, restore, checklista właściciela. Dostawcy: Bunny.net (DNS, CDN strony i docs), Hetzner (VPS, Object Storage) zamiast Cloudflare. Bez nowych zależności Pythona (rclone i age tylko w obrazie backupu).
  • Fetchery w GSPAMJUDGE_ENV=staging (#254, zmiana kontraktu FetcherProfile.envs: list[Literal[dev,staging,prod]], domyślnie [dev, staging, prod]): staging używa dostawców jak prod (własne klucze, niski cap), kind: direct poza [dev] odrzucany przy walidacji, nieznane GSPAMJUDGE_ENV = ValueError zamiast pustego łańcucha.
  • Migracje działają na świeżym Postgresie 16 (#253, zmiana kontraktu legal: Acceptance.org_id i LegalAcceptanceRow.org_id int (pk organizations, poza JSON) zamiast UUID): poprawione 0016 (org_id Integer) i 0024 (unikalny indeks ix_leads_public_id), nowa 0025_legal_org_id_int dla starych baz SQLite; scripts/check_migrations.py (upgrade head + dryf model/migracja). Bez nowych zależności.
  • Strona www jako statyczny eksport na Cloudflare Pages i leady przez API (#247, nowy feature leads, zmiana kontraktów core (ustawienia leads_allowed_origins, leads_rate_per_hour, prefiks led), analytics (zdarzenia lead.created, lead.rejected), migracja 0024_leads): POST /v1/public/leads (beta|sales, honeypot, limit per IP, Origin z allowlisty), CORS tylko /v1/public/* (starlette CORSMiddleware), konsola /console/leads (filtr, CSV przez core.csvsafe, „zaproszony”); website/scripts/build_static.sh (--mode coming-soon domyślnie, --mode full) -> website/dist/ z _headers (CSP) i _redirects, strona „Coming soon” EN/PL, formularze fetch() bez cookies, moduły: Spam & Quality Check, Search Intent, Taxonomy Labeling; Grav bez lokalnego zapisu zgłoszeń. Bez nowych zależności (wget i curl tylko przy buildzie).
  • Dokumentacja infrastruktury docs/infra/ (#240): przegląd z diagramem, środowiska, usługi, dostawcy, sekrety (tylko nazwy), sieć/DNS, dane, deploy, RunPod, monitoring i koszty, runbooki; luki jako TODO z numerami issue. Bez zmian w kodzie.
  • Raw store i analityka produktu (#242, ADR 0028, nowy moduł core/rawstore i feature analytics, zmiana kontraktów core (ustawienia rawstore_*, analytics_enabled), inference (first_token(..., meta=)), migracja 0023_events_raw_objects): pełne odpowiedzi LLM (logprobs, usage, dostawca, czas, prompt_sha/prompt_ver, profil), DataForSEO (bez limitu czasu), metadane pobrań i treść stron (poza trybem prywatnym, retencja polityki prywatności) w JSONL.gz (runs/raw albo S3-kompatybilny R2/B2), indeks raw_objects; zdarzenia produktu events (rejestracja, zaproszenie, logowanie, judge/audit/intent, oferty i zakupy upgrade, zasilenie salda, limity, linki share, klucze API, playground) bez zewnętrznych trackerów; konsola /console/analytics (lejek, użycie per moduł, przychód vs koszt referencyjny vs rzeczywisty i marża per moduł/plan/org, top błędy, CSV). Nowa zależność httpx-auth (SigV4 dla S3 bez boto3, czysty Python nad httpx). runs/rawlog zostaje równolegle. Odpowiedzi evidence (cytaty) w module evidence z retencją treści stron; poza dev GSPAMJUDGE_RAWSTORE_BACKEND=local blokuje start (wymagany s3 albo off).
  • Copy: zwykły myślnik - zamiast pauzy/półpauzy (U+2014/U+2013) w tekstach dla użytkownika (#229): strona www, panel, raporty, katalogi i18n EN/PL (msgid+msgstr, 100% przetłumaczone), dokumenty prawne (tylko interpunkcja - zmienia sha256, wersje bez podbicia), docs/site, GTM/marketing, skille gsj-slides/gsj-video, pola dla ludzi w regułach (bez pól promptu, bez ver); scripts/ui_lint.py ostrzega na U+2014/U+2013 w szablonach i .po; zasada w docs/design/AGENTS.md i voice-tone; strona www: bez pozycji „Home” w nawigacji (logo prowadzi na główną).
  • Rozliczenia w USD zamiast kredytów i cennik ADR 0027 w becie (#221, zmiana kontraktów usage, audits, tiers, legal, API): ceny tylko z docs/pricing/pricing.yaml (features/usage/pricing.py, walidacja podłogi koszt×2,0 przy ładowaniu (GSPAMJUDGE_PRICING_FLOOR_MULTIPLIER), GSPAMJUDGE_PRICING_FILE|TIER, beta = Just Ask ×2,5; Lite $0.0115, Basic $0.0378, Premium $0.0412, pobranie $0.0006 tylko za faktycznie pobierane strony (wsad/zadania/audyty bez opłaty dla pozycji z HTML), render $0.0050, intent $0.0070/$0.0628, porównanie $0.0015, min. $0.001; opcjonalny mnożnik modułu modules.<moduł>.multiplier z podłogą ×2,0; cennik 2026-10-v2: podłoga ×2,0 w pliku, batch = max(payg × 0,5, koszt × 2,0), strona www przegenerowana (sync_content.py, legal billing pod adresem balance-refunds); dopisek do ADR 0027), wagi GSPAMJUDGE_CREDITS_WEIGHT_*/CREDIT_USD usunięte; wewnątrz int w jednostkach $0.0001 (core/money.py, migracja 0022 ×100), na zewnątrz tylko *_usd: GET /v1/usage unit: "usd", used_usd/limit_usd/remaining_usd, balance_usd, plan limit_month_usd; Estimate.cost_usd/cost_per_page_usd; dopłaty cost_usd, steps_usd, paid_usd, charged_usd, refunded_usd, reserved_usd, GET /v1/tiers price_usd (stare pola kredytowe usunięte); 429 quota_exceeded „Monthly limit of $20.00 reached…” z details.*_usd (EN/PL); dzienny limit użytkownika domyślnie wyłączony, GSPAMJUDGE_AUDITS_MAX_PAGES=200; panel, konsola (zasilenie salda w USD, POST …/balance), regulamin i „Billing & Refund Policy” (/legal/credits → 301 /legal/billing, accept_billing_policy, wersje dokumentów 2026-10-13.2), przewodnik guide/billing.md (zamiast credits.md), copy GTM; obraz Dockera zawiera docs/pricing. Bez nowych zależności.
  • Limity zamkniętej bety w USD (#220, zmiana kontraktów costs, usage, jobs, audits, API): globalny miesięczny budżet GSPAMJUDGE_BETA_MONTHLY_BUDGET_USD (0 = wył.) → 503 beta_budget_reached „Beta paused…” EN/PL, alerty 50/80/100% (log/webhook/e-mail); limit testera GSPAMJUDGE_BETA_TESTER_MONTH_USD=20 (centy wewnętrznie), superadmin ustawia limit orgu w USD w konsoli (quota.org_month w dzienniku); maks. stron audytu bety GSPAMJUDGE_BETA_AUDIT_MAX_PAGES=200, maks. aktywnych zadań/audytów na org GSPAMJUDGE_JOBS_MAX_ACTIVE_ORG=2; luki: panel audytu bez limitu kosztu, dopłata audytu bez limitu per strona — naprawione; FAQ guide/beta.md EN/PL. Bez nowych zależności.
  • Cennik pay-per-use po becie (#216, ADR 0027): saldo USD, stała cena za wywołanie (koszt referencyjny Vertex MaaS × PAYG 2,0 … pakiet $999+ 1,5, podłoga 1,5), batch −50%, cache i błędy $0; dane w docs/pricing/pricing.yaml, przewodnik guide/pricing.md EN/PL. Bez zmian w kodzie.
  • Portal API dla deweloperów (#218, nowe feature'y requestlog i webhooks, zmiana kontraktów core (callctx, ustawienia platform_host/app_host/auth_cookie_domain/requestlog_days), usage (kredyty do core.callctx), accounts (domena cookie sesji), API): wejście panel/platform pod /platform albo na własnym hoście (GSPAMJUDGE_PLATFORM_HOST, inne hosty → 308), wspólna sesja (GSPAMJUDGE_AUTH_COOKIE_DOMAIN, także cookie impersonacji — baner działa w portalu), przełącznik orgu/projektu; strony: przegląd, klucze API (/v1/keys: wszystkie typy właściciela, sekret raz, rotacja, allowlista IP, wygaśnięcie, odwołanie), zużycie (miesiąc vs przydział planu, saldo, dzień/endpoint/klucz), dziennik wywołań, limity, webhooki, dokumentacja (szybki start curl/Python/JS, Scalar 1.74.0 serwowany lokalnie z /panel/static z przypiętą sumą sha256, z /openapi.json), playground (wewnętrzne /v1/judge kluczem albo sesją, żądanie/JSON/koszt); wszystkie kwoty w USD. request_log (metadane każdego wywołania /v1 w orgu, bez treści i sekretów, retencja 30 dni w workerze), nagłówek X-Cost-USD (koszt netto wywołania), /v1/webhooks (punkty orgu, sekret whsec_ wyprowadzany z GSPAMJUDGE_WEBHOOK_SECRET, job.finished z workera jako osobne zadania arq deliver_webhook, test, log dostarczeń, ponowienie; rotacja sekretu per punkt — #225); cookie orgu i projektu z domeną auth_cookie_domain, wylogowanie czyści też stare host-only gsj_session; migracja 0021_platform; przewodnik guide/developer-portal.md. Bez nowych zależności.
  • Strona askaniela.com na Grav (#217): katalog website/ — motyw askaniela wg docs/design (tokeny STYLES.md, Inter self-hosted, bez cieni, jeden koralowy CTA, light/dark), strony EN (domyślnie) i PL: start (z tabelą „General AI chat vs AskAniela”), moduły, jak to działa, cennik, FAQ, o nas, zapis na zamkniętą betę i „Contact sales” (wtyczka form, zapis lokalny website/data/beta-signups), dokumenty prawne generowane z features.legal (website/scripts/sync_content.py; strona credits jako „Balance & Refund Policy”, treść → #226); bez know-how i bez słowa „credits” w copy; SEO: canonical, hreflang, sitemap.xml, robots, JSON-LD. Zmiana danych cennika docs/pricing/pricing.yaml (jedyne źródło): plany Just Ask ×2,5, Ani Sip $20, Ani Cup $100, Ani Teapot $200/mies. (stawki ×2,4; rocznie „2 months free”: płatność 10×, saldo 12×), doładowania ponad saldo topup_multiplier 2,4/2,2/2,0, Ani Tea House (Enterprise) od ×2,0, min. doładowanie $20 + opłata serwisowa 2,9% + $0,30; na stronę trafiają tylko ceny. Ikony planów (placeholder) docs/design/brand-ani/plans/. Nowa zależność (tylko strona www): obraz lscr.io/linuxserver/grav:2.2.6-ls286 — oficjalny getgrav/grav nie ma tagów dokładnych wersji.
  • Render JS do wyboru i dopłata za faktyczny render (#201, zmiana kontraktów ingest, projects, usage, judge, report, jobs, audits, API): ustawienie projektu render_js_default off (domyślnie) / on / auto (migracja 0019_project_render_js, zakładka projektu „Settings”), nadpisanie render_js (true/false/auto) w POST /v1/judge, pozycjach batch/jobs, audycie (js: true = on), kolumnie CSV render_js, checkboksie formularza i kreatora, CLI --render-js; +1 kr. tylko gdy rendered=true (rezerwacja szacunku w kredytach po 1, zwrot różnicy w settle; zadania przez JobCreate.billing + JobItemResult.rendered); Document.provider/rendered/js_shell → JudgeReport.fetch (poza promptem); tryb off + pusta powłoka JS = ostrzeżenie w raporcie i „+ Retry with rendering · 1 credit” (POST /panel/rerender/{id}). Bez nowych zależności.
  • Nazwa produktu AskAniela („Ani”) by Zwinnie (#206, ADR 0026): core.config product_name/product_short/product_tagline/maker + brand() (global brand w szablonach) w panelu, e-mailach, eksporcie HTML/PDF, share, OpenAPI title, admin, CLI; mkdocs.yml site_name/copyright, company.yaml SERVICE_NAME, gsj-slides PRODUCT_NAME, messaging; copy „Ask Ani” / „Ani found N potential issues” (EN/PL). Pakiet i repo bez zmian. Bez nowych zależności.
  • Znak AskAniela — placeholder (#207): oryginalny koncept B bez aureoli w panelu (sidebar, logowanie, nagłówek publiczny, favicon svg/ico/apple-touch), przy „Ani found N potential issues”, w nagłówku eksportu HTML/PDF i w MkDocs; koncepty A/B/C, B2, sygnety w docs/design/brand-ani.html, opis docs/design/brand.md. Bez nowych zależności.

  • Powtarzalność ocen (#195, zmiana kontraktów inference, judge): diagnoza — prompt identyczny między presetami, jeden dostawca, szum batchowania dostawcy (seed nie pomaga; docs/perf/2026-10-13-determinism.md); Profile.resample_band/resample_n (wynik w paśmie → dodatkowe próbki, średnia P(1)), RuleScore.samples/spread/prompt_sha, judge.merge_reports (nadpisanie tylko przy tym samym prompcie), openrouter-gemma4: pasmo [0,2; 0,8], 2 próbki, seed: 0. Bez nowych zależności.

  • Pobieranie przez dostawców (#74, ADR 0022 pkt 5 i 11, zmiana kontraktu ingest, core.meter, costs, usage, audits): interfejs ingest.fetch(FetchRequest) -> FetchResult (html, body, final_url, status, rendered, provider, cost_usd, ms, attempts), dostawcy jako dane w fetchers/*.yaml (direct tylko dev przez core.safefetch, zyte primary, zenrows backup, spider-cloud trzeci, brightdata stub z trybem task); łańcuch z klasyfikacją blocked|captcha|not_found|timeout|provider|config|invalid, render JS automatycznie po blokadzie/pustej powłoce, ponowienia, limit kosztu na stronę; FetchError.kind; koszt prób do Meter.fetch_usd -> cost_ledger.fetch_usd, Meter.rendered pod dopłatę +1 kr.; usage.js_rendering_available() = aktywny dostawca z renderem; sitemapy audytu przez ten sam interfejs (kind="sitemap"); ustawienia GSPAMJUDGE_FETCHERS_DIR, GSPAMJUDGE_FETCHERS, GSPAMJUDGE_FETCH_MAX_ATTEMPTS; przewodnik guide/fetching.md. Bez nowych zależności (httpx, PyYAML).
  • Reguły po regresji #162 (#196): podbito SP-LNK-FOOTER v2 (tylko linki z frazami do innych domen w nawigacji/stopce, ≥ 2; linki wewnętrzne, w tym katalogowe „ ”, profile, udostępnianie i siostrzane serwisy = 0), QR-WIKI v4 (kopia/parafraza konkretnego artykułu Wikipedii z konkretnym śladem; sam encyklopedyczny ton = 0; przykład zanonimizowany, bez domeny), SP-SCA-STITCH v2 (wyjątek: oficjalne informacje publiczne urzędów i instytucji; przerwa tylko przy markerze [...]), QR-HC-PADDING v3 (powtórzenie tylko wokół markera [...] = 0), QR-COMMON v3 (nowe brzmienie: „poziom podstawowego przeglądu tematu” z wymienionymi rodzajami szczegółów). Teksty dla ludzi SP-LNK-FOOTER i QR-COMMON dopasowane. Judge: przy document.truncated QR-TRUNC i QR-HC-PADDING nie są pytane, trafiają do not_assessed z powodem EN/PL (judge.service.TRUNCATION_BLIND). Preset audit-quick/quick 32 → 35 reguł: + QR-HC-PADDING, QR-HC-SLOPPY, QR-HC-SOURCED (Quick daje werdykt helpful); koszt wg wag features.usage. Bez zmiany kontraktów i PROMPT_VER.
  • Dopłaty Lite → Basic → Premium i kategorie jako dodatki (#192, #190, #189 pkt 1, ADR 0023 dopisek, nowy feature features/tiers, zmiana kontraktów storage (replace_report, zrzuty treści report_documents, get_document/store_document/purge_documents), judge (JudgeReport.with_document, pole prywatne), usage (release_hold), audits (refresh_audit, hold fraz intent:<job> ze zwrotem), core (wagi credits_weight_premium=1, credits_weight_refetch=1, credits_weight_intent_full=5), API): poziom = kategorie × głębokość (tiers.yaml), dopłata = cena(cel) − zapłacone wg credits_per_page (Lite 1, Basic 3, Premium 4), tylko brakujące reguły i kroki, ten sam rep_ z historią, zwrot za nieudane kroki (reguły, dowody, intencja — także w ocenie strony i audycie); okno dopłat = retencja treści (30 dni, potem ponowne pobranie +1 kr.; tryb prywatny bez dopłat); GET /v1/tiers, POST /v1/reports/{id}/upgrade[/estimate], POST|GET /v1/audits/{id}/upgrade[/estimate] (202, w tle, hold upg:<id>), 409 upgrade_unavailable; panel: poziom raportu, zablokowane sekcje z ceną i czasem, potwierdzenie, dopłata oflagowanych/wszystkich stron audytu; tabela poziomów Intent (2/4/5) jako dane; migracja 0018_upgrades. Zmiana wagi quick (usage): 1 kr. za kategorię, maks. credits_quick_cap=2 (audit-quick = 2), dodatki kategorii płatne; blokada dopłaty w bazie (report_tiers.version/busy_since, 409 busy), sweeper osieroconych holdów upg: w workerze (tiers_upgrade_stale_minutes=60, wpis admin_audit_log). Bez nowych zależności.
  • Konsola superadmina i impersonacja (#128, #129, ADR 0021 pkt 5, 6, 9, ADR 0022 pkt 2, zmiana kontraktów accounts, orgs, apikeys): nowe wejście panel/console pod /console w design systemie (EN/PL, nie SQLAdmin) — tenanci (plan, kredyty w miesiącu, saldo PAYG, projekty, koszt 30 dni, status, filtry; zakładanie orgu, zmiana planu, przyznanie kredytów przez usage.grant_credits, zawieszenie/przywrócenie — powód wymagany), użytkownicy we wszystkich orgach (orgi, ostatnie logowanie, blokada z odwołaniem kluczy użytkownika), klucze API (maska, odwołanie), zadania i audyty z błędami, koszty (dzień vs limit, 14 dni, per profil, top projekty), skrzynka feedbacku, zaproszenia, dziennik admin_audit_log z filtrami; dostęp: superadmin pełny, globalny admin tylko agregaty (bez e-maili, członków, projektów, zaproszeń), pozostali 404; każda akcja i każde wejście w dane tenanta (TenantScope.superadmin) = wpis w dzienniku. Impersonacja „Log in as”: tylko superadmin (nie siebie ani superadmina), powód, cookie gsj_imp z losowym sid i sesją w tabeli impersonation_sessions (tylko hashe, związana z gsj_session; wyjście, wylogowanie i nowe logowanie ją unieważniają) na 30 min bez przedłużania, baner na każdej stronie (ui/_impersonation.html, COMPONENTS §31), domyślnie tylko odczyt (zapis → 403 impersonation_read_only), zapis po przełączniku z potwierdzeniem (każdy zapis = impersonate.action), klucze API, zaproszenia (e-maile), konta, auth (poza wylogowaniem, które kończy impersonację), /admin i /console zawsze zablokowane (impersonation_blocked), nigdy dla klucza API ani Bearer JWT; wpisy impersonate.start|write_enabled|action|denied|end. Kontrakty: Principal.impersonator_id|impersonation_mode|impersonation_expires_at, accounts.register_principal_swap, accounts.to_principal, users.last_login_at i impersonation_sessions (migracja 0020_console), accounts.search_users|get_user_admin|set_user_active|start_impersonation|get_impersonation|set_impersonation_mode|end_impersonation(s); orgs.list_orgs|set_org_status|set_org_plan|get_org_by_public_id, list_admin_audit z filtrami (action prefiks, org_id, actor_id, target, before_id), AdminAuditEntry.ip; apikeys.org_keys, apikeys.revoke_key_admin. Flagi funkcji, zdrowie systemu i „Support access log” — osobne issue. Bez nowych zależności.
  • Intencja wyszukiwania i Needs Met (#174, ADR 0024, nowy feature features/intent, zmiana kontraktu rules, judge, ingest, inference, usage, audits, jobs, report, pipeline, API): opcjonalna fraza docelowa + rynek (query, market w POST /v1/judge, panelu „New check”, CLI --query/--market, kolumny CSV query,market w imporcie audytu → JobCreate.targets); SERP przez SerpProvider (GSPAMJUDGE_SERP_PROVIDER: fixture na 4 snapshotach intent-labeler w content/demo/intent/ z atrybucją MIT; dataforseo napisany, wyłączony bez klucza i zgody), etykietowanie intencji przez intent-labeler z chatem z inference.complete_text (nowe: odpowiedź JSON bez logprobs), cache serp_snapshots + intent_profiles (migracja 0017_intent_cache, TTL GSPAMJUDGE_INTENT_CACHE_TTL_DAYS=7, tryb prywatny tylko odczyt), IntentFit w JudgeReport.intent; Document.elements/schema_types/word_count (ingest elements.py); pole reguły context + JudgeInput.context (blok <search_intent> budowany kodem; bez frazy reguła w „Not assessed”); nowe reguły QR-NM (choice FailsM…FullyM, SQRG 13.2–13.6) i QR-HC-INTENT-MISMATCH; sekcja raportu „Search intent” z lukami formy (EN/PL); kredyty GSPAMJUDGE_CREDITS_WEIGHT_INTENT=2 za unikalną frazę (cache SERP 1, profilu 0, usage.intent_credits); przewodnik guide/search-intent.md. Nowa zależność intent-labeler (MIT, repo właściciela, przypięta do commitu b2c1b78 przez git+https, bez zależności runtime): metoda etykietowania SERP-u i jej kod liczący udziały/długość/ostrzeżenia — zamiast kopii kodu; hatch allow-direct-references.
  • Linki udostępnienia (#126, ADR 0021 pkt 8, nowy kontrakt sharing): nowy feature features/sharing — tabela share_links (migracja 0015_share_links; token secrets.token_urlsafe(24) pokazany raz, w bazie SHA-256, wygaśnięcie domyślnie 30 dni albo bez, odwołanie, licznik wyświetleń, include_evidence/show_project_name domyślnie wyłączone, opcjonalne hasło: password tylko do zapisu, has_password w ShareRead, PATCH /v1/share-links/{id} ustawia/zmienia/usuwa, hash hashlib.scrypt ze stdlib, cookie odblokowania HMAC 12 h na ścieżce linku HttpOnly/Secure/SameSite=Strict, 5 błędnych prób per token+IP → blokada 15 min); API POST/GET /v1/reports/{id}/shares, POST/GET /v1/audits/{id}/shares, DELETE …/shares/{share_id}, GET /v1/share-links, DELETE /v1/share-links/{id} (tworzy owner/admin/member orgu, nie viewer ani klucz API), nowy kod 404 share_not_found; publiczna strona /share/{token} i /share/{token}/pdf bez logowania: findings-only bez kosztów, kredytów, danych kont i id wewnętrznych, tryb prywatny projektu = sam host, X-Robots-Tag: noindex, nofollow + meta robots, Referrer-Policy: no-referrer, no-store, nosniff, ścisłe CSP, limit 30/min per IP i 60/min per link (429, IP z apikeys.client_ip), nieznany/wygasły/odwołany/org zawieszony = ta sama 404; raport i audyt renderowane z flagą public=True (kontrakt report.render_*, audits.render_*: nowy parametr public), w trybie prywatnym bez wzorców URL, tytułu wsadu i cytatów, URL-e skal/PQ (#157) maskowane; DELETE celu sprawdza zgodność linku z celem; panel: przycisk Share na raporcie i audycie (modal z kopiowaniem linku i listą z odwołaniem, EN/PL), base.html dostał blok head; core.logs.safe_path maskuje segment po /share/ (i /s/), a log 500 w api/errors.py używa safe_path. Przewodnik docs/site/guide/sharing.md. Bez nowych zależności.
  • Teksty reguł dla ludzi (#168, zmiana kontraktów rules, report, audits, CSV audytu): Rule ma opcjonalne title/title_pl (≤ 60), summary/summary_pl (≤ 300), why/why_pl, fix/fix_pl (≤ 400) z walidacją długości i Rule.copy_text(pole, lang); wszystkie 130 reguł (w tym 7 skal z #157 i C1-YMYL) dostały komplet tekstów EN/PL (B1, bez skrótów; fix z dokumentacji Google). Pola nie trafiają do promptu → bez podbicia ver i PROMPT_VER. Raport/panel/eksport: RuleView.title|summary|why|fix (describe(v, lang)), wiersz reguły pokazuje tytuł + opis, ID i polityka drugorzędnie, „Dlaczego to ważne i co zrobić” w <details> (_rule_more.html; w druku/PDF rozwinięte dla naruszeń), etykiety, linia pozytywów i skale ocen (ChoiceView.title|summary|why|fix, choice_views(r, full, lang)) z tytułami; audyty: localized(a, lang) i tytuły skal (rule_titles) przy renderze; CSV audytu (findings) ?lang=en|pl i nowe kolumny na końcu title, summary. Dokumentacja: reference/rules.md + nowe rules.pl.md (tytuł, opis, szczegóły „dlaczego / co zrobić” na regułę), akapit w guide/concepts.md. Teksty i18n PL 454/454. Bez nowych zależności.
  • Ochrona kluczy API (#125, ADR 0021 pkt 7, ADR 0022 pkt 9, zmiana kontraktu apikeys, core, orgs): właściciel klucza owner_type = project|user|org (dostęp = zakresy ∩ bieżące uprawnienia właściciela; klucz user ginie z członkostwem w orgu — apikeys.revoke_user_keys w orgs.members.remove), nowy format gsj_live_<prefix>_<secret><CRC32> z udokumentowanym regexem pod secret scanning (stary gsj_<prefix>_<secret> działa), expires_at (domyślnie 90 dni, maks. rok, „never” tylko klucz orgu od owner/admin), rotacja POST /v1/keys/{id}/rotate z okresem łaski (domyślnie 7 dni, rotated_from), allowlista IP/CIDR (IPv4/IPv6; X-Forwarded-For tylko od GSPAMJUDGE_TRUSTED_PROXIES), last_used_ip, POST /v1/keys/leaks (auto-odwołanie + e-mail ownerów), GET/POST /v1/keys, DELETE /v1/keys/{id}; ApiKeyRead + owner_type, owner_user_id, display, allowed_ips, expires_at, last_used_ip, revoke_reason, rotated_from, project_id opcjonalne; nowe kody api_key_expired, ip_not_allowed, invalid_key_expiry, key_not_active; wpisy apikey.create|rotate|revoke|leaked|owner_left w admin_audit_log; core/scrub.py maskuje gsj_…, Authorization i sekretne parametry we wszystkich logach (także uvicorn.access); ustawienia GSPAMJUDGE_APIKEY_TTL_DAYS, GSPAMJUDGE_APIKEY_ROTATION_GRACE_HOURS, GSPAMJUDGE_TRUSTED_PROXIES; projects: list_projects łączy członkostwo po Principal.user_id także dla klucza user (projekty restricted właściciela), klucz API (każdy typ) nie tworzy projektów (403) ani projektu „Default”; klucz orgu widzi wszystkie projekty orgu (rola admin, bez zarządzania); limit endpointu wycieków ze stabilnym kubełkiem sha256 IP, LeakReport.keys maks. 128 znaków na element; panel: zakładka kluczy z wygaśnięciem, listą IP, ostatnim IP, rotacją i stanami (EN/PL). Migracja 0014_api_key_protection. Bez nowych zależności (stdlib ipaddress, zlib).
  • Skille (#173): gsj-slides (deck HTML 1920×1080 w tokenach docs/design, Inter, marka + głos EN/PL, dozwolone/zakazane twierdzenia, źródła liczb z repo) i gsj-video (studio HyperFrames z bramkami G1–G6, storyboard, krytyka, zrzuty tylko panelu lokalnego z danymi demo, synteza muzyki/SFX, prompts/ z szablonem promptu); endorsement „by Zwinnie” na końcu. Zaadaptowane z wewnętrznego skilla. Bez nowych zależności projektu (HyperFrames, Playwright, numpy — tylko poza repo).
  • Dokumenty prawne (#171, zmiana kontraktu legal): projekty EN (wiążące) + PL w features/legal/docs/ — Terms of Service (B2B/B2C), Privacy Policy (RODO, podprocesorzy, SCC, retencja), Credits & Refund Policy (bez zwrotów, 12 mies., błąd systemu → zwrot kredytów), AUP, Cookie Notice, DPA, Disclaimer; front matter version/effective/draft, dane operatora (Zwinnie) w company.yaml jako {{KLUCZ}}; LegalDoc{version, effective, sha256, draft}, PAGES + credits|aup|dpa, doc_version, accept_docs/pending_docs (terms + privacy, signup/reaccept), record_purchase_consent(PurchaseConsent) pod checkout #138 (zgoda na natychmiastowe świadczenie + utrata prawa odstąpienia), email_footer; TERMS_VERSION z front matter (2026-10-13, wszyscy akceptują ponownie). Migracja 0016_legal_acceptances (wersja, sha256, kontekst, waiver, HMAC IP, org). Panel: /legal/* z wersją i banerem „Draft”, stopka z nowymi linkami i operatorem, link do regulaminu w rejestracji; e-mail zaproszenia ze stopką prawną; strona docs Legal. Bez nowych zależności.
  • Kredyty zamiast stron (#117, ADR 0023, zmiana kontraktu usage, audits): 1 kr. = 0,01 USD; quick 1, pełna 3, JS +1 i stealth +2 tylko za wykonane pobranie (dziś brak renderera — bez dopłaty, także w szacunku), dowody w cenie; preset:quick = alias audit-quick w rules/presets.yaml (credits_per_page w features.usage — jedyny wzór, także szacunek audytu; quick = do GSPAMJUDGE_CREDITS_QUICK_MAX_RULES=40 reguł po presetach i kategoriach, zamiast „waga × udział reguł”); QuotaLimits{credits_day, credits_month}, Subject.org, UsageRead{unit, org, plan, balance}, reserve_quota(..., pages, per_page, hold_ref), settle(..., ref) -> bool, release_job_pages(job_id, pages) (idempotentny), jobs.create_job(..., job_id), 429 quota_exceeded z unit i balance; plany jako dane features/usage/plans.yaml (beta 2000 kr./mies. domyślny, free 100, payg, starter 1000, pro 3500, agency 10000, business 28000; limity projektów/miejsc/kluczy/stron audytu — egzekwowanie osobno) przypięte do organizations.plan; saldo PAYG credit_ledger (ważność 12 mies., pula planu → saldo, zasilenie tylko przez grant_credits z wpisem credits.grant w admin_audit_log; widoki Quotas/Usage/Credit ledger tylko dla superadmina; zwrot do wygasłego wpisu → najbliższy aktywny albo nowy wpis refund); GSPAMJUDGE_QUOTA_USER_DAY 50 stron → 150 kr., _USER_MONTH → brak (miesiąc liczy plan orgu). Migracja 0013_usage_credits (strony ×3, credit_ledger, credit_holds). Panel: kredyty w pasku bocznym, na dashboardzie i w szacunku audytu (EN/PL). Bez nowych zależności.
  • Reguły wielokrotnego wyboru (#157, zmiana kontraktów rules, judge, report, audits): Rule.answer_type (binary|choice), Rule.options (Option{letter,label,anchor,source,concern}), Rule.ordered; RuleScore.answer_type/distribution/modal/expected/entropy, JudgeReport.choices (poza werdyktem 0/1); prompt choice (question_choice.md, answer_choice_short.md; system prompt reguł 0/1 bez zmian; PROMPT_VER p5 → p6; top-1 spoza wielkich liter opcji albo coverage < 0.5 = brak wyniku z błędem top1_other/low_coverage); nowe reguły QR-PQ (9 stopni), QR-MCQ, QR-EEAT-EXPERIENCE/EXPERTISE/TRUST, C6-YMYL-LEVEL, C7-PAGE-PURPOSE (kategoria quality); C1-YMYL zostaje dla zgodności (ver bez zmian); raport: ReportView.choices (ChoiceView, ChoiceBar), pasek rozkładu, findings-only pokazuje skalę tylko przy P(Lowest/Low) ≥ 0.5; audyt: AuditSummary.scales/pq_patterns/pq_low_pages; kredyty: reguła choice = jedna reguła; EN/PL, docs. Bez nowych zależności.
  • Publiczne identyfikatory (#142, zmiana łamiąca kontraktów storage, projects, jobs, audits, apikeys, feedback, invites, report, API/panel/CLI): każdy zasób w URL, ciałach, JSON, CSV, webhookach i CLI ma losowe public_id (prj_, rep_, aud_, job_, key_, fb_, inv_; orgi org_ od #124) — prefiks + 16 znaków base32 z secrets (core/public_id.py, wspólne z orgs); int PK (i hex PK jobs/audits) zostaje wewnętrzny (pk/project_pk z exclude=True), stare ścieżki i nagłówki z int → 404. id/project_id/report_id(s)/job_id w odpowiedziach to stringi; ?project_id=, X-Project-ID, project_id w ciałach i cookie gsj_project przyjmują tylko prj_…; save_report zwraca rep_…, get_job(..., by_pk=True) tylko dla workera, nowe jobs.read_job. Migracja 0012_public_ids: kolumny public_id z backfillem i unikalnym indeksem (SQLite + Postgres), przepisanie jobs.results[].report_id, audits.report_ids i feedback.submitted_by (api_key:key_…). Wyjątek bety od polityki wersjonowania (bez /v2). Bez nowych zależności.
  • Kategorie spam / quality / helpful (#156, zmiana kontraktów rules, report, audits): Rule.category (domyślnie helpful przy źródle Google „creating-helpful-content” — QR-HC-*, C4-PURPOSE — inaczej z layer), category_of/filter_rules/parse_categories, presety preset:spam|quality|helpful i spam-quick; raport: Verdict.helpful_*, ReportView.helpful, FindingCounts.helpful, sekcja „Helpful content” (stempel nadal tylko spam; quality bez helpful); audyty: SiteAuditCreate.categories (domyślnie wszystkie; etykiety kontekstu C1–C3, C5 zawsze; zawęża reguły zadania, kredyty i czas: Estimate.categories/rules), PolicyRisk.category, PageRisk/AuditSummary/AuditBrief.helpful_level, PageFindings.helpful, SamplePlan.categories; CSV audytu: kolumna category (findings) i helpful_level (full); panel: kategorie i preset „Spam quick” w kreatorze, kolumna i filtry quality/helpful w historii, kafel helpful na dashboardzie (EN/PL). Stare raporty: kategoria z id reguły przy renderze. Bez nowych zależności.
  • Dokumentacja (#158): katalog błędów API uzupełniony o org_not_found i org_suspended, odświeżone docs/HANDOFF.md, drzewo projektu w README, wygenerowane docs/site/reference/rules.md i openapi.json, notatka Obsidian.
  • Prywatność URL (#137, kontrakt ingest, storage, projects, jobs, costs): nowy feature features/privacy (normalize_url, strip_query, host_of, url_hash/url_ref HMAC z GSPAMJUDGE_PRIVACY_PEPPER, mask_url dla publicznych linków #126, tryb prywatny w ContextVar). IngestInput.url normalizowany przy przyjęciu (bez #fragmentu i parametrów z konfigurowalnej GSPAMJUDGE_PRIVACY_STRIP_PARAMS: utm_*, gclid, fbclid, msclkid, session/sid, token, key, auth, sig, password, email, code…), oryginał tylko w nieserializowanym fetch_url do pobrania; reports.host + reports.url_hash, ReportSummary{host, url_hash}, list_reports(url=) po hashu; projekt private_mode (ProjectUpdate{name?, private_mode?}, panel → Privacy, EN/PL): raport bez URL (host + hash), treść i URL-e zadania usuwane po jego końcu; cost_ledger.url, rawlog i błąd pobrania — tylko host; prompt URL: bez query (PROMPT_VER p4 → p5); retencja GSPAMJUDGE_RETENTION_DAYS (30): cron workera i gspamjudge retention usuwają html/text zadań, wyniki zostają; nota prywatności EN/PL zaktualizowana; audyt witryny w trybie prywatnym zapisuje w audits.meta strony jako host; poza GSPAMJUDGE_ENV=dev start API/workera wymaga GSPAMJUDGE_PRIVACY_PEPPER. Migracja 0011_url_privacy. Bez nowych zależności.
  • Organizacje (#124, ADR 0021, zmiana kontraktów): nowy feature features/orgs — organizations (public_id org_…, status, plan), org_members (owner/admin/member/viewer; użytkownik w wielu orgach), admin_audit_log; /v1/orgs (moje, bieżący, nowy, członkowie i role), panel: przełącznik orgu w górnym pasku (ui/_org_switcher.html, cookie gsj_org) i /org (członkowie, role; EN/PL). Izolacja: core.tenancy.TenantScope jedynym filtrem w repozytoriach — storage.get_report(id, scope), list_reports(scope, limit, url), jobs.get_job/list_jobs, audits.get_audit/list_audits, apikeys list/revoke, feedback.list_feedback przyjmują TenantScope zamiast project_ids; projects.tenant_scope/read_scope zwracają TenantScope, can_see_report usunięte z API, nowe require_write, may_manage, project_org, ProjectRead.restricted, create_project(..., org_id, restricted), ProjectUpdate z restricted; członek projektu musi być członkiem orgu. accounts: Principal.org_id/org_role, register_principal_enricher; bieżący org z X-Org-ID (public id) / cookie / pierwszy z członkostwem, klucz API → org projektu, cudzy org 404, zawieszony 403 org_suspended. Rola globalna admin/superadmin nie widzi już danych tenantów; obejście tylko TenantScope.superadmin(reason) z wpisem scope.bypass (GET /v1/feedback dla superadmina, CLI export); GET /v1/feedback także dla owner/admin orgu (swój org). Viewer: ocena, klucze, projekty, feedback → 403. Zaproszenia dostają org_id (przyjęcie = member orgu). Migracja 0010_organizations: org „ (personal)” per właściciel projektu, członkowie → member, org_id w projects (NOT NULL), reports, jobs, audits, api_keys, feedback, invites, cost_ledger; raporty/feedback bez projektu → org „Legacy”. Po recenzji PR #149: zawieszony org z cookie → pierwszy aktywny (nagłówek → 403), TenantScope.narrow przecina z widocznymi, SQLAdmin Reports/Feedback/Invites tylko dla superadmina z wpisem scope.bypass, wspólny komunikat 404 przy dodawaniu członka. Kwoty per org: #117. Bez nowych zależności.
  • Infra (#25): deploy/runpod/ (szablon endpointu RunPod Serverless z workerem vLLM, deploy.sh <PRO4500|RTX5090|PRO5000|PRO6000> dry-run / --apply, smoke.sh), profile runpod-gemma4-pro4500, runpod-gemma4-5090 (48 seq) i runpod-gemma4-pro6000 (64 seq), oba nvfp4 jak DGX GB10, niekalibrowane; base_url profilu obsługuje ${ZMIENNA} z env/.secret/.env; plan pomiaru i rachunek VRAM w docs/perf/runpod-plan.md. Bez nowych zależności, nic nie wdrożono.
  • Inference (#148, kontrakt inference): jeden dostawca na profil — Profile.spread_providers domyślnie false (rotacja zostaje tylko do eksperymentów), concurrency 24 → 8, max_inflight 64 → 12 (jeden dostawca, mniej 429); openrouter-gemma4: tylko dekallm/bf16 w provider.only/order, spread_providers: false (decyzja właściciela: rotacja zmieniała wyniki, łamała „profil = profil kalibracyjny”, +20–30% kosztu). Backoff/Retry-After bez zmian. Bez nowych zależności.
  • Raport (#147, kontrakt report/audits): domyślny widok findings-only w panelu, eksporcie HTML/PDF/CSV i CLI — werdykt + liczby, tylko reguły ≥ progu (spam i jakość osobno) z dowodami i 1-zdaniowym opisem reguły, pozytywy jedną linią, „Not assessed” jako krótka lista; audyt: karty stron z trafieniami + jedna linia „N pages without potential violations” (lista zwinięta, pomijana w druku). Pełne szczegóły: przełącznik „Show all rules” (?detail=full) i ?detail=full w eksportach (--detail full w gspamjudge export); nowe pola ReportView.detail|counts|positive_names|not_assessed_names, RuleView.description, AuditSummary.findings|clean (PageFindings); CSV audytu domyślnie wiersz na trafienie. PDF (Onet, 30 stron, zapisane raporty): audyt 6 → 4 strony, raport strony 19 → 2. Bez nowych zależności.
  • Design (#127): makiety konsoli superadmina /console (tenanci, szczegóły tenanta, użytkownicy, impersonacja: okno + stały baner, dziennik działań, koszty i limit wydatków, zgłoszenia, zdrowie systemu, flagi funkcji), zakładka „Shared links” projektu, publiczny raport /s/{token} i szczegóły klucza API (wygaśnięcie, rotacja, lista IP, ostatnie IP) w docs/design/mockups/; mapa widok→trasa→szablon w docs/design/README.md. Bez zmian w kodzie.
  • Design (#131): skill design-system (zasady, proces makieta → przegląd → wdrożenie, przepisy układów, dodawanie komponentów, checklista QA z komendami) i scripts/ui_lint.py (kolory hex, monospace, style=, cienie, pogrubienia; ostrzeżenia o tekście bez _()) oraz scripts/ui_shots.py (zrzuty headless Chrome light/dark 1440/390; wąskie szerokości w iframe, bo Chrome headless ma min. 500 px). Bez nowych zależności.
  • Decyzje (#116): ADR 0019 przyjęty (kredyt jako jednostka rozliczeń; ceny planów do potwierdzenia), nowy ADR 0020 — produkcja na RunPod Serverless + vLLM, OpenRouter jako dev/fallback, własny profil i kalibracja; wdrożenie kredytów w usage: #117.
  • Legal + onboarding (#91, #92, kontrakt): nowy feature features/legal (legal_doc, accept_terms, needs_acceptance, TERMS_VERSION=beta-2026-10-13) i treści Markdown w src/gspamjudge/content/legal/ (regulamin bety, prywatność z retencją 90/30 dni, zastrzeżenia — znak towarowy Google, cookies tylko niezbędne, bez banera; EN + PL, PROJEKT do przeglądu prawnika). Panel: publiczne /legal/{terms|privacy|cookies|disclaimer} i /demo (zanonimizowany raport z blind-03, bez kwoty), stopka prawna na każdej stronie (makro legal_footer, COMPONENTS §29), wymagany checkbox regulaminu na /invite/{token}, /welcome (checklista checklist, §30; akceptacja dla kont spoza zaproszeń i po zmianie wersji; dashboard przekierowuje do akceptacji/zamknięcia, cookie gsj_welcome). Migracja 0008_terms_acceptances. Teksty PL w katalogu (357/357). Nowa zależność markdown-it-py (lekki parser CommonMark, już tranzytywnie przez typer/rich; render bez surowego HTML).
  • Koszt i obserwowalność (#94, #93, kontrakt): nowy feature features/costs — ledger kosztu per strona (cost_ledger: usage.cost dostawcy, tokeny, cached tokens, pobrania × GSPAMJUDGE_FETCH_COST_USD, 429 dostawcy, request_id/job:<id>, projekt) zbierany przez core.meter (ContextVar; klient inferencji i safefetch.get dopisują, pipeline.analyze zapisuje także po błędzie); globalny dzienny limit GSPAMJUDGE_DAILY_SPEND_CAP_USD (20, doba UTC) sprawdzany w /v1/judge, batch, POST /v1/jobs, POST /v1/audits/site, panelu, rerun i przed każdą stroną w pipeline.analyze (wyjątek domenowy SpendCapReached, mapowany w api/errors.py) → nowy kod 503 spend_cap_reached (EN/PL, details{day, spent_usd, cap_usd}, Retry-After); alert raz na dobę przy GSPAMJUDGE_SPEND_ALERT_RATIO (0.8) i 100%: log WARNING + opcjonalnie GSPAMJUDGE_ALERT_WEBHOOK_URL (POST JSON przez core.safefetch.post, tylko https i adres publiczny) i GSPAMJUDGE_ALERT_EMAIL (SMTP #89). /admin → System → Metrics (superadmin, odczyt): koszt dnia vs limit, strony/koszt/koszt na stronę/udział cache/429/5xx per dzień, top projekty i użytkownicy. Logi JSON z request_id (core/logs.py, GSPAMJUDGE_LOG_JSON, GSPAMJUDGE_LOG_LEVEL; access log gspamjudge.access zamiast uvicorna: szablon trasy albo ścieżka z maskowanymi tokenami (core.logs.safe_path), bez query stringu, także 500; worker z request_id=job:<id>); bez zewnętrznego śledzenia błędów (GlitchTip opisany w core/README.md). Migracja 0009 (cost_ledger, ops_counters). Bez nowych zależności.
  • Perf (#114, kontrakty inference, judge, rules): audyt 30 stron z ~20–25 min do ~7,4 min (pełny katalog), audit-quick ~5 s/stronę. Profile: concurrency 8 → 24, nowe max_inflight (64, wspólny limit w procesie), spread_providers (rotacja provider.order po provider.only per strona), retries (6), backoff_base_s/backoff_max_s (pełny jitter, Retry-After); 429 → dostawca na koniec kolejności. GSPAMJUDGE_BATCH_CONCURRENCY 2 → 6; GSPAMJUDGE_JUDGE_WARMUP (jedna reguła przed resztą, cache prefiksu) i GSPAMJUDGE_JUDGE_UTILITY_GATE (C5-UTILITY ≥ 0.5 → reguły quality niepytane, nowe pole JudgeReport.skipped). Presety reguł preset:<nazwa> (rules/presets.yaml, rule_presets, expand_presets), audit-quick (32 reguły) w API/CLI i panelu audytu. Profil openrouter-gemma4: spread_providers: true (decyzja koordynatora: OpenRouter to tylko dev/fallback wg ADR 0020, kalibracja per profil RunPod) — koszt strony +20–30%, rzadkie różnice wyników między dostawcami bf16 (docs/perf/2026-10-13-batch.md). scripts/bench_batch.py. Bez nowych zależności.
  • Audyty (#119, kontrakt audits): kreator audytu witryny w dwóch krokach — POST /v1/audits/site/preview {site|sitemap_url|urls[]} → SitePreview (liczba URL-i, wzorce z liczbami, zakresem lastmod i przykładami, preview_id), lista URL-i z pliku TXT/CSV albo urls (normalizacja, duplikaty, ingest.check_fetch_url, opcjonalnie ten sam host, GSPAMJUDGE_AUDITS_UPLOAD_MAX_URLS); POST /v1/audits/site/estimate i POST /v1/audits/site z scope (wzorce include/exclude, glob/re:, ręczne wykluczenia), strategie all|newest|stratified|newest+stratified, preset, js → Estimate (kredyty z wag GSPAMJUDGE_CREDITS_WEIGHT_FULL/QUICK/JS, GSPAMJUDGE_CREDIT_USD, czas GSPAMJUDGE_AUDITS_SECONDS_PER_CREDIT); SamplePlan + source, scope, selected, estimate (bez migracji), wybór w raporcie witryny; import items [{url, html?}] / w panelu CSV url,html i ZIP (index.csv + .html) — pozycje z HTML oceniane bez pobierania, limity jak /v1/jobs (413 item_too_large); /a/ = /a (opcja distinct_slash); tylko glob (bez regexów, ReDoS); preview_id przypięty do użytkownika/klucza i projektu, magazyn podglądu w Redisie (fallback pamięć); kody invalid_scope, empty_selection, preview_expired. Panel: kreator /audits/new (makro range_field), teksty PL. Przewodnik guide/site-audits.md, openapi.json odświeżony. Bez nowych zależności (redis z arq).
  • Ingest/API (#111, kontrakt ingest): SSRF przy pobieraniu strony — fetch.py przez core.safefetch (adresy is_global, przypięty IP, porty 80/443, przekierowania walidowane ponownie, ≤ 10 MB strumieniowo, 20 s, ten sam User-Agent); nowe publiczne check_fetch_url, InvalidURL, FetchError (podklasa httpx.HTTPError). Nowy kod 400 invalid_url (literał IP, localhost, .local/.internal, dane logowania, schemat, port) w /v1/judge, batch, jobs, panelu i rerun; adres prywatny po DNS / przekierowanie → 502 fetch_failed. Dotyczy też stron próby audytu. Dev: GSPAMJUDGE_WEBHOOK_ALLOW_INSECURE (tylko GSPAMJUDGE_ENV=dev). Bez nowych zależności.
  • Reguły/API (#108, kontrakt rules): walidacja id reguł w jednym miejscu — rules.validate_rule_ids(ids) -> RuleCheck{unknown, disabled}, select_rules rzuca RuleSelectionError (podklasa KeyError) i deduplikuje id. API: siatka bezpieczeństwa (RuleSelectionError → 400 unknown_rule/rule_disabled zamiast 500), /v1/jobs waliduje reguły przed 503 queue_unavailable; panel pokazuje przetłumaczony komunikat (wcześniej ApiError wymykał się do KeyError: ...), CLI judge/batch → stderr + exit 2. Bez nowych zależności.
  • Panel (#105): czytelny dashboard — URL jako host+ścieżka (skrót w środku, pełny w title), polityka spamu + kryterium jakości jako tekst pomocniczy, tabela bez poziomego przewijania (kolumny chowane na wąskich ekranach), licznik wysokiego ryzyka jakości zamiast „Saved reports”, liczba raportów bez projektu, projekt „Default” tworzony przy pierwszym wejściu (jak przy pierwszej ocenie), bez placeholdera kredytów, email w pasku obcinany.
  • Audyty (#87, kontrakt): nowy feature features/audits — grupa raportów batch|job|site jako jedna pozycja historii i raport witryny. POST /v1/audits/site {site|sitemap_url, pages=30 (≤ GSPAMJUDGE_AUDITS_MAX_PAGES=100), strategy newest+stratified, seed, rules, profile, explain} → sitemap z robots.txt/indeksów (≤ GSPAMJUDGE_AUDITS_SITEMAP_MAX_URLS), wzorce URL, próba (~1/3 najnowszych wg lastmod + losowanie warstwowe, seed) → zadanie features/jobs; GET /v1/audits, GET /v1/audits/{id} (postęp + ryzyko per polityka, wzorce URL, najgorsze strony, werdykt kodem z progami features.report), eksport export.html|.pdf|.csv; kody audit_not_found, audit_too_large, sitemap_not_found. POST /v1/jobs, /v1/judge/batch i lista URL-i w panelu tworzą audyt (batch zapisuje raporty sam: pipeline.analyze_many(..., store=) — nowy parametr). Panel: /audits/new, /audits/{id}, historia z wierszem audytu rozwijanym na strony. Migracja 0007_audits. Teksty PL w katalogu. Kwoty (#89): rezerwacja stron próby w start_site_audit, zwrot nieocenionych przez worker. SSRF: wspólne core/netguard.py (walidacja URL: schemat, porty 80/443, bez literałów IP/localhost/.local/.internal, DNS → tylko is_global) i core/safefetch.py (połączenie na przypięty IP, ręczne przekierowania z ponowną walidacją, limit bajtów w strumieniu) — logika przeniesiona z features/jobs/guard.py/pinned.py (zostają jako cienkie re-eksporty; webhooki dodatkowo tylko porty 80/443); sitemap ≤ 20 plików, ≤ 10 MB na plik, gzip z max_length, łączny czas GSPAMJUDGE_AUDITS_DISCOVERY_TIMEOUT (45 s). core/csvsafe.safe_cell (przeniesione z admin/csvsafe.py) także w eksporcie CSV audytu. Bez nowych zależności (lxml z trafilatura).
  • i18n (#60, ADR 0017): katalog pl (features/report/locales/pl/LC_MESSAGES/messages.po/.mo, 240 tekstów: panel, raport, API) i scripts/i18n.sh extract|update|compile; .mo w repo (runtime: stdlib gettext, Babel tylko w extra dev, bez zmian w Dockerfile). Komunikaty błędów /v1 po angielsku, Accept-Language: pl → polskie (code bez zmian). Eksport GET /v1/reports/{id}/export.html|.pdf?lang=en|pl, CLI export --lang; panel linkuje eksport w bieżącym języku. Zmiana kontraktu rules.NotAssessed: reason po angielsku + nowe reason_pl; raport bierze powód w swoim języku z bieżącego katalogu (także dla starych raportów).
  • Reguły (#83, #98, #99): FP z blind-04 ikrk.pl. SP-HACK-CODE v1 → v3 (wymaga eval( z atob(/unescape(; samo atob( + document.write z loaderów reklam/wtyczek CMS oraz linki komercyjne w menu/treści to nie hacking), SP-HACK-TEXT v1 → v2 (redakcyjna wstawka „sprawdź również” i linki w menu/stopce = link spam), SP-DOOR-CITY v1 → v2 (miasto + fraza w TITLE, ogólnikowy szablon, jeden cel; wyjątki: nazwa własna z miastem, usługa publiczna, treść lokalna, katalog/ranking wielu firm, tagi), SP-LNK-PAID v2 → v3 i SP-LNK-UNLABELED v2 → v4 komplementarne (oznaczone vs nieoznaczone, wzajemne odesłania w answer_0_if). Nowa SP-SCAM-FAKE-REVIEWS v2 (#98, Scam and fraud: promocja/link do sprzedaży opinii, ocen, followersów; SQRG 4.5.3, 4.6.4). Gemma: SP-HACK-CODE na 8 stronach FP 0,78–1,00 → 0,00, syntetyczny eval(atob()) 1,00; SP-DOOR-CITY na 6 FP 0,65–1,00 → 0,00, otwartawarszawa (osy) 1,00 → 1,00. Zerowe QR-HC-* i SP-LNK-ANCHOR na ikrk.pl to błąd ingestu (#84). Włączone reguły +1. Zasada „bez przeuczenia na stronach testowych” w skillu rule-authoring i features/rules/AGENTS.md (#99).
  • Jobs (#51, ADR 0013, kontrakt): nowy feature features/jobs — POST /v1/jobs {items, rules, profile, explain, webhook_url, project_id} → 202 {job_id, status}, GET /v1/jobs, GET /v1/jobs/{id} (queued/running/done/failed, done/total, report_ids, errors), GET /v1/jobs/{id}/results; zakres klucza jobs, obcy projekt = 404 job_not_found; nowe kody job_too_large, queue_unavailable, webhooks_unavailable. Worker gspamjudge worker (arq WorkerSettings, gspamjudge/worker.py) i usługa worker w compose; każda pozycja przez pipeline.analyze, raport zawsze zapisany w projekcie zadania (pipeline.analyze(..., store=) — nowy parametr). Webhook po zakończeniu z podpisem X-GSPAMJUDGE-Signature: sha256=HMAC(GSPAMJUDGE_WEBHOOK_SECRET, "{timestamp}.{body}"), 3 próby. Migracja 0004 (jobs). Ustawienia GSPAMJUDGE_JOBS_MAX (1000), GSPAMJUDGE_WEBHOOK_SECRET. Klucze API: GSPAMJUDGE_APIKEY_RATE_BACKEND=redis (okno minutowe w Redisie, wiele procesów); apikeys.ratelimit.enforce async. Panel /jobs: lista ze statusem i postępem (makro job_status, odpytywanie co 5 s tylko przy aktywnych). Dokumentacja: guide/webhooks, errors, deploy. Po recenzji PR #86: ochrona SSRF webhooków (tylko https i nazwa hosta → 400 invalid_webhook_url; w workerze DNS → tylko adresy is_global, połączenie na sprawdzony IP przez httpcore, bez przekierowań; GSPAMJUDGE_WEBHOOK_ALLOW_INSECURE tylko w dev), nagłówek X-GSPAMJUDGE-Delivery (deduplikacja) i wymóg okna ±300 s w przewodniku; limity GSPAMJUDGE_JOBS_ITEM_MAX_CHARS/JOBS_MAX_CHARS (413 item_too_large) i GSPAMJUDGE_JOBS_MAX_ACTIVE (429 too_many_active_jobs); GSPAMJUDGE_JOBS_TIMEOUT (1800 s) → failed + webhook, także przy awarii bazy; url zapisany w wynikach pozycji (/results bez dociągania raportów); awaria Redisa w limicie → 503 rate_limiter_unavailable (GSPAMJUDGE_APIKEY_RATE_FAIL_OPEN); API: reguła wyłączona → 400 rule_disabled w /v1/judge, batch i jobs (wcześniej błąd w trakcie oceny); ApiError(headers=). Sekret per projekt: #101. Nowa zależność arq (lekka kolejka asyncio na Redisie, ADR 0013; ciągnie redis, używany też przez limit kluczy).
  • Proces (#97): każda praca — agentów i koordynatora — na własnym issue (decyzja właściciela 2026-10-13): (#nr) w każdym commicie, komentarz startowy i końcowy na issue, Closes #nr w PR, zmiany w katalogu jednego feature'a; zapisane w AGENTS.md (reguła 12), CONTRIBUTING.md, rolach .agents/agents/, skillach worktree-workflow i forgejo-issues oraz docs/HANDOFF.md (retroaktywne issue #96).
  • Feedback (#90, kontrakt): nowy feature features/feedback — POST /v1/reports/{id}/feedback (FeedbackCreate{rule_id|None, label: false_positive|false_negative|unclear|problem, comment}; rule_ver, prompt_ver, profil, score_raw, projekt brane z zapisanego raportu), GET /v1/feedback (admin, filtry rule_id, label, report_id); pole submitted_by (e-mail albo api_key:<id>); nowy zakres klucza API feedback:write (klucz bez niego → 403 insufficient_scope); migracja 0005_feedback; raport: data-rule-id na tr.rule (hak panelu); eksport CSV sanityzowany przed formula injection (admin/csvsafe.py); panel /history/{id}: „Wrong?” przy każdej regule (modal, makro ui/_modal.html) i „Report a problem” (tylko panel, nie eksport); /admin → Feedback tylko do odczytu z eksportem CSV — etykiety do kalibracji (#12). Bez nowych zależności.
  • Beta (#89, kontrakt): nowe feature'y features/invites (admin: POST/GET /v1/invites, DELETE /v1/invites/{id}, widok /admin → Beta → Invites; jednorazowy, wygasający link /invite/{token} — w bazie tylko SHA-256 tokenu — gdzie tester ustawia hasło → konto user i członkostwo w projekcie; e-mail przez stdlib smtplib, gdy GSPAMJUDGE_SMTP_HOST, inaczej link dla admina) i features/usage (kwoty stron per użytkownik i projekt, dzień/miesiąc UTC: GSPAMJUDGE_QUOTA_USER_DAY=50, _USER_MONTH=500, _PROJECT_* bez limitu, nadpisanie w /admin → Quotas; egzekwowane atomową rezerwacją (UPDATE … WHERE pages + n <= limit, zwrot nieocenionych stron po ocenie) w /v1/judge, /v1/judge/batch, POST /v1/jobs, panelu i rerun → 429 quota_exceeded z details; licznik udanych stron w usage_counters, GET /v1/usage, pasek boczny i dashboard panelu; admin+ liczony, nieblokowany). Nowy kod 409 conflict. Kontrakt jobs: JobRow/JobRead.created_by (autor zadania; klucz API = null, kolumna w migracji #89), run_job(job_id, analyze, on_done=None) — worker zwraca na końcu niewykorzystaną rezerwację kwoty autora i projektu. accounts: create_account/AccountError; projects: add_member publiczne. Migracja 0006 (invites, quotas, usage_counters). Ustawienia public_url, invite_ttl_hours, smtp_*. Bez nowych zależności.
  • Reguły (#79): nowa C5-UTILITY v1 (context, info) — strona użytkowa/prawna/nawigacyjna (polityka prywatności, regulamin, disclaimer, cookies, kontakt, logowanie, koszyk, wyniki wyszukiwania, 404). Wyjątek „the page is a utility/legal page (...)” w answer_0_if reguł oceniających oryginalność, wartość, ekspertyzę i lanie wody: QR-COMMON v1 → v2, QR-WIKI v2 → v3, QR-SUMMARY v1 → v2, QR-HC-PADDING v1 → v2, QR-YMYL-NO-EXPERTISE v1 → v2, QR-YMYL-INEXPERT v1 → v2, QR-EFF v2 → v3 i QR-PARA v2 → v3 (obie wyłączone). QR-THINMC, QR-GIB i QR-NOMC już miały wyjątek; pozytywy (QR-HC-*) bez zmian. Powód: https://ikrk.pl/disclaimer/ dostawał QR-COMMON = 1,0 (High risk). Gemma: disclaimer QR-COMMON 0,00005, C5-UTILITY 1,0; artykuł otwartawarszawa.pl QR-WIKI 1,0, C5-UTILITY 0,00000004.
  • Raport (#80, zmiana kontraktu Verdict): stempel = Spam risk tylko z warstwy spam; nowe pola quality_level (high|medium|low|unknown|na), quality_label, quality_note — Quality risk (SQRG) pokazywany obok i nie podnosi stempla (ADR 0010); etykieta C5-UTILITY ≥ 0.5 → „Utility page — quality criteria not applicable”. report.HIGH/MEDIUM publiczne (panel rows.py bez duplikatu progu, #78). Panel: kolumny „Spam risk” i „Quality” w listach; filtr ryzyka/polityki, trafienia i licznik dashboardu liczone tylko ze spamu — historyczne raporty przeliczają się na nowo (wcześniej „High” przez sam sygnał jakości teraz spadają). Linia Quality risk ukryta, gdy brak wyniku.
  • Raport (#61, PR #77): zmiana kontraktu ReportView — evidence_not_requested: bool i nowy stan RuleView.evidence_state="not_requested" (trafienie ≥ 0.5 bez zebranych dowodów); szablon raportu (panel i eksport) pokazuje „Evidence not requested — rerun with “Evidence for hits””. Panel: POST /panel/rerun/{id} i przycisk „Rerun with evidence” (ten sam URL, reguły i profil, explain, ten sam projekt). Poprawki recenzji: next w POST /panel/project tylko jako ścieżka lokalna (bez //, \\, schematu/hosta), makra bez surowego HTML (empty_row(colspan, text, (label, href))), codeblock bez kolorów hex w szablonie, komunikaty błędów przez data-*. Issue #78: werdykt w ReportSummary.
  • Klucze API (#50, ADR 0015, kontrakt): nowy feature features/apikeys — klucze projektu gsj_<prefix>_<secret> (w bazie tylko HMAC-SHA256 z pepperem GSPAMJUDGE_APIKEY_PEPPER, domyślnie auth secret), GET/POST /v1/projects/{id}/keys, DELETE .../keys/{key_id} (odwołanie), klucz pokazywany raz; Authorization: Bearer gsj_... → Principal(kind="api_key", project_id), raporty do projektu klucza; zakresy judge, reports:read, jobs, projects:read (403 insufficient_scope), zły/odwołany klucz 401 invalid_api_key, limit per klucz token bucket w pamięci (GSPAMJUDGE_APIKEY_RATE_LIMIT=60/min, GSPAMJUDGE_APIKEY_RATE_BACKEND=memory; Redis osobno) → 429 rate_limited z Retry-After/RateLimit-*. accounts: Principal.key_id, Principal.scopes, register_principal_resolver. Migracja 0003 (api_keys). Po recenzji PR #73: klucz działa tylko na /v1 i nie loguje do panelu; CSRF sprawdza każde żądanie z cookie sesji (także z Authorization); /v1/projects/{id}/members → 403 dla klucza; walidacja backendu limitu i ostrzeżenie o losowym pepperze przy starcie; last_used_at co ≥ 60 s; accounts.MIN_SECRET publiczne; openapi.json przegenerowany. Bez nowych zależności (stdlib hmac).
  • Admin (#52, hardening): Settings jako allowlista (+ maskowanie SecretStr i nazw secret/key/token/password); osobny klucz sesji SQLAdmin (HMAC z auth_secret); is_superuser ustawiane na modelu w transakcji zmiany roli; zakaz dezaktywacji własnego konta; członkostwa w adminie walidowane jak w API projektów (tylko member, istniejący projekt/konto, bez duplikatów, owner nieusuwalny), projekty bez tworzenia i zmiany właściciela. Kontrakt: modele ORM usunięte z publicznych __init__ (accounts.UserRow/get_sessionmaker, projects.ProjectRow/MemberRow, storage.ReportRow) i przeniesione do features/*/admin_models.py (tylko dla admina). Bez nowych zależności.
  • Design: inwentarz design systemu dla agentów — docs/design/COMPONENTS.md (28 komponentów: markup daisyUI, stany, a11y, i18n, makra Jinja2 ui/_*.html), docs/design/STYLES.md (tokeny light/dark, typografia Inter bez monospace, odstępy, promienie, do/don't), docs/design/AGENTS.md, skill ui-components. Bez zmian w kodzie.
  • Admin (#52, ADR 0012): panel SQLAdmin pod /admin (src/gspamjudge/admin/, mount_admin w api/app.py) — widoki Users (bez hashy haseł, bez tworzenia/usuwania), Projects, Members, Reports (tylko odczyt, link do /history/{id}); dostęp przez sesję cookie panelu tylko dla admin/superadmin (anonim → 303 /login, user → 403); admin nie zarządza kontami admin+ ani nie nadaje ról admin+, superadmin tak; superadmin dodatkowo widzi (tylko odczyt) ustawienia bez sekretów i profile z profiles/*.yaml. Publiczne __init__ eksportują modele ORM dla admina: accounts.UserRow, accounts.get_sessionmaker, projects.ProjectRow/MemberRow, storage.ReportRow. Zależności: sqladmin (gotowy panel CRUD nad SQLAlchemy dla Starlette/FastAPI, ADR 0012), itsdangerous (wymagany przez SessionMiddleware, którego SQLAdmin używa do komunikatów).
  • Panel (#61, #54, ADR 0016/0017): przebudowa na design system Sanity — Tailwind 4.1.11 (browser build) + daisyUI 5.0.43 z CDN, wersje przypięte, bez kroku budowania i bez nowych zależności Pythona (Tailwind+daisyUI dają gotowe komponenty i motywy jasny/ciemny z tokenów DESIGN.md bez Node; produkcyjnie Tailwind standalone CLI w osobnym issue). Pico.css odpada. Nowe strony: dashboard /, /judge/new (URL/tekst/lista URL-i przez analyze_many z limitem batch_max, preset quick/full/custom, dowody), /history z filtrami (URL, projekt, ryzyko, polityka, daty; HTMX), strona raportu, /projects/{id} (oceny, członkowie, klucze API przez /v1/projects/{id}/keys), /jobs (placeholder #51), logowanie/rejestracja w zakładkach; przełącznik EN/PL (cookie lang, katalog PL w #60) i motywu; komponenty jako makra panel/templates/ui/_*.html zgodne z docs/design/COMPONENTS.md. features/report/templates/report.css przestylowany tokenami Sanity (fallback jasny w eksporcie offline), szablony raportu bez zmian.
  • Dokumentacja: ADR 0018 (dokumentacja i strona produktu tylko na darmowych narzędziach: MkDocs Material + Scalar, EN/PL; strona na Grav CMS 2); komentarz w #55, issue #69.
  • Dokumentacja (#55): publiczny serwis MkDocs Material (free) w docs/site/ + mkdocs.yml — Home, Quickstart, Authentication, Concepts, Webhooks/Jobs (placeholder #51), API reference (Scalar z openapi.json z scripts/export_openapi.py), katalog reguł generowany scripts/docs_rules.py (hook docs/hooks.py regeneruje oba przed budową), Errors, Versioning, Changelog; EN domyślnie, PL przez mkdocs-static-i18n; kolory Sanity, Inter także dla kodu; mike przygotowany. Nowy extra docs: mkdocs<2 (2.0 łamie wtyczki), mkdocs-material, mkdocs-static-i18n — darmowe, open-source, statyczna strona bez Dockera. FastAPI /docs bez zmian (opis z odesłaniem). ADR-y i dziennik nie są publikowane. docs/api/errors.md i versioning.md przetłumaczone na angielski (ADR 0017), polskie w *.pl.md; strony Errors/Versioning dołączają je snippetem (jedno źródło). Scalar przypięty do 1.74.0.
  • Ingest/judge (#63, kontrakt): Link.in_content (anchor w tekście głównym, poza nav/header/footer/aside) i Link.external (inny host; dla --html z canonical/og:url), linki z treści mają pierwszeństwo w MAX_LINKS; document.md rozdziela LINKS IN MAIN CONTENT i OTHER LINKS (limit 60 — przy 25 spadał SP-LNK-ANCHOR); PROMPT_VER p3 → p4 (kalibracje p3 nieważne). Bez nowych zależności.
  • Design: system Sanity (Open Design) w docs/design/DESIGN.md; makiety panelu EN/PL w docs/design/mockups/ (Tailwind 4.1.11 + daisyUI 5.0.43 z CDN, motywy gsj-light/gsj-dark, bez fontów monospace); README z tokenami, komponentami, mapą widok→trasa→szablon i podejściem i18n (Jinja2 + Babel). Bez zmian w kodzie.
  • Reguły (#66): C4-PURPOSE v3 → v5 przepisana wg Google „Creating helpful, reliable, people-first content” (policy: People-first content; jedno zjawisko: treść dla czytelnika, nie pod frazy; v3 dawał 1,0 na każdej stronie, także na serwisie z ręcznym działaniem za spam; v4 tylko w PR). Nowe (v1): QR-HC-PADDING, QR-HC-NO-ANSWER, QR-HC-SLOPPY, QR-HC-FAKE-AUTHOR v2 (violation), QR-HC-FIRSTHAND, QR-HC-SOURCED, QR-HC-AUTHOR-INFO (positive) oraz SP-SCR-HIDDEN-SOURCE v2 (Scraping: źródło podane, link ukryty w data/base64/JS — zgłoszenie z zeszczecina.pl). Sekcje linków zgodnie z promptem z PR #64 („LINKS IN MAIN CONTENT” / „OTHER LINKS”, kolumna external), podbite: QR-FAKE-BUTTON v2, QR-YMYL-NO-OWNER v2, SP-HID-CHAR v3, SP-HID v3 (wyłączona), SP-LNK-GIFT v2, SP-LNK-PAID v2, SP-LNK-TEXTAD v2, SP-LNK-UNLABELED v2, SP-MAL-DOWNLOAD v2, SP-MIS-TOOL v2. not_assessed.yaml: +5 pozycji helpful content (sztuczna świeżość, cechy serwisu, ujawnienie AI, zdjęcia autorów, porównanie z SERP). Włączone reguły: 99 → 107.
  • Dokumentacja: ADR 0016 (UI: design system Sanity na Tailwind + daisyUI, bez monospace; zastępuje Pico.css z 0003) i ADR 0017 (English-first, i18n EN/PL); issues #60, #61; PR #58 zamknięty.
  • API (#46, kontrakt): jeden format błędów /v1/* — ErrorResponse{error:{code, message, details, request_id}} dla 400/404/405/413/422/500 oraz 502 fetch_failed dla błędów pobierania strony (wcześniej {"detail": ...}); kody unknown_rule, unknown_profile, report_not_found, batch_too_large, validation_error, internal_error i in. (docs/api/errors.md); nagłówki X-Request-ID i X-API-Version: v1; /v1/judge sprawdza reguły/profil przed analizą; polityka wersjonowania i deprecjacji (Deprecation/Sunset, 6 mies.) w docs/api/versioning.md. Nowe ustawienie GSPAMJUDGE_EXPOSE_ERRORS (domyślnie false: ogólny komunikat 500/502). Bez nowych zależności.
  • Raport (#53): nowy feature features/report — ReportView + werdykt liczony w kodzie (progi 0.5/0.8 na rankingu surowym), jeden zestaw szablonów Jinja2 (partiale: werdykt, etykiety, naruszenia wg polityki, dowody ✓/✗/“No evidence”, sygnały pozytywne, “Not assessed”, disclaimer) po angielsku z i18n (jinja2.ext.i18n + gettext, build_view(lang=), locales/messages.pot) i neutralny wymienialny report.css; używany przez panel, GET /v1/reports/{id}/export.html|.pdf i gspamjudge export <id> --format html|pdf. Nowa zależność weasyprint (HTML/CSS -> PDF w Pythonie, ten sam szablon co panel, bez przeglądarki headless); Dockerfile instaluje pango/harfbuzz i fonty DejaVu. babel w extra dev (tylko ekstrakcja/kompilacja katalogów; runtime to stdlib gettext). Dowody z typowanego RuleScore.evidence (#57), kolumna „Evidence”. Panel renderuje raport tym szablonem (templates/report.html, _scores.html panelu nieużywane).
  • Konta i projekty (#48, #49, ADR 0012): nowe feature'y features/accounts (fastapi-users: /v1/auth/register, /v1/auth/jwt/login|logout dla API, /v1/auth/cookie/login|logout dla panelu, /v1/auth/forgot-password|reset-password, /v1/users/me, GET /v1/users i PATCH /v1/users/{id}/role dla admina; role user|admin|superadmin; zależności current_principal, require_role; abstrakcja Principal pod klucze API #50; CLI gspamjudge create-superadmin --email) i features/projects (CRUD /v1/projects, członkowie owner/member, brak dostępu = 404). Zmiana kontraktu: wszystko poza /health i /v1/rules wymaga logowania (401); JudgeRequest/BatchRequest + project_id (albo nagłówek X-Project-ID; brak = domyślny projekt użytkownika); /v1/reports zawężone do projektów użytkownika (admin widzi wszystko); storage: ReportSummary.project_id, save_report(report, project_id), list_reports(..., project_ids), eksport db_url; pipeline.analyze/analyze_many(..., project_id). Migracja 0002: users, projects, project_members, reports.project_id (NULL dla starych raportów). Panel: /login, /register, przekierowanie niezalogowanych, wybór i tworzenie projektu; nowe teksty po angielsku w _() (jinja2.ext.i18n). Ustawienia GSPAMJUDGE_AUTH_SECRET, GSPAMJUDGE_AUTH_COOKIE_SECURE, GSPAMJUDGE_AUTH_TOKEN_TTL. Zależności: fastapi-users[sqlalchemy] (gotowa rejestracja/logowanie/reset, haszowanie argon2/bcrypt przez pwdlib, JWT i cookie — ADR 0012), aiosqlite (async sterownik SQLite wymagany przez adapter fastapi-users; Postgres przez async psycopg, bez nowej zależności); htmx-ext-json-enc 2.0.1 z CDN (formularze JSON do REST). Po recenzji PR #65: eksport /v1/reports/{id}/export.html|.pdf zawężony do projektów (404 report_not_found); usunięcie projektu ustawia project_id = NULL w jego raportach (storage.detach_project, jedna transakcja), projects z sqlite_autoincrement; GSPAMJUDGE_ENV (poza dev sekret ≥ 32 znaki albo błąd startu); GSPAMJUDGE_REGISTRATION_OPEN (domyślnie false) i POST /v1/users (admin); CSRF dla mutacji z cookie (Origin/Referer, 403 csrf_failed), cookie SameSite=Lax, POST /panel/project; rola owner niezmienna przez API członków; superadmin nie zmienia własnej roli; UserUpdate bez is_superuser; URL bazy normalizowany make_url (postgresql:// → psycopg 3). Błędy kont/projektów w formacie ErrorResponse (#46): unauthorized 401, forbidden 403, not_found 404, report_not_found; komunikaty po angielsku (ADR 0017).
  • Dokumentacja: ADR 0012 (konta, role, projekty, SQLAdmin), 0013 (Postgres + Alembic, kolejka Redis/arq), 0014 (PR z recenzją reviewer; CONTRIBUTING i skille worktree-workflow, forgejo-issues), 0015 (API-first); kamień milowy M3.5 i issues #46–#55.
  • Infra (#47): docker-compose.yml z postgres:16 (wolumen pgdata) i redis:7 z healthcheckami, aplikacja depends_on healthy, GSPAMJUDGE_DB_URL (Postgres) i GSPAMJUDGE_REDIS_URL w compose, port hosta GSPAMJUDGE_PORT; alembic w features/storage/migrations (migracja 0001 = tabela reports), gspamjudge migrate i entrypoint kontenera migrate && serve; SQLite migruje się sam, stara baza stemplowana jako 0001. Nowe ustawienie redis_url, nowe API storage upgrade_db. Zależności: alembic (standard migracji dla SQLAlchemy, zamiast create_all), psycopg[binary] (sterownik Postgresa bez kompilacji w obrazie slim). GSPAMJUDGE_DB_URL/GSPAMJUDGE_REDIS_URL w compose nadpisywalne. Redis na razie bez użycia — worker i arq w #51.
  • Storage (#19): nowy feature features/storage — save_report/get_report/list_reports, tabela reports(id, created_at, url, profile, prompt_ver, payload JSON); pipeline.analyze zapisuje każdy raport (judge, batch, panel, CLI); GET /v1/reports[?limit,url], GET /v1/reports/{id}, panel /history. Ustawienia GSPAMJUDGE_DB_URL (domyślnie sqlite:///runs/gspamjudge.db, w Dockerze wolumen ./runs; Postgres po URL) i GSPAMJUDGE_STORE_REPORTS. Nowa zależność sqlalchemy>=2.0: SQLAlchemy 2 zamiast SQLModel — jeden pakiet (SQLModel i tak go ciągnie), typowane Mapped[] wystarczają, kontrakty zostają w pydantic.
  • Reguły (#41, #42, #44): nowa SP-LNK-UNLABELED v1 (Link spam) — nieoznaczony artykuł sponsorowany: jeden zewnętrzny biznes, komercyjny anchor exact-match bez rel sponsored/nofollow, temat obcy serwisowi (SP-LNK-PAID wymaga jawnej deklaracji). SP-HID-CHAR v1 → v2: wyjątki dla separatorów nawigacji/okruszków (|, ·, /, ») i paginacji. QR-WIKI v1 → v2: jawne ślady kopiowania z Wikipedii (dopisek „Źródło: Wikipedia”, przypisy z artefaktami szablonów „a b c”, „[dostęp …]”, podwojone inicjały autorów, infobox). Tabela reguł w README zregenerowana (była nieaktualna). Włączone reguły: 98 → 99.
  • Evidence (#15, ADR 0011): ocena i dowody w dwóch krokach. Krok 1 zawsze krótkim promptem 0/1 (explain nie zmienia score_raw, prompt_ver zawsze p3, ścieżki p2e/p3e wycofane); nowy feature features/evidence — dla reguł kind=violation z score_raw >= GSPAMJUDGE_EVIDENCE_THRESHOLD (0,5) cytaty ze strony (ten sam prefiks, bez etykiety, EVIDENCE_PROMPT_VER e1) weryfikowane kodem względem treści strony — tytuł + main_text + linki + ukryte, bez etykiet szablonu (difflib, bez nowych zależności). Kontrakt: RuleScore.evidence: Evidence{quotes[{text, verified}], none, supported, prompt_ver, error} | None, RuleScore.unsupported: bool | None; justification/contradiction zawsze null; usunięto JudgeInput.explain; Profile.explain_max_tokens → evidence_max_tokens (150); judge eksportuje build_messages, render_document. Panel: cytaty ✓/✗ i plakietka „bez dowodu”; scripts/show_report.py też. JudgeReport.labels/positives mają domyślnie [], a raporty sprzed ADR 0010 (pole context) wczytują się jako labels — zapisane w storage raporty znowu się ładują.
  • Rodzaje reguł (#45, ADR 0010): kontrakt Rule.kind: violation|info|positive (brak w YAML → context = info, reszta = violation); C1–C3 kind: info, C4-PURPOSE kind: positive v3 („Does the page fulfil a clear purpose… 1 = yes”). RuleScore.kind; JudgeReport.context zastąpione przez labels (info) i positives, violations/quality_signals tylko violation. Szablony answer_positive_*, system.md, PROMPT_VER p2 → p3 (stare kalibracje nieważne). contradiction tylko dla violation. Panel: czerwony/zielony pasek, etykiety info jako tagi; show_report.py, rules_table.py (kolumna Rodzaj), CLI rules pokazuje kind.
  • Poprawki MVP (fix/mvp-polish): panel — pole „reguły” (id po przecinku) i preset szybki (~15 reguł spamu) / pełny; error w batch API/CLI i panelu przycięty do 300 znaków; przykład items[] w README API; obraz uv przypięty do 0.13.0; docs/deploy.md — ./runs dla uid 1000 na Linuksie.
  • Docker (#22): Dockerfile (python 3.12-slim + uv, non-root), .dockerignore, docker-compose.yml (port 8077, env_file .env/.secret/.env, wolumen ./runs, healthcheck /health); instrukcja w docs/deploy.md. Bez nowych zależności Pythona.
  • API (#31): POST /v1/judge/batch — BatchRequest{items[], rules?, profile?, explain?} → BatchResponse{results[]{index, report|null, error|null}}; limit GSPAMJUDGE_BATCH_MAX (10) → 413 z odesłaniem do /v1/jobs, strony równolegle wg GSPAMJUDGE_BATCH_CONCURRENCY (2), częściowe błędy per strona; pipeline.analyze_many; CLI gspamjudge batch <plik-z-url> [--out x.jsonl].
  • Judge (#32): jawna polaryzacja odpowiedzi — naruszenia „1 = problem JEST na stronie, 0 = NIE MA”, pytania kontekstowe „1 = tak, 0 = nie” (system prompt + szablony answer_* wybierane po warstwie); PROMPT_VER p1 → p2 (explain p2e), stare kalibracje nieważne. Kontrakt: nowe pole RuleScore.contradiction: bool | None (explain; heurystyka consistency.py), pokazywane w panelu i scripts/show_report.py. CLI judge kończy się kodem 1, gdy któraś reguła ma ok=False (JSON nadal wypisany).
  • Reguły (#6): katalog atomowy — jedna obserwowalna praktyka = jedna reguła. Włączone reguły: 21 → 98 (48 spam, 46 quality, 4 context). Dodano 46 reguł SP-<POLITYKA>-<PRAKTYKA> (praktyki z list Google) i 45 reguł QR-* (kryteria SQRG, w tym warunkowe YMYL/płatności: QR-YMYL-NO-OWNER, QR-TRANS-NO-OWNER, QR-YMYL-INEXPERT, QR-YMYL-NO-EXPERTISE, QR-YMYL-THIN-OWNER, QR-TRANS-THIN-CONTACT oraz QR-DECEPTIVE-PURPOSE), wszystkie v1. Wyłączono (enabled: false, v2) SP-HACK, SP-HID, SP-KWS, SP-LNK, SP-MIS, SP-SCA, SP-SCAM, SP-SRP, SP-UGC, QR-AIA, QR-ADMC, QR-EFF, QR-GIB, QR-PARA. Podbito QR-TPL v2, SP-AFF v2, SP-EXP v2, C1–C4 v2 (pytania kontekstowe oznaczone „Context question, not a violation”). Źródła: kotwice polityk Google i publiczny URL SQRG ze sprawdzonym #page=N. not_assessed.yaml: 22 pozycje z powodami. Tabela pokrycia w features/rules/README.md. Uwaga: pełny przebieg = 98 zapytań na stronę.
  • Proces: definicje subagentów w .agents/agents/ (feature-builder, rule-author, evaluator, reviewer, docs-keeper, researcher), skille w .agents/skills/ (judge-run, rule-authoring, forgejo-issues, worktree-workflow, feature-scaffold, log-and-decide), symlink .claude/agents; zasady gałęzi i worktree w CONTRIBUTING.md.
  • Dokumentacja: ADR 0007 (cztery produkty nad jednym potokiem, batchowanie POST /v1/judge/batch + /v1/jobs, cena za stronę) i ADR 0008 (tryb explain, prompt p1e); issue #31, #32 (#18, #30).
  • Dokumentacja: AGENTS.md dla agentów w każdym katalogu feature'a/wejścia (core, rules, ingest, inference, judge, api, panel, profiles), zakaz zagnieżdżonych CLAUDE.md; README główne z drzewem projektu, tabelą reguł i sekcją „Zasady pracy”; skrypt scripts/rules_table.py generujący tabelę reguł.

0.1.0 - 2026-10-13

Szkielet MVP. - Feature rules: katalog 21 reguł w YAML (12 polityk spamu Google, 6 kryteriów SQRG 4.6 / Lowest, 4 pytania kontekstowe), lista „nie oceniono”. - Feature ingest: URL / HTML / tekst → Document (trafilatura, linki z rel, elementy ukryte stylem inline, sygnały JS), budżet tokenów. - Feature inference: klient OpenAI-compatible z logprobs/top_logprobs, profile w profiles/ (OpenRouter Gemma 4 bf16, OpenRouter Qwen 3.7 Flash, lokalny vLLM). - Feature judge: jedno zapytanie 0/1 na regułę, dokument jako wspólny prefiks, score_raw/z/coverage/top1_other, surowy log w runs/rawlog/. - CLI (gspamjudge judge|rules|profiles|serve), REST API POST /v1/judge, GET /v1/rules, panel Jinja2 + HTMX. - Dokumentacja: AGENTS.md (+ symlinki CLAUDE.md, GEMINI.md), CONTRIBUTING.md, ADR 0001–0006, dziennik pracy, raporty z researchu.