Changelog¶
Format: sekcja Unreleased na bieżące zmiany; przy wydaniu zamieniana na ## X.Y.Z - RRRR-MM-DD.
Unreleased¶
- Cennik v4
2026-10-v4od 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 pozycjastealth$0.0125 (tylko na żądanie, zamiast renderu;credits_per_page(stealth=True)); strona www, docs/site, legalbilling2026-10-15.1 (przykład ceny). v3 wdocs/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_VERe2) dostaje fakty i piszeFIX:wreport_lang(Evidence.fix,fix_lang); raport pokazuje „Co zrobić na tej stronie” (RuleView.action,ActionPage.action) i poziom + siłę sygnału 0-100 zamiast0.995/„probability” (także noty werdyktu i disclaimery). Kontrakt: nowe polaEvidence,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,
.envstaging i prod), limitmax_cost_usd_per_pagespider.cloud 0,005 liczony od sumy prób u dostawcy na stronę,cost_ledger.fetch_provideriengine(migracja 0033, expand); kontrakt:CostEntry.fetch_provider,CostEntry.engine,core.meter.add_fetch(..., provider). - Kontrakt API (#479): opcjonalne
report_lang(en|pl, inne = 422) wPOST /v1/judge,/v1/judge/batch,/v1/jobs,/v1/audits/site,/v1/intent,/v1/intent/batch(/csv); brak = Accept-Language, inaczejen. 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/registerzawsze 202RegisterAccepted{status: pending, message}(było 201UserRead/400registration_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 wdeploy/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 - kolumnaevents.once_keyz 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 zWebhookEvent.id(job_…:job.finished) do deduplikacji (#443). - analytics:
emit(..., once=True)zapisuje najwyżej jeden wiersz na (event,request_id); worker używa go dlaaudit.finished/failed, więc ponowienie rozliczenia nie duplikuje zdarzeń (#439). - intent:
unsettled_refssprawdza „cena wymaga korekty” w SQL przedLIMIT 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_atzostaje NULL); kontraktOnDonedostajeretry- 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_creditsz 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 nieaktualnegoused(#429). - audits: błąd zwrotu fraz (
refund_failed_intents) wfinalize_job_auditspropaguje zamiast być połykany - zadanie nie dostajesettled_atprzy aktywnym holdzieintent:<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ł tylkojob:<id>); 0029 nie była wdrożona, poprawka w samej migracji (#427). -
api/audits (kilka feature'ów):
POST /v1/jobsztargetswycenia i rezerwuje frazy (holdintent:<job>) wspólną funkcjąaudits.reserve_intents, jak audyt witryny; mapa fraz w audycie zadania, zwrot nieudanych wfinalize; zły rynek frazy = 422invalid_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,settlezwraca różnicę i obniżaprice) i pojedyncza ocena (intent.charged_credits) (#413). -
jobs/tiers (kontrakt bazy, migracja 0029):
jobs.settled_atiupgrade_runs.settled_at- trwały stan rozliczenia holda; zakończone bez rozliczenia (awaria zwrotu, restart) ponawiarun_job/jobs.sweep_unsettleditiers.sweep_stale_runs(cron workera co 10 min); nowejobs.sweep_unsettled(on_done)(#418). -
judge/pipeline/api (zmiana kontraktu, kilka feature'ów): zero udanych reguł = błąd strony -
JudgeReport.scored,pipeline.analyzerzucaJudgeFailed, API 502inference_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 gorecord_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 zusage.credits_per_page; dopłaty liczą deltę od tej ceny;pricing_checksprawdza pary trybów (#411). - intent: koszt DataForSEO trafia do
cost_ledgerz polacostodpowiedzitask_post/live(add_fetch(cost)); polling nie jest liczony jako pobranie (#415).
0.1.6 - 2026-10-14¶
- inference:
redact_textmaskuje URL, któregourlsplitnie parsuje (np. nazwa projektuhttp://[), zamiast rzucaćValueError(500 przy liście projektów) (#397). - core/rawstore: backend
localprzy błędzie zapisu obcina segment do rozmiaru sprzed próby - ponowienie nie zostawia uszkodzonego członu gzip (#398). -
storage:
upgrade_dbtrzyma jedną blokadę procesu i pamięta zmigrowane URL-e; równoległa inicjalizacjastorage.get_engine()iaccounts.get_sessionmaker()nie uruchamia dwóch migracji alembica (#399). -
API (zmiana kontraktu): błędy
/v1/auth/*iPATCH /v1/users/memają stabilne kody (registration_failed,email_unavailable,invalid_password,invalid_credentials,account_not_verified,invalid_token) i komunikaty Ani EN/PL zamiastErrorCode.*; 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 (zamiastlru_cache); pierwsze zadanie workera nie gubi już strony zKeyError: '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 wtests/e2e/test_submit_lock.py(#385). - API (zmiana kontraktu):
GET /v1/ruleswymaga logowania lub klucza z zakresemjudge; zwykły użytkownik dostajeRuleBrief(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_limitedz komunikatem Ani EN/PL; panel pokazuje go przy logowaniu; ustawieniaGSPAMJUDGE_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 przezuv run --with(bez nowej zależności). - security (#302): lokalne testy bezpieczeństwa;
inference.redact_textmaskuje URL-e dostawcy zbase_urlprofili (błąd 401 dostawcy ujawniał host werrorreguły),core.netguard.resolveodrzuca 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_publicwdocs/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-healthdla API rozliczanego per token (GEThealth_urlz profilu, 2xx = gotowy, błąd = niedostępny; bez workerów i strażnika); profilopenrouter-gemma4z blokiemtelemetry; na serwerachGSPAMJUDGE_ENGINE_PROFILEpuste, 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.2bez wzmianek o CDN (statyki panelu i strony z własnego serwera, #331), nowy wierszgsj_notices(zamknięte komunikaty o stanie usługi, 30 dni); polityka rozliczeń2026-10-14.2z 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 przegenerowanasync_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-mutedbez opacity; przełącznik językaaria-currentzamiastaria-pressed, przełącznik motywu zaria-pressed; radio-taby wrole=radiogroup(logowanie, nowe sprawdzenie, audyt);<main>w logowaniu/rejestracji,aria-invalidpo błędnym logowaniu;langw/docsi/redoc, 404 HTML z<title>dla przeglądarki; SQLAdmin: kontrast Logout; www (Grav) ciemny:--linkdla nav/hover/FAQ/fokusu,--faint#8a8a8a,--field-linedla 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 zdocs/design/STYLES.md; usunięta wzmianka o logprobs ze strony startowej i Concepts. - intent (#346, zmiana kontraktu): poziomy Intent
basic(dawnylite, „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/fullprzyjmowane jako aliasy, inne wartości 422validation_error, niedostępny poziom 422tier_unavailable,compare_urltylko ztier=compare; stare raportylitezwracane jakobasic. Nowe polafreshness(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_cardztag/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, etapcssw Dockerfile) z modułówpanel/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 zdefer; linki?v=<sha>+Cache-Control: public, max-age=31536000, immutable(także/report.css,/audit.css,/intent.css); CSP/share/*tylkoself. 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.csspusty (treść wpanel/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 Twigasset_v, cache 30 dni nie trzyma starych stylów); poprawioneautocomplete/maxlengthw formularzu (bez dosłownych cudzysłowów). - panel/audits (#345, rozszerzenie kontraktu, zgodne wstecz): kreator
/audits/neww 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, adapterqueue-serverlesswybierany blokiemtelemetryprofilu, adresy i id endpointu z env; tylko bezpłatne odczyty health/endpoint/billing) w tabeliengine_samples(migracja0028, takżeannouncements,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; publicznyGET /v1/status(up/warming/busy/degraded, komunikat w stylu Ani EN/PL,eta_s, aktywne komunikaty, cache 5 s) i nagłówekX-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; CLIgspamjudge 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_costinformacyjnie; ceny tryby audytu Spam $0.025 / Quality $0.025 / Helpful $0.015 za stronę (nowe pozycjemode_*), 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 wdocs/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
modew YAML (aliasRule.category, poza promptem, bez podbiciaver; C4-PURPOSE helpful -> quality, konteksty = Quality);/judge/newi kreator audytu: karty trybów (checkboxy, wszystkie = pełny audyt) z opisem jednym zdaniem i ceną za stronę z cennika (obecna logikacredits_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(aliascategories), zgodne zrules/presetami, OpenAPI zaktualizowane; raport HTML/PDF/share i raport witryny: sekcje Spam / Quality / Helpful content z opisem, tylko uruchomione tryby (ReportView.modes); makrocheck_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 serveufaX-Forwarded-Proto/For/Hosttylko odGSPAMJUDGE_TRUSTED_PROXIES(preset: Caddy w sieci edge) -/admini linki zrequest.base_urlpo HTTPS zamiasthttp://(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); stagingcpu_shares256 i limity CPU, status/observability małe limity CPU;scripts/deploy/cleanup.shpo 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(syncuser/z commita, Sendy id, kopia do S3), Caddywww-staging.askaniela.com(basic auth, noindex, admin/API 404), siećaskaniela-www-edge; runbook przełączeniadocs/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;profilew/v1/judge|judge/batch|jobs|audits/sitetylko dla superadmina, innym ignorowany po cichu i usunięty z OpenAPI; odpowiedzi JSON (RedactedJSONResponse,inference.redact) bezprofile.name/model/quant,ReportSummary.profile,FeedbackRead.profile, kluczyprovider; nazwy zprofiles/*.yamlw 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żaniescripts/refresh_markets.py); panel: dwa pola z wyszukiwaniem (komponentsearch_select, pamięta ostatni wybór) w nowej kontroli, Search Intent i playgroundzie; formularz Intent pokazuje tylko pole wybranego trybu; API:marketwalidowany wobec list (422), nowe polalocation_code+language_code(JudgeRequest, IntentRequest/IntentItem, UpgradeRequest); DataForSEO obsługuje wszystkie kraje z listy. -
infra (#301): zaszyfrowana kopia
.secret/wsecrets/(SOPS + age, klucz w Passbolt), skryptyscripts/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.txtDisallow: /, blokada botów po IP Google i User-Agent (scripts/deploy/google_ranges.py,deploy/caddy/);edge.shkopiujedeploy/caddy/*.caddy. - deploy (#295):
deploy/compose.observability.yml- agenci Beszel i Dozzle (128 MB, bind tylko na IP NetBird) dla huba na dgx-p2p; runbookdocs/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 przezuv 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_HASHjako 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 wdocs/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 zfacts.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/+ skillmarketing-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.svgw katalogu głównymdist(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), APIPOST /v1/intent(+/batch,/batch/csv),GET /v1/intent[/{id}], eksporty; tabelaintent_reportsishare_links.intent_report_id(migracja 0027), iditr_/itb_; cenaintent_liteza 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 profilus2); review:intent_reports.started_at, warunkowe zmiany stanu (bez failed -> done), w projekcie prywatnym brak linków publicznych do raportu intencji (403private_project), CHECKck_share_links_one_targettakż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, nadpisaniaog_*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=300w_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 zfill/stroke=nonena wypadek braku CSS;lead-formobsł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) wcompany.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 projektybeta-termsicookies2026-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-soonbuduje tylko stronydraft: falsei przerywa, gdy baner projektu trafi dodist/(sitemap bez projektów). - Strona: rozbudowany przykładowy raport
#samplena 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 matterhome.{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, migracja0026_leads_sendy),core(ustawieniasendy_url,sendy_api_key,sendy_list_beta- także bez prefiksuSENDY_*- isendy_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.yamlsignup.target: sendy|app,fetchno-cors, stan „sprawdź skrzynkę”, CSP z originem Sendy, lista zSENDY_LIST_BETAprzy buildzie), strony/beta/check-emaili/beta/confirmedEN/PL (noindex), mail potwierdzający i powitalny wwebsite/emails/(szkice do akceptacji); app: leady beta ze zgodą -> Sendy API (poza dev), status w/console/leads;build_static.shrobi kopiex/index.html(czyste URL-e na Bunny),scripts/deploy/bunny_edge.py(nagłówki i przekierowania z_headers/_redirectsjako 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|prodz presetemcore/envpresets.py, nowe ustawieniaapi_host,site_url,docs_url,stripe_mode,auth_cookie_name(devgsj_session, prodani_session, stagingani_session_stg;accounts.COOKIE_NAMEz ustawień); błąd startu: nieznaneENV,STRIPE_MODE=livepoza prod, raw stores3bez endpointu/bucketu poza dev): hostapi.askaniela.comtylko z/v1,/openapi.json,/docs,/redoc,/health(api/apihost.py); Dockerfile dwuetapowy (git tylko w builderze - naprawia build, kopiujefetchers/, HEALTHCHECK, bez roota);docker-entrypoint.sh:GSPAMJUDGE_SKIP_MIGRATE=1(stosy VPS migrują jednorazowym jobem wdeploy.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, kopiepg_dump | agedo Hetzner Object Storage z retencją),scripts/deploy/(deploy, rollback, smoke, backup, restore, restore_test, edge,publish_static.pydla 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 kontraktuFetcherProfile.envs:list[Literal[dev,staging,prod]], domyślnie[dev, staging, prod]): staging używa dostawców jak prod (własne klucze, niski cap),kind: directpoza[dev]odrzucany przy walidacji, nieznaneGSPAMJUDGE_ENV=ValueErrorzamiast pustego łańcucha. - Migracje działają na świeżym Postgresie 16 (#253, zmiana kontraktu
legal:Acceptance.org_idiLegalAcceptanceRow.org_idint (pkorganizations, poza JSON) zamiast UUID): poprawione0016(org_idInteger) i0024(unikalny indeksix_leads_public_id), nowa0025_legal_org_id_intdla 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ówcore(ustawienialeads_allowed_origins,leads_rate_per_hour, prefiksled),analytics(zdarzenialead.created,lead.rejected), migracja0024_leads):POST /v1/public/leads(beta|sales, honeypot, limit per IP, Origin z allowlisty), CORS tylko/v1/public/*(starletteCORSMiddleware), konsola/console/leads(filtr, CSV przezcore.csvsafe, „zaproszony”);website/scripts/build_static.sh(--mode coming-soondomyślnie,--mode full) ->website/dist/z_headers(CSP) i_redirects, strona „Coming soon” EN/PL, formularzefetch()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/rawstorei featureanalytics, zmiana kontraktówcore(ustawieniarawstore_*,analytics_enabled),inference(first_token(..., meta=)), migracja0023_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/rawalbo S3-kompatybilny R2/B2), indeksraw_objects; zdarzenia produktuevents(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/rawlogzostaje równolegle. Odpowiedzi evidence (cytaty) w moduleevidencez retencją treści stron; poza devGSPAMJUDGE_RAWSTORE_BACKEND=localblokuje start (wymaganys3albooff). - 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 - zmieniasha256, wersje bez podbicia), docs/site, GTM/marketing, skille gsj-slides/gsj-video, pola dla ludzi w regułach (bez pól promptu, bezver);scripts/ui_lint.pyostrzega na U+2014/U+2013 w szablonach i.po; zasada wdocs/design/AGENTS.mdi 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 zdocs/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łumodules.<moduł>.multiplierz podłogą ×2,0; cennik2026-10-v2: podłoga ×2,0 w pliku, batch = max(payg × 0,5, koszt × 2,0), strona www przegenerowana (sync_content.py, legalbillingpod adresembalance-refunds); dopisek do ADR 0027), wagiGSPAMJUDGE_CREDITS_WEIGHT_*/CREDIT_USDusunięte; wewnątrz int w jednostkach $0.0001 (core/money.py, migracja0022×100), na zewnątrz tylko*_usd:GET /v1/usageunit: "usd",used_usd/limit_usd/remaining_usd,balance_usd, planlimit_month_usd;Estimate.cost_usd/cost_per_page_usd; dopłatycost_usd,steps_usd,paid_usd,charged_usd,refunded_usd,reserved_usd,GET /v1/tiersprice_usd(stare pola kredytowe usunięte); 429quota_exceeded„Monthly limit of $20.00 reached…” zdetails.*_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ów2026-10-13.2), przewodnikguide/billing.md(zamiastcredits.md), copy GTM; obraz Dockera zawieradocs/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żetGSPAMJUDGE_BETA_MONTHLY_BUDGET_USD(0 = wył.) → 503beta_budget_reached„Beta paused…” EN/PL, alerty 50/80/100% (log/webhook/e-mail); limit testeraGSPAMJUDGE_BETA_TESTER_MONTH_USD=20 (centy wewnętrznie), superadmin ustawia limit orgu w USD w konsoli (quota.org_monthw dzienniku); maks. stron audytu betyGSPAMJUDGE_BETA_AUDIT_MAX_PAGES=200, maks. aktywnych zadań/audytów na orgGSPAMJUDGE_JOBS_MAX_ACTIVE_ORG=2; luki: panel audytu bez limitu kosztu, dopłata audytu bez limitu per strona — naprawione; FAQguide/beta.mdEN/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, przewodnikguide/pricing.mdEN/PL. Bez zmian w kodzie. - Portal API dla deweloperów (#218, nowe feature'y
requestlogiwebhooks, zmiana kontraktówcore(callctx, ustawieniaplatform_host/app_host/auth_cookie_domain/requestlog_days),usage(kredyty docore.callctx),accounts(domena cookie sesji), API): wejściepanel/platformpod/platformalbo 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/staticz przypiętą sumą sha256, z/openapi.json), playground (wewnętrzne/v1/judgekluczem albo sesją, żądanie/JSON/koszt); wszystkie kwoty w USD.request_log(metadane każdego wywołania/v1w orgu, bez treści i sekretów, retencja 30 dni w workerze), nagłówekX-Cost-USD(koszt netto wywołania),/v1/webhooks(punkty orgu, sekretwhsec_wyprowadzany zGSPAMJUDGE_WEBHOOK_SECRET,job.finishedz workera jako osobne zadania arqdeliver_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-onlygsj_session; migracja0021_platform; przewodnikguide/developer-portal.md. Bez nowych zależności. - Strona askaniela.com na Grav (#217): katalog
website/— motywaskanielawg 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 lokalnywebsite/data/beta-signups), dokumenty prawne generowane zfeatures.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 cennikadocs/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 saldotopup_multiplier2,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): obrazlscr.io/linuxserver/grav:2.2.6-ls286— oficjalnygetgrav/gravnie 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 projekturender_js_defaultoff (domyślnie) / on / auto (migracja0019_project_render_js, zakładka projektu „Settings”), nadpisanierender_js(true/false/auto) wPOST /v1/judge, pozycjach batch/jobs, audycie (js: true= on), kolumnie CSVrender_js, checkboksie formularza i kreatora, CLI--render-js; +1 kr. tylko gdyrendered=true(rezerwacja szacunku w kredytach po 1, zwrot różnicy wsettle; zadania przezJobCreate.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.configproduct_name/product_short/product_tagline/maker+brand()(globalbrandw szablonach) w panelu, e-mailach, eksporcie HTML/PDF, share, OpenAPItitle, admin, CLI;mkdocs.ymlsite_name/copyright,company.yamlSERVICE_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, opisdocs/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): interfejsingest.fetch(FetchRequest) -> FetchResult(html,body,final_url,status,rendered,provider,cost_usd,ms,attempts), dostawcy jako dane wfetchers/*.yaml(directtylko dev przezcore.safefetch,zyteprimary,zenrowsbackup,spider-cloudtrzeci,brightdatastub 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 doMeter.fetch_usd->cost_ledger.fetch_usd,Meter.renderedpod dopłatę +1 kr.;usage.js_rendering_available()= aktywny dostawca z renderem; sitemapy audytu przez ten sam interfejs (kind="sitemap"); ustawieniaGSPAMJUDGE_FETCHERS_DIR,GSPAMJUDGE_FETCHERS,GSPAMJUDGE_FETCH_MAX_ATTEMPTS; przewodnikguide/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: przydocument.truncatedQR-TRUNCiQR-HC-PADDINGnie są pytane, trafiają donot_assessedz powodem EN/PL (judge.service.TRUNCATION_BLIND). Presetaudit-quick/quick32 → 35 reguł: + QR-HC-PADDING, QR-HC-SLOPPY, QR-HC-SOURCED (Quick daje werdykt helpful); koszt wg wagfeatures.usage. Bez zmiany kontraktów iPROMPT_VER. - Dopłaty Lite → Basic → Premium i kategorie jako dodatki (#192, #190, #189 pkt 1, ADR 0023 dopisek, nowy feature
features/tiers, zmiana kontraktówstorage(replace_report, zrzuty treścireport_documents,get_document/store_document/purge_documents),judge(JudgeReport.with_document, pole prywatne),usage(release_hold),audits(refresh_audit, hold frazintent:<job>ze zwrotem),core(wagicredits_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 wgcredits_per_page(Lite 1, Basic 3, Premium 4), tylko brakujące reguły i kroki, ten samrep_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, holdupg:<id>), 409upgrade_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; migracja0018_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, 409busy), sweeper osieroconych holdówupg:w workerze (tiers_upgrade_stale_minutes=60, wpisadmin_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ściepanel/consolepod/consolew 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 przezusage.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, dziennikadmin_audit_logz 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, cookiegsj_impz losowym sid i sesją w tabeliimpersonation_sessions(tylko hashe, związana zgsj_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 → 403impersonation_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ę),/admini/consolezawsze zablokowane (impersonation_blocked), nigdy dla klucza API ani Bearer JWT; wpisyimpersonate.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_atiimpersonation_sessions(migracja0020_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_auditz filtrami (actionprefiks,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 kontrakturules,judge,ingest,inference,usage,audits,jobs,report,pipeline, API): opcjonalna fraza docelowa + rynek (query,marketwPOST /v1/judge, panelu „New check”, CLI--query/--market, kolumny CSVquery,marketw imporcie audytu →JobCreate.targets); SERP przezSerpProvider(GSPAMJUDGE_SERP_PROVIDER:fixturena 4 snapshotach intent-labeler wcontent/demo/intent/z atrybucją MIT;dataforseonapisany, wyłączony bez klucza i zgody), etykietowanie intencji przez intent-labeler z chatem zinference.complete_text(nowe: odpowiedź JSON bez logprobs), cacheserp_snapshots+intent_profiles(migracja0017_intent_cache, TTLGSPAMJUDGE_INTENT_CACHE_TTL_DAYS=7, tryb prywatny tylko odczyt),IntentFitwJudgeReport.intent;Document.elements/schema_types/word_count(ingestelements.py); pole regułycontext+JudgeInput.context(blok<search_intent>budowany kodem; bez frazy reguła w „Not assessed”); nowe regułyQR-NM(choice FailsM…FullyM, SQRG 13.2–13.6) iQR-HC-INTENT-MISMATCH; sekcja raportu „Search intent” z lukami formy (EN/PL); kredytyGSPAMJUDGE_CREDITS_WEIGHT_INTENT=2 za unikalną frazę (cache SERP 1, profilu 0,usage.intent_credits); przewodnikguide/search-intent.md. Nowa zależnośćintent-labeler(MIT, repo właściciela, przypięta do commitub2c1b78przez 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; hatchallow-direct-references. - Linki udostępnienia (#126, ADR 0021 pkt 8, nowy kontrakt
sharing): nowy featurefeatures/sharing— tabelashare_links(migracja0015_share_links; tokensecrets.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_namedomyślnie wyłączone, opcjonalne hasło:passwordtylko do zapisu,has_passwordwShareRead,PATCH /v1/share-links/{id}ustawia/zmienia/usuwa, hashhashlib.scryptze 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); APIPOST/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 404share_not_found; publiczna strona/share/{token}i/share/{token}/pdfbez 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 zapikeys.client_ip), nieznany/wygasły/odwołany/org zawieszony = ta sama 404; raport i audyt renderowane z flagąpublic=True(kontraktreport.render_*,audits.render_*: nowy parametrpublic), 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.htmldostał blokhead;core.logs.safe_pathmaskuje segment po/share/(i/s/), a log 500 wapi/errors.pyużywasafe_path. Przewodnikdocs/site/guide/sharing.md. Bez nowych zależności. - Teksty reguł dla ludzi (#168, zmiana kontraktów
rules,report,audits, CSV audytu):Rulema opcjonalnetitle/title_pl(≤ 60),summary/summary_pl(≤ 300),why/why_pl,fix/fix_pl(≤ 400) z walidacją długości iRule.copy_text(pole, lang); wszystkie 130 reguł (w tym 7 skal z #157 iC1-YMYL) dostały komplet tekstów EN/PL (B1, bez skrótów;fixz dokumentacji Google). Pola nie trafiają do promptu → bez podbiciaveriPROMPT_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|pli nowe kolumny na końcutitle,summary. Dokumentacja:reference/rules.md+ nowerules.pl.md(tytuł, opis, szczegóły „dlaczego / co zrobić” na regułę), akapit wguide/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 kluczaowner_type=project|user|org(dostęp = zakresy ∩ bieżące uprawnienia właściciela; klucz user ginie z członkostwem w orgu —apikeys.revoke_user_keysworgs.members.remove), nowy formatgsj_live_<prefix>_<secret><CRC32>z udokumentowanym regexem pod secret scanning (starygsj_<prefix>_<secret>działa),expires_at(domyślnie 90 dni, maks. rok, „never” tylko klucz orgu od owner/admin), rotacjaPOST /v1/keys/{id}/rotatez okresem łaski (domyślnie 7 dni,rotated_from), allowlista IP/CIDR (IPv4/IPv6;X-Forwarded-Fortylko odGSPAMJUDGE_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_idopcjonalne; nowe kodyapi_key_expired,ip_not_allowed,invalid_key_expiry,key_not_active; wpisyapikey.create|rotate|revoke|leaked|owner_leftwadmin_audit_log;core/scrub.pymaskujegsj_…,Authorizationi sekretne parametry we wszystkich logach (takżeuvicorn.access); ustawieniaGSPAMJUDGE_APIKEY_TTL_DAYS,GSPAMJUDGE_APIKEY_ROTATION_GRACE_HOURS,GSPAMJUDGE_TRUSTED_PROXIES;projects:list_projectsłączy członkostwo poPrincipal.user_idtakże dla klucza user (projektyrestrictedwł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.keysmaks. 128 znaków na element; panel: zakładka kluczy z wygaśnięciem, listą IP, ostatnim IP, rotacją i stanami (EN/PL). Migracja0014_api_key_protection. Bez nowych zależności (stdlibipaddress,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) igsj-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 wfeatures/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 matterversion/effective/draft, dane operatora (Zwinnie) wcompany.yamljako{{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_VERSIONz front matter (2026-10-13, wszyscy akceptują ponownie). Migracja0016_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 docsLegal. 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= aliasaudit-quickwrules/presets.yaml(credits_per_pagewfeatures.usage— jedyny wzór, także szacunek audytu; quick = doGSPAMJUDGE_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), 429quota_exceededzunitibalance; plany jako danefeatures/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 doorganizations.plan; saldo PAYGcredit_ledger(ważność 12 mies., pula planu → saldo, zasilenie tylko przezgrant_creditsz wpisemcredits.grantwadmin_audit_log; widoki Quotas/Usage/Credit ledger tylko dla superadmina; zwrot do wygasłego wpisu → najbliższy aktywny albo nowy wpisrefund);GSPAMJUDGE_QUOTA_USER_DAY50 stron → 150 kr.,_USER_MONTH→ brak (miesiąc liczy plan orgu). Migracja0013_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_VERp5 → p6; top-1 spoza wielkich liter opcji albo coverage < 0.5 = brak wyniku z błędemtop1_other/low_coverage); nowe regułyQR-PQ(9 stopni),QR-MCQ,QR-EEAT-EXPERIENCE/EXPERTISE/TRUST,C6-YMYL-LEVEL,C7-PAGE-PURPOSE(kategoria quality);C1-YMYLzostaje 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 losowepublic_id(prj_,rep_,aud_,job_,key_,fb_,inv_; orgiorg_od #124) — prefiks + 16 znaków base32 zsecrets(core/public_id.py, wspólne z orgs); int PK (i hex PK jobs/audits) zostaje wewnętrzny (pk/project_pkzexclude=True), stare ścieżki i nagłówki z int → 404.id/project_id/report_id(s)/job_idw odpowiedziach to stringi;?project_id=,X-Project-ID,project_idw ciałach i cookiegsj_projectprzyjmują tylkoprj_…;save_reportzwracarep_…,get_job(..., by_pk=True)tylko dla workera, nowejobs.read_job. Migracja0012_public_ids: kolumnypublic_idz backfillem i unikalnym indeksem (SQLite + Postgres), przepisaniejobs.results[].report_id,audits.report_idsifeedback.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 zlayer),category_of/filter_rules/parse_categories, presetypreset:spam|quality|helpfulispam-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: kolumnacategory(findings) ihelpful_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_foundiorg_suspended, odświeżonedocs/HANDOFF.md, drzewo projektu w README, wygenerowanedocs/site/reference/rules.mdiopenapi.json, notatka Obsidian. - Prywatność URL (#137, kontrakt
ingest,storage,projects,jobs,costs): nowy featurefeatures/privacy(normalize_url,strip_query,host_of,url_hash/url_refHMAC zGSPAMJUDGE_PRIVACY_PEPPER,mask_urldla publicznych linków #126, tryb prywatny w ContextVar).IngestInput.urlnormalizowany przy przyjęciu (bez#fragmentui parametrów z konfigurowalnejGSPAMJUDGE_PRIVACY_STRIP_PARAMS: utm_*, gclid, fbclid, msclkid, session/sid, token, key, auth, sig, password, email, code…), oryginał tylko w nieserializowanymfetch_urldo pobrania;reports.host+reports.url_hash,ReportSummary{host, url_hash},list_reports(url=)po hashu; projektprivate_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; promptURL:bez query (PROMPT_VERp4 → p5); retencjaGSPAMJUDGE_RETENTION_DAYS(30): cron workera igspamjudge retentionusuwają html/text zadań, wyniki zostają; nota prywatności EN/PL zaktualizowana; audyt witryny w trybie prywatnym zapisuje waudits.metastrony jako host; pozaGSPAMJUDGE_ENV=devstart API/workera wymagaGSPAMJUDGE_PRIVACY_PEPPER. Migracja0011_url_privacy. Bez nowych zależności. - Organizacje (#124, ADR 0021, zmiana kontraktów): nowy feature
features/orgs—organizations(public_idorg_…, 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, cookiegsj_org) i/org(członkowie, role; EN/PL). Izolacja:core.tenancy.TenantScopejedynym filtrem w repozytoriach —storage.get_report(id, scope),list_reports(scope, limit, url),jobs.get_job/list_jobs,audits.get_audit/list_audits,apikeyslist/revoke,feedback.list_feedbackprzyjmująTenantScopezamiastproject_ids;projects.tenant_scope/read_scopezwracająTenantScope,can_see_reportusunięte z API, nowerequire_write,may_manage,project_org,ProjectRead.restricted,create_project(..., org_id, restricted),ProjectUpdatezrestricted; członek projektu musi być członkiem orgu.accounts:Principal.org_id/org_role,register_principal_enricher; bieżący org zX-Org-ID(public id) / cookie / pierwszy z członkostwem, klucz API → org projektu, cudzy org 404, zawieszony 403org_suspended. Rola globalna admin/superadmin nie widzi już danych tenantów; obejście tylkoTenantScope.superadmin(reason)z wpisemscope.bypass(GET /v1/feedbackdla superadmina, CLIexport);GET /v1/feedbacktakże dla owner/admin orgu (swój org). Viewer: ocena, klucze, projekty, feedback → 403. Zaproszenia dostająorg_id(przyjęcie = member orgu). Migracja0010_organizations: org „(personal)” per właściciel projektu, członkowie → member, org_idwprojects(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.narrowprzecina z widocznymi, SQLAdmin Reports/Feedback/Invites tylko dla superadmina z wpisemscope.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), profilerunpod-gemma4-pro4500,runpod-gemma4-5090(48 seq) irunpod-gemma4-pro6000(64 seq), oba nvfp4 jak DGX GB10, niekalibrowane;base_urlprofilu obsługuje${ZMIENNA}z env/.secret/.env; plan pomiaru i rachunek VRAM wdocs/perf/runpod-plan.md. Bez nowych zależności, nic nie wdrożono. - Inference (#148, kontrakt
inference): jeden dostawca na profil —Profile.spread_providersdomyślniefalse(rotacja zostaje tylko do eksperymentów),concurrency24 → 8,max_inflight64 → 12 (jeden dostawca, mniej 429);openrouter-gemma4: tylkodekallm/bf16wprovider.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=fullw eksportach (--detail fullwgspamjudge export); nowe polaReportView.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) wdocs/design/mockups/; mapa widok→trasa→szablon wdocs/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) iscripts/ui_lint.py(kolory hex, monospace,style=, cienie, pogrubienia; ostrzeżenia o tekście bez_()) orazscripts/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 wsrc/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 (makrolegal_footer, COMPONENTS §29), wymagany checkbox regulaminu na/invite/{token},/welcome(checklistachecklist, §30; akceptacja dla kont spoza zaproszeń i po zmianie wersji; dashboard przekierowuje do akceptacji/zamknięcia, cookiegsj_welcome). Migracja0008_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.costdostawcy, tokeny, cached tokens, pobrania ×GSPAMJUDGE_FETCH_COST_USD, 429 dostawcy,request_id/job:<id>, projekt) zbierany przezcore.meter(ContextVar; klient inferencji isafefetch.getdopisują,pipeline.analyzezapisuje także po błędzie); globalny dzienny limitGSPAMJUDGE_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ą wpipeline.analyze(wyjątek domenowySpendCapReached, mapowany wapi/errors.py) → nowy kod 503spend_cap_reached(EN/PL,details{day, spent_usd, cap_usd},Retry-After); alert raz na dobę przyGSPAMJUDGE_SPEND_ALERT_RATIO(0.8) i 100%: log WARNING + opcjonalnieGSPAMJUDGE_ALERT_WEBHOOK_URL(POST JSON przezcore.safefetch.post, tylko https i adres publiczny) iGSPAMJUDGE_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 zrequest_id(core/logs.py,GSPAMJUDGE_LOG_JSON,GSPAMJUDGE_LOG_LEVEL; access loggspamjudge.accesszamiast uvicorna: szablon trasy albo ścieżka z maskowanymi tokenami (core.logs.safe_path), bez query stringu, także 500; worker zrequest_id=job:<id>); bez zewnętrznego śledzenia błędów (GlitchTip opisany wcore/README.md). Migracja0009(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:concurrency8 → 24, nowemax_inflight(64, wspólny limit w procesie),spread_providers(rotacjaprovider.orderpoprovider.onlyper strona),retries(6),backoff_base_s/backoff_max_s(pełny jitter,Retry-After); 429 → dostawca na koniec kolejności.GSPAMJUDGE_BATCH_CONCURRENCY2 → 6;GSPAMJUDGE_JUDGE_WARMUP(jedna reguła przed resztą, cache prefiksu) iGSPAMJUDGE_JUDGE_UTILITY_GATE(C5-UTILITY ≥ 0.5 → reguły quality niepytane, nowe poleJudgeReport.skipped). Presety regułpreset:<nazwa>(rules/presets.yaml,rule_presets,expand_presets),audit-quick(32 reguły) w API/CLI i panelu audytu. Profilopenrouter-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 albourls(normalizacja, duplikaty,ingest.check_fetch_url, opcjonalnie ten sam host,GSPAMJUDGE_AUDITS_UPLOAD_MAX_URLS);POST /v1/audits/site/estimateiPOST /v1/audits/sitezscope(wzorce include/exclude, glob/re:, ręczne wykluczenia), strategieall|newest|stratified|newest+stratified,preset,js→Estimate(kredyty z wagGSPAMJUDGE_CREDITS_WEIGHT_FULL/QUICK/JS,GSPAMJUDGE_CREDIT_USD, czasGSPAMJUDGE_AUDITS_SECONDS_PER_CREDIT);SamplePlan+source,scope,selected,estimate(bez migracji), wybór w raporcie witryny; importitems[{url, html?}] / w panelu CSVurl,htmli ZIP (index.csv+.html) — pozycje z HTML oceniane bez pobierania, limity jak/v1/jobs(413item_too_large);/a/=/a(opcjadistinct_slash); tylko glob (bez regexów, ReDoS);preview_idprzypięty do użytkownika/klucza i projektu, magazyn podglądu w Redisie (fallback pamięć); kodyinvalid_scope,empty_selection,preview_expired. Panel: kreator/audits/new(makrorange_field), teksty PL. Przewodnikguide/site-audits.md,openapi.jsonodświeżony. Bez nowych zależności (redis z arq). - Ingest/API (#111, kontrakt
ingest): SSRF przy pobieraniu strony —fetch.pyprzezcore.safefetch(adresyis_global, przypięty IP, porty 80/443, przekierowania walidowane ponownie, ≤ 10 MB strumieniowo, 20 s, ten sam User-Agent); nowe publicznecheck_fetch_url,InvalidURL,FetchError(podklasahttpx.HTTPError). Nowy kod 400invalid_url(literał IP, localhost,.local/.internal, dane logowania, schemat, port) w/v1/judge, batch, jobs, panelu i rerun; adres prywatny po DNS / przekierowanie → 502fetch_failed. Dotyczy też stron próby audytu. Dev:GSPAMJUDGE_WEBHOOK_ALLOW_INSECURE(tylkoGSPAMJUDGE_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_rulesrzucaRuleSelectionError(podklasaKeyError) i deduplikuje id. API: siatka bezpieczeństwa (RuleSelectionError→ 400unknown_rule/rule_disabledzamiast 500),/v1/jobswaliduje reguły przed 503queue_unavailable; panel pokazuje przetłumaczony komunikat (wcześniejApiErrorwymykał się doKeyError: ...), CLIjudge/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ówbatch|job|sitejako jedna pozycja historii i raport witryny.POST /v1/audits/site{site|sitemap_url, pages=30 (≤GSPAMJUDGE_AUDITS_MAX_PAGES=100), strategynewest+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) → zadaniefeatures/jobs;GET /v1/audits,GET /v1/audits/{id}(postęp + ryzyko per polityka, wzorce URL, najgorsze strony, werdykt kodem z progamifeatures.report), eksportexport.html|.pdf|.csv; kodyaudit_not_found,audit_too_large,sitemap_not_found.POST /v1/jobs,/v1/judge/batchi 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. Migracja0007_audits. Teksty PL w katalogu. Kwoty (#89): rezerwacja stron próby wstart_site_audit, zwrot nieocenionych przez worker. SSRF: wspólnecore/netguard.py(walidacja URL: schemat, porty 80/443, bez literałów IP/localhost/.local/.internal, DNS → tylkois_global) icore/safefetch.py(połączenie na przypięty IP, ręczne przekierowania z ponowną walidacją, limit bajtów w strumieniu) — logika przeniesiona zfeatures/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 zmax_length, łączny czasGSPAMJUDGE_AUDITS_DISCOVERY_TIMEOUT(45 s).core/csvsafe.safe_cell(przeniesione zadmin/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) iscripts/i18n.sh extract|update|compile;.mow repo (runtime: stdlib gettext, Babel tylko w extradev, bez zmian w Dockerfile). Komunikaty błędów/v1po angielsku,Accept-Language: pl→ polskie (codebez zmian). EksportGET /v1/reports/{id}/export.html|.pdf?lang=en|pl, CLIexport --lang; panel linkuje eksport w bieżącym języku. Zmiana kontrakturules.NotAssessed:reasonpo angielsku + nowereason_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(zatob(/unescape(; samoatob(+document.writez 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 wanswer_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, syntetycznyeval(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 skillurule-authoringifeatures/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 kluczajobs, obcy projekt = 404job_not_found; nowe kodyjob_too_large,queue_unavailable,webhooks_unavailable. Workergspamjudge worker(arqWorkerSettings,gspamjudge/worker.py) i usługaworkerw compose; każda pozycja przezpipeline.analyze, raport zawsze zapisany w projekcie zadania (pipeline.analyze(..., store=)— nowy parametr). Webhook po zakończeniu z podpisemX-GSPAMJUDGE-Signature: sha256=HMAC(GSPAMJUDGE_WEBHOOK_SECRET, "{timestamp}.{body}"), 3 próby. Migracja0004(jobs). UstawieniaGSPAMJUDGE_JOBS_MAX(1000),GSPAMJUDGE_WEBHOOK_SECRET. Klucze API:GSPAMJUDGE_APIKEY_RATE_BACKEND=redis(okno minutowe w Redisie, wiele procesów);apikeys.ratelimit.enforceasync. Panel/jobs: lista ze statusem i postępem (makrojob_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 → 400invalid_webhook_url; w workerze DNS → tylko adresyis_global, połączenie na sprawdzony IP przez httpcore, bez przekierowań;GSPAMJUDGE_WEBHOOK_ALLOW_INSECUREtylko w dev), nagłówekX-GSPAMJUDGE-Delivery(deduplikacja) i wymóg okna ±300 s w przewodniku; limityGSPAMJUDGE_JOBS_ITEM_MAX_CHARS/JOBS_MAX_CHARS(413item_too_large) iGSPAMJUDGE_JOBS_MAX_ACTIVE(429too_many_active_jobs);GSPAMJUDGE_JOBS_TIMEOUT(1800 s) →failed+ webhook, także przy awarii bazy;urlzapisany w wynikach pozycji (/resultsbez dociągania raportów); awaria Redisa w limicie → 503rate_limiter_unavailable(GSPAMJUDGE_APIKEY_RATE_FAIL_OPEN); API: reguła wyłączona → 400rule_disabledw/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ągnieredis, 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 #nrw PR, zmiany w katalogu jednego feature'a; zapisane w AGENTS.md (reguła 12), CONTRIBUTING.md, rolach.agents/agents/, skillachworktree-workflowiforgejo-issuesorazdocs/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, filtryrule_id,label,report_id); polesubmitted_by(e-mail alboapi_key:<id>); nowy zakres klucza APIfeedback:write(klucz bez niego → 403insufficient_scope); migracja0005_feedback; raport:data-rule-idnatr.rule(hak panelu); eksport CSV sanityzowany przed formula injection (admin/csvsafe.py); panel/history/{id}: „Wrong?” przy każdej regule (modal, makroui/_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 → kontouseri członkostwo w projekcie; e-mail przez stdlibsmtplib, gdyGSPAMJUDGE_SMTP_HOST, inaczej link dla admina) ifeatures/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 → 429quota_exceededzdetails; licznik udanych stron wusage_counters,GET /v1/usage, pasek boczny i dashboard panelu; admin+ liczony, nieblokowany). Nowy kod 409conflict. 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_memberpubliczne. Migracja0006(invites,quotas,usage_counters). Ustawieniapublic_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_ifreguł 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 polaquality_level(high|medium|low|unknown|na),quality_label,quality_note— Quality risk (SQRG) pokazywany obok i nie podnosi stempla (ADR 0010); etykietaC5-UTILITY≥ 0.5 → „Utility page — quality criteria not applicable”.report.HIGH/MEDIUMpubliczne (panelrows.pybez 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: booli nowy stanRuleView.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:nextwPOST /panel/projecttylko jako ścieżka lokalna (bez//,\\, schematu/hosta), makra bez surowego HTML (empty_row(colspan, text, (label, href))),codeblockbez kolorów hex w szablonie, komunikaty błędów przezdata-*. Issue #78: werdykt wReportSummary. - Klucze API (#50, ADR 0015, kontrakt): nowy feature
features/apikeys— klucze projektugsj_<prefix>_<secret>(w bazie tylko HMAC-SHA256 z pepperemGSPAMJUDGE_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; zakresyjudge,reports:read,jobs,projects:read(403insufficient_scope), zły/odwołany klucz 401invalid_api_key, limit per klucz token bucket w pamięci (GSPAMJUDGE_APIKEY_RATE_LIMIT=60/min,GSPAMJUDGE_APIKEY_RATE_BACKEND=memory; Redis osobno) → 429rate_limitedzRetry-After/RateLimit-*. accounts:Principal.key_id,Principal.scopes,register_principal_resolver. Migracja0003(api_keys). Po recenzji PR #73: klucz działa tylko na/v1i nie loguje do panelu; CSRF sprawdza każde żądanie z cookie sesji (także zAuthorization);/v1/projects/{id}/members→ 403 dla klucza; walidacja backendu limitu i ostrzeżenie o losowym pepperze przy starcie;last_used_atco ≥ 60 s;accounts.MIN_SECRETpubliczne;openapi.jsonprzegenerowany. Bez nowych zależności (stdlibhmac). - Admin (#52, hardening): Settings jako allowlista (+ maskowanie
SecretStri nazw secret/key/token/password); osobny klucz sesji SQLAdmin (HMAC zauth_secret);is_superuserustawiane na modelu w transakcji zmiany roli; zakaz dezaktywacji własnego konta; członkostwa w adminie walidowane jak w API projektów (tylkomember, 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 dofeatures/*/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 Jinja2ui/_*.html),docs/design/STYLES.md(tokeny light/dark, typografia Inter bez monospace, odstępy, promienie, do/don't),docs/design/AGENTS.md, skillui-components. Bez zmian w kodzie. - Admin (#52, ADR 0012): panel SQLAdmin pod
/admin(src/gspamjudge/admin/,mount_adminwapi/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 dlaadmin/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 zprofiles/*.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 przezSessionMiddleware, 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 przezanalyze_manyz limitembatch_max, preset quick/full/custom, dowody),/historyz 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 (cookielang, katalog PL w #60) i motywu; komponenty jako makrapanel/templates/ui/_*.htmlzgodne zdocs/design/COMPONENTS.md.features/report/templates/report.cssprzestylowany 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 zopenapi.jsonzscripts/export_openapi.py), katalog reguł generowanyscripts/docs_rules.py(hookdocs/hooks.pyregeneruje oba przed budową), Errors, Versioning, Changelog; EN domyślnie, PL przez mkdocs-static-i18n; kolory Sanity, Inter także dla kodu;mikeprzygotowany. Nowy extradocs:mkdocs<2(2.0 łamie wtyczki),mkdocs-material,mkdocs-static-i18n— darmowe, open-source, statyczna strona bez Dockera. FastAPI/docsbez zmian (opis z odesłaniem). ADR-y i dziennik nie są publikowane.docs/api/errors.mdiversioning.mdprzetł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) iLink.external(inny host; dla--htmlz canonical/og:url), linki z treści mają pierwszeństwo wMAX_LINKS;document.mdrozdzielaLINKS IN MAIN CONTENTiOTHER LINKS(limit 60 — przy 25 spadał SP-LNK-ANCHOR);PROMPT_VERp3 →p4(kalibracje p3 nieważne). Bez nowych zależności. - Design: system Sanity (Open Design) w
docs/design/DESIGN.md; makiety panelu EN/PL wdocs/design/mockups/(Tailwind 4.1.11 + daisyUI 5.0.43 z CDN, motywygsj-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 wdata/base64/JS — zgłoszenie z zeszczecina.pl). Sekcje linków zgodnie z promptem z PR #64 („LINKS IN MAIN CONTENT” / „OTHER LINKS”, kolumnaexternal), 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 502fetch_faileddla błędów pobierania strony (wcześniej{"detail": ...}); kodyunknown_rule,unknown_profile,report_not_found,batch_too_large,validation_error,internal_errori in. (docs/api/errors.md); nagłówkiX-Request-IDiX-API-Version: v1;/v1/judgesprawdza reguły/profil przed analizą; polityka wersjonowania i deprecjacji (Deprecation/Sunset, 6 mies.) wdocs/api/versioning.md. Nowe ustawienieGSPAMJUDGE_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 wymienialnyreport.css; używany przez panel,GET /v1/reports/{id}/export.html|.pdfigspamjudge 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.babelw extradev(tylko ekstrakcja/kompilacja katalogów; runtime to stdlib gettext). Dowody z typowanegoRuleScore.evidence(#57), kolumna „Evidence”. Panel renderuje raport tym szablonem (templates/report.html,_scores.htmlpanelu nieużywane). - Konta i projekty (#48, #49, ADR 0012): nowe feature'y
features/accounts(fastapi-users:/v1/auth/register,/v1/auth/jwt/login|logoutdla API,/v1/auth/cookie/login|logoutdla panelu,/v1/auth/forgot-password|reset-password,/v1/users/me,GET /v1/usersiPATCH /v1/users/{id}/roledla admina; roleuser|admin|superadmin; zależnościcurrent_principal,require_role; abstrakcjaPrincipalpod klucze API #50; CLIgspamjudge create-superadmin --email) ifeatures/projects(CRUD/v1/projects, członkowie owner/member, brak dostępu = 404). Zmiana kontraktu: wszystko poza/healthi/v1/ruleswymaga logowania (401);JudgeRequest/BatchRequest+project_id(albo nagłówekX-Project-ID; brak = domyślny projekt użytkownika);/v1/reportszawężone do projektów użytkownika (admin widzi wszystko); storage:ReportSummary.project_id,save_report(report, project_id),list_reports(..., project_ids), eksportdb_url;pipeline.analyze/analyze_many(..., project_id). Migracja0002: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). UstawieniaGSPAMJUDGE_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|.pdfzawężony do projektów (404report_not_found); usunięcie projektu ustawiaproject_id = NULLw jego raportach (storage.detach_project, jedna transakcja),projectszsqlite_autoincrement;GSPAMJUDGE_ENV(pozadevsekret ≥ 32 znaki albo błąd startu);GSPAMJUDGE_REGISTRATION_OPEN(domyślnie false) iPOST /v1/users(admin); CSRF dla mutacji z cookie (Origin/Referer, 403csrf_failed), cookieSameSite=Lax,POST /panel/project; rolaownerniezmienna przez API członków; superadmin nie zmienia własnej roli;UserUpdatebezis_superuser; URL bazy normalizowanymake_url(postgresql://→ psycopg 3). Błędy kont/projektów w formacieErrorResponse(#46):unauthorized401,forbidden403,not_found404,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 skilleworktree-workflow,forgejo-issues), 0015 (API-first); kamień milowy M3.5 i issues #46–#55. - Infra (#47):
docker-compose.ymlzpostgres:16(wolumenpgdata) iredis:7z healthcheckami, aplikacjadepends_onhealthy,GSPAMJUDGE_DB_URL(Postgres) iGSPAMJUDGE_REDIS_URLw compose, port hostaGSPAMJUDGE_PORT; alembic wfeatures/storage/migrations(migracja0001= tabelareports),gspamjudge migratei entrypoint konteneramigrate && serve; SQLite migruje się sam, stara baza stemplowana jako0001. Nowe ustawienieredis_url, nowe API storageupgrade_db. Zależności:alembic(standard migracji dla SQLAlchemy, zamiastcreate_all),psycopg[binary](sterownik Postgresa bez kompilacji w obrazie slim).GSPAMJUDGE_DB_URL/GSPAMJUDGE_REDIS_URLw compose nadpisywalne. Redis na razie bez użycia — worker iarqw #51. - Storage (#19): nowy feature
features/storage—save_report/get_report/list_reports, tabelareports(id, created_at, url, profile, prompt_ver, payload JSON);pipeline.analyzezapisuje każdy raport (judge, batch, panel, CLI);GET /v1/reports[?limit,url],GET /v1/reports/{id}, panel/history. UstawieniaGSPAMJUDGE_DB_URL(domyślniesqlite:///runs/gspamjudge.db, w Dockerze wolumen./runs; Postgres po URL) iGSPAMJUDGE_STORE_REPORTS. Nowa zależnośćsqlalchemy>=2.0: SQLAlchemy 2 zamiast SQLModel — jeden pakiet (SQLModel i tak go ciągnie), typowaneMapped[]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
relsponsored/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 (
explainnie zmieniascore_raw,prompt_verzawszep3, ścieżkip2e/p3ewycofane); nowy featurefeatures/evidence— dla regułkind=violationzscore_raw >= GSPAMJUDGE_EVIDENCE_THRESHOLD(0,5) cytaty ze strony (ten sam prefiks, bez etykiety,EVIDENCE_PROMPT_VERe1) 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/contradictionzawszenull; usuniętoJudgeInput.explain;Profile.explain_max_tokens→evidence_max_tokens(150); judge eksportujebuild_messages,render_document. Panel: cytaty ✓/✗ i plakietka „bez dowodu”;scripts/show_report.pyteż.JudgeReport.labels/positivesmają domyślnie[], a raporty sprzed ADR 0010 (polecontext) wczytują się jakolabels— 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–C3kind: info, C4-PURPOSEkind: positivev3 („Does the page fulfil a clear purpose… 1 = yes”).RuleScore.kind;JudgeReport.contextzastąpione przezlabels(info) ipositives,violations/quality_signalstylko violation. Szablonyanswer_positive_*,system.md,PROMPT_VERp2 →p3(stare kalibracje nieważne).contradictiontylko dla violation. Panel: czerwony/zielony pasek, etykiety info jako tagi;show_report.py,rules_table.py(kolumna Rodzaj), CLIrulespokazuje kind. - Poprawki MVP (fix/mvp-polish): panel — pole „reguły” (id po przecinku) i preset
szybki(~15 reguł spamu) /pełny;errorw batch API/CLI i panelu przycięty do 300 znaków; przykładitems[]w README API; obraz uv przypięty do 0.13.0;docs/deploy.md—./runsdla 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 wdocs/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}}; limitGSPAMJUDGE_BATCH_MAX(10) → 413 z odesłaniem do/v1/jobs, strony równolegle wgGSPAMJUDGE_BATCH_CONCURRENCY(2), częściowe błędy per strona;pipeline.analyze_many; CLIgspamjudge 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_VERp1 →p2(explainp2e), stare kalibracje nieważne. Kontrakt: nowe poleRuleScore.contradiction: bool | None(explain; heurystykaconsistency.py), pokazywane w panelu iscripts/show_report.py. CLIjudgekończy się kodem 1, gdy któraś reguła maok=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 wfeatures/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 wCONTRIBUTING.md. - Dokumentacja: ADR 0007 (cztery produkty nad jednym potokiem, batchowanie
POST /v1/judge/batch+/v1/jobs, cena za stronę) i ADR 0008 (tryb explain, promptp1e); issue #31, #32 (#18, #30). - Dokumentacja:
AGENTS.mddla agentów w każdym katalogu feature'a/wejścia (core,rules,ingest,inference,judge,api,panel,profiles), zakaz zagnieżdżonychCLAUDE.md; README główne z drzewem projektu, tabelą reguł i sekcją „Zasady pracy”; skryptscripts/rules_table.pygenerują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.