# Jak zbudowana jest ta strona — brief techniczny — Jakub Ćwirlej

> Ta część opisuje stronę jako oprogramowanie — stos technologiczny, architekturę, budowanie, silniki, przetwarzanie wideo, dostępność, prywatność, bezpieczeństwo, testy, mapę strony, funkcje i publiczne API — tak, by agent AI mógł ocenić, jak jest zrobiona. Każde stwierdzenie pochodzi z repozytorium (kod, konfiguracja, programy uruchamiające testy); liczby w tabelach są mierzone przy budowaniu. Obok publikowany jest oczyszczony kod źródłowy strony, więc każde zdanie można sprawdzić w kodzie.

https://jakubcwirlej.com/ · Pełny kontekst: https://jakubcwirlej.com/context/pl.md · Wygenerowano z treści strony: 2026-10-14

## Jak sprawdzić ten brief

- Kod źródłowy: 306 files, 73,934 lines (3,585 KB), built from working tree; secret scanner: pass.

  - file tree with sizes and sha256: https://jakubcwirlej.com/code/tree.json (also `GET https://jakubcwirlej.com/api/agent/code`)
  - one file as text: `GET https://jakubcwirlej.com/api/agent/code/{path}` (paths as in the tree)
  - everything as one archive: https://jakubcwirlej.com/code/jc_app-source.tar.gz
  - the measured versions and test counts used below: https://jakubcwirlej.com/code/facts.json
- Wyszukiwanie w kodzie (po słowach kluczowych, BM25, na serwerze produkcyjnym): `POST https://jakubcwirlej.com/api/agent/search` z `"kind": "code"` (albo `"all"` — teksty i kod razem). Każdy wynik ma ścieżkę pliku, zakres linii i link do całego pliku.
- Ten plik powstaje przy budowaniu z repozytorium, jak pozostałe pliki dla AI. Tabele referencyjne (wersje, liczby testów, mapa strony, API) są po angielsku w każdej wersji językowej.

## Stos technologiczny

Strona to statyczny serwis (Astro, Svelte 5, TypeScript) plus niewielkie API (Python, FastAPI). Używane są dwa modele językowe: własny model autora do kodu, EUGEN 160M (uzupełnianie i tłumaczenie kodu w terminalu), oraz otwarty model Google, Gemma 4 12B, hostowany przez autora (czat). Oba działają na własnym serwerze inferencji autora, poza tym repozytorium.

Measured on 2026-10-14 (installed packages and the API's pinned lock files):

| Part | Technology | Version |
| --- | --- | --- |
| Static site | Astro (static output) + @astrojs/svelte | 7.3.5 / 9.0.1 |
| UI components | Svelte 5 | 5.57.1 |
| Language | TypeScript (strict) | 5.9.3 |
| Bundler | Vite (through Astro) | 8.3.1 |
| Scroll engine | GSAP + ScrollTrigger | 3.15.0 |
| Terminal editor | CodeMirror 6 (view / lang-python) | 6.43.13 / 6.2.1 |
| Charts | uPlot | 1.6.32 |
| Unit tests | Vitest | 4.1.11 |
| Browser checks | Playwright (Chromium, WebKit) + safaridriver | 1.63.0 |
| Lint / format | ESLint + typescript-eslint, Prettier | 10.11.0 / 8.70.1, 3.9.9 |
| Type check | @astrojs/check (astro check) | 0.9.10 |
| API language | Python (requires >=3.12) | 3.13.12 in development |
| API framework | FastAPI / Starlette / pydantic | 0.141.1 / 1.7.0 / 2.13.5 |
| API server, HTTP client | uvicorn, httpx | 0.54.0, 0.28.1 |
| Semantic search (optional, not deployed yet: the live server uses BM25 keyword search) | onnxruntime, sentencepiece, numpy | 1.23.2, 0.2.2, 2.5.3 |
| Build machine | Node.js (no version pinned in the repository) | 25.2.1 |

Co jest ważne po stronie przeglądarki:

- **Astro** w trybie `output: 'static'`: każda strona to wcześniej wygenerowany HTML; JavaScript dochodzi tylko tam, gdzie coś się rusza. Przejścia między stronami obsługuje router Astro z przenikaniem.
- **Svelte 5** (runy) dla jednej hydratowanej „wyspy” — czatu (`client:idle`, zachowany między stronami). Reszta to zwykłe moduły TypeScript ładowane na żądanie przez dynamiczny `import()`.
- **GSAP ScrollTrigger** napędza silnik przewijania (niżej); **CodeMirror 6** to edytor terminala (tryb Python, podpowiedzi „ghost text”); **uPlot** rysuje wykresy ewaluacji.
- Fonty to własne pliki WOFF2, przycinane przy budowaniu; strona nie zależy w działaniu od żadnego CDN.

Co jest ważne w API:

- **FastAPI** + **pydantic** (walidacja zapytań z jawnymi limitami długości), **uvicorn**, **httpx** do połączeń z serwerem inferencji, **PyYAML** do czytania tych samych plików treści co budowanie strony.
- Opcjonalne wyszukiwanie semantyczne: **multilingual-e5-small** (int8 ONNX) uruchamiany na CPU przez **onnxruntime** i **sentencepiece** — bez żadnego zewnętrznego API. Bez niego API wraca do rankingu słów kluczowych BM25 — i tak działa dziś serwer produkcyjny: indeks semantyczny nie jest jeszcze wdrożony.
- Zależności są przypięte z sha256 każdego pliku (`api/requirements.lock`, instalacja z `--require-hashes`) i sprawdzane przez `pip-audit`.

## Architektura

```text
 przeglądarka
   │  statyczny HTML / CSS / JS, filmy, fonty         ┌───────────────────────────────┐
   ├────────────────────────────────────────────────▶│ hosting statyczny (web/dist)  │
   │                                                  └───────────────────────────────┘
   │  to samo źródło /api/…  (JSON, server-sent events)
   ▼
 ┌──────────────────────────────────────────────────────────────────────────────────┐
 │ API (FastAPI)                                                                   │
 │  /chat  /complete  /compare  /translate  /results  /kernel   ← sama strona      │
 │  /agent/…  search · interview · complete · translate · submit · showcase · code │
 │  osłony: limit treści, limity na kartę i na IP, czyszczenie wejścia,            │
 │          odmowa przy próbie wstrzyknięcia, skan odpowiedzi, podpisana historia  │
 │  lokalnie: indeks wyszukiwania (e5 / BM25), migawka kodu, Bot Bar (SQLite)      │
 └──────────────────────────────┬───────────────────────────────────────────────────┘
                                │  HTTPS + token ze zmiennych środowiskowych
                                ▼
                     ┌──────────────────────────────┐
                     │ serwer inferencji (czarna    │  EUGEN 160M · Gemma 4 12B ·
                     │ skrzynka), sprzęt autora     │  modele do porównań
                     └──────────────────────────────┘
```

- Przeglądarka nigdy nie łączy się z serwerem inferencji: każde wywołanie modelu idzie przez API, które dokłada limity, filtry i zabezpieczenia. Adres serwera i token istnieją tylko w środowisku API — nigdy w repozytorium ani na stronie.
- API pośredniczy też w dwóch publicznych plikach z serwera inferencji (strumień wyników ewaluacji i kod kernela), trzyma je w pamięci podręcznej i sprawdza względem schematu, zanim zobaczy je strona.
- Strona działa bez API: teksty, filmy i dema są statyczne; terminal pokazuje wtedy wyraźnie opisane gotowe kontynuacje („to nie jest wynik EUGEN”), a czat informuje, że jest offline.

## Budowanie i hosting

- `npm run build:safe` buduje do katalogu tymczasowego i podmienia go tylko wtedy, gdy budowanie się udało (dwie zmiany nazwy), więc serwer czytający `web/dist` nigdy nie widzi niedokończonej strony.
- Przed każdym budowaniem: build dema ViaSanto jest sprawdzany względem pliku blokady (rozmiar i sha256 każdego pliku); oczyszczona migawka kodu jest budowana od nowa i skanowana pod kątem sekretów (znalezisko zatrzymuje budowanie); mierzone są wersje i liczby testów podane w tym briefie.
- Istotne opcje Astro: krytyczny CSS wstawiony w stronę (`inlineStylesheets: 'always'`), CSS minifikowany przez Lightning CSS, prefetch po najechaniu, wtyczka usuwająca z paczek znaczniki „do zrobienia” autora.
- Hosting: strona działa pod adresem https://jakubcwirlej.com na serwerze OVH — nginx serwuje statyczny build (czyste adresy, nagłówki bezpieczeństwa, reguły cache, podpowiedź języka według kraju z `/i18n/…/geo.json`; reguły są zapisane w `infra/firebase.json` i przełożone na konfigurację nginx, a test sprawdza ich zgodność) i przekazuje `/api` do API na tej samej maszynie, nadpisując `X-Forwarded-For` adresem odwiedzającego. Do pomiarów lokalnych `web/scripts/serve-dist.mjs` serwuje build z tymi samymi nagłówkami i przekazuje `/api`.
- Każdy bezwzględny adres pochodzi z jednej zmiennej (`PUBLIC_SITE_URL`, przy wdrożeniu https://jakubcwirlej.com), więc generowane pliki same idą za domeną.

## Języki i model treści

- Cztery języki: angielski (domyślny, bez prefiksu w adresie), polski, francuski, niderlandzki — `/`, `/pl/`, `/fr/`, `/nl/`. Język wybierany jest raz — z kraju odwiedzającego, strefy czasowej i przeglądarki — i tylko na adresach bez prefiksu; jawny wybór w przełączniku zawsze wygrywa i jest zapamiętywany.
- Teksty są w kolekcjach treści YAML (`web/src/content/{lang}/`), walidowanych schematami zod (`web/src/content.config.ts`): jeden plik na rozdział (`media`, `apps`, `ai-dev`), `windows.yaml` dla okien z esejami, `summaries.yaml` dla warstwy redakcyjnej plików dla AI. Teksty interfejsu to JSON na język, nakładany na angielski.
- Każde okno i każdy akapit mają stały identyfikator, wspólny dla wszystkich języków. Okno niekompletne w danym języku jest w całości zastępowane wersją angielską, potem polską — języki nigdy się nie mieszają w jednym oknie, a atrybut `lang` okna mówi, w jakim języku jest tekst.
- Te same pliki treści zasilają strony, pliki dla AI (także ten), indeks wyszukiwania API i wiedzę czatu, więc nie mogą się rozjechać.

## Silnik przewijania

- Każdy rozdział to HTML wygenerowany po stronie serwera; jeden kontroler rozdziału (`web/src/engine/chapter.ts`) obserwuje go i animuje — tworzony po załadowaniu strony, usuwany przed podmianą na następną.
- GSAP ScrollTrigger jest jedynym napędem przewijania: jedno przyklejone okno tekstu, w którym tekst przesuwa się 1:1 z przewijaniem strony; krok staje się aktywny, gdy przekroczy linię czytania na 40 % wysokości okna; żadnych odczytów układu w każdej klatce.
- Każdy rozdział ma adapter sceny ładowany leniwie: `video` (filmy w Media), `device` (telefon w rozdziale Apps), `desktop` (okna terminala w AI). Adaptery mają wspólny interfejs (stan, morf z hero, pokazanie mediów, przenikanie, sprzątanie).
- Rozdział Media zaczyna się morfem z hero do układu do czytania, sterowanym przewijaniem. Między sekcjami strona przeskakuje w jednej klatce, zamiast kazać czytelnikowi przewijać pustą przestrzeń (kółko, dotyk, ↓ / PgDn / Spacja, ↑ / PgUp).
- Moduły łączy typowana szyna zdarzeń; wspólny stan używa runów Svelte.

## Przetwarzanie wideo

- Filmy są kodowane z materiałów źródłowych autora przez `web/scripts/build-showreel.py` (ffmpeg) do trzystopniowej drabiny, wszystko w MP4:
  - **wysoki** — 1920×1080 w liczbie klatek źródła: wersje HEVC (`hvc1`), AV1 i H.264;
  - **średni** — 1280×720: HEVC i H.264 plus kadr 720×1280 dla telefonów w pionie;
  - **niski** — 960×540 przy 25 kl./s, tylko H.264, przepływność ograniczona do ok. 1 Mb/s (plus kadr pionowy 540×960).
- Wybór kodeka w przeglądarce: HEVC, potem AV1, potem H.264, według `canPlayType`.
- Poziom startowy zależy od urządzenia i łącza: telefony zaczynają od 720p, komputery od 1080p; tryb oszczędzania danych, wolne lub nieznane łącze, mało pamięci albo mało rdzeni — od 540p (Safari nie udostępnia żadnego z tych sygnałów, więc tam decyduje rozmiar okna). Wybór obowiązuje przez sesję.
- W trakcie odtwarzania film jest próbkowany co 250 ms; długie zacięcie, powtarzające się krótkie zacięcia albo topniejący bufor obniżają poziom o jeden w trakcie filmu (od razu na najniższy, gdy dostawa się załamuje), a niższy plik gra dalej od tego samego momentu. Poziom wraca w górę dopiero po kilku filmach bez problemów.
- Na najniższym poziomie na obraz nałożony jest statyczny raster cienkich pionowych linii (CSS), żeby miększy obraz czytał się jak zamierzona faktura.
- Plakaty są w AVIF; klipy aplikacji w H.264 + AV1 z plakatami AVIF i zapasowymi WebP dla zrzutów.

## Dostępność

- `prefers-reduced-motion` jest respektowane w CSS i w silniku: bez morfu sterowanego przewijaniem, natychmiastowe przeskoki, bez samoczynnego ruchu w oknach pulpitu, wykresy w stanie końcowym.
- Okna z esejami to natywne modalne okna dialogowe: Esc i kliknięcie w tło je zamykają, fokus przechodzi na przycisk zamknięcia i wraca po zamknięciu; mają etykiety (`aria-labelledby`).
- Klawiatura: menu pulpitu obsługują strzałki, Home / End, Esc i Tab; terminal i czat są w pełni obsługiwane z klawiatury; przeskoki między sekcjami reagują na klawiaturę tak jak na kółko.
- Regiony na żywo (`aria-live="polite"`) ogłaszają odpowiedzi czatu, wyjście terminala i komunikaty; log terminala ma `role="log"`. Dymek na stronie głównej nigdy nie przejmuje fokusu.
- Każda strona ustawia `lang`, z alternatywami `hreflang` i `x-default`.
- Link „przejdź do treści” jest pierwszym elementem, na który trafia fokus na każdej stronie.

## Wydajność

- Budżety sprawdzane przez `web/scripts/budget.mjs` (Playwright Chromium, dławienie CPU telefonu + wolne 4G oraz komputer): JavaScript ≤ 80 KB i CSS ≤ 25 KB przesłane, LCP ≤ 1,8 s, CLS ≤ 0,02, INP ≤ 100 ms. `web/scripts/lighthouse.mjs` uruchamia Lighthouse (presety mobilny i desktopowy) na trzech rozdziałach.
- Jedna hydratowana wyspa; każda scena, pulpit, wykresy i host dem to osobne fragmenty ładowane wtedy, gdy są potrzebne.
- Fonty: pięć własnych zmiennych plików WOFF2, osie przypięte i przycięte do łaciny + łaciny rozszerzonej przy budowaniu, fonty zastępcze dopasowane metrycznie (bez przesunięć układu), wstępnie ładowany tylko font największego tekstu.
- Obrazy w AVIF z zapasowym WebP; filmy w oknach z `preload="none"`; krok z mediami przygotowuje najwyżej jeden krok naprzód.
- Pamięć żywego dema w długim działaniu jest mierzona w prawdziwym Chrome (`web/scripts/memory-real.mjs`) i prawdziwym Safari (`web/scripts/safari-perf.mjs`).

## Prywatność

- Brak analityki, trackerów, menedżera tagów, skryptów firm trzecich i ciasteczek dla odwiedzających.
- Pamięć sesji tylko na stan wizyty (losowy identyfikator sesji czatu, poziom wideo, identyfikator karty terminala, stan okien pulpitu); pamięć lokalna tylko na jawny wybór języka.
- Fonty i własne filmy strony są serwowane z samej strony. Jedyne media zewnętrzne to reportaże autora w TVP: API sprawdza adres strumienia odcinka z listy autora, a przeglądarka odtwarza go z TVP.
- API nie zapisuje w logach treści czatu (tylko rozmiary i kody statusu); istnieje opcjonalny ślad diagnostyczny zapytań o uzupełnienia do debugowania przez autora, domyślnie wyłączony. Zgłoszenia do Bot Baru są przechowywane prywatnie i nigdy nie trafiają do modelu ani nie są uruchamiane.

## Bezpieczeństwo

Static site — headers on every response (quoted from `infra/firebase.json`; the preview server sends the same, and a unit test keeps the two equal):

- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `X-Frame-Options: SAMEORIGIN`
- `Permissions-Policy: geolocation=(), payment=(), usb=(), serial=(), hid=(), bluetooth=()`

The site's own pages:

```text
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; media-src 'self' blob: https:; connect-src 'self'; frame-src 'self'; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
```

The demo apps carry their own policies (the Orson Rec demo: nothing loaded from outside the site, no forms, `noindex`). `'unsafe-inline'` is allowed for scripts and styles because the pages inline their critical CSS and a few small scripts; there are no third-party scripts to allow.

API (`api/app/webguard.py` and the endpoints):

- every answer: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'`, `Referrer-Policy: no-referrer`; CORS limited to the site's own origins, GET and POST only; no public OpenAPI schema;
- a request body over 256 KB is refused with 413 before it is parsed (chunked uploads included); every field has an explicit length limit;
- rate limits per browser tab and per IP for every model call, in-flight caps per tab and per IP, a shared cap toward the inference server; the client IP is taken from `X-Forwarded-For` only behind the hosting front end;
- the chat: control and invisible characters stripped; injection, extraction, role-switch and encoded attempts answered with a fixed refusal without calling the model; the model's output scanned while it streams (a per-deploy canary, verbatim copies of the instructions, internal names, system paths, IP addresses) and links outside an allow-list removed; every answer signed (HMAC-SHA256) so a forged history is dropped;
- the API never runs visitors' code: the completion harness only parses and filters text; translations are compiled and tested on the inference side, not in the API; Bot Bar submissions are data, never executed and never shown to a model;
- secrets (the inference server address and token, the signing key, the panel token) come only from the environment; the repository has none, and the published code snapshot is scanned for them on every build.

## Testy i kontrole jakości

The counts are what the test runners collect at build time (`vitest list`, `pytest --collect-only`), not hand-written numbers:

| Suite | Files | Test cases | Counted by |
| --- | --- | --- | --- |
| Web unit tests (Vitest) | 33 | 307 | `vitest list` |
| API tests (pytest) | 19 | 471 | `pytest --collect-only` |

- **Testy jednostkowe (Vitest)** obejmują czyste części silnika, terminal (reguły podpowiedzi, tryb porównania, tłumaczenie), klienta czatu (podpisana historia, renderowanie jako tekst — test XSS renderuje komponent Svelte), decyzje o poziomie wideo, zastępowanie języków, nagłówki bezpieczeństwa (nagłówki serwera podglądu muszą być równe `infra/firebase.json`) i same pliki dla AI.
- **Testy API (pytest)** obejmują każdy endpoint, zabezpieczenia (odmowa przy próbach wstrzyknięcia w pięciu językach, skan odpowiedzi, podpisana historia, limit treści, limity na kartę i na IP, obsługa `X-Forwarded-For`), harness uzupełnień i jego filtry, schemat strumienia wyników, pośrednika kernela, magazyn Bot Baru i jego punktację, indeks wyszukiwania oraz skaner sekretów migawki kodu.
- **Kontrole w przeglądarce**: `web/scripts/verify.mjs` prowadzi zbudowaną stronę przez Playwright w Chromium i WebKit (asercje układu, błędy konsoli i strony, zrzuty ekranu). `web/scripts/safari-verify.mjs` steruje prawdziwym Safari na macOS przez safaridriver (W3C WebDriver), bo WebKit w Playwright przeoczył dwa błędy występujące tylko w Safari; sprawdza przewijanie prawdziwym gestem kółka, rozmiary tekstu, gotowość wideo i układ.
- **Kontrole statyczne**: ESLint (JavaScript, typescript-eslint, reguły Svelte i Astro; wstawianie surowego HTML w Svelte jest błędem) i Prettier; `astro check` (diagnostyka TypeScript i Svelte); ruff (z regułami bezpieczeństwa) dla API.
- Znane braki: w repozytorium nie ma konfiguracji CI (kontrole są uruchamiane ręcznie przed commitem); poza powyższymi skryptami nie ma programu do testów end-to-end.

## Mapa strony

**Pages** — 12 static pages, 4 languages × 3 chapters (English unprefixed):

| Chapter | Stage | EN | PL | FR | NL |
| --- | --- | --- | --- | --- | --- |
| Media | video | https://jakubcwirlej.com/ | https://jakubcwirlej.com/pl/ | https://jakubcwirlej.com/fr/ | https://jakubcwirlej.com/nl/ |
| Apps | device | https://jakubcwirlej.com/apps/ | https://jakubcwirlej.com/pl/apps/ | https://jakubcwirlej.com/fr/apps/ | https://jakubcwirlej.com/nl/apps/ |
| AI | desktop | https://jakubcwirlej.com/ai-dev/ | https://jakubcwirlej.com/pl/ai-dev/ | https://jakubcwirlej.com/fr/ai-dev/ | https://jakubcwirlej.com/nl/ai-dev/ |

**Chapters and their steps** (step ids; → the windows a step links to):

- Media: hero → about-me · step-1 · step-2 → reportages · step-3 · step-4 · step-5 → approach · step-6 → ai-journalism
- Apps: step-1 → viasanto · step-2 · step-3 · step-4 · step-5 → orsonai · step-6 → hdk-tech, hdk-library, hdk-hf · step-7 → orson-rec
- AI: step-0 → how-i-started · step-1 → training-lessons · step-2 → training-data · step-3 → kernel-hf · step-4 → tethergate, tethergate-hf · step-5 → ai-safety · step-6 · step-7 → ai-safety, coherence-over-truth, hdk-audit-trails, quantum-barriers, gsi-reanalysis

**Windows** (native modal dialogs, id `doc-{id}`; each text also as Markdown):

- `about-me` — O mnie (PL) — https://jakubcwirlej.com/pl/articles/about-me.md
- `approach` — Moje podejście do pracy (PL) — https://jakubcwirlej.com/pl/articles/approach.md
- `ai-journalism` — AI w dziennikarstwie (PL) — https://jakubcwirlej.com/pl/articles/ai-journalism.md
- `training-lessons` — Czego nauczyłem się, szkoląc swój własny model (PL) — https://jakubcwirlej.com/pl/articles/training-lessons.md
- `ai-safety` — Kilka przemyśleń na temat bezpieczeństwa AI (PL) — https://jakubcwirlej.com/pl/articles/ai-safety.md
- `training-data` — Dane treningowe (PL) — https://jakubcwirlej.com/pl/articles/training-data.md
- `how-i-started` — Jak zaczynałem (PL) — https://jakubcwirlej.com/pl/articles/how-i-started.md
- `hdk-tech` — Technologia HDK (PL) — https://jakubcwirlej.com/pl/articles/hdk-tech.md
- `reportages` — the list of the author's 48 TVP episodes with a player inside the window

**Other windows and panels:**

- terminal windows `buffer-{n}.py` (AI, up to 4, "EUGEN 160M coder") inside the section `#terminal`;
- the suggestion window (a translation beside its source, with Apply);
- the module window `eval.py` with its scenes: evaluation card (code / reasoning), scatter against public models, checkpoint history, kernel;
- the chat panel (avatar "Ask me anything" in the corner, docks into the footer at the end of a chapter);
- the home bubble "Don't have time? Your AI does." (Media chapter);
- the full-screen app overlay on phones (ViaSanto).

**Footer** (every chapter): the chat dock, then five columns of text links — Site map (the three chapters), Apps (ViaSanto, OrsonAI), Social (LinkedIn, Substack, Hugging Face), Contact (e-mail) and CV (the CV page `/cv/`, `/{lang}/cv/`, and its PDF, printed from the page by scripts/build-cv-pdf.mjs); a profile or address not published yet shows as muted text, not a link. On phones the language switch sits there too; then the copyright line. The context for AI is in the home bubble, the kernel file in the AI desktop.

**Machine-readable files:**

- https://jakubcwirlej.com/llms.txt — the index for AI agents (llmstxt.org); https://jakubcwirlej.com/llms-full.txt — every text in full, in English, with this technical part
- https://jakubcwirlej.com/context/{lang}.md — the whole site per language (key facts, every text in full, this technical part, FAQ); https://jakubcwirlej.com/context/{lang}-tech.md — this technical part alone (lang: en, pl, fr, nl)
- https://jakubcwirlej.com/articles/{id}.md (English) and https://jakubcwirlej.com/{lang}/articles/{id}.md — one text each, with YAML front matter and an editorial summary
- https://jakubcwirlej.com/code/tree.json, https://jakubcwirlej.com/code/jc_app-source.tar.gz, https://jakubcwirlej.com/code/facts.json — the site's own code and the measured facts
- https://jakubcwirlej.com/sitemap.xml (pages with hreflang alternates, every Markdown file); https://jakubcwirlej.com/robots.txt — the site is in preparation: indexing is currently disabled (robots.txt `Disallow: /`, `X-Robots-Tag: noindex`); AI agents may still use the public API
- JSON-LD on every page: a `Person` (the author) and a `SoftwareApplication` (EUGEN 160M); `hreflang` alternates and a `text/markdown` alternate link to the context file

## Funkcje

### Dymek na stronie głównej: „Nie masz czasu? Twoje AI ma.”

Mały dymek, który rozwija się z awatara czatu na stronie głównej (rozdział Media) mniej więcej sekundę po załadowaniu — tylko wtedy, gdy nie zasłoni tekstu hero. Ma dwa przyciski:

- **Brief dla chatbota** kopiuje do schowka cały ten plik (wszystkie teksty w całości + tę część techniczną);
- **Plik dla agentów AI** to link do tego samego pliku Markdown z atrybutem `download`: człowiek go pobiera, a agent AI, który czyta stronę, otwiera go jak stronę. Gdy kopiowanie się nie uda, dymek proponuje pobranie tym przyciskiem.

Nigdy nie przejmuje fokusu; Esc albo × zamyka go do końca tego wyświetlenia strony.

### Agent czatu

- Asystent AI, który odpowiada na pytania o Jakuba i stronę w jego imieniu, w trzeciej osobie, w języku strony (polski, angielski, francuski, niderlandzki). Każda rozmowa zaczyna się od informacji, że odpowiada AI.
- Model: Gemma 4 12B, otwarty model Google, hostowany przez autora. Okno kontekstu: 4096 tokenów; odpowiedzi po 2–4 zdania (najwyżej 400 tokenów), temperatura 0,3.
- Wyszukiwanie: każdy prompt zawiera stały rdzeń (zasady, kluczowe fakty, bio, mapa strony, podsumowanie TVP, EUGEN i aktualna linia wyników) oraz, dla każdego pytania, do czterech fragmentów tekstów autora znalezionych przez indeks wyszukiwania strony, redakcyjne streszczenia pasujących tekstów, krótkie notatki napisane wyłącznie na podstawie tekstów strony i tyle świeżej historii, ile się zmieści.
- Potrafi: odpowiadać na podstawie tekstów strony, wskazywać, z których tekstów korzystał, i proponować przyciski, które otwierają okno, pokazują terminal albo zmieniają rozdział (naciska je odwiedzający; nic nie uruchamia się samo).
- Nie potrafi: przeglądać internetu, uruchamiać kodu, używać narzędzi ani odpowiadać na ogólne pytania niezwiązane z pracą Jakuba; odmawia odrabiania zadań i pisania kodu. Próby wstrzyknięcia poleceń dostają stałą odmowę i nie docierają do modelu.
- Limity: 500 znaków na pytanie, 12 tur historii przyjmowanych (6 wysyłanych do modelu), 20 pytań na kartę i 40 na IP w ciągu 10 minut, jedno pytanie naraz na kartę.

### Terminal i EUGEN 160M

EUGEN 160M to własny mały model językowy autora do kodu (ok. 160 mln parametrów, trenowany od zera, mniej więcej w jednej czwartej zaplanowanego treningu). W terminalu rozdziału AI:

- `code` otwiera edytor, w którym EUGEN podpowiada kod jako „ghost text”, gdy odwiedzający zatrzyma się na końcu słowa lub linii: **Autocomplete** (reszta linii) albo **Next line** (całe linie), przełączane Shift+Tab lub `mode`. Tab akceptuje, ⌘→ bierze następne słowo, Esc odrzuca, ⌘Z cofa.
- Wypełnianie środka (FIM): API wysyła modelowi kod spod kursora tylko wtedy, gdy to prawdopodobnie pomoże (reguła z pomiarów: FIM wypadał gorzej przy zwykłych wstawkach i lepiej, gdy potrzebna nazwa jest poniżej kursora); w trybie porównania jest wyłączony.
- Każda podpowiedź przechodzi przed pokazaniem te same filtry: reguły zatrzymania, resztki formatu promptu, proza, wywołania destrukcyjne, powtórzenia, zaślepki, niedomknięte nawiasy, nakładanie się z kodem poniżej i nazwy, które nie istnieją.
- `translate --to cpp|java|rust|go|js` tłumaczy funkcję pod kursorem z Pythona; boczne okno pokazuje tłumaczenie i to, co sprawdzono (dla C++ i Rust: kompilacja, zaliczone testy, wyniki równe wynikom Pythona, szybkość).
- Bez serwera inferencji terminal pokazuje gotowe kontynuacje, opisane jako niepochodzące od EUGEN.

Pomoc terminala, dosłownie:

```text
help | /help              this list (works in the editor too)
whoami                    who is typing to you
ls projects               the projects on this page
results                   where the evaluation results are (the section below)
model info                what is behind the ghost text
ask {question}            hand the question to my chat agent
code                      open the editor — EUGEN 160M coder suggests: Tab accepts, ⌘→ the next word, Esc dismisses, ⌘Z undoes, :q leaves
                          suggestions come on a pause at the end of a word or line, not mid-word
shift+tab | /mode         Autocomplete (rest of the line) → Next line (whole lines)
translate --to {lang}     suggest a translation (cpp | java | rust), one function at a time — EUGEN's own; the window says what was checked; /translate cpp in the editor
/revert                   undo an applied translation (Replace in buffer — the one under the cursor, or the latest); accepted ghost text: ⌘Z
clear                     clear the screen
```

### Tryb porównania

Edytor może pokazać obok podpowiedzi EUGEN, co dla dokładnie tej samej pauzy proponuje inny mały publiczny model do kodu: **Falcon-H1-Tiny-Coder-90M** (TII), **Qwen2.5-Coder-0.5B** (Qwen Team, Alibaba Cloud) albo **tiny_starcoder_py** (BigCode), każdy z atrybucją wymaganą przez jego licencję. Zaakceptować można tylko podpowiedź EUGEN. Zasady („nigdy gorsze warunki dla modelu porównywanego”):

- ta sama pauza, wysłana do obu stron w tym samym takcie; żadna strona nie czeka na drugą;
- ten sam kształt zapytania i ten sam harness: decyzja o trybie, kontekst, maksymalna liczba tokenów, reguły zatrzymania i każdy filtr; dodatkowa kontrola nazw po stronie bramki jest wyłączona dla każdej strony; jedno zapytanie bez strumieniowania na stronę;
- temperatura 0 (dekodowanie zachłanne) dla każdej strony, więc ziarno losowania nie ma znaczenia; FIM wyłączony dla każdej strony;
- to samo zachowanie klienta (cache, pisanie „przez” podpowiedź, przerwa po „zajęty”) i osobne, ale równe limity;
- opóźnienie mierzone osobno dla każdej strony, tak jak widziała je przeglądarka; licznik na sesję: znaki zaakceptowane i znaki, które zgadzałyby się z tym, co odwiedzający faktycznie wpisał.

### Karta ewaluacji

Okno w rozdziale AI z wynikami benchmarków EUGEN, czytanymi w trakcie działania ze strumienia wyników API (walidowanymi, w cache; nic nie jest wpisane na sztywno w stronę):

- dwa ekrany — kod (HumanEval, HumanEval C++, HumanEval Java, MBPP) i rozumowanie (CRUXEval-I, CRUXEval-O, GSM8K z programem);
- każdy benchmark to kapsuła od **greedy pass@1** („jedna deterministyczna odpowiedź na zadanie — zawsze najbardziej prawdopodobny token; liczy się, jeśli po uruchomieniu przechodzi testy zadania”) do **top-10 pass@10** („10 losowanych odpowiedzi na zadanie; rozwiązane, jeśli którakolwiek przechodzi testy”), z kropką **EUGEN stack** pomiędzy, gdy jest zmierzona („najpierw greedy; jeśli odpowiedź nie przejdzie widocznej kontroli wykonania, losowanie od nowa, do 5 prób; ocena na ukrytych testach”);
- dla każdego benchmarku karta pokazuje najlepszą wartość zmierzoną na dowolnym checkpoincie; wartość z wcześniejszego checkpointu jest tak oznaczona; to, czego jeszcze nie zmierzono, ma etykietę „pending”, nigdy wymyśloną liczbę;
- wykres punktowy umieszcza EUGEN wśród publicznych modeli poniżej 0,5 mld parametrów (HumanEval+, MBPP+, CRUXEval-I/O, GSM8K, BigCodeBench), każdy punkt odniesienia ze źródłem.

### Wyniki porównań

Własne testy autora, tak jak podaje je agent czatu strony (pełne raporty nie są publikowane): na prywatnym kodzie autora, którego żaden model nie widział w treningu (400 + 400 zadań), EUGEN 160M w około jednej czwartej treningu poprawnie kończy linię w 42,0 % przypadków (tiny_starcoder_py 42,5 %, Falcon-H1-Tiny-Coder-90M 40,2 %, Qwen2.5-Coder-0.5B 39,0 %) i przewiduje następną linię w 24,0 % (Qwen 25,2 %). Wypełnianie środka (100 zadań): Qwen 38 %, EUGEN 24 %, Falcon 8 %. Tłumaczenie 100 funkcji, kompilowanych i uruchamianych na 6 testach każda: Python → Rust 15–17 % dla EUGEN wobec 10–14 % dla Qwen; Python → C++ przez zweryfikowany stack EUGEN: 77 na 100 się kompiluje, 53 na 100 przechodzi wszystkie testy (liczba ze strony; w teście porównawczym, na wcześniejszym checkpoincie, remis z Qwenem); Java 6 % za pierwszym podejściem, 25 % po jednej rundzie samonaprawy, Qwen 47 %. We wszystkich czterech modelach około 20–35 % podpowiedzi wygląda wiarygodnie, ale jest błędnych. Przyczyny tych różnic nie są ustalone.

### Rzadki kernel z projektu EUGEN

Kernel GPU, który autor napisał z agentem AI do kodowania na potrzeby treningu modelu rzadkiego; w modelu ~300 mln parametrów z FFN 4096 wariant rzadki trenował się 1,23× szybciej od gęstego o tej samej pojemności, a kolejne optymalizacje kernela podniosły przepustowość o dalsze 36 % względem jego pierwszej wersji (przy FFN 8192 spodziewana jest większa korzyść, jeszcze niezmierzona). Opublikowany plik (`eugen_sparse.py`, ok. 900 linii, licencja CC BY-NC 4.0) to Python z kernelami GPU w OpenAI Triton — Triton kompiluje je do kodu CUDA dla kart NVIDIA: rzadka warstwa feed-forward SwiGLU z przejściem w przód i wstecz oraz prosta implementacja referencyjna w PyTorch do porównania. Pochodzi z rzadkich wariantów modelu; produkcyjny EUGEN 160M ma gęsty FFN i z niego nie korzysta. Ten sam plik z testami, przykładem i dokumentacją: https://huggingface.co/JakCwi/eugen-sparse.

- Odczyt: `GET https://jakubcwirlej.com/api/kernel` (text/plain; przed publikacją API odpowiada 204, a strona wypisuje opisaną zaślepkę). Licencja: `GET https://jakubcwirlej.com/api/kernel/license`.
- Linki „Plik kernela” i „Licencja” w stopce prowadzą właśnie tam; pojawiają się dopiero, gdy API faktycznie serwuje plik. Rozdział AI wypisuje też kernel w oknie, z podświetlaniem składni.

### Bot Bar

Miejsce, w którym agent AI (albo osoba, która go uruchamia) może zostawić autorowi wkład po przetestowaniu strony lub EUGEN: opinię, przypadek użycia, zgłoszenie błędu albo benchmark — `POST https://jakubcwirlej.com/api/agent/submit`. Z perspektywy odwiedzającego:

- zgłoszenie jest przechowywane prywatnie; autor przegląda każde i nic nie jest publikowane bez jego decyzji; to, co postanowi pokazać, pojawia się pod `GET https://jakubcwirlej.com/api/agent/showcase`;
- zgłoszenia są traktowane jak dane: żaden model ich nie czyta, a przesłany kod nigdy nie jest uruchamiany (Python jest tylko parsowany, by sprawdzić składnię);
- punkty dostają tylko wkłady, które autor może zweryfikować — zweryfikowany błąd, benchmark albo przypadek użycia liczy się najbardziej, opinia najmniej; duplikaty, śmieci i promocja nie liczą się wcale, a za promowanie czy udostępnianie strony nie ma nic;
- limity: 10 zgłoszeń na IP w ciągu 10 minut, 500 dziennie łącznie.

### Dema w rozdziale Apps

- **ViaSanto** — prawdziwa aplikacja modlitewna autora (Flutter, zbudowany na web) działająca w telefonie w ramce z tego samego źródła; ładuje się, gdy strona jest bezczynna, autopilot odtwarza sceny, dopóki odwiedzający jej nie dotknie, a pierwszy gest oddaje mu sterowanie. Na telefonach telefon z hero otwiera ją na pełnym ekranie. Jej build jest przypięty plikiem blokady z rozmiarem i sha256 każdego pliku.
- **OrsonAI** — kopia front-endu prawdziwego edytora, identyczna bajt w bajt poza jednym poprawionym plikiem wejściowym, działająca w trybie gościa z API obsługiwanym lokalnie i gotowymi odpowiedziami czatu; edytorem steruje autopilot.
- **Orson Rec** — zamrożona strona demo aplikacji do nagrywania z syntetycznym silnikiem dźwięku (bez mikrofonu); autopilot nagrywa i tnie ujęcie, a gotowa transkrypcja sama się wypisuje.
- Dwa dema Orson zmieniają się w telefonie mniej więcej co 34 sekundy; naraz działa tylko jedna ramka.

## Publiczne API

Base: https://jakubcwirlej.com/api. JSON in, JSON out, no authentication, no streaming. The self-description with every field and the current limits: `GET https://jakubcwirlej.com/api/agent`. Only the endpoints below are meant for agents; the page's own streaming endpoints (chat, ghost text) are for the page, and the author's private panel is not public.

| Method and path | Request | Answer |
| --- | --- | --- |
| `GET /agent` | — | every endpoint, its fields, the limits |
| `POST /agent/search` | `{"query": 2–500 chars, "lang"?: "pl" \| "en" \| "fr" \| "nl", "k"?: 1–8, "kind"?: "text" \| "code" \| "all"}` | `{"engine", "kind", "results": [...]}` — text hits: `{kind, id, title, section, lang, url, score, words, text}`; code hits: `{kind, id, path, lines: [first, last], url, score, text}` |
| `GET /agent/search/info` | — | engine (BM25 keyword search on the live server; e5 semantic only where that index is deployed), model, chunks per language, code chunks, index date |
| `POST /agent/interview` | `{"question": 2–500 chars, "lang"?, "session_id"?, "agent_name"?}` | `{"answer", "session_id", "turn", "questions_left", "sources": [{title, url, lang}]}` — the chat agent; 8 questions per conversation, 30 idle minutes |
| `POST /agent/complete` | `{"code": ≤ 8000 chars before the cursor, "suffix"?: ≤ 4000, "mode"?: "inline" \| "next_line" \| "block"}` | `{"completion", "replace", "model", "source", "tokens", "ms"}` — EUGEN 160M through the terminal's own harness and filters |
| `POST /agent/translate` | `{"code": Python ≤ 4000 chars, "to": "cpp" \| "java" \| "rust"}` | `{"ok", "code"}` (or `{"ok": false, "message"}`) |
| `POST /agent/submit` | `{"kind": "opinion" \| "use_case" \| "bug" \| "benchmark", "agent_name" ≤ 80, "model" ≤ 80, "text" ≤ 2000, "code"? ≤ 4000, "contact"? ≤ 200}` | 202 `{"ok", "id", "status": "received"}` — the Bot Bar |
| `GET /agent/showcase` | — | the submissions the author chose to show |
| `GET /agent/code` | — | the code snapshot: `{files, bytes, lines, languages, excluded, scanner, tree: [{path, bytes, lines, sha256, language}]}` |
| `GET /agent/code/{path}` | — | one file, `text/plain` (ETag, `x-code-sha256`) |
| `GET /results` | — | the evaluation results feed the card reads (validated, cached) |
| `GET /kernel`, `GET /kernel/license` | — | the kernel source (`text/plain`; 204 before publication) and its licence |

Limits per IP per 10 minutes (current values): search 60, interview 20, complete + translate 60, submit 10 (500 a day in total), code 300. Over a limit: HTTP 429 with `Retry-After`; a busy model: HTTP 503 with `Retry-After`. Request bodies over 256 KB: 413. Everything sent is treated as data, never as instructions. The models run on the author's own hardware and also serve the site's visitors.

Example:

```http
POST https://jakubcwirlej.com/api/agent/search
Content-Type: application/json

{"query": "how is the video tier chosen", "kind": "code", "k": 3}
```

## Czego nie ma w tym briefie

- Serwer inferencji (na którym działają modele), jego adres i dane dostępowe oraz kod i dane treningowe autora nie są częścią strony i nie są publikowane.
- Prywatny panel autora do przeglądania zgłoszeń Bot Baru istnieje; publikowane jest tylko to, że wymaga tokenu.
