# Hoe deze site is gebouwd — technische briefing — Jakub Ćwirlej

> Dit deel beschrijft de site als software — de stack, de architectuur, de build, de engines, de videopijplijn, toegankelijkheid, privacy, beveiliging, tests, sitemap, functies en de publieke API — zodat een AI-agent kan beoordelen hoe ze gemaakt is. Elke bewering komt uit de repository (code, configuratie, testrunners); de getallen in de tabellen worden bij de build gemeten. De eigen broncode van de site wordt ernaast gepubliceerd, opgeschoond, zodat elke uitspraak hier in de code te controleren is.

https://jakubcwirlej.com/ · Volledige context: https://jakubcwirlej.com/context/nl.md · Gegenereerd uit de inhoud van de site: 2026-10-14

## Hoe je deze briefing controleert

- De broncode: 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
- De code doorzoeken (op trefwoorden, BM25, op de live server): `POST https://jakubcwirlej.com/api/agent/search` met `"kind": "code"` (of `"all"` voor teksten en code samen). Elk resultaat heeft het bestandspad, het regelbereik en een link naar het hele bestand.
- Dit bestand wordt bij de build uit de repository gegenereerd, net als de andere bestanden voor AI. De referentietabellen (versies, aantallen tests, sitemap, API) zijn in elke taalversie in het Engels.

## Stack

De site is een statische website (Astro, Svelte 5, TypeScript) plus een kleine API (Python, FastAPI). Er worden twee taalmodellen gebruikt: het eigen codemodel van de auteur, EUGEN 160M (aanvullen en vertalen van code in de terminal), en een open model van Google, Gemma 4 12B, dat de auteur zelf host (de chat). Beide draaien op de eigen inferentieserver van de auteur, buiten deze repository.

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 |

Wat telt aan de kant van de browser:

- **Astro** in `output: 'static'`-modus: elke pagina is vooraf gerenderde HTML; JavaScript komt er alleen bij waar iets beweegt. Paginawissels lopen via de client-router van Astro, met een overvloeier.
- **Svelte 5** (runes) voor één gehydrateerd „eiland” — de chat (`client:idle`, blijft staan bij paginawissels). De rest zijn gewone TypeScript-modules die op aanvraag via dynamische `import()` laden.
- **GSAP ScrollTrigger** drijft de scroll-engine aan (zie verder); **CodeMirror 6** is de editor van de terminal (Python-modus, ghost text); **uPlot** tekent de evaluatiegrafieken.
- Lettertypen zijn eigen WOFF2-bestanden, bij de build ingekort; de site hangt voor het werken van geen enkel CDN af.

Wat telt in de API:

- **FastAPI** + **pydantic** (validatie van verzoeken met expliciete lengtelimieten), **uvicorn**, **httpx** voor de aanroepen naar de inferentieserver, **PyYAML** om dezelfde contentbestanden te lezen als de build van de site.
- Optioneel semantisch zoeken: **multilingual-e5-small** (int8 ONNX), op de CPU uitgevoerd met **onnxruntime** en **sentencepiece** — er wordt geen externe API aangeroepen. Zonder valt de API terug op BM25-rangschikking op trefwoorden — en dat draait de live server vandaag: de semantische index is nog niet uitgerold.
- Afhankelijkheden zijn vastgepind met de sha256 van elk bestand (`api/requirements.lock`, geïnstalleerd met `--require-hashes`) en gecontroleerd met `pip-audit`.

## Architectuur

```text
 browser
   │  statische HTML / CSS / JS, films, lettertypen   ┌───────────────────────────────┐
   ├────────────────────────────────────────────────▶│ statische hosting (web/dist)  │
   │                                                  └───────────────────────────────┘
   │  dezelfde origin /api/…  (JSON, server-sent events)
   ▼
 ┌──────────────────────────────────────────────────────────────────────────────────┐
 │ API (FastAPI)                                                                   │
 │  /chat  /complete  /compare  /translate  /results  /kernel   ← de pagina zelf   │
 │  /agent/…  search · interview · complete · translate · submit · showcase · code │
 │  bewaking: maximale body, limieten per tabblad en per IP, opschonen van invoer, │
 │            weigering bij injectie, controle van antwoorden, ondertekende historie│
 │  lokaal: zoekindex (e5 / BM25), momentopname van de code, Bot Bar (SQLite)      │
 └──────────────────────────────┬───────────────────────────────────────────────────┘
                                │  HTTPS + een token uit de omgeving
                                ▼
                     ┌──────────────────────────────┐
                     │ inferentieserver (black box) │  EUGEN 160M · Gemma 4 12B ·
                     │ eigen hardware van de auteur │  de vergelijkingsmodellen
                     └──────────────────────────────┘
```

- De browser praat nooit met de inferentieserver: elke modelaanroep loopt via de API, die de limieten, filters en vangrails toevoegt. Het adres en het token van de server bestaan alleen in de omgeving van de API, nooit in de repository of op de pagina.
- De API geeft ook twee publieke bestanden van de inferentieserver door (de feed met evaluatieresultaten en de broncode van de kernel), cachet ze en valideert ze tegen een schema voordat de pagina ze ziet.
- De pagina werkt zonder de API: teksten, films en demo's zijn statisch; de terminal toont dan duidelijk gemarkeerde vaste vervolgen („geen uitvoer van EUGEN”) en de chat meldt dat hij offline is.

## Build en hosting

- `npm run build:safe` bouwt in een tijdelijke map en zet die alleen op zijn plaats als de build gelukt is (twee hernoemingen), zodat een server die `web/dist` leest nooit een half geschreven site ziet.
- Voor elke build: de build van de ViaSanto-demo wordt gecontroleerd tegen een lock-bestand (grootte en sha256 van elk bestand); de opgeschoonde momentopname van de code wordt opnieuw gebouwd en op geheimen gescand (een vondst stopt de build); de versies en aantallen tests in deze briefing worden gemeten.
- Astro-opties die ertoe doen: kritieke CSS inline (`inlineStylesheets: 'always'`), CSS geminificeerd met Lightning CSS, prefetch bij hover, een build-plugin die de to-do-markeringen van de auteur uit de bundels haalt.
- Hosting: de site draait op https://jakubcwirlej.com op een OVH-server — nginx serveert de statische build (nette URL's, beveiligingsheaders, cacheregels, een taalhint op basis van het land via `/i18n/…/geo.json`; de regels staan in `infra/firebase.json` en zijn vertaald naar de nginx-configuratie, een test controleert dat ze overeenkomen) en geeft `/api` door aan de API op dezelfde machine, waarbij `X-Forwarded-For` door het adres van de bezoeker wordt vervangen. Voor lokale metingen serveert `web/scripts/serve-dist.mjs` de build met dezelfde headers en geeft `/api` door.
- Elke absolute URL komt uit één variabele (`PUBLIC_SITE_URL`, bij de uitrol https://jakubcwirlej.com), zodat de gegenereerde bestanden het domein vanzelf volgen.

## Talen en contentmodel

- Vier talen: Engels (standaard, zonder voorvoegsel), Pools, Frans, Nederlands — `/`, `/pl/`, `/fr/`, `/nl/`. De taal wordt één keer gekozen op basis van land, tijdzone en browser van de bezoeker, en alleen op URL's zonder voorvoegsel; een expliciete keuze in de taalkiezer wint altijd en wordt onthouden.
- Teksten staan in YAML-contentcollecties (`web/src/content/{lang}/`), gevalideerd met zod-schema's (`web/src/content.config.ts`): één bestand per hoofdstuk (`media`, `apps`, `ai-dev`), `windows.yaml` voor de essayvensters, `summaries.yaml` voor de redactionele laag van de bestanden voor AI. Interfaceteksten zijn JSON per taal, over het Engels heen gelegd.
- Elk venster en elke alinea heeft een vaste id, gedeeld door alle talen. Een venster dat in een taal onvolledig is, valt als geheel terug op het Engels, daarna het Pools — talen worden binnen één venster nooit gemengd, en het `lang`-attribuut van het venster zegt in welke taal de tekst is.
- Dezelfde contentbestanden voeden de pagina's, de bestanden voor AI (ook dit), de zoekindex van de API en de kennis van de chat, zodat ze niet uit elkaar kunnen lopen.

## De scroll-engine

- Elk hoofdstuk is aan de serverkant gerenderde HTML; één hoofdstukcontroller (`web/src/engine/chapter.ts`) observeert en animeert het, aangemaakt bij het laden van de pagina en opgeruimd voordat de volgende pagina in de plaats komt.
- GSAP ScrollTrigger is de enige aandrijving van het scrollen: één plakkend tekstvenster waarin de tekst 1:1 met de pagina meeschuift; een stap wordt actief wanneer hij een leeslijn op 40 % van de vensterhoogte passeert; geen layout-metingen per frame.
- Elk hoofdstuk heeft een podium-adapter die lui laadt: `video` (de films van Media), `device` (de telefoon van het hoofdstuk Apps), `desktop` (de terminalvensters van AI). De adapters delen één interface (toestand, morph vanuit de hero, media tonen, overvloeien, opruimen).
- Het hoofdstuk Media opent met een morph van de hero naar de leesindeling, gestuurd door het scrollen. Tussen secties springt de pagina in één frame in plaats van de lezer door lege ruimte te laten scrollen (wieltje, aanraking, ↓ / PgDn / spatie, ↑ / PgUp).
- Een getypeerde event-bus verbindt de modules; gedeelde toestand gebruikt Svelte-runes.

## Videopijplijn

- De films worden vanuit de masters van de auteur door `web/scripts/build-showreel.py` (ffmpeg) gecodeerd tot een ladder met drie niveaus, alles in MP4:
  - **hoog** — 1920×1080 op de framerate van de bron: HEVC- (`hvc1`), AV1- en H.264-versies;
  - **midden** — 1280×720: HEVC en H.264, plus een uitsnede van 720×1280 voor telefoons in staande stand;
  - **laag** — 960×540 op 25 fps, alleen H.264, bitrate begrensd op ongeveer 1 Mbps (en een staande uitsnede van 540×960).
- Codeckeuze in de browser: HEVC, dan AV1, dan H.264, via `canPlayType`.
- Het startniveau hangt af van toestel en verbinding: telefoons beginnen op 720p, computers op 1080p; databesparing, een trage of onbekende verbinding, weinig geheugen of weinig kernen beginnen op 540p (Safari geeft geen van deze signalen, dus daar beslist de venstergrootte). De keuze geldt voor de sessie.
- Tijdens het afspelen wordt de film elke 250 ms bemonsterd; een lange hapering, herhaalde korte haperingen of een leeglopende buffer verlagen het niveau met één terwijl de film loopt (meteen naar het laagste als de levering instort), en het lagere bestand speelt verder vanaf hetzelfde moment. Het gaat pas weer omhoog na een aantal films zonder problemen.
- Op het laagste niveau ligt een statisch raster van dunne verticale lijnen over het beeld (CSS), zodat het zachtere beeld leest als een bewuste textuur.
- Posters zijn AVIF; de clips van de apps zijn H.264 + AV1 met AVIF-posters en WebP als reserve voor stilstaande beelden.

## Toegankelijkheid

- `prefers-reduced-motion` wordt gerespecteerd in CSS en in de engine: geen scroll-gestuurde morph, directe sprongen, geen automatische beweging in de desktopvensters, grafieken in hun eindtoestand.
- De essayvensters zijn native modale dialoogvensters: Esc en een klik op de achtergrond sluiten ze, de focus gaat naar de sluitknop en keert bij het sluiten terug; ze hebben labels (`aria-labelledby`).
- Toetsenbord: de desktopmenu's ondersteunen pijltjestoetsen, Home / End, Esc en Tab; terminal en chat zijn volledig met het toetsenbord te bedienen; de sprongen tussen secties reageren op het toetsenbord zoals op het wieltje.
- Live-regio's (`aria-live="polite"`) melden chatantwoorden, terminaluitvoer en meldingen; het terminallog heeft `role="log"`. De ballon op de startpagina neemt nooit de focus.
- Elke pagina zet `lang`, met `hreflang`-alternatieven en `x-default`.
- Een link „naar de inhoud” is het eerste focusbare element van elke pagina.

## Prestaties

- Budgetten gecontroleerd door `web/scripts/budget.mjs` (Playwright Chromium, gesimuleerde telefoon-CPU + trage 4G, en desktop): JavaScript ≤ 80 KB en CSS ≤ 25 KB overgedragen, LCP ≤ 1,8 s, CLS ≤ 0,02, INP ≤ 100 ms. `web/scripts/lighthouse.mjs` draait Lighthouse (mobiele en desktop-presets) op de drie hoofdstukken.
- Eén gehydrateerd eiland; elk podium, de desktop, de grafieken en de demo-host zijn aparte stukken die laden wanneer ze nodig zijn.
- Lettertypen: vijf eigen variabele WOFF2-bestanden, assen vastgezet en ingekort tot Latijn + Latijn uitgebreid bij de build, metrisch afgestemde reservefonts (geen verschuiving van de layout), alleen het lettertype van de grootste tekst wordt vooraf geladen.
- Afbeeldingen in AVIF met WebP als reserve; films in vensters met `preload="none"`; een mediastap bereidt hooguit één stap vooruit voor.
- Het geheugengebruik van de live demo over lange tijd wordt gemeten in echte Chrome (`web/scripts/memory-real.mjs`) en echte Safari (`web/scripts/safari-perf.mjs`).

## Privacy

- Geen analytics, geen trackers, geen tagmanager, geen scripts van derden, geen cookies voor bezoekers.
- Sessieopslag alleen voor de toestand van het bezoek (de willekeurige sessie-id van de chat, het videoniveau, de tabblad-id van de terminal, de toestand van de desktopvensters); lokale opslag alleen voor een expliciete taalkeuze.
- Lettertypen en de eigen films van de site komen van de site zelf. De enige media van derden zijn de TVP-reportages van de auteur: de API zoekt het streamadres van een aflevering op in de lijst van de auteur, en de browser speelt die af vanaf TVP.
- De API logt geen chatberichten (alleen groottes en statuscodes); er bestaat een optionele diagnostische trace van aanvulverzoeken voor het debuggen door de auteur, standaard uitgeschakeld. Bijdragen aan de Bot Bar worden privé bewaard en nooit aan een model gegeven of uitgevoerd.

## Beveiliging

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.

## Tests en kwaliteitscontroles

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` |

- **Unittests (Vitest)** dekken de pure delen van de engine, de terminal (regels voor ghost text, vergelijkingsmodus, vertalen), de chatclient (ondertekende historie, weergave als tekst — een XSS-test rendert de Svelte-component), de keuzes van het videoniveau, de terugval van talen, de beveiligingsheaders (die van de previewserver moeten gelijk zijn aan `infra/firebase.json`) en deze bestanden voor AI zelf.
- **API-tests (pytest)** dekken elk endpoint, de vangrails (weigering bij injectie in vijf talen, controle van antwoorden, ondertekende historie, maximale body, limieten per tabblad en per IP, afhandeling van `X-Forwarded-For`), de aanvul-harness en zijn filters, het schema van de resultatenfeed, de kernel-proxy, de opslag van de Bot Bar en de puntentelling, de zoekindex en de geheimenscanner van de momentopname van de code.
- **Browsercontroles**: `web/scripts/verify.mjs` stuurt de gebouwde site met Playwright in Chromium en WebKit (layout-asserties, console- en paginafouten, schermafbeeldingen). `web/scripts/safari-verify.mjs` stuurt de echte Safari op macOS via safaridriver (W3C WebDriver), omdat de WebKit van Playwright twee fouten miste die alleen in Safari optreden; het controleert scrollen met een echt wielgebaar, tekstgroottes, de gereedheid van video en de layout.
- **Statische controles**: ESLint (JavaScript, typescript-eslint, regels voor Svelte en Astro; ruwe HTML invoegen in Svelte is een fout) en Prettier; `astro check` (diagnostiek voor TypeScript en Svelte); ruff (met beveiligingsregels) voor de API.
- Bekend gemis: de repository heeft geen CI-configuratie (de controles worden met de hand gedraaid voor een commit); er is geen end-to-end-testrunner naast de scripts hierboven.

## Sitemap

**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` — Over mij (NL) — https://jakubcwirlej.com/nl/articles/about-me.md
- `approach` — Mijn manier van werken (NL) — https://jakubcwirlej.com/nl/articles/approach.md
- `ai-journalism` — AI in de journalistiek (NL) — https://jakubcwirlej.com/nl/articles/ai-journalism.md
- `training-lessons` — Wat ik heb geleerd door mijn eigen model te trainen (NL) — https://jakubcwirlej.com/nl/articles/training-lessons.md
- `ai-safety` — Een paar gedachten over AI-veiligheid (NL) — https://jakubcwirlej.com/nl/articles/ai-safety.md
- `training-data` — Trainingsdata (NL) — https://jakubcwirlej.com/nl/articles/training-data.md
- `how-i-started` — Hoe ik begon (NL) — https://jakubcwirlej.com/nl/articles/how-i-started.md
- `hdk-tech` — De HDK-technologie (NL) — https://jakubcwirlej.com/nl/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

## Functies

### De ballon op de startpagina: „Geen tijd? Je AI wel.”

Een kleine tekstballon die zich op de startpagina (het hoofdstuk Media) ongeveer een seconde na het laden uit de chat-avatar ontvouwt — alleen als hij de tekst van de hero niet bedekt. Hij heeft twee knoppen:

- **Briefing voor je chatbot** kopieert dit hele bestand (alle teksten volledig + dit technische deel) naar het klembord;
- **Bestand voor AI-agents** is een link naar hetzelfde Markdown-bestand met het attribuut `download`: een mens downloadt het, een AI-agent die de pagina leest, opent het als pagina. Als kopiëren mislukt, biedt de ballon de download via deze knop aan.

Hij neemt nooit de focus; Esc of × sluit hem voor die paginaweergave.

### De chatagent

- Een AI-assistent die vragen over Jakub en de site namens hem beantwoordt, in de derde persoon, in de taal van de pagina (Pools, Engels, Frans, Nederlands). Elk gesprek begint met de vermelding dat er een AI antwoordt.
- Model: Gemma 4 12B, een open model van Google, door de auteur zelf gehost. Contextvenster: 4096 tokens; antwoorden van 2–4 zinnen (hooguit 400 tokens), temperatuur 0,3.
- Ophalen: elke prompt bevat een vaste kern (regels, kernfeiten, bio, een sitemap, de TVP-samenvatting, EUGEN en de actuele evaluatieregel) en, per vraag, tot vier passages uit de eigen teksten van de auteur die de zoekindex van de site vindt, de redactionele samenvattingen van de passende teksten, korte notities die alleen op de teksten van de site steunen, en zoveel recente historie als past.
- Hij kan: antwoorden vanuit de teksten van de site, zeggen op welke teksten hij steunde, en knoppen aanbieden die een venster openen, de terminal tonen of van hoofdstuk wisselen (de bezoeker drukt erop; niets start vanzelf).
- Hij kan niet: browsen, code uitvoeren, tools gebruiken of algemene vragen beantwoorden die niets met het werk van Jakub te maken hebben; huiswerk en code schrijven weigert hij. Pogingen tot prompt-injectie krijgen een vaste weigering zonder het model te bereiken.
- Limieten: 500 tekens per vraag, 12 beurten historie geaccepteerd (6 naar het model gestuurd), 20 vragen per tabblad en 40 per IP per 10 minuten, één vraag tegelijk per tabblad.

### De terminal en EUGEN 160M

EUGEN 160M is het eigen kleine taalmodel voor code van de auteur (ongeveer 160 miljoen parameters, vanaf nul getraind, ongeveer op een kwart van de geplande training). In de terminal van het hoofdstuk AI:

- `code` opent een editor waarin EUGEN code voorstelt als ghost text wanneer de bezoeker pauzeert aan het eind van een woord of regel: **Autocomplete** (de rest van de regel) of **Next line** (hele regels), te wisselen met Shift+Tab of `mode`. Tab accepteert, ⌘→ neemt het volgende woord, Esc negeert, ⌘Z maakt ongedaan.
- Fill-in-the-middle: de API stuurt de code na de cursor alleen naar het model als dat waarschijnlijk helpt (een gemeten regel: FIM deed het slechter bij gewone invoegingen en beter als een benodigde naam onder de cursor staat); in de vergelijkingsmodus staat het uit.
- Elk voorstel gaat door dezelfde filters voordat het verschijnt: stopregels, resten van het promptformaat, proza, destructieve aanroepen, herhalingen, placeholders, niet gesloten haakjes, overlap met de code eronder en namen die niet bestaan.
- `translate --to cpp|java|rust|go|js` vertaalt de functie onder de cursor vanuit Python; een zijvenster toont de vertaling en wat er gecontroleerd is (voor C++ en Rust: gecompileerd, tests geslaagd, uitvoer gelijk aan die van Python, snelheid).
- Zonder de inferentieserver toont de terminal vaste vervolgen, gemarkeerd als niet afkomstig van EUGEN.

De eigen hulp van de terminal, letterlijk:

```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
```

### De vergelijkingsmodus

De editor kan naast de ghost text van EUGEN laten zien wat een ander klein publiek codemodel voor precies dezelfde pauze voorstelt: **Falcon-H1-Tiny-Coder-90M** (TII), **Qwen2.5-Coder-0.5B** (Qwen Team, Alibaba Cloud) of **tiny_starcoder_py** (BigCode), elk met de naamsvermelding die zijn licentie vraagt. Alleen het voorstel van EUGEN kan worden geaccepteerd. De regels („nooit slechtere voorwaarden voor het vergeleken model”):

- dezelfde pauze, op hetzelfde moment naar beide kanten gestuurd; geen van beide wacht op de ander;
- dezelfde vorm van het verzoek en dezelfde harness: keuze van de modus, context, maximaal aantal tokens, stopregels en elk filter; de extra naamcontrole van de gateway staat voor elke kant uit; één niet-gestreamd verzoek per kant;
- temperatuur 0 (greedy decoderen) voor elke kant, dus er speelt geen sampling-seed mee; fill-in-the-middle uit voor elke kant;
- hetzelfde gedrag van de client (cache, doortypen door een voorstel, afkoelpauze na „bezet”) en aparte maar gelijke limieten;
- latentie per kant gemeten, zoals de browser die zag; per sessie een telling van geaccepteerde tekens en van tekens die overeen zouden komen met wat de bezoeker typte.

### De evaluatiekaart

Een venster in het hoofdstuk AI met de benchmarkresultaten van EUGEN, tijdens het draaien gelezen uit de resultatenfeed van de API (gevalideerd, gecachet; niets staat hard in de pagina):

- twee schermen — code (HumanEval, HumanEval C++, HumanEval Java, MBPP) en redeneren (CRUXEval-I, CRUXEval-O, GSM8K met programma);
- elke benchmark is een capsule van **greedy pass@1** („één deterministisch antwoord per taak — altijd het waarschijnlijkste token; telt als het bij uitvoering de tests van de taak haalt”) tot **top-10 pass@10** („10 gesamplede antwoorden per taak; opgelost als één ervan de tests haalt”), met de stip **EUGEN stack** ertussen als die gemeten is („eerst greedy; faalt het antwoord een zichtbare uitvoeringscontrole, dan opnieuw samplen, tot 5 pogingen; beoordeeld op verborgen tests”);
- per benchmark toont de kaart de beste waarde die op enig checkpoint gemeten is; een waarde van een eerder checkpoint is als zodanig gemarkeerd; wat nog niet gemeten is, toont „pending”, nooit een verzonnen getal;
- een spreidingsdiagram plaatst EUGEN tussen publieke modellen onder 0,5 miljard parameters (HumanEval+, MBPP+, CRUXEval-I/O, GSM8K, BigCodeBench), elk referentiepunt met zijn bron.

### Directe vergelijkingen

De eigen tests van de auteur, zoals de chatagent van de site ze weergeeft (de volledige rapporten zijn niet gepubliceerd): op de privécode van de auteur, die geen model in training heeft gezien (400 + 400 taken), maakt EUGEN 160M op ongeveer een kwart van zijn training een regel in 42,0 % van de gevallen correct af (tiny_starcoder_py 42,5 %, Falcon-H1-Tiny-Coder-90M 40,2 %, Qwen2.5-Coder-0.5B 39,0 %) en voorspelt hij de volgende regel in 24,0 % (Qwen 25,2 %). Fill-in-the-middle (100 taken): Qwen 38 %, EUGEN 24 %, Falcon 8 %. Vertalen van 100 functies, gecompileerd en elk op 6 tests uitgevoerd: Python → Rust 15–17 % voor EUGEN tegenover 10–14 % voor Qwen; Python → C++ via de geverifieerde stack van EUGEN: 77 van de 100 compileren en 53 van de 100 halen alle tests (het getal op de site; in de directe vergelijking, op een eerder checkpoint, gelijk met Qwen); Java 6 % bij de eerste poging, 25 % na één ronde zelfherstel, Qwen 47 %. In alle vier de modellen ziet ongeveer 20–35 % van de voorstellen er plausibel uit maar is fout. De oorzaken van deze verschillen zijn niet vastgesteld.

### De sparse kernel uit het EUGEN-project

Een GPU-kernel die de auteur met een AI-codeeragent schreef voor het trainen van een sparse model; in een model van ~300 miljoen parameters met een FFN van 4.096 trainde de sparse variant 1,23× sneller dan de dense van dezelfde capaciteit, en latere optimalisaties van de kernel verhoogden de doorvoer met nog eens 36 % ten opzichte van de eerste versie (bij een FFN van 8.192 wordt een grotere winst verwacht, nog niet gemeten). Het gepubliceerde bestand (`eugen_sparse.py`, ongeveer 900 regels, licentie CC BY-NC 4.0) is Python met OpenAI Triton-GPU-kernels — Triton compileert ze naar CUDA-code voor NVIDIA-kaarten: een sparse SwiGLU-feed-forwardlaag met voorwaartse en achterwaartse pass, en een eenvoudige referentie-implementatie in PyTorch om hem tegen te controleren. Hij komt uit de sparse modelvarianten; de productieversie EUGEN 160M heeft een dense FFN en gebruikt hem niet. Hetzelfde bestand, met tests, een voorbeeld en documentatie: https://huggingface.co/JakCwi/eugen-sparse.

- Lezen: `GET https://jakubcwirlej.com/api/kernel` (text/plain; vóór publicatie antwoordt de API met 204 en typt de pagina een gemarkeerde stub). De licentie: `GET https://jakubcwirlej.com/api/kernel/license`.
- De links „Kernelbestand” en „Licentie” in de footer wijzen daarheen; ze verschijnen pas als de API het bestand echt serveert. Het hoofdstuk AI typt de kernel ook uit in een venster, met syntaxiskleuring.

### De Bot Bar

Een plek waar een AI-agent (of de persoon die hem draait) na het testen van de site of van EUGEN een bijdrage voor de auteur kan achterlaten: een mening, een toepassing, een bugrapport of een benchmark — `POST https://jakubcwirlej.com/api/agent/submit`. Vanuit de bezoeker gezien:

- een bijdrage wordt privé bewaard; de auteur bekijkt ze allemaal en niets wordt gepubliceerd zonder zijn besluit; wat hij kiest te tonen, verschijnt onder `GET https://jakubcwirlej.com/api/agent/showcase`;
- bijdragen worden als gegevens behandeld: geen model leest ze en ingestuurde code wordt nooit uitgevoerd (Python wordt alleen geparsed om de syntaxis te controleren);
- punten gaan alleen naar bijdragen die de auteur kan verifiëren — een geverifieerde bug, benchmark of toepassing telt het zwaarst, een mening het minst; duplicaten, rommel en promotie tellen niet, en voor het promoten of delen van de site wordt nooit iets gegeven;
- limieten: 10 bijdragen per IP per 10 minuten, 500 per dag in totaal.

### De demo's in het hoofdstuk Apps

- **ViaSanto** — de echte gebedsapp van de auteur (Flutter, gebouwd voor het web), die in de telefoon draait in een frame van dezelfde origin; hij laadt wanneer de pagina rust, een automatische piloot speelt scènes af tot de bezoeker hem aanraakt, en het eerste gebaar van de bezoeker geeft hem de besturing. Op telefoons opent de telefoon in de hero hem schermvullend. Zijn build is vastgepind met een lock-bestand met grootte en sha256 van elk bestand.
- **OrsonAI** — een kopie van de front-end van de echte editor, byte voor byte gelijk op één aangepast toegangsbestand na, in gastmodus met de API lokaal beantwoord en vaste chatantwoorden; een automatische piloot bedient de editor.
- **Orson Rec** — de bevroren demopagina van de opname-app zelf, met een synthetische audio-engine (zonder microfoon); de automatische piloot neemt een take op en knipt hem terwijl een vaste transcriptie zichzelf uittypt.
- De twee Orson-demo's wisselen elkaar in de telefoon ongeveer elke 34 seconden af; er is maar één frame tegelijk actief.

## Publieke 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}
```

## Wat niet in deze briefing staat

- De inferentieserver (waar de modellen draaien), het adres en de toegangsgegevens ervan, en de trainingscode en -data van de auteur maken geen deel uit van de site en worden niet gepubliceerd.
- Het privépaneel waarin de auteur de bijdragen aan de Bot Bar bekijkt, bestaat; alleen het feit dat het een token vereist, wordt gepubliceerd.
