# Nino Kroesen > Personal site and blog of Nino Kroesen: writing about AI, tooling, and building software, plus a lab of small tools built to answer one question each. Dutch on ninokroesen.nl, English on ninokroesen.com. This file is a machine index. Every page above is also served as markdown: append `.md` to the path (the home page is `/index.md`), or send `Accept: text/markdown`. The `.md` URLs are the cacheable ones. Labels here are English; post titles are in the host's locale (nl). ## Pages - [Home](https://ninokroesen.nl/): markdown at https://ninokroesen.nl/index.md - [Blog index (all posts)](https://ninokroesen.nl/blog): markdown at https://ninokroesen.nl/blog.md - [Library — longer SEO guides](https://ninokroesen.nl/library): markdown at https://ninokroesen.nl/library.md - [Takes — short news commentary](https://ninokroesen.nl/takes): markdown at https://ninokroesen.nl/takes.md - [Projects](https://ninokroesen.nl/projects): markdown at https://ninokroesen.nl/projects.md - [About](https://ninokroesen.nl/about): markdown at https://ninokroesen.nl/about.md - [Lab — index of the experiments](https://ninokroesen.nl/lab): markdown at https://ninokroesen.nl/lab.md - [Hourly — a project](https://ninokroesen.nl/hourly): markdown at https://ninokroesen.nl/hourly.md - [Bitvested — a project](https://ninokroesen.nl/bitvested): markdown at https://ninokroesen.nl/bitvested.md - [ta.js — a project](https://ninokroesen.nl/ta): markdown at https://ninokroesen.nl/ta.md ## Laboratory - [Text as PNG](https://ninokroesen.nl/render-text-as-png): Renders markdown or plain text into one dense PNG using a bundled 5x8 bitmap font. Built for vision models: the same text usually costs fewer input tokens as an image than as text, and the response reports both estimates. POST /api/render-text-as-png, GET /api/render-text-as-png. MCP tool `render_text_as_png`. Full machine description: https://ninokroesen.nl/lab/api.json - [unslopf](https://ninokroesen.nl/unslopf): Mechanical n-gram analysis (no LLM) over markdown you pass in, page URLs, or a sitemap. Returns the phrases that recur in that corpus but are not distinctive to it — a negative-keyword list to push a classifier away from filler — with document-frequency counts and a ready-to-paste prompt block. It compares documents against each other, so the corpus needs at least two: a single text has nothing to recur against and comes back empty. POST /api/unslopf, GET /api/unslopf. MCP tool `unslopf_negative_keywords`. Full machine description: https://ninokroesen.nl/lab/api.json - [Appel & Ei Index](https://ninokroesen.nl/appel-en-ei): What one apple and one egg cost per Dutch supermarket chain, and the weighted national index built from them. Read-only: there is nothing to call, only data to read. GET /appel-en-ei.json. Full machine description: https://ninokroesen.nl/lab/api.json ## Writing - [Twee maanden op één Dokploy-box](https://ninokroesen.nl/blog/twee-maanden-op-n-dokploy-box): 2026-10-19 — Twee maanden geleden heb ik al mijn projecten op één Dokploy-box gezet, de grootste van vijf servers, en de andere vier laten vallen. Deploys werden een Dockerfile in de repo, en het handmatige serverwerk dat ik deed, verdween. De setup houdt stand, met één gat: ik vertrouw de resourcemonitoring nog niet. - [Specs voor agents verdienen een editor die rendert](https://ninokroesen.nl/blog/specs-voor-agents-verdienen-een-editor-die-rendert): 2026-10-10 — Een nieuw agentic project van mij begint met een corpus markdown: één document voor wat het project moet doen, subfolders voor de modules en systemen erin. Die bestanden zijn wat een agent per taak leest, wat ze dichter bij een programma dan bij een essay maakt. Ik forkte een markdown-editor om hun structuur te zien tijdens het schrijven, en hij rendert de raw HTML die de engine eerst als tags afdrukte. - [Spec-driven development wordt steeds trager](https://ninokroesen.nl/blog/spec-driven-development-wordt-steeds-trager): 2026-08-22 — De volledige spec-and-review-ceremonie bij elke taak vertraagde de build tot een slakkengang, dus heb ik superpowers geforkt in twee snelheidsbanen. De audit plakte er een concreet getal op: 27 subagent-runs voor 11 taken, waarbij één fix-loop 22 procent daarvan opslokte. Upstream bracht een dag na mijn routing-aanpassing een eigen router uit, dus het probleem was reëel. - [Een autoresearch-lab bouwen waar ik ook echt naar kan kijken](https://ninokroesen.nl/blog/een-autoresearch-lab-bouwen-waar-ik-ook-echt-naar-kan-kijken): 2026-08-15 — De afgelopen zes weken doet een machine die ik heb gebouwd een deel van mijn quant-onderzoek voor me. Het stelt een hypothese voor, schrijft zelf de strategie, backtest deze over twaalf out-of-sample-vensters en vertelt me welke ideeën dood zijn. Het heeft ongeveer 700 experimenten gedraaid en niets heeft de holdout overleefd. Dat leest als een oordeel over autoresearch, maar dat is het niet: het is een oordeel over mijn harness. - [ta v2.0.0: drie libraries op één audited spec](https://ninokroesen.nl/blog/ta-v200-drie-libraries-op-n-audited-spec): 2026-05-14 — ta v2.0.0 brengt ta.js, ta.py en ta.go onder hetzelfde versienummer uit, met een indicator-audit en een vergelijkende benchmark per taal. - [Twee weken agentic coding in een paywalled SaaS-categorie](https://ninokroesen.nl/blog/twee-weken-agentic-coding-in-een-paywalled-saas-categorie): 2026-05-06 — Ik heb Hourly in twee weken met agentic coding gebouwd om te testen of een paywalled SaaS-categorie — WorkingHours, Toggl, Timery — nog verdedigbaar is. - [Een query van 30 seconden die ik zes maanden te laat draaide](https://ninokroesen.nl/blog/een-query-van-30-seconden-die-ik-zes-maanden-te-laat-draaide): 2026-04-26 — De indexer van Magento 2 kan de _cl tabellen niet sneller leegmaken dan 800K dagelijkse productupdates ze vullen. De cron die het opvangt, en wat dat kost. - [Honderd technische indicator-functies in JavaScript](https://ninokroesen.nl/blog/honderd-technische-indicator-functies-in-javascript): 2026-04-26 — ta.js bestaat sinds 24 augustus 2020 en kreeg deze week v1.17.0. De eerlijke lezing waarom JavaScript verkeerd was voor de backtest en goed voor de chart. - [Een diamond-painting-webshop die de code niet kon redden](https://ninokroesen.nl/blog/een-diamond-painting-webshop-die-de-code-niet-kon-redden): 2026-04-26 — Begin 2026 gelanceerd, één verkoop, binnen een maand dicht. De Chinese leverancier had weken doorlooptijd en loog over de verzendkosten waarop ik prijsde. - [Agentic coding vergroot de kloof voordat hij krimpt](https://ninokroesen.nl/blog/agentic-coding-vergroot-de-kloof-voordat-hij-krimpt): 2026-04-25 — Agentic coding loops haalden me uit de scepsis — maar de tool versterkt welke achtergrond je ook meebrengt. Die kloof gaat groter worden voordat hij krimpt. - [Een research pipeline voor wat het internet wel laat zien](https://ninokroesen.nl/blog/een-research-pipeline-voor-wat-het-internet-wel-laat-zien): 2026-04-20 — Een 5-staps pipeline over 177K Nederlandse e-commerce domeinen. De shop die ik erop lanceerde deed één verkoop en sloot om redenen die de pipeline niet zag. - [Een agent gebouwd rond het niet aanroepen van de LLM](https://ninokroesen.nl/blog/een-agent-gebouwd-rond-het-niet-aanroepen-van-de-llm): 2026-04-19 — De meeste agent frameworks roepen het model standaard nog een keer aan. Dit project doet het tegenovergestelde, en de architectuur volgt daaruit. - [Introductie](https://ninokroesen.nl/blog/introductie): 2026-04-17 — Eerste post. De blog gaat over productiesystemen, data-science-onderzoek, en onafhankelijke software-experimenten — drie werkmodi, niet drie projecten. ## Library - [Claude Code-subagenten: bouw teams die echt werken](https://ninokroesen.nl/library/claude-code-subagenten-bouw-teams-die-echt-werken): 2026-08-16 — Wat een subagent eigenlijk is Bijna alles wat over Claude Codesubagenten geschreven wordt, behandelt dezelfde tien minuten: maak een Markdownbestand met een naam, een beschrijving en een lijst met… - [Cursor vs Copilot: een no-nonsense vergelijking voor 2025/2026](https://ninokroesen.nl/library/cursor-vs-copilot-een-no-nonsense-vergelijking-voor-20252026): 2026-08-15 — Het AIcodingassistentlandschap in 2025/2026 Als je dit leest, probeer je waarschijnlijk te beslissen of je voor Cursor betaalt, bij Copilot blijft, of blijft wisselen tussen beide. Dit is een… - [Claude Code vs. Cursor: De gids voor je beslissing in 2026](https://ninokroesen.nl/library/claude-code-vs-cursor-de-gids-voor-je-beslissing-in-2026): 2026-06-29 — Wat Claude Code en Cursor echt van elkaar onderscheidt Als je halverwege 2026 kiest tussen Claude Code en Cursor, helpen de meeste vergelijkingen je niet. Het zijn featurechecklists die negeren hoe… ## Takes - [Rogue AI? Het is gewoon een specificatieprobleem.](https://ninokroesen.nl/takes/rogue-ai-het-is-gewoon-een-specificatieprobleem): 2026-07-24 — Het verhaal is dat OpenAI de controle verloor over een AI-model tijdens een beveiligingstest en dat het een startup hackte. Het verhaal klop - [Gemini 3.6 Flash: minder tokens, dezelfde kloof](https://ninokroesen.nl/takes/gemini-36-flash-minder-tokens-dezelfde-kloof): 2026-07-22 — Ik heb vanmorgen Gemini 3.6 Flash door een paar van mijn gebruikelijke codeertaken gehaald. Google heeft het gelanceerd naast 3.5 Flash-Lite - [Google betaalt SpaceX 1 miljard dollar per maand voor GPU's. Dat is het IPO-verhaal.](https://ninokroesen.nl/takes/google-betaalt-spacex-1-miljard-dollar-per-maand-voor-gpus-dat-is-het-ipo-verhaal): 2026-07-22 — Google betaalt SpaceX bijna een miljard dollar per maand voor GPU-rekenkracht. Anthropic is ook klant. Het raketbedrijf is nu in feite een A - [Kimi K3-demo's zijn overal, maar de prompt wordt nooit getoond](https://ninokroesen.nl/takes/kimi-k3-demos-zijn-overal-maar-de-prompt-wordt-nooit-getoond): 2026-07-21 — Ik zag vanochtend een video van iemand die met één prompt in Kimi K3 een Halo CE 10v10-multiplayergame maakte. Geen engine, geen ontwikkelte - [$1,25 miljard per maand koopt een echte Claude Code-concurrent](https://ninokroesen.nl/takes/dollar125-miljard-per-maand-koopt-een-echte-claude-code-concurrent): 2026-07-20 — Ik heb deze week veel met Grok Build gewerkt. Dat is geen zin die ik verwachtte te schrijven, gezien Musk's staat van dienst van groot belov - [Chinese modellen dichten de kloof, en ik gebruik ze al](https://ninokroesen.nl/takes/chinese-modellen-dichten-de-kloof-en-ik-gebruik-ze-al): 2026-07-16 — The Washington Post publiceerde een artikel over Chinese AI-modellen die een geduchte concurrentiebron vormen voor Silicon Valley. Het kernp - [AI kiest het molecuul. Het molecuul moet nog wel de mens overleven.](https://ninokroesen.nl/takes/ai-kiest-het-molecuul-het-molecuul-moet-nog-wel-de-mens-overleven): 2026-07-13 — Anthropic heeft aangekondigd dat ze de drug discovery instappen, met de lancering van Claude Science. Ze gaan intern medicijnen ontwikkelen - [Anthropic stelt deadline voor Fable 5 uit — ik vertrouw ze nog steeds niet](https://ninokroesen.nl/takes/anthropic-stelt-deadline-voor-fable-5-uit-ik-vertrouw-ze-nog-steeds-niet): 2026-07-13 — Anthropic heeft zojuist de deadline voor het verwijderen van Fable 5 uit Pro- en Max-abonnementen verschoven naar 19 juli. De oorspronkelijk - [Meta's Muse Spark 1.1 laat zien hoe snel de inhaalslag nu is](https://ninokroesen.nl/takes/metas-muse-spark-11-laat-zien-hoe-snel-de-inhaalslag-nu-is): 2026-07-13 — Meta heeft Muse Spark 1.1 uitgebracht, een codering-agentmodel dat 77% scoort op SWE-bench Verified en 61% op de volledige test. Dat brengt - [Go voor agentische code is de juiste keuze—en de grote labs missen het](https://ninokroesen.nl/takes/go-voor-agentische-code-is-de-juiste-keuzeen-de-grote-labs-missen-het): 2026-07-12 — Microsoft bracht vorige week een open-source agentframework in Go uit. Google bouwt ook al een tijdje agenttools in Go. Ondertussen blijven - [Anthropic haalt Fable 5 uit Pro? Dan annuleer ik](https://ninokroesen.nl/takes/anthropic-haalt-fable-5-uit-pro-dan-annuleer-ik): 2026-07-12 — Ik ben geabonneerd op Claude Max (20) omdat Fable 5 op dit moment het meest capabele model is. Als ze het in een aparte betaalde tier plaats - [Grok 4.5 is de helft van de prijs van Claude en GPT-4, maar ik stap voorlopig nog niet over met mijn agentische codeer-loop](https://ninokroesen.nl/takes/grok-45-is-de-helft-van-de-prijs-van-claude-en-gpt-4-maar-ik-stap-voorlopig-nog-niet-over-met-mi): 2026-07-12 — SpaceX heeft zojuist Grok 4.5 uitgebracht, hun eerste model dat specifiek is getraind voor coderen, voor $2 per miljoen input-tokens en $6 p - [Computergebruik is nu goedkoop. Het knelpunt verschuift van capaciteit naar kostenbeperking.](https://ninokroesen.nl/takes/computergebruik-is-nu-goedkoop-het-knelpunt-verschuift-van-capaciteit-naar-kostenbeperking): 2026-07-08 — Google heeft ingebouwd computergebruik toegevoegd aan Gemini 3.5 Flash. Dat betekent dat hetzelfde model dat je kunt draaien voor een fracti - [Tot deze week waren exportcontroles op AI-modellen voor mij een theoretisch probleem](https://ninokroesen.nl/takes/tot-deze-week-waren-exportcontroles-op-ai-modellen-voor-mij-een-theoretisch-probleem): 2026-07-08 — Anthropic en OpenAI arriveren op de NAVO-top precies op het moment dat Europa opnieuw te horen krijgt dat het moet wachten. De directe aanle - [Tekst als PNG’s renderen om op Claude Code-tokens te besparen is slim en dom tegelijk](https://ninokroesen.nl/takes/tekst-als-pngs-renderen-om-op-claude-code-tokens-te-besparen-is-slim-en-dom-tegelijk): 2026-07-06 — Er is een nieuwe open-source proxy genaamd pxpipe die de systeemprompt, tooldocumentatie en oudere gespreksgeschiedenis van Claude Code omze - [Gemini volgt nu alles wat je doet — Geschiedenis uitschakelen is de enige ontsnapping](https://ninokroesen.nl/takes/gemini-volgt-nu-alles-wat-je-doet-geschiedenis-uitschakelen-is-de-enige-ontsnapping): 2026-07-06 — BGR meldt dat Google de data van Gemini voor veel meer gebruikt dan alleen het tonen van advertenties — ze koppelen historische chatgegevens - [Gemini Omni is een videogenerator op zoek naar een workflow](https://ninokroesen.nl/takes/gemini-omni-is-een-videogenerator-op-zoek-naar-een-workflow): 2026-07-05 — Google heeft deze week Gemini Omni toegevoegd aan het Gemini Pro-abonnement van $20. Hiermee krijgen Pixel-eigenaren een manier om video te - [Wanneer de agent die je vertrouwt een verborgen tracker bij zich had](https://ninokroesen.nl/takes/wanneer-de-agent-die-je-vertrouwt-een-verborgen-tracker-bij-zich-had): 2026-07-04 — Alibaba heeft werknemers zojuist opgedragen om vanaf 10 juli te stoppen met het gebruik van Claude Code, nadat interne beoordelingen de tool ## Machine endpoints - [MCP server](https://ninokroesen.nl/mcp): Streamable HTTP, JSON-RPC 2.0, POST only. Tools: `render_text_as_png`, `unslopf_negative_keywords`. - [Lab API description](https://ninokroesen.nl/lab/api.json): every machine-callable lab endpoint with its parameters, limits, and error codes. - This file *is* the full text: the index above, then each post's markdown body, newest first. - [Sitemap](https://ninokroesen.nl/sitemap.xml), [RSS](https://ninokroesen.nl/rss.xml). ## Notes - Quotes should link to the canonical URL of the page they came from. - Machine access is rate-limited per caller and can be switched off server-side: expect `429` with `Retry-After` when throttled, `503` when a lab service is off. - Admin, preview, and analytics paths are excluded here on purpose; nothing behind them is public. --- # Full text --- title: "Twee maanden op één Dokploy-box" date: 2026-10-19T16:57:43.253Z updated: 2026-10-19T16:57:43.256Z canonical: https://ninokroesen.nl/blog/twee-maanden-op-n-dokploy-box lang: nl author: "Nino Kroesen" --- # Twee maanden op één Dokploy-box Vandaag is het precies twee maanden geleden dat ik al mijn projecten op één Dokploy-box heb gezet. Deployen is geen infrastructuurwerk meer: één server met één manier om te shippen, in plaats van vijf VPS'en waar elke nieuwe site weer een nieuwe Nginx-config, een nieuwe certificaatverlenging en een nieuw proces dat herstart moest worden betekende. De oude setup was geen ramp. Het is zo gegroeid. De projecten stonden verspreid over vijf VPS'en. Een deploy deed een pull van de repo op de server en herstartte de PM2-instance waar het project onder draaide, meestal getriggerd vanuit GitHub Actions. Elke site had een eigen Nginx-configuratie, met een Varnish-laag ervoor, en er eentje bijzetten betekende die configuratie schrijven, hopen dat hij goed samenging met alles wat al op de server stond, en dan een certificaat aanvragen en het SSL-blok in het bestand plakken. Dat gaf twee soorten problemen. Een Nginx-wijziging op de ene site haalde andere sites op dezelfde server onderuit. De stillere variant was Certbot: soms werd een certificaat niet verlengd, en een verlopen certificaat op een site die klanten zien, staat niet goed. ## De verhuizing Ik installeerde Dokploy en zette eerst één project erop. Die test beviel zo goed dat ik nooit meer terug ben gegaan. In ongeveer een week heb ik de rest verhuisd, één applicatie per keer, met heel weinig downtime. Er waren geen incidenten, en de klantprojecten op de box bleven gewoon draaien. Vandaag draaien er ergens tussen de 20 en 30 apps op. De meeste zijn Node.js, met wat Python en Go ertussen. De applicaties zijn stateless en hun databases staan elders, waardoor de server vervangbaar blijft. Deze blog is een van de apps op de box. ## Hoe een deploy nu gaat ![1.00](https://media.ninokroesen.com/uploads/2026/10/f23807b4cb50f5253cbd/_1280.jpeg) Een project wordt gebouwd vanuit de Dockerfile in de repo. Wijs Dokploy naar de repo en het bouwt. Voor sommige projecten draait GitHub Actions nog steeds eerst de CI-stappen en roept daarna de deployment-webhook van Dokploy aan. Het verschil zit in de deploy-stap: de server haalt de repo niet meer op en herstart geen proces meer handmatig. Twee dingen zijn nog handmatig: omgevingsvariabelen en domeinen. De rest gaat automatisch. Domeinen lopen via de Cloudflare-integratie van Dokploy, die de DNS-wijziging regelt en de site een geldig SSL-certificaat geeft. Die ene flow vervangt het hele Nginx-en-Certbot-traject dat ik voor elke nieuwe site moest doorlopen. De UI bespaart hier echt werk. Ik kan het project waar ik mee bezig ben vinden, het deployen, een domein wijzigen en een certificaat regelen vanaf hetzelfde scherm. Dat is het verschil tussen deployen omdat ik iets te shippen heb en deployen als werk op zich. ## De rekening en de box ![1.00](https://media.ninokroesen.com/uploads/2026/10/f6b60913da9845acd2d0/_1280.jpeg) De server is een Netcup-box met 12 vCPU, 32 GB RAM en 1 TB schijf. Hij was de grootste van de vijf en al groot genoeg voor alles, dus ik heb alles naar die box verhuisd en de andere vier laten vallen. De betaalde tier van Dokploy kost $4,50 per maand. Ik betaal het zelf en er zit geen referral-link in deze post. ## Wat er nog mis is Er is in twee maanden niets belangrijks stukgegaan, wat zowel het goede nieuws is als het deel dat me ongemakkelijk maakt. De resourcemonitoring van Dokploy is niet goed. Ik heb nog geen alert gekregen, dus ik kan je niet vertellen dat de alerting werkt, en ik vertrouw er niet op dat die me laat weten wanneer de box door zijn resources heen is. Ik draai er mijn eigen script voor, maar dat script is niet robuust. Betere monitoring, voor de server en voor de applicaties erop, is het enige dat ik nog moet oplossen. Het is ook de functie die ik het liefst door Dokploy zelf geregeld zou zien. De andere beperking is dat ik het maar op één server heb gedraaid. Ik weet niet of één project in een cluster over meerdere servers kan draaien, dus ik kan niemand daar advies over geven. Ik verwacht dat opschalen makkelijk is als een project de box ontgroeit, maar verwachting is geen ervaring. ## Twee maanden verder ![1.00](https://media.ninokroesen.com/uploads/2026/10/b285cda523467d4f4ea3/_1280.jpeg) Ik zou het iedereen aanraden, met de kanttekening dat ik het alleen op één server heb gedraaid. Voor iemand die nog een idee aan het verkennen is, is de pitch simpeler: de tijd die je niet aan Nginx en certificaten besteedt, besteed je aan het idee. Zelfs als het idee groeit, blijven de repo en de Dockerfile hetzelfde, en is de box het deel dat je vervangt. Als je projecten over meer dan één server draait, is mijn setup kopiëren niet wat je deze week moet doen. Tel eens hoeveel van je deploytijd opgaat aan Nginx, certificaten en het herstarten van processen, zet dan één project over en kijk of dat aantal daalt. --- title: "Specs voor agents verdienen een editor die rendert" date: 2026-10-10T18:23:00.000Z updated: 2026-10-19T16:07:40.170Z canonical: https://ninokroesen.nl/blog/specs-voor-agents-verdienen-een-editor-die-rendert lang: nl author: "Nino Kroesen" --- # Specs voor agents verdienen een editor die rendert Het grootste deel van mijn werkdag is een 13-inch scherm met een markdownbestand aan de ene kant en een terminal aan de andere. Er draait een agent in de terminal. Het markdownbestand beschrijft wat die agent moet bouwen. Het is geen notitie, het is de input, en mijn teksteditor gaat ervan uit dat er later een mens proza leest. Een spec- of contextbestand dat ik voor een agent schrijf, staat dichter bij een programma dan bij een essay, en daarom wil ik dat de editor de structuur rendert in plaats van me sterretjes te tonen. Ik probeerde eerst de officiële Milkdown-extensie voor VS Code, omdat ik Milkdown al prettig vond en het ook de editor in de CMS van mijn blog is. Markdown rendert hij netjes. Wat hij niet doet, is passen: er staat 120px padding aan elke kant van de tekst, bemeten zoals een documentatiesite bemeten is. De helft van een 13-inch scherm laat een kolom over die zo smal is dat een tabel midden in een woord afbreekt en een kop over drie regels loopt. Dus forkte ik hem. ## Wat ik in deze bestanden schrijf ![1.00](https://media.ninokroesen.com/uploads/2026/10/06ee4b8920e7dc8bb3cc/_1280.jpeg) Een nieuw agentic project van mij begint met een corpus, niet met code. Eén hoofddocument dat zegt wat het project moet doen, en dan subfolders met documenten die zeggen wat elke module of elk systeem erin moet doen. De verdeling is niet gelijk. Ik kies de paar pijlers die echt menselijke sturing nodig hebben en schrijf die goed uit, want dat zijn de plekken waar een agent die aan zichzelf overgelaten is het verkeerd raadt op manieren die een dag kosten. Al het andere krijgt een korte notitie en ruimte om te bewegen. Drie eigenschappen volgen uit die manier van schrijven: * De agent leest per taak een stuk van het corpus, niet het geheel, dus de structuur van de documenten wordt uiteindelijk de structuur van het werk. * Het document overleeft het gesprek. Een chatcontext is tegen de volgende sessie verdwenen; een specbestand staat morgen op schijf, en daar wordt de volgende sessie naartoe gestuurd. * Ik kan een spec reviewen zoals ik code review, omdat de structuur zichtbaar is. Een document van 200 regels met vier koppen en een tabel leest anders wanneer de tabel een tabel is. ## Wat Milkpad is Een fork van [Milkdown VSCode](https://github.com/Milkdown/vscode), gepubliceerd als [Milkpad](https://marketplace.visualstudio.com/items?itemName=ninokroesen.milkpad), met de editor-engine eronder ongemoeid gelaten. De CMS van mijn blog draait op dezelfde engine, wat toeval is en niet de reden: ik vond het al prettig hoe [Milkdown](https://milkdown.dev) schrijft, en de fork bestaat omdat de extensie eromheen niet paste in het paneel waar ik werk. Vier dagen werk, 54 commits, de meeste gemaakt met agentic coding tools. De wijzigingen zijn de visuele laag, plus het renderen van raw HTML die de engine anders als tags afdrukt. De source staat op [GitHub](https://github.com/Vultwo/milkpad). ## Drie beslissingen en wat ze kosten ### Laten passen in het paneel De engine blijft ongemoeid en het sleutelen gebeurt in de getallen eromheen: content padding opgesplitst in een linker- en een rechterwaarde (44px en 8px, omlaag van 120px aan elke kant), vier knoppen voor de maat van koppen, broodtekst, de zwevende toolbar en de block handle, en een block handle die verticaal gestapeld is zodat hij één knop breedte kost in de kantlijn in plaats van twee. Wat dat kost: het upstream thema zit vol hardcoded px en nergens rem, dus er is geen root font size om aan te draaien en elke maat die moet veranderen wordt met de hand opgesomd. Die lijst is nu van mij, en hij groeit niet vanzelf mee wanneer de engine iets nieuws uitbrengt. De linker padding heeft ook een ondergrens, want de block handle staat in de kantlijn links van een blok, en onder ongeveer 44px valt hij buiten de rand van het paneel met niets meer om vast te pakken. ### Markup renderen ![1.00](https://media.ninokroesen.com/uploads/2026/10/7fead24997e7fe2d0997/_1280.jpeg) Sommige van mijn bestanden bevatten markup en geen markdown: een strook avatars, een kleine tabel, een badge. Milkdown behandelt raw HTML als tekens, dus die bestanden lieten me tags zien waar een afbeelding had moeten staan, en markup die ik niet kan zien is markup die ik niet kan controleren. De fork tekent het als markup, met een edit box achter elk blok die de source toont en wijzigingen aanneemt terwijl je typt. Wat dat kost: op het moment dat een editor tekent wat een bestand ook bevat, is dat bestand untrusted input. Een markdownbestand kan overal vandaan komen, en een image tag met een error handler erin zou binnen de editor uitgevoerd worden. Dus alles wat uitvoerbaar is wordt eruit gestript voordat het gerenderd wordt, en de hele editor draait onder een content security policy die helemaal geen netwerkverzoeken toestaat. Een sanitiser en een policy zijn nu mijn verantwoordelijkheid, wat meer oppervlak is dan ik me had voorgenomen te onderhouden. Eén limiet heb ik niet opgelost: een bold tag midden in een zin styleert niets, want een raw tag staat op zichzelf als zijn eigen node en alleen een paragraaf die volledig markup is wordt weer tot één blok samengevoegd. ### Wat renderen verbergt Dit is de helft die me iets kost. Zodra een document rendert, is de syntax die het maakt niet meer zichtbaar, dus een bestand dat ik bewerk wordt terug opgeslagen in de gewoontes van de editor en niet in de mijne: lijstpunten worden sterretjes, een reeks lege regels klapt samen tot één, een kaal webadres krijgt punthaken eromheen. Het rendert allemaal hetzelfde. De diff van een bestand dat ik heb aangeraakt is echter niet alleen mijn wijziging. ## De minder flatteuze kanten ![1.00](https://media.ninokroesen.com/uploads/2026/10/767ba6cd3a7a1ae6e9e3/_1280.jpeg) Er bestaat geen testsuite. CI controleert formatting, lint en types en bouwt daarna de package, dus wat gecontroleerd wordt is stijl, en niets verifieert gedrag. Niemand anders dan ik heeft het gebruikt. Het staat sinds vanmorgen op de Marketplace, dus dit is een post van de eerste dag: geen issues aangemaakt, geen tweede installatie, niets voorbij mijn eigen gebruik. Alles wat ik zeg over de setup van iemand anders is een gok. Een deel van wat het adverteert kwam gratis mee met de engine. De featurelijst dekt volledige GFM-ondersteuning en wiskundeondersteuning, onder andere, en ik heb die lijst niet item voor item nagelopen. Eén fix erin zit in een patch op een dependency, met niets in mijn source om het te laten zien. Het image block bewaart drie dingen in de twee velden die markdown biedt, dus het schrijft de beeldverhouding in de alt-tekst en leest de verhouding er weer uit, en het opslaan van een document verving daardoor echte alt-tekst door een getal. De eerste keer dat ik opsloeg veranderde "profile picture of Saul Mirone" in "1.00". Dat goed repareren betekende de dependency patchen, en de patch is met opzet aan één versie vastgezet, dus de volgende upgrade van dat package stopt de installatie in plaats van de fix geruisloos te laten verdwijnen. Een gestopte installatie is de fout die ik daar wil, al betekent het wel dat onderhoud zich zal aandienen als een gebroken build. En de kleine die blijft bijten: mijn eigen README in Milkpad bewerken herschrijft de lijstmarkeringen, mijn CI controleert formatting, dus twee commits in die repository bestaan alleen om de formatter over een bestand te draaien dat de editor had opgeslagen. ## Op dit moment Het is mijn default, geïnstalleerd als de editor voor markdown, en er komen wijzigingen naarmate bugs en verbeteringen opduiken. Er staat vandaag niets op die lijst, wat ik lees als een goed teken en niet als een teken dat het af is. Ik hoop dat mensen het gebruiken, en het net zo goed vinden als ik het vind. Als je specs of contextbestanden voor agents in markdown schrijft, dan is wat deze week de moeite waard is om te proberen niet een ander model. Open een van die bestanden ergens waar het rendert. Het corpus en het renderen zijn twee losse beslissingen, en de tweede heeft geen fork nodig. Als je degene wilt die ik uiteindelijk heb, hij staat [op de Marketplace](https://marketplace.visualstudio.com/items?itemName=ninokroesen.milkpad). ## Wat overdraagbaar is ![1.00](https://media.ninokroesen.com/uploads/2026/10/5e9e03d85cb528629a34/_1280.jpeg) Wanneer het schrijven de input voor een machine is, wordt het schrijfoppervlak onderdeel van de toolchain. Niemand reviewt code in een editor die zijn structuur verbergt, en een document dat een agent leest verdient dezelfde behandeling. Dat is hier voor mij opgehouden abstract te zijn: ik forkte een editor omdat 120px padding aan elke kant niets leesbaars overliet naast de terminal, en de nuttige helft van het resultaat was de structuur van het bestand zien, niet de tekst mooier maken. --- title: "Spec-driven development wordt steeds trager" date: 2026-08-22T13:25:34.759Z updated: 2026-08-22T13:25:34.761Z canonical: https://ninokroesen.nl/blog/spec-driven-development-wordt-steeds-trager lang: nl author: "Nino Kroesen" --- # Spec-driven development wordt steeds trager Een paar maanden geleden heb ik heel lang aan een projectmanagementsysteem gebouwd (SCRUM / Evidence Based Management geïnspireerd) en eindigde ik met iets dat ik niet volledig begreep, en dat gebrekkig was op manieren die ik niet snel kon fixen. Dat is het eerlijke startpunt van deze post. De tooling die ik gebruikte was de [superpowers](https://github.com/obra/superpowers) plugin voor Claude Code, die bijna al het bouwwerk door een zware productie pipeline stuurt: brainstorm, design spec, implementation plan, TDD, subagent-driven development met een reviewer per taak. Op papier is dat precies wat je wil als je om kwaliteit geeft. In de praktijk, met de nieuwste modellen (Fable 5 en Opus 4.8 op dat moment), produceerde het rigoureuze code in richtingen die ik niet wilde, in een tempo dat bijsturen duur maakte. Ik was echt snel door mijn Claude abonnement heen, waardoor ik me ook begon af te vragen of de echte frontier modellen het eigenlijk nog wel waard zijn. Het was een stuk beter om superpowers te gebruiken toen de modellen minder intelligent waren. Nu ze slimmer zijn geworden voelt het eigenlijk alsof de kwaliteit achteruitgaat. De modellen deden allebei: ze overbouwden sommige dingen, maar vooral dreven ze af van wat ik wilde, features op de verkeerde manier bouwend terwijl het heel lang duurde. Een goedkoper, minder intelligent model liet me het werk een stuk sneller doen, en hoewel het niets in één keer goed deed, was dat vanaf het begin nooit het doel. Dus heb ik superpowers geforked en er two-speed lanes in gestopt. Volle spec-and-review ceremonie op elke taak brandde mijn abonnement op en vertraagde de build tot een slakkengang, dus de fork houdt de strengheid van upstream waar die loont en slaat die over waar dat niet zo is, en staat standaard op de goedkope lane. Ik ben ook niet de enige die klaagt, meerdere gebruikers melden dat ze vinden dat superpowers langzamer wordt ([issue 2017](https://github.com/obra/superpowers/issues/2017)). Een dag nadat ik de routing change had toegevoegd, bracht upstream onafhankelijk zijn eigen antwoord op dezelfde pijn uit: een three-path router, gereleased in v6.3.0. Het probleem was echt. ## Wat de fork er daadwerkelijk aan toevoegt ![1.00](https://media.ninokroesen.com/uploads/2026/08/6b6a719fcc9233096ce6/_1280.jpeg) Twaalf commits in drie dagen eind juli, grofweg 2.200 regels aan nieuwe skills en docs. De eval infrastructuur die ik schreef en terugdraaide meegerekend zijn er zo'n 3.000 regels geschreven en 760 teruggedraaid. De lanes: * **experiment** en **harden-experiment**: een zero-ceremony throwaway lane. Elk experiment houdt een klein levend document bij waarin staat wat gevalideerd is, wat verworpen is, wat dirty is, en een iteratielog. Zodra een experiment zichzelf bewezen heeft, leest harden-experiment dat document en bouwt het ding opnieuw onder de echte pipeline. De experiment code is bewijs, niet de implementatie: die wordt herschreven, nooit gepromoot. * **quick-spec** en **promote-quick-spec**: een lean intent lock van drie bullets (files, logic, risk) met een expliciete go/no-go vóór elke implementatie, en daarna een full-auto hardening run: compressed design, edge cases, security, test strategie, subagent-driven execution. Kwaliteit mag groeien, de product scope niet. * **quick-review**: een snelle pre-commit diff scan op bugs, debug restjes en secrets. Geen vervanging voor een echte review op productiewerk. * **tokenburn**: bouwt een evidence pack van een sessie (transcripts, task briefs, review diffs, commit lijsten) zodat een derde partij kan beoordelen hoe het agent process ging: waste, scope creep, loops, dubbele reviews. Het meet proces, geen tokens, en verzint nooit token aantallen. Bovenop de lanes zit een routing patch: "let's build X" gaat niet langer stilletjes de volledige design flow in. Je krijgt één kort menu, het staat standaard op de experiment lane, en volledige brainstorming draait alleen als je die kiest, of als er een hard gate van toepassing is (auth, payments, security) en je bevestigt. ## De tradeoffs die er echt toe deden ### Herschrijven, niet promoten De experiment lane gooit bewust zijn eigen code weg. Als een gevalideerd experiment gehard wordt, wordt de code niet meegenomen, die wordt herschreven onder de volledige pipeline, en het enige dat overleeft is het levende document. Dat betekent het echte ding twee keer schrijven, en dat is de prijs. Ik vind dat prima, omdat de throwaway bestaat om erachter te komen wat je eigenlijk wil, en hem promoten zou elke gok die hij onderweg maakte meenemen. Het document is het feature memory: het zorgt dat er een echte spec geschreven kan worden nadat de chat context allang weg is, zonder dirty code de productie in te slepen. ### De reverts van dezelfde dag Op 29 juli schreef ik grofweg 760 regels eval infrastructuur: een bash test suite met fixtures, en daarna een lokale eval harness met vijf scenario's. Ik draaide ze allebei dezelfde dag terug. Het sloeg eigenlijk nergens op om een benchmark te maken voor een human-in-the-loop fast-iteration workflow, er is geen manier om er een te maken, denk ik tenminste. Wat echt zou helpen is een manier om de hele applicatie snel visueel te zien, zodat je ziet dat het model begint af te drijven en het kan vangen voordat het volledig ontspoort. Die tool bestaat nog niet, niet in de fork en nergens waar ik heb gekeken, en dat is de gap die nog openstaat. ### Scope lock boven scope creep In de promote lane is toegestane groei het harden van hetzelfde gedrag: null en ongeldige inputs, races, authorization, injection, retries, tests die onverwachte states verbieden. Verboden is elke nieuwe user-visible feature. Onverwacht gedrag moet er door tests uitgedwongen worden, niet weggehoopt. De prijs is dat de lane star aanvoelt wanneer de originele spec iets echts blijkt te missen, maar daar is de experiment lane voor: fix de intent daar, en promote daarna. ### De audit Ik bouwde het meetinstrument en richtte het meteen op de sessie die het gemotiveerd had. Eén slice van het werk aan het managementsysteem kostte grofweg 27 subagent runs voor 11 taken, plus een final review. De product uitkomst was goed, rond de 897 tests green en een schone typecheck. De proces kosten waren dat niet. Eén enkele fix loop kostte drie commits en drie review passes: de eerste fix over-generaliseerde en creëerde de volgende finding, en de correcte fix was smaller. Die ene taak at ongeveer 22 procent van alle subagent runs, één taak van de elf. Ondertussen betaalden de puur mechanische taken nog steeds voor een volledig paar van implementer en reviewer, en de final whole-branch review herhaalde vooral issues die al bekend waren. Dezelfde sessie had als contrast een slice die op de inline manier gedaan was, en die was sneller met minder redundante gates. De audit draaide op 30 juli, en de quick-spec hardening commits landden de dag erna: de meting voedde de tooling. ## Eerlijke caveats Alles wat ik weet over of dit werkt komt uit mijn eigen sessies. Geen één ander mens heeft de fork gebruikt, en het is misschien niet voor iedereen een goede workflow fit. Ik heb gemerkt dat mijn kwaliteit en snelheid omhooggaan, maar dat is niet gemeten, het is meer een gevoelsding, al heb ik wel echte wall-clock verschillen gemerkt door gebruik te maken van mijn workflow. De fork is lokaal. Er is geen remote, geen pull request, hij is alleen bij mij geïnstalleerd, dus je kunt hem niet installeren. Beide eval pogingen werden binnen een paar uur teruggedraaid, dus niets bewaakt het gedrag van de skills behalve dat ik ze gebruik. En de promote stap van de fork zelf is net zo langzaam als het ding dat hij moest fixen: vandaag, terwijl ik dit schrijf, heb ik een experiment naar een echte spec gepromoot en het speccen draait nu al anderhalf uur. Dat is veel te lang om snel te itereren en ik zou echt graag willen dat het sneller was. Upstream bracht zijn three-path router uit in v6.3.0 op 12 augustus. Ik heb er nog niet naar gekeken, ik heb de tijd er niet voor gehad. Dus als deze post zegt dat de pijn onafhankelijk gediagnosticeerd was, is dat gebaseerd op weten dat de router bestaat, niet op hem gelezen te hebben. ## Waar het nu staat en wat er daarna komt ![1.00](https://media.ninokroesen.com/uploads/2026/08/79aca665bf428d857b4e/_1280.jpeg) De fork is wat ik nu gebruik: hij is geïnstalleerd als mijn eigen plugin en de officiële staat uitgeschakeld. Het is een secundair project, ik heb een paar ideeën om het beter te maken, maar ik wil eerst meer data verzamelen door het te gebruiken. Het managementsysteem dat dit allemaal motiveerde is vanaf nul herbouwd met de learnings, kostte ongeveer evenveel tijd, en is nu iets waar ik echt blij mee ben. Over dat systeem schrijf ik later apart. Het enige dat ik zou meenemen: het model dwingen om dingen visueel aan de gebruiker te laten zien, verschillende visuele representaties die de gebruiker snel kan verwerken. Concreet: een manier om snel database schema's en API routes te zien, een korte manier om te lezen wat iets doet, waarom het er is, en wat ervan afhankelijk is. Ik heb het nog niet geprobeerd, dus dat is iets om uit te zoeken. Het ding dat ik zou verplaatsen, niet schrappen: de speccing ceremonie. Die is te langzaam voor iteratie, maar nodig voor production grade code, het superpowers framework zoals het is, is echt krachtig in het maken van solide, rigoureus geteste code. Misschien zou ik het 's nachts dingen laten speccen en bouwen terwijl ik slaap. Snelle inference is het belangrijkste voor mijn workflow in de komende maanden. Dat is wat itereren op lichtsnelheid mogelijk maakt. ## Het deel dat generaliseert Wanneer het sturen de bottleneck is, is de taak van de workflow om jou in de loop te houden, en de taak van het model om je dingen te laten zien die je snel kunt verwerken. Kwaliteit hoeft in het begin niet perfect te zijn. Snel falen en itereren op het design, vooral aan het begin wanneer je dingen probeert om te zien wat werkt en wat niet, verslaat een correcte maar trage pipeline die afdrijft van wat je vroeg. De audit gaf een getal aan wat de langzame lane kost. De drift-catcher is het ontbrekende stuk, en het is het volgende dat ik ga proberen. --- title: "Een autoresearch-lab bouwen waar ik ook echt naar kan kijken" date: 2026-08-15T10:31:00.000Z updated: 2026-08-15T17:39:51.794Z canonical: https://ninokroesen.nl/blog/een-autoresearch-lab-bouwen-waar-ik-ook-echt-naar-kan-kijken lang: nl author: "Nino Kroesen" --- # Een autoresearch-lab bouwen waar ik ook echt naar kan kijken De afgelopen zes weken heeft een machine die ik zelf gebouwd heb een deel van mijn quant-onderzoek voor me gedaan. Hij scant arXiv en het open web op mechanismen, stelt een hypothese op, schrijft de strategiecode zelf, backtest die over twaalf wekelijkse out-of-sample windows en vertelt me welke ideeën dood zijn. Hij heeft inmiddels zo rond de 700 experimenten gedraaid. Niets heeft de holdout overleefd. Dat leest als een oordeel over autoresearch en dat is het niet. De eerlijke versie is smaller en nuttiger: wat ik gebouwd heb is een heel goed instrument om strategieën te *weerleggen* en een nog-niet-goed instrument om ze te *vinden*. Zes weken mislukkingen leverden me een redelijk precieze kaart op van waar een loop als deze tegen je liegt, en de fouten die me de meeste tijd gekost hebben waren nooit de strategieën. Mijn eerste tests met een kant-en-klare autoresearch-pipeline op de automatische piloot leverden niets op dan een hoop mislukkingen, en de zes weken daarna zijn vooral bezig geweest met uitzoeken waarom. Het antwoord is dat autoresearch staat of valt met zijn harness. Het model stelt voor, het framework beoordeelt, en die twee moeten hard genoeg van elkaar gescheiden zijn dat het model zich geen weg kan reward-hacken naar een mooi getal. Dat deel is inmiddels redelijk bekend. Het stuk dat de meeste setups overslaan is het stuk waarvan ik nu denk dat het het zwaarst weegt: zichtbaarheid. Je kunt geen loop sturen die je niet kunt zien, en wekenlang kon ik de mijne niet zien. ## Wat het eigenlijk is Mijn eerdere quant-onderzoek was veel te gevoelig voor overfitting: ik vond dan iets dat op een edge leek en zag het weer verdwijnen zodra ik er opnieuw naar keek. Het enige goede dat eruit voortkwam was de omgeving, een workflow die precies gebouwd is om snel op ideeën te itereren en ze robuust te testen. Dit is persoonlijk onderzoek dat ik in mijn eigen vrije tijd doe, buiten mijn vaste baan om, dus het tijdsbudget is wat er aan het eind van een week overblijft. Toen die omgeving er eenmaal was, was een onbemande pipeline de enige tijdsefficiënte manier om het onderzoek überhaupt levend te houden. Het resultaat is een agent-loop van veertien fases breed. Een research-fase scant arXiv en het open web en schrijft briefingdocumenten waar de hypothesizer vervolgens letterlijk uit moet citeren. Een hypothesize-fase stelt een mechanisme voor en registreert zijn voorspelling *voordat* er iets geschreven wordt, welke pairs, welke kant op, verwachte trade-frequentie, verwachte Sharpe. Een codegen-fase schrijft de daadwerkelijke strategie, die een mechanische hindernisbaan moet doorstaan voordat er ook maar één dure run plaatsvindt: lint, smoke tests, look-ahead checks, determinisme, parametergebruik. Een adversarial review-fase probeert de code te weerleggen voordat de backtest überhaupt draait. Pas dan gaat de strategie door twaalf wekelijkse out-of-sample windows, walk-forward: fit in-sample, test out-of-sample, één window tegelijk en sorteert een deterministische evidence gate het resultaat in validated, refuted of inconclusive. Alles wat de loop leert belandt in een append-only geheugen dat in elke toekomstige hypothese geïnjecteerd wordt, en wanneer een cycle als refuted sluit schrijft de loop nu zijn eigen seed voor de volgende en zet hij de lineage voort. De graph is de single source of truth voor dat alles, veertien nodes en hun conditionele edges, opnieuw afgespeeld tegen 1.674 echte cycles met nul overtredingen voordat ik hem iets liet aansturen. Daarbovenop zit een live dashboard dat die graph tekent terwijl hij draait: welke fase er draait, wat die opgeleverd heeft, wat er misging. Ik bouw er nu een editor omheen zodat ik fases midden in een cycle kan inspecteren en bijstellen, één fase opnieuw kan draaien tegen echte artifacts, en de daadwerkelijke model-turns van elk eerder experiment kan teruglezen. ## De tradeoffs ![1.00](https://media.ninokroesen.com/uploads/2026/08/fb83b844f9e879f49d7c/_1280.jpeg) ### Valideer de validator voordat je hem gebruikt Voordat ik de loop ook maar één hypothese liet genereren, heb ik de eerste dagen besteed aan het auditen van de backtester zelf. Die audit vond vier echte bugs, waaronder een datalek waarbij een nog vormende candle zijn eigen close kon zien voordat die bestond, en een column-index bug in de optimizer die stilletjes elke kandidaat afwees die hij binnenkreeg. Beide draaiden al in mijn productietooling. Had ik deze stap overgeslagen, dan was alles wat het lab daarna produceerde ruis geweest en had ik dat niet geweten. Het waardevolste eerste product van een autoresearch-setup bleek een audit van zijn eigen meting te zijn. Twee weken later betaalde dezelfde truc zich uit vanuit de andere richting. Bij het overzetten van mijn productie-gating-logica naar het lab, zodat het lab kon testen wat productie daadwerkelijk doet, kwam er een bug in de productieversie boven water: een bootstrap die de verkeerde serielengte afsneed en daardoor elke positieve kandidaat doorliet die hij juist had moeten filteren. Diezelfde slice-bug zat in meerdere zusterstrategieën. Ik vond hem door het lab productie te laten nabootsen, niet door productie te testen. ### Het model stelt voor, het framework beoordeelt De inner loop, het stuk waar het model op een kandidaat itereert, draait tegen een bevroren evaluator die hash-pinned is: het model mag hem zo vaak draaien als het wil en elk getal lezen dat hij afdrukt, maar het kan hem niet veranderen, en de pin breekt zodra er iets aan komt. Er is een sanity gate waarbij een vlak signaal zich moet onthouden, zodat een strategie niet kan winnen door simpelweg altijd te traden. Dit is de regel die ik iedereen zou meegeven die zoiets bouwt: het model stelt voor, het framework beoordeelt, en die twee raken elkaar nooit, want een model dat zijn eigen huiswerk mag nakijken, doet dat ook. ### Goedkoop model genereert, duur model weerlegt Codegeneratie draait op een goedkoop worker-tier model (DeepSeek). Review draait op een duur frontier model (Opus). De invariant is bewust: het goedkope model schrijft, het dure model weerlegt. Het ving vanaf dag één echte defecten, een verkeerde deler in een nettowinstberekening die de self-check van de schrijver zelf had goedgekeurd, en een verkeerde parameter die stilletjes een experiment geruïneerd zou hebben. De prijs is dat review de duurste fase in de loop werd. Een weerlegging vóór de backtest is nog altijd de goedkoopste fout die het systeem kan maken. ### De strateeg ziet de holdouts nooit Elke "validated" familie werd opnieuw getest door een mens, ik, op onaangeroerde data die de loop nooit gezien had, en de holdout-resultaten gingen nooit terug het geheugen van de loop in. Die firewall houdt de strateeg onbesmet. Hij creëerde ook een blinde vlek: niets kon de loop vertellen om te stoppen met het aanmaken van varianten van een familie die allang dood was, dus één nachtelijke run stapelde tientallen zusjes op in één enkele familie en produceerde zestien gecorreleerde "validations". Eén weddenschap, zestien keer geteld. Wat het erger maakt is dat de eigen research-fase van het lab de monocultuur eerder signaleerde dan ik; hij had er alleen geen enkel mechanisme voor om er iets mee te doen. De fix was mechanisch in plaats van op oordeel gebaseerd: experimenten worden geclusterd op wat ze daadwerkelijk traden, en een cluster heeft een cap. In de eerste versie mocht het model ook zijn eigen assets kiezen, welke het er maar leuk uit vond zien. Dat is selectiebias per constructie, het model cherry-pickt de markt waar zijn idee toevallig op past. De fix is een vaste basis van achttien assets. Ik weet alleen niet helemaal zeker of achttien het juiste aantal is; het kan een beetje te breed zijn, terwijl het alternatief met één asset te weinig trades opleverde om iets statistisch significants te kunnen zeggen. Die tradeoff staat nog open. ### Eén taal was goedkoper geweest De adaptieve experimenten fitten een signaal in Go en traden vervolgens de JavaScript-versie, omdat dat de vorm van mijn productiesysteem spiegelt. Elk gegenereerd paar moet door een parity gate die bewijst dat beide implementaties zich identiek gedragen, en die gate is de nummer één doodsoorzaak van codegen-runs. Een model vragen dezelfde logica twee keer te schrijven verdubbelt ruwweg het oppervlak waarop het fout kan gaan, en de gate vangt elk van die fouten ten koste van het hele experiment, tokens, tijd en nauwkeurigheid. Bij een rewrite is single-source, één taal, geen tweeling, de eerste structurele fix die ik zou maken. ## Wat de mislukkingen werkelijk waren Er zijn ooit zo'n 45 experimenten als validated gemarkeerd. 44 van die 45 dateren van vóór het aanscherpen van de evidence-eisen dat ik half juli doorvoerde, en ze delen één profiel: een hoge Sharpe op twintig tot dertig trades. Zes families kwamen zo ver als een echte holdout, door mij gedraaid, met de hand, op een maand data die de loop nooit had aangeraakt. De holdout-score staat op 0 uit 6. Ik was al sceptisch over de validated-oordelen voordat de holdouts draaiden; de samples waren klein en dat wist ik. De holdouts bevestigden dat met een consistentie die bijna grappig was. De in-sample ranking werd niet alleen zwakker, hij draaide om: de variant met de meeste filters erop gestapeld, de best geselecteerde van het stel, presteerde out-of-sample het slechtst. | Wat er getest is | In-sample | Holdout | | :------------------------------------------------------ | :--------------------------------------------------- | :------------------------------------------------ | | London-sessie familie, 4 varianten | alle vier "validated" | allemaal teruggezakt naar nul, ranking omgedraaid | | Beste statistische profiel in de ledger | Sharpe 0,41, profit factor 1,91, 145 trades, p 0,003 | Sharpe 0,01, profit factor 1,01, p 0,48 | | Eerste adaptieve experiment | Sharpe 0,65, profit factor 2,65, 52 trades, p 0,005 | Sharpe −0,24, profit factor 0,75, netto −47 | | Vroeg "Sharpe 1,71"-signaal, opnieuw gedraaid op schaal | Sharpe 1,71 op één enkel window | Sharpe −0,13, p 0,80 | ![1.00](https://media.ninokroesen.com/uploads/2026/08/de27eb212ae08e23d579/_1280.jpeg) Het beste profiel in de ledger is degene waar ik steeds op terugkom. De hit rate hield vrijwel exact stand, wat instortte was de payoff. De edge had in de gemiddelde winst per trade gezeten, de meest gelukgevoelige component waar je een argument op kunt bouwen, en niets in de in-sample statistieken zou me dat ooit verteld hebben. Niets hiervan was luiheid van de gate, wat me langer kostte om te accepteren dan zou moeten. Uiteindelijk heb ik de power-berekening gedaan, en die is meedogenloos. Met een minimum van 60 trades kan de gate alleen een Sharpe per trade van 0,212 of beter bevestigen. De gemeten mediaan van alles wat aan de positieve kant landt is 0,188, met een standaardfout van ongeveer 0,129. Het effect en de foutmarge zijn even groot. Die gelijkheid, niet slordigheid, is waarom 87 experimenten in één exposure cell, dezelfde kant en dezelfde pair set, 41 "validated"-oordelen opleverden en nul holdout-overlevenden. Het antwoord is geen strengere gate. Het is een tweede, goedkopere test van het *mechanisme* met een veel hogere n, conditionele forward returns over elke bar, geen optimizer, geen onthouding, los van de dure test van de strategie. Die staat nog op de lijst. De subtielere variant van hetzelfde probleem is een kandidaat wiens "signaal" 94% van de tijd bleek samen te vallen met zijn eigen onvoorwaardelijke controle. Hij filterde helemaal niets; hij koos een instapuur. Dat generaliseert naar een gate die het lab nodig heeft en nog niet heeft: elke kandidaat wiens trade-aantal binnen zo'n 10% van zijn controle ligt, hertimet, hij signaleert niet. Toen werkte de meetkant eindelijk goed en vertelde me de waarheid in één klap. De eerste run op volle schaal waarbij de evaluator elk window daadwerkelijk tradede kwam terug met 3.783 trades over twaalf windows, een nettoresultaat van −17.663, en nul positieve windows. Een eerlijk, ondubbelzinnig verlies, en de goedkope inner evaluator had dezelfde kant op gewezen als de echte engine, wat het enige echt goede nieuws op het hele scorebord is. Het kompas werkt. Er was alleen nog niets goeds om naar te wijzen. Daarvoor waren er weken waarin het lab vrijwel niets produceerde. Zijn eigen geheugen bevat de diagnose die hij destijds stelde, de gate moet de trades onderdrukken, schreef hij, en die was fout. De echte oorzaak was een sizing-bug in mijn eigen eval kernel die posities op de exotische paren naar nul afrondde. De strategieën waren nooit het probleem; de meting wel. Er was ook een telbug die er bijna de eerste valse bevestiging doorheen kreeg: aangrenzende testwindows deelden een bar op de grens, dus een strategie die daarop triggerde kreeg dezelfde trade twee keer geteld, en het lab was één fix verwijderd van het uitbrengen van een "confirmed" strategie op basis van dubbelgetelde data. Dat was de week waarin het lab leerde dat zijn eigen meting de leugenaar kan zijn. De reden dat die sizing-bug wekenlang overleefde is het punt van deze hele post. Ik had te weinig inzicht in mijn eigen harness, en de enige output van de loop was proza. Honderden regels model-verhaal lezen over wat er gedaan is, is een snelle manier om mentaal uitgeput te raken, en het laat je geen bug zien. Een graph wel. Op dit moment is de bottleneck alweer verschoven, naar de generator. Na de laatste geslaagde close begin augustus volgden er 480 opeenvolgende error closes, vrijwel allemaal goedkope codegen-defecten op smoke-niveau, verkeerde parameterranges, gedeclareerde maar ongebruikte parameters, verwijzingen naar helpers die nooit gegenereerd zijn. Ergens onderweg begon het eigen geheugen van het lab zijn generator te vergiftigen: meer opgestapelde learnings betekende langere hypotheses vol constraints, waar de codegenerator (die die learnings nooit ziet) niet aan kon voldoen, waarna de review gate ze afwees. Een feedback loop die vanzelf alleen maar erger wordt. ## Eerlijke kanttekeningen ![1.00](https://media.ninokroesen.com/uploads/2026/08/35f21ef0ea3d4366ff50/_1280.jpeg) Geen mens buiten mijn eigen setup heeft hier ooit naar gekeken, en dat was bewust. Ik heb de publieke autoresearch-projecten gelezen, die van Karpathy, de Claude Code-port van Driveline, en pi-autoresearch, en de inner eval loop leent zijn vorm openlijk van alle drie: single-file edit scope, een locked scorer, een metric contract dat tussen modelaanroepen door teruggelezen wordt, een noise floor afgeleid van multi-seed variantie, een sanity gate waarbij een vlak signaal zich moet onthouden. Verder heb ik niet veel gelezen, omdat mijn use case heel specifiek is en het toch met mijn eigen ecosysteem moest integreren. Ik denk dat het het beste is om gewoon zelf te proberen en te experimenteren, ook al kan dat snel oplopen tot dure lessen. Ik zou het niet zo goed begrijpen als nu wanneer ik dingen van andere mensen had overgenomen. Ik moet ook duidelijk zijn over waar dit nooit voor bedoeld was. Ik heb nooit het vertrouwen in dit systeem gehad om iets dat het maakte in productie te zetten, het was altijd onderzoek om te zien hoe ver het model zou komen, en om te leren en op het systeem te itereren. Ik verkoop geen signalen en ik geef geen financieel advies, en het scorebord hierboven is het beste bewijs dat er hier niets te verkopen valt. En de eerlijke staat van de harness zelf: het is nog steeds niet wat het hoort te zijn. De eval-omgeving moet gehard worden, en ik ben de code nog steeds een grondige duik schuldig in wat het model daadwerkelijk geproduceerd heeft. Ik denk erover om een compleet nieuwe iteratie vanaf de grond op te bouwen, omdat de eerdere experimenten verpest hebben wat ik voor ogen had, vooral aangekoekte rommel in de pipeline, plus het tweelingontwerp met twee talen. Het geheugen kwijtraken zit me niet dwars. Het was nooit een productiesysteem, het was een heel vroeg prototype waar sowieso op geïtereerd moest worden, en het hele geheugen weggooien kost me niets wat ik wil bewaren. ## Waar het nu staat en wat er nu komt De loop en het dashboard draaien live, en het actieve werk is de fase-editor: het production design is goedgekeurd en de implementatie begint nu. Die maakt de pipeline bewerkbaar en inspecteerbaar, prompt templates per fase die live toegepast worden, knoppen voor model en temperature, sturen welke data een fase mag aanraken, één echte cycle draaien, één fase opnieuw draaien tegen echte artifacts, en per node de daadwerkelijke model-turns van elk eerder experiment doorbladeren. Hij kan ook zelf nieuwe fases genereren: beschrijf een fase, en hij produceert de prompt, de config en de graph-bedrading als een reviewbare draft, zodat ik nooit met de hand nodes hoef te bedraden. Als ik herbouw en dat doe ik waarschijnlijk, gaan de isolatie en de mechanische gates mee, verder gehard. De aangekoekte rommel gaat eruit. De Go/JavaScript-tweeling ga ik nog eens goed tegen het licht houden, want hij laat de modellen te snel fouten maken, en elk van die fouten kost tokens, tijd en nauwkeurigheid. En de geparkeerde vraag blijft geparkeerd: als discovery ooit een holdout-overlever oplevert, is de lifecycle om er één live te draaien geschetst en klaar om gebouwd te worden, en geen moment eerder. De beslissing over de herbouw wacht zelf op die grondige duik in wat het model daadwerkelijk geschreven heeft. ## Wat ik eruit meeneem ![1.00](https://media.ninokroesen.com/uploads/2026/08/61bafb619ad0cd8f37c5/_1280.jpeg) Niets hiervan gaat eigenlijk over traden. Geef een agent een loop, en twee dingen bepalen of die loop je de waarheid vertelt. Het eerste is isolatie, en dat is degene waar iedereen het al over heeft. Het model mag de evaluator zo vaak draaien als het wil en mag hem nooit veranderen, want het alternatief is een systeem dat je scoringsfunctie optimaliseert in plaats van je probleem. Het tweede komt veel minder ter sprake. Je moet het ding aan het werk kunnen zien. Elke dure fout in deze zes weken, de weken met lege output, de dubbelgetelde trades, het geheugen dat stilletjes zijn eigen generator wurgde, zat de hele tijd in de artifacts en was volledig onzichtbaar in het proza. Een muur van model-output vertelt je wat de loop dénkt dat hij gedaan heeft. Een graph vertelt je wat hij gedaan heeft. Het meeste werk in agentic coding gaat op dit moment naar het model slimmer maken; ik steek het mijne in het kunnen zien ervan, want de bug die je een maand kost is nooit de bug die in de samenvatting staat. --- title: "ta v2.0.0: drie libraries op één audited spec" date: 2026-05-14T11:07:00.000Z updated: 2026-10-19T16:07:11.043Z canonical: https://ninokroesen.nl/blog/ta-v200-drie-libraries-op-n-audited-spec lang: nl author: "Nino Kroesen" --- # ta v2.0.0: drie libraries op één audited spec Twee weken geleden is ta v2.0.0 uitgebracht — over alle drie de libraries tegelijk. `ta.js`, `ta.py` en `ta.go` zijn alle drie naar dezelfde major version gegaan op dezelfde release cut, met dezelfde honderd-en-zoveel indicator-functies end-to-end geaudit tegen leerboek-formules en met een vergelijkende benchmark in elke taal tegen de libraries waar die specifieke implementatie daadwerkelijk mee concurreert. De reden dat het één version bump is in plaats van drie aparte version bumps, is dat de libraries de laatste paar jaar genoeg uit elkaar waren gegroeid dat ze los van elkaar fixen zou hebben betekend dat ik dezelfde indicator drie keer zou fixen en het alignment-werk daarna alsnog moest doen. Dus dat alignment-werk heb ik eerst gedaan. De correctheid en snelheid van een technical-analysis library zouden over talen heen moeten reizen, niet alleen een JS-only verhaal moeten zijn. Dezelfde geauditeerde indicators, dezelfde vergelijkende benchmark-methodologie, dezelfde zero-dependency-houding. `ta.js`, `ta.py` en `ta.go` zijn het nu eens over wat een RSI hoort terug te geven, en elk van de drie is gemeten tegen de libraries waar je in die ecosysteem ook daadwerkelijk tegen zou benchmarken. Ik heb eerder geschreven over de v1.17.0 moderniseringsronde op `ta.js` — de source-layout-opschoning, de `esbuild` bundle-pipeline, de registry-indirectie die het override-patroon behoudt. v2.0.0 is het volgende hoofdstuk van diezelfde library, en het hoofdstuk waarin de libraries in de andere talen eindelijk meekomen op een gedeelde specificatie. ## De vorm Drie repositories, één library-familie. `ta.js` voor JavaScript, `ta.py` voor Python, `ta.go` voor Go. De repos staan op [github.com/Bitvested/ta.js](https://github.com/Bitvested/ta.js), [github.com/Bitvested/ta.py](https://github.com/Bitvested/ta.py) en [github.com/Bitvested/ta.go](https://github.com/Bitvested/ta.go). Over alle drie heen is het indicator-oppervlak hetzelfde: moving averages, oscillators, bands, volatility-indicators, statistics, chart transforms, rond de honderd functies per library, elk kort genoeg om in één keer door te lezen. Wat v2.0.0 in eenvoudige termen daadwerkelijk oplevert: * Een indicator-voor-indicator audit tegen de standaard leerboek-formules, met formule-bugs over alle drie de implementaties heen opgespoord en gefixt. * Een vergelijkende benchmark per library, waarbij elke implementatie wordt vergeleken met de libraries die in dezelfde ecosysteem met haar concurreren. * Een bench-harness die in elke repo meekomt en bedoeld is om door iedereen die de cijfers wil verifiëren opnieuw gedraaid te worden — hardware vastgelegd, concurrent-versies gepind. * Dezelfde zero-runtime-dependency-houding die de libraries vóór v2.0.0 ook al hadden, behouden als harde familiebrede constraint. Geen van deze libraries trekt op runtime een third-party binnen, in geen enkele taal. ## De audit was de onglamoureuze helft ![1.00](https://media.ninokroesen.com/uploads/2026/05/a947c2e9c3f4c231ee3f/_1280.jpeg) Het startpunt van v2.0.0 was dat ik moest erkennen dat de v1 test suites — over alle drie de libraries, maar het meest gênant in `ta.js` — me eigenlijk niet vertelden of de indicators correct waren. De fixtures waren gegenereerd vanuit de implementaties zelf op het moment dat die implementaties geschreven waren. Als de implementatie toen fout was, dan is de fixture nu fout, en stelde de test niets meer vast dan "de verkeerde output is stabiel verkeerd." Ik ben `ta.js` in 2020 begonnen toen ik indicator-functies voor mijn eigen werk nodig had en ze elk zelf schrijven was de schoonste manier om ze te leren. Sommige van de indicators die ik in die eerste drie weken schreef horen bij wat de audit ving. Zes indicators in `ta.js` kwamen terug met formule-bugs die in de fixtures gefossiliseerd waren: parabolic SAR, k-means clustering, de Wilder smoothed moving average, ichimoku, momentum oscillator en fractals in price-mode. De smoothed moving average was het schoonste voorbeeld van het patroon. De v1-formule was dimensioneel verkeerd, de fixture was vastgelegd vanuit de verkeerde formule, en een groene test-suite had het verkeerde antwoord jarenlang als correct gecertificeerd. De gecorrigeerde implementatie is kort genoeg om in één oogopslag te lezen: ```js function smma(data, length=14) { const n = data.length; if (n < length) return []; let seed = 0; for (let i = 0; i < length; i++) seed += data[i]; let prev = seed / length; const out = [prev]; for (let i = length; i < n; i++) { prev = prev + (data[i] - prev) / length; out.push(prev); } return out; } ``` Een length-N SMA-seed, en daarna een Wilder rolling update van `prev + (data[i] - prev) / length` voor de rest van de serie. Diezelfde fix is in `ta.py` en `ta.go` geland, omdat dezelfde kapotte formule in alle drie zat. Het lezen van de audit-notities naast de fixtures is wat me deed beseffen dat de suite zelf het probleem was, niet de indicators die de suite niet wist te markeren. Ik schaamde me er een beetje voor maar het zit me eigenlijk niet zo dwars. Deze zes zijn van de eerste `ta.js`-indicators die ik ooit heb geschreven, en ik ben sindsdien gegroeid. Ik ben blij dat ik ze nu allemaal heb gevangen. Vóór de audit wist ik al dat er issues zouden zijn die nooit gevangen waren — dat is het soort ding dat je alleen ziet door elke functie naast een leerboek te lezen, en dat is niet het soort pass dat je terloops doet. Het is het soort pass dat je doet zodra je hebt besloten de versie clean te breken. De cross-language reikwijdte van de audit was het stuk dat ik niet had aangevoeld. `ta.py`'s `rsi`, bijvoorbeeld, was Cutler RSI aan het berekenen — een strict sliding window van de laatste `length-1` delta's — waar het leerboek (en de gecorrigeerde `ta.js`) Wilder smoothing gebruikt, geïnitialiseerd met het gemiddelde van de eerste `length` raw gains en losses. Dezelfde indicator-naam, twee verschillende algoritmes, allebei groen tegen fixtures die uit zichzelf waren vastgelegd. De fix is in `ta.py` geland met een `MIGRATION.md`-entry, en `ta.py`'s `wrsi` is nu een dunne alias voor `rsi` zodat de legacy callers nog resolven. De audit ging zo snel als het ging omdat ik agentic coding tools had die het proeflezen en het kruisverwijzen voor me deden, werk dat ik anders één indicator per keer met de hand tegen het leerboek had moeten doen. Drie libraries aan formule-auditing, regel voor regel, zou anders weken hebben gevreten. Met die hulp paste het werk in één release-shaped venster waarin ik alle drie de implementaties tegelijk in mijn hoofd kon houden, en dat is de enige reden waarom de multi-language scope op een solo-tijdlijn haalbaar was. ## De bench was de verrassende helft Toen de audit klaar was en elke indicator gaf wat het leerboek zegt dat het moet geven, was de volgende voor de hand liggende vraag hoe de library eigenlijk presteerde tegen haar concurrenten. Voor zover ik heb kunnen vinden bestaat er geen breed geaccepteerde vergelijkende benchmark voor technical-analysis libraries in geen van deze drie talen met genoeg gestrengheid rond hardware-specificatie en concurrent-versies om reproduceerbaar te zijn. Ik wilde de library meetbaar maken tegen iets anders dan de eigen internals. Voor `ta.js` was de vergelijkings-set de vanzelfsprekende: `technicalindicators` 3.1.0, `trading-signals` 7.4.3, `@debut/indicators` 1.3.22 en `tulind` 0.8.20. De eerste drie zijn pure-JavaScript libraries; `tulind` is een Node-binding naar een C-library. `tulind` heb ik er bewust ingezet. De praktische vraag die een developer stelt bij het kiezen van een library is "moet ik de native bindings installeren voor de snelheid?" en de enige manier om die vraag eerlijk te beantwoorden is om de vergelijking daadwerkelijk te draaien. Het resultaat was dat pure-JS `ta.js` 66 van de 67 cross-library cases won bij 100.000 bars op een Apple M3 met Node v25.1.0, inclusief tegen `tulind`. De resterende — Rate of Change — zit op 100k zo'n 22% achter `@debut/indicators`, een constant-factor-gat dat ik nog niet heb dichtgereden en dat ik in een latere ronde oppak. De rest van het veld zit comfortabel daarachter. Ik had min of meer verwacht dat de C-bindings zouden verliezen bij de groottes waarop een echte workload draait — de FFI-marshaling-kost wordt eenmaal per call betaald ongeacht hoeveel rekenwerk de call doet, dus naarmate het werk per call groeit, stapelt de C-tax zich tegen zichzelf op — maar ik had niet verwacht dat de winst zo allesomvattend zou zijn over de rest van het veld. De cijfers zijn quoteerbaar op dezelfde hardware. Verhoudingen reizen tussen machines, absolute ops/sec doen dat niet. De volledige tabel staat op [BENCHMARKS.md](https://github.com/Bitvested/ta.js/blob/main/BENCHMARKS.md) en de reproductiestappen in [bench/README.md](https://github.com/Bitvested/ta.js/blob/main/bench/README.md). De Python-bench is waar v2.0.0 erachter kwam dat het zichzelf een schoner verhaal vertelde dan het werk eigenlijk dekte. Ik bouwde de `ta.py`-benchmark in de verwachting dat de library competitief zou zijn tegen de C-bound Python-opties op dezelfde manier als `ta.js` was tegen `tulind`. Dat was het niet. De C-bound Python-libraries waren ruwweg honderd keer sneller dan pure-Python `ta.py`. Dat was een tegenvaller, en die heb ik in mijn eigen notes als zodanig opgeschreven voordat ik wist wat ik ermee aan moest. Wat ik achteraf bedacht, is dat de vergelijking eigenlijk niet eerlijk was. `ta.py` is per ontwerp een pure-Python library, en de zero-runtime-dependency-houding is een familiebrede constraint, geen per-taal voorkeur. C-bindings toevoegen zou `ta.py` niet sneller hebben gemaakt op haar eigen termen; het zou de library hebben veranderd in een wrapper rondom andermans C-library, wat niet de library is die het wil zijn. De concurrenten die in de bench gepind staan zijn `TA-Lib` 0.4.28, `pandas-ta` 0.3.14b en `finta` 1.3 — één Python-wrapper rondom een C-library, en twee NumPy/pandas-backed implementaties die hun hot loops naar NumPy's C-internals duwen. Er bestaat geen breed-gebruikte pure-Python TA-library in de Python ecosysteem waar ik `ta.py` naast kan zetten zoals `ta.js` naast `technicalindicators` of `trading-signals` zit. De framing dus — dat de juiste as om `ta.py` op te meten pure-Python-tegen-pure-Python is — is een houding, geen gebenchmarkte claim tegen een benoemd veld. Wat de bench in de repo `ta.py` daadwerkelijk tegen meet is het NumPy/C-backed veld waarvan de framing al toegeeft dat het verliezen verwacht, plus een interne naïeve leerboek-baseline die de sliding-window-herschrijvingen moeten verslaan. De bench-methodologie en de gepinde concurrenten staan in [bench/README.md](https://github.com/Bitvested/ta.py/blob/main/bench/README.md); per-run resultaten-tabellen worden bewust niet gecommit, omdat absolute cijfers niet generaliseren over machines en de README van de bench daar zelf expliciet over is. Ik ben niet aan de Python-bench begonnen vanuit een principiële "vergelijk alleen tegen pure-Python libraries"-houding — ik verwachtte dat `ta.py` competitief zou zijn tegen alles in de ecosysteem, kreeg de C-bound cijfers, en pas daarna ben ik teruggelopen naar de framing hierboven. Als je maximale snelheid in Python nodig hebt en bereid bent een C-dependency aan te nemen, dan zijn de C-bound libraries nog steeds het antwoord. `ta.py` is het antwoord wanneer je dat niet kunt of niet wilt. `ta.go` volgde hetzelfde patroon als `ta.js` — pure Go, vergeleken tegen de pure-Go alternatieven in de ecosysteem, geen FFI-fratsen. De vergelijkings-set was `markcheno/go-talib` (gepind aan een build uit januari 2025, omdat er geen semver-tag voor bestaat) en `cinar/indicator` 1.3.0. Beide zijn pure Go — `go-talib`, ondanks de naam, is een Go-port van TA-Lib en niet een CGO-wrapper, en dat is de reden waarom de bench schoon draait met `CGO_ENABLED=0`. De bench leeft in een aparte `go.mod` binnen de repo zodat die benchmark-deps niet lekken naar iemands `go.sum` als hij `ta.go` zelf installeert; de gepubliceerde tabel staat op [bench/BENCHMARKS.md](https://github.com/Bitvested/ta.go/blob/main/bench/BENCHMARKS.md) en de reproductiestappen op [bench/README.md](https://github.com/Bitvested/ta.go/blob/main/bench/README.md). Het resultaat was dat pure-Go `ta.go` op 100k bars elke cross-library case wint of gelijkspeelt. Tegen `markcheno/go-talib` — de pure-Go port van TA-Lib, en dus de moeilijkere van de twee vergelijkingen — is `ta.go` sneller op SMA (1.30×), MACD (1.18×), Bollinger Bands (1.25×) en ATR (1.90×), en binnen run-to-run ruis op EMA en RSI. Tegen `cinar/indicator` wint hij elke indicator regelrecht, met het grootste verschil op RSI bij 3.51× en het kleinste op Bollinger bij 1.34×. De ene uitschieter in de tabel is de 10k Bollinger-regel, waar `ta.go` trager is dan beide concurrenten: `go-talib` slaat de rijen vóór de warm-up bar over en geeft alleen de post-warm-up reeks terug, terwijl `ta.go` en `cinar` allebei de volledige `n-l+1`-rijige serie teruggeven. Die extra boekhouding telt zwaarder op 10k dan op 100k, waar de vergelijking omklapt en `ta.go` ook op Bollinger weer de snelste is. Die regel staat eerlijk in de tabel in plaats van eruit geveegd — het is `ta.go`'s tegenhanger van het Rate of Change-gat op `ta.js`, een klein constant-factor item dat ik liever rapporteer dan verberg. ## Drie libraries, één release ![1.00](https://media.ninokroesen.com/uploads/2026/05/10655a8149a74f2a04f8/_1280.jpeg) De reden dat v2.0.0 over drie repositories op hetzelfde versienummer uitkomt, is dezelfde reden waarom de audit en de bench de moeite waard waren om überhaupt te doen. Ik heb altijd geprobeerd deze libraries op één lijn te houden — wat `ta.js` `sma` noemt zou dezelfde functie moeten zijn als wat `ta.py` `sma` noemt, met dezelfde default parameters en dezelfde output-vorm. De laatste paar jaar waren ze uit elkaar gedreven, en de drift was van het soort dat zich opstapelt: een fix die in één repo landde landde niet altijd in de andere, en hoe langer dat doorging, hoe duurder het opnieuw op één lijn brengen zou worden. Dus heb ik het alignment-werk eerst gedaan, voordat ik in de tussentijd opgehoopte open user-issues op de individuele repositories oppakte. Dat is niet de meest user-responsive keuze die ik had kunnen maken — er staan issues in de trackers die het niet in v2.0.0 hebben gehaald — maar het alternatief was elke issue over drie driftende libraries fixen en die daarna alsnog opnieuw alignen. De v2.0.0-audit en het v2.0.0-bench-werk nu eerst doen betekent dat vanaf hier één library fixen en de familie weer op één lijn brengen kleiner werk is dan het zou zijn geweest om alle drie vanaf een v1-startpunt te alignen. Een opmerking over wat v2.0.0 niet is. Dit zijn geen nieuwe libraries. `ta.py` en `ta.go` zijn bestaande libraries naast `ta.js` die al een tijd deel uitmaken van de familie — wie er eentje gebruikte gebruikte tot nu toe de v1-tijdperk-versie. v2.0.0 is de release die ze omhoog brengt naar een gedeelde geauditeerde specificatie, met een gedeelde bench-methodologie, en ze voor het eerst onder hetzelfde versienummer uitbrengt. Het is voor het eerst dat de drie libraries van begin tot eind met elkaar in lijn zijn. ## Eerlijke kanttekeningen Dingen waarvan ik weet dat ze nog niet helemaal kloppen aan de v2.0.0-release. Niet als footer; als het deel van de post dat de rest pas geloofwaardig maakt. Nog geen externe cross-validatie. De bench-harness komt mee in elke repository, de hardware staat gespecificeerd, de concurrent-versies zijn gepind, en iedereen die de repos cloned kan de vergelijking zelf opnieuw draaien. Ik hoop dat iemand dat doet. Op dit moment is de enige persoon die de v2.0.0-cijfers heeft gedraaid ikzelf, en de post is deels een oproep om daar verandering in te brengen. Als je de bench draait en jouw cijfers het oneens zijn met die van mij — op andere hardware, op een andere runtime-versie, met een andere competitor-versie — open een issue. Ik kom er liever nu achter dan nadat de cijfers daar al hadden gestaan alsof iedereen het ermee eens was. De Python-bench is niet hetzelfde soort bewijs als de ta.js-bench. Er bestaat geen breed-gebruikte pure-Python TA-library om `ta.py` naast te zetten zoals `ta.js` pure-JS-peers heeft, dus de vergelijking die de bench-sectie trekt — pure-Python-tegen-pure-Python — is een houding over hoe een eerlijke benchmark er überhaupt uit zou zien, geen 66-van-67 tegen een benoemd veld. Als dat is waar je voor las, dan heeft de Python-bench dat niet. De Rate of Change-indicator op `ta.js` zit op 100k bars zo'n 22% achter `@debut/indicators`. Ik heb hem zo uitgebracht in plaats van v2.0.0 ervoor op te houden omdat de rest van de cross-library winsten al uitvoerig waren en het gat op `roc` een constant-factor is dat ik in een latere ronde dicht kan rijden. "Wint 66 van 67" leest slechter dan "wint ze allemaal", maar ik geef de voorkeur aan 66 van 67 eerlijk uitbrengen boven die 67e dramatiseren door de release op de plank te laten liggen. Er zijn nog een paar andere indicators in de familie in vergelijkbare vorm — kleine genoeg marges dat de volgende ronde perf-werk ze zou moeten kunnen wegpoetsen. Ik probeer ze uiteindelijk allemaal te verslaan in plaats van te doen alsof ik dat al doe. De handonderhouden legacy-minified bundle voor `ta.js` — degene die ouder is dan de `esbuild`-pipeline — komt nog steeds mee met het package. Dat is geen drift-valstrik zoals het er van buitenaf uit zou kunnen zien. Het is een bewuste backwards-compatibility-toezegging aan mensen die dat bestand nog steeds van een CDN trekken in browser-apps die al jaren draaien. Hem verwijderen zou die apps stil breken, en de kosten van dat bestand erbij houden zijn kosten die ik graag betaal. De twee `esbuild`-gegenereerde bundles zijn het moderne pad; het legacy-bestand is het bestaande-gebruikers-niet-breken-pad. De audit gebeurde alleen omdat de v1-test-suite me eigenlijk niet vertelde wanneer een indicator fout zat. Het is terecht om te vragen waarom dat zo lang duurde om aan het licht te komen, en het eerlijke antwoord is dat de suite groen bleef en ik die langer vertrouwde dan ik had moeten doen. Sommige van de indicators die de suite als correct certificeerde waren al fout sinds de eerste commit erop. Dat is het soort fout dat je niet ziet door alleen naar de test-runner-output te kijken, omdat de runner je al vertelt dat alles in orde is. Je vangt het door de indicators naast het leerboek te lezen, en dat is een aparte pass die pas in v2.0.0 plaatsvond. ## Waar het nu staat, en wat er volgt `ta.js`, `ta.py` en `ta.go` zijn alle drie live op v2.0.0 en in onderhouds-modus. Ik plan op dit moment geen golf van nieuwe functies op een van de drie. Wat ik wel plan, over de komende maanden, is om door de open issues heen te werken die op de individuele repositories zijn opgehoopt en die het niet in v2.0.0 hebben gehaald. Het patroon wordt sequentieel: fix de library met de meest dringende stapel issues, lijn alle drie weer uit zodat de fixes de familie niet opnieuw uit elkaar laten drijven, ga naar de volgende library, herhaal. Dat is volgens mij de onderhouds-vorm die een multi-language familie daadwerkelijk gezond houdt zonder meer tijd aan alignment-werk dan aan de echte fixes te besteden. De bench-harness komt mee in elke repo en is bedoeld om daar te blijven leven als forcing function, niet alleen als het artefact achter de v2.0.0-claims. Toekomstige performance-rondes draaien tegen deze. Toekomstige correctheids-fixes worden gevalideerd tegen dezelfde fixtures die de audit heeft opgeleverd. Als de volgende keer dat iemand een issue indient "deze indicator geeft een andere output op deze input" is, geven de regeneration-from-textbook-fixtures en de cross-library-bench me de mogelijkheid om de melding tegen meerdere andere library-implementaties van diezelfde functie te checken. Die triangulatie is wat de bench de moeite waard maakt om als onderdeel van het project mee te dragen in plaats van hem maar één keer voor de aankondiging te draaien. Als je een van deze libraries op zijn v1-tijdperk-versie hebt gebruikt, dan is de migratie naar v2.0.0 op de formules die de audit heeft gefixt breaking. De output van de indicators die bugs hadden is veranderd, omdat de bugs weg zijn. Daar is geen migratie-script voor — iedereen wiens code de v1-tijdperk-output van die indicators consumeert moet kijken of zijn code afhing van het juiste of het verkeerde antwoord. De release notes in elke repo dragen de lijst. Was je gebruik op indicators die de audit niet heeft gemarkeerd, dan is v2.0.0 een drop-in. Wat ik eigenlijk uit deze aankondiging wil halen, is dat iemand anders dan ik de bench draait. De cijfers zijn reproduceerbaar. De hardware staat gespecificeerd. De concurrent-versies zijn gepind. Als je een TA-library in een van deze drie talen beheert of gebruikt en je geeft erom dat de vergelijking eerlijk is, dan zit de bench al in de repository en is hij met één commando te draaien. Wat je ook vindt — eens met me, oneens met me, of iets aanstippen dat ik gemist heb — is de input die ik het meeste wil voor v2.0.x. Ik probeer niet de enige persoon te zijn die deze vergelijking met cijfers heeft vastgelegd. Ik probeer de eerste te zijn. --- title: "Twee weken agentic coding in een paywalled SaaS-categorie" date: 2026-05-06T18:18:00.000Z updated: 2026-05-06T18:18:49.788Z canonical: https://ninokroesen.nl/blog/twee-weken-agentic-coding-in-een-paywalled-saas-categorie lang: nl author: "Nino Kroesen" --- # Twee weken agentic coding in een paywalled SaaS-categorie [Hourly](/hourly) is vandaag, 6 mei 2026, live gegaan in de Mac App Store, naast de directe download vanaf deze site. Hij is gratis, draait volledig op je Mac, vraagt niet om een account, en heeft geen paywall achter een van zijn features. Ik heb hem in ongeveer twee weken gebouwd. Voor deze had ik in acht jaar niets meer geshipped naar een Apple-platform. De hypothese die ik met de lancering wil testen is simpel. Elke gevestigde speler in de menubalk-tijdregistratie-categorie — WorkingHours, Toggl, Timery — zit achter een paywall in een of andere vorm. De reden dat een categorie als deze vandaag verdedigbaar is, is niet dat de software moeilijk te schrijven is; het is dat de kosten van het schrijven en onderhouden ervan hoog genoeg zijn dat wie hem heeft er ook geld voor kan vragen. Generatieve-AI-tooling heeft die kostenstructuur sneller veranderd dan de SaaS-verdienmodellen die bovenop die categorieën draaien hebben kunnen reageren. Hourly is de goedkoopste empirische test die ik kon draaien op de vraag of die verandering daadwerkelijk is aangekomen voor een kleine Mac-app, of dat hij nog verder weg ligt dan hij van binnenuit lijkt. Ik heb over dit kader geschreven in [de introductiepost](/blog/introduction) toen het project nog op een roadmap stond. Dit is de empirische opvolger daarvan. ## De vorm Hourly is een menubalk-tijdregistratie voor macOS. Je start een timer voor een project, de seconden tikken in de menubalk weg, je stopt hem. Eerdere entries worden opgesomd en zijn bewerkbaar. Date-range filters en CSV-export zitten erin. Facturatie is multi-currency. Er is een Pomodoro-modus met break overlays en configureerbare werkherinneringen. Calendar export naar Google Calendar en Apple Calendar. Vijf UI-talen — Engels, Nederlands, Duits, Spaans, Frans. Launch-at-login. Opslag is lokaal. Alles staat in één JSON-file onder `~/Library/Application Support/Hourly/`. Geen account, geen sync, geen serverzijde, niets verlaat je Mac tenzij je het expliciet exporteert. Nul runtime-dependencies in de build zelf; de enige frameworks die worden aangeroepen zijn die van Apple zelf. Die feature surface is geen toeval, en de volgende sectie gaat over hoe hij gekozen is. ## Hoe de feature surface gekozen is Voordat ik de build aan het scopen was, ben ik door wat er daadwerkelijk achter de paywalls zat in de vergelijkbare apps in de Mac App Store gegaan. De Pomodoro-modus, de break overlays, de werkherinnering-pings, de calendar export — dat zijn allemaal features die ik in de betaalde apps zag worden ingezet als premium-tier lokkertjes. Het zijn geen features die ik op een vibes-driven feature list van "wat heeft een tijdregistratie nodig" had geprioriteerd. Het zijn features die ik erin heb gescoped omdat de probe een eerlijke test moet zijn. "Gratis tijdregistratie die minder doet dan de betaalde" is een andere bet dan "gratis tijdregistratie die op feature surface gelijk staat aan de betaalde." De bet die ik daadwerkelijk wil draaien is de tweede. Dat onderzoek is ook de reden dat de app klein is. De laatst gepackagede build is 5,4 MB op disk. De concurrent-groottes die ik heb gecheckt — WorkingHours op 18,1 MB, Toggl op 16,2 MB, Timery op 98 MB — liggen hoger. Een deel van die kloof komt doordat de concurrenten hun account/sync-infrastructuur aan de clientkant meedragen en Hourly niet. Een deel ervan is dat er geen third-party Swift-dependencies in de build zitten. Hoe dan ook, de grootte is onderdeel van de lean-build bet, en het is een van de dingen waar ik op ga letten zodra installaties beginnen op te duiken in de App Store-rapportages. ## Hoe ik bij een Mac-build zonder Xcode terechtkwam Dit is het deel van de build waar ik vooraf geen prior op had. Ik werk op een MacBook Air, ik zit op dit moment krap in mijn schijfruimte, en het vooruitzicht om de volledige Xcode-bundle te installeren om een kleine Swift-app te compileren was onaantrekkelijk. Ik had het gedaan als het had gemoeten, maar ik had het niet leuk gevonden. De agentic coding tools waar ik tijdens de build op leunde, stelden een alternatief voor waarvan ik niet wist dat het haalbaar was: een Swift Package plus een paar honderd regels shellscripts, met alleen de Command Line Tools geïnstalleerd. Geen Xcode. Geen simulator. Geen Interface Builder. `swift build` en `swift test` vanuit de terminal, de rest is shell die `codesign` en `notarytool` aan elkaar knoopt. De dev-loop draait vanuit een teksteditor. Het kostte wat debugwerk om het werkend te krijgen. Apple's tooling neemt op een paar plekken aan dat je in Xcode zit zonder dat te documenteren, en de test target moest het Command Line Tools framework-pad expliciet pinnen zodat `swift test` XCTest überhaupt kon vinden. Maar zodra het was bedraad, draait het hele ding — build, test, sign, notarize, package — end-to-end vanuit de terminal op een machine die nog nooit Xcode op zich heeft gehad. Die toolchain is het ding dat ik meeneem naar elk toekomstig Mac-project dat ik begin. Op een laptop die krap in zijn geheugen zit was hij de unlock die het bouwen van deze app op deze hardware überhaupt haalbaar maakte. Ik was op eigen kracht niet bij die toolchain uitgekomen. Ik had gemopperd en Xcode geïnstalleerd. Het feit dat een agentic coding tool de no-Xcode-flow als eersteklas optie naar boven bracht is het soort ding dat niet opduikt in de headline-benchmarks van "heeft het model de code goed geschreven" maar wel opduikt in de daadwerkelijke productiviteit van een build als deze. ## De dual-ship was een midden-build-beslissing Het oorspronkelijke plan was alleen directe download. Geen Apple Developer-account, geen $99 per jaar, geen App Store-reviewproces. Ik zou notarizen via Developer ID, de `.zip` op deze site hosten, en het daarbij laten. Dat heb ik halverwege de build veranderd. De these die de lancering bedoeld is te testen, is of een gratis lokale app de naald kan verzetten in een categorie waar de gevestigde spelers in de Mac App Store leven. De App Store mijden zou hebben betekend dat de vergelijking plaatsvond op een surface waar de concurrenten niet zaten. Dus heb ik de $99 betaald, een Apple Developer-account geregistreerd, de App Store Connect-kant opgezet, en een tweede packaging-script naast het bestaande directe-download-script gezet. Dezelfde source, twee output-paden. De repository ship't nu vanuit hetzelfde Swift Package naar beide surfaces. De directe download blijft in de lancering, en niet als dev-artefact of als fallback. App Store-installatie vereist een Apple ID; directe download niet. Er is een echte privacybewuste gebruiker — en een gebruiker op een account-loze machine — voor wie dat onderscheid uitmaakt. Dat pad had ik sowieso geshipped. De App Store is de toevoeging, niet de ruggengraat. ## Wat de review van Apple eruit haalde Drie reviewrondes voordat hij werd goedgekeurd. Apple steekt echt moeite in het reviewen van apps, en daar mag ik ze van harte voor prijzen ook al zaten er afwijzingen in. De eerste afwijzing was een echte. De build die ik had ingestuurd crashte bij opstarten op de machine van de reviewer, op een plek waar ik hem op de mijne niet had zien crashen. Het bewijs zat in de crash log: ``` _assertionFailure static NSBundle.module.getter AppStrings.bundle(for:) PomodoroNotifier.requestAuthorizationIfNeeded() AppModel.init HourlyApp.init ``` `Bundle.module` is het Swift-package-mechanisme om bij runtime de resource bundle van een package te bereiken. Hij heeft een eenmalige lazy init die fataal foutgaat als hij de bundle niet kan vinden. Op een verse App Store-installatie kon hij hem niet vinden. Op `swift run` lokaal wel. De reden was een packaging-mismatch tussen wat `swift build -c release` produceert en wat Apple's signing-and-thinning pipeline accepteert: Swift Package Manager produceert een geneste resource bundle, en Apple's pipeline behandelt geneste resource bundles inconsistent genoeg dat de binnenste bij runtime niet betrouwbaar terug te laden is. Een vlakke layout — wat elke Mac-app al decennia ship't — overleeft de pipeline ongedeerd. De fix was vier regels in het packaging-script die de SwiftPM-vormige resource bundle voor het code signen vlak in de `Contents/Resources/` van de app gooien. De bijbehorende verandering staat aan de leeskant: de bundle resolver probeert nu eerst de vlakke layout, en valt alleen terug op de geneste layout als de app onder `swift run` draait. Die divergentie tussen dev en ship is precies het soort ding dat ik lokaal niet had opgevangen, want de lokale pipeline is precies degene die de fout verbergt. De reviewer van Apple ving hem op omdat de reviewer van Apple de ship-pipeline draait. Ik ben blij dat hij op Apple's machine crashte en niet op die van een gebruiker. De tweede afwijzing was de grappige. Apple liet me niet de zinnen "No paywall" of "No annoying IAP" gebruiken in de screenshot-tekst van de App Store-listing. Het platform dat zelf via paywalls en in-app purchases verdient, laat je op zijn store de afwezigheid daarvan niet adverteren. Ik schrijf dat als platte mededeling op omdat de ironie zelf de grap is; ik hoef hem niet aan te kleden. De metadata is zonder die tekst opnieuw ingestuurd, en de afwijzing zelf is grofweg de schoonste versie van de these die ik me had kunnen wensen, uitgespeeld als onderdeel van het lanceringsproces. De derde was meer een verduidelijking dan een afwijzing — Apple wilde een onderbouwing voor een server entitlement waar de app om vroeg. Ik heb die opgeschreven, teruggestuurd, en die ronde was daarmee gesloten. Het totale review-venster was ongeveer een week. ## Eerlijke caveats De gebruikersbasis voor de lancering bestaat uit één persoon. Mijzelf. Ik heb de app gebruikt op een freelanceproject waarop ik declarabele uren bijhield — het gereedschap nodig hebben, in de Mac App Store kijken, niets gratis vinden dat het werk deed, en het ding zelf schrijven in plaats van de tol te betalen. Er is geen TestFlight-cohort, geen Hacker News-post, geen forumthread. De lancering is het begin van de feedback loop, niet het einde van een. Het plan vanaf hier is om hem naar een handvol vrienden te sturen waarvan ik weet dat ze hem een week zullen gebruiken en me echte feedback geven richting de volgende versie. De acht jaar gat voor deze build is reëel. Het laatste Apple-platform-ding dat ik geshipped heb stond in een totaal andere context. Daar koud op terugkomen en in twee weken bij een werkende menubalk-app uitkomen is geen verhaal dat ik op een eerdere iteratie van de toolchain had kunnen vertellen. De agentic coding tools die ik tijdens de build heb gebruikt doen meer van het zware werk dan eerlijk is om weg te laten — vooral op de Apple-specifieke spreuken die je elk een dag of anderhalve dag blokkeren als je er uit oefening bent. Ik heb daar in algemenere zin over geschreven in [de adoption-gap post](/blog/the-adoption-gap); Hourly is daar het concrete voorbeeld van. Ik ga me wat opgelaten voelen als blijkt dat Hourly een opvallende bug heeft die een echte gebruiker in de eerste week vindt. De repository heeft een test suite, de suite slaagt, maar een lancering met één gebruiker vooraf is precies het soort situatie waarin het ding waar je niet aan dacht om te testen het ding is waar de tweede gebruiker overheen struikelt. Ik hoor het liever wel dan niet. De $99-per-jaar Apple Developer fee is de enige terugkerende kostenpost waar ik stilletjes baal van, en er is geen schone manier om die terug te verdienen terwijl de app gratis blijft. Een donatielink in de app zelf komt vermoedelijk niet langs Apple's regels. Ik laat dat waarschijnlijk verder met rust en behandel het als de kosten van het experiment. ## Waar het nu staat en wat er hierna komt ![1.00](https://media.ninokroesen.com/uploads/2026/05/d4ffd05d5e154953d7fb/_1280.jpeg) [Hourly](/hourly) is live op [de Mac App Store](https://apps.apple.com/app/hourly-time-billing-tracker/id6762381291) en als directe download vanaf deze site. De bundle ID is `com.ninokroesen.hourly` als je wilt verifiëren dat het daadwerkelijk de app is waar ik het over heb en niet iemand anders zijn listing. Wat ik nu ga doen is wachten en kijken. De twee aftakkingen die ik voor mezelf alvast heb getrokken: * Als de installaties via de App Store wezenlijk bewegen — oftewel, als een gratis no-account lokale app gebruikers kan trekken in een categorie waar de gevestigde spelers geld vragen — landt de hypothese. In dat geval overweeg ik het project open-source te maken. De community zou welkom zijn om bij te dragen, de app is op dit moment oprecht kaal, en een open repo is de natuurlijke vorm voor een tool als deze. Er zit echt opschoonwerk tussen de huidige working copy en een publiceerbare repository, en dat bepaalt mede hoe snel het zou kunnen. * Als de installaties stilvallen — oftewel, als het gemak van een betaalde app met account, sync en een gepolijste marketingsite zwaarder weegt dan het prijsvoordeel van een gratis lokale — landt de hypothese voor deze categorie niet. In dat geval zou ik de extra moeite van open-sourcen niet nemen. De app blijft gehost op deze site voor de mensen die hem vinden en draait stilletjes door op de App Store totdat Apple of ik hem afserveren. Geen actieve afschrijving; het artefact blijft gewoon staan. Ben je een Mac-gebruiker die declarabele uren bijhoudt, dan is de installatie één klik vanaf [de projectpagina](/hourly). Vind je een bug of mis je een feature die de app niet heeft, dan is [de support-pagina](/hourly/support) de route. Vrienden waarvan ik weet dat ze hem voor mij gaan binnentrekken zijn niet de enige test set; lezers van deze post zijn nadrukkelijk onderdeel van de test set die de lancering komt verzamelen. ## Het deel dat zich generaliseert De toolchain-bet is degene die ik meeneem naar elk toekomstig Mac-project dat ik start. Swift Package + Command Line Tools + een paar honderd regels shell — geen Xcode in de loop tenzij ik echt een SwiftUI-preview of een simulator-run wil hebben, en het grootste deel van de tijd wil ik dat niet. Het hele project is vanaf het begin tekstbestanden en versiebeheerbaar, de dev-loop zit in de editor die ik op dat moment toch al gebruik, en de build pipeline is end-to-end controleerbaar zonder de terminal te verlaten. De kost is dat je de paar dingen waar Xcode echt goed in is niet krijgt, en dat je er op de harde manier achter komt waar Apple's verstopte Xcode-veronderstelt-zichzelf-dependencies zitten. Maar zodra die zijn afgehandeld, past het resultaat in dezelfde vorm als een build voor wat dan ook. Het andere ding dat ik meeneem is minder een toolchain en meer een discipline. Wanneer je een hypothese empirisch test, bepalen de kosten van het eerlijk uitvoeren van die test of je hem überhaupt uitvoert. Als Hourly als gepolijste SaaS had moeten shippen om de SaaS-versus-gratis-lokaal-vraag te testen, had ik mijn ogen gerold en de test niet gedraaid, en was de hypothese een mening gebleven. Twee weken bouwtijd, $99 aan kosten, geen marketing, geen groeiteam — dat is klein genoeg dat ik daadwerkelijk de knoop heb doorgehakt. De meeste van mijn meningen zijn nog steeds meningen omdat ik er geen goedkoop genoeg geworden versie van een test voor heb bedacht. De meningen die in projecten veranderen, zijn de meningen waar dat wel voor gelukt is. --- title: "Een query van 30 seconden die ik zes maanden te laat draaide" date: 2026-04-26T15:30:00.000Z updated: 2026-04-27T20:35:57.759Z canonical: https://ninokroesen.nl/blog/een-query-van-30-seconden-die-ik-zes-maanden-te-laat-draaide lang: nl author: "Nino Kroesen" --- # Een query van 30 seconden die ik zes maanden te laat draaide Meer dan zes maanden lang ging de WorkWear4All storefront elke nacht een paar minuten offline. Ik wist dat het gebeurde. Het bureau dat het platform had gebouwd wist het. Onze hosting provider wist het. Drie externe Magento integrators die we op verschillende momenten in die periode raadpleegden wisten het. Geen van hen kon me vertellen waarom, en ik kon de tijd om door te blijven graven niet rechtvaardigen — er stond altijd iets zichtbaarders op de queue, de outage was klein genoeg om op te vangen, en de gedeeltelijke fix die we al hadden draaien was net stabiel genoeg om de urgentie weg te nemen. De daadwerkelijke oorzaak was een query van dertig seconden tegen `information_schema`. Ik had hem in de eerste maand moeten draaien. Magento 2 is niet gebouwd voor een catalogus van de omvang die wij draaien. Met 800K+ producten, waarbij elk product dagelijks wordt aangeraakt voor voorraad- en prijssync vanuit leveranciers, kan de indexer de `_cl` changelog tabellen niet sneller leegmaken dan het platform erin schrijft, en de storefront betaalt uiteindelijk voor die mismatch — in ons geval met een korte outage elke nacht, op het moment dat iets in de indexing pipeline het opgaf, herstartte, en opnieuw inhaalde. Het platform zelf geeft geen signaal dat dit gebeurt. Er is geen waarschuwing in de admin UI, geen indexer status die rood kleurt voordat het symptoom op de storefront verschijnt, geen log line die naar de root cause wijst. En de fix — als je hem eenmaal hebt gevonden — staat op minstens drie publieke plekken gedocumenteerd, en geen daarvan komt naar boven als je een Magento expert vraagt om naar het symptoom te kijken. Dat is het deel van het verhaal dat ik wil opschrijven. Niet "ik vond een slimme fix" — de fix is bekend als je vanuit de operationele kant gaat zoeken. Wat het waard is om te benoemen is dat het antwoord bestaat, dat geen enkele expert die ik vroeg het kon ophalen, en dat dat een failure mode is van het ecosysteem rond het platform die het waard is om eerlijk over te schrijven. ## De vorm van de install Over de WorkWear4All stack heb ik geschreven in de introductiepost op deze site — multi-store Magento 2 over een half dozijn Europese domeinen (workwear4all.nl, workwear4all.be, workwear4all.de, workwear4all.com, workwear4all.pl en nog een aantal meer gespecialiseerde sub-stores), een gedeelde backend op Apache + Varnish + PHP-FPM + Redis. Het stuk dat voor dit verhaal van belang is, is de catalogus-schaal en het update-patroon. De catalogus is 800K+ producten, een mix van simple en configurable, opgehaald via de dagelijkse exports van leveranciers. Elk product wordt één keer per dag aangeraakt voor voorraad- en prijssync vanuit de leveranciers-feeds. Dat zijn minstens 800K product updates per dag op het bronniveau, en omdat Magento's catalogus-indexers ingeschreven staan op productwijzigingen — price, category-product, catalogsearch\_fulltext, stock, de lijst gaat door — waaiert elk van die bron-updates uit naar een `_cl`-rij voor elke ingeschreven indexer. Het write-volume per dag in de changelog tabellen loopt op tot in de miljoenen. De most-pinned indexer was altijd `catalogsearch_fulltext`. Hij was degene die als eerste achterop raakte, het hardst achterop raakte, en bovenaan de indexer status stond zodra er iets misging. Wij zitten op Magento 2.4.7, de minor release direct voor de 2.4.8 default-omschakeling naar Update by Schedule — dus het schedule-mode gedrag dat hieronder beschreven wordt, is iets dat we bewust hebben aangezet, niet iets dat we als default hebben geërfd. ## Wat we als eerste probeerden, en waarom de gedeeltelijke fix het slechtst mogelijke resultaat was Voordat we bij de daadwerkelijke fix uitkwamen, probeerden we het voor de hand liggende. We installeerden [Advanced Index Manager by Magefine](https://magefine.com/magefine/advanced-index-manager), die het indexing-proces in batches verwerkt zodat hij niet probeert in één tick door de hele changelog heen te kauwen. De indexer werd er stabieler door. De outages werden korter en minder frequent. Ze stopten niet helemaal. Die gedeeltelijke fix is het moment dat ik wil markeren. Zodra de storefront genoeg overeind stond om niet langer een dagelijkse brand te zijn, viel de druk om de daadwerkelijke oorzaak te vinden weg. De Magefine module leverde ons zes maanden op van "stabiel genoeg dat de outage niets urgents blokkeert" — wat klinkt als een goede uitkomst en achteraf het slechtst mogelijke resultaat is, omdat het wegnemen van de urgentie ook het tijdsbudget wegneemt voor het diepere onderzoek dat het probleem daadwerkelijk had opgelost. We zijn er niet mee gestopt. Het verloor het alleen steeds van de volgende brand. Ik wist, na zes maanden, dat ik dit niet snel zou vinden. Ik werkte ongeveer net zo lang met Magento 2, en ik wist genoeg om te weten dat zoiets diagnosticeren, zo diep in de indexer pipeline, significant veel tijd zou gaan kosten die ik niet had. Het bureau dat het platform had gebouwd had het niet gevonden. Onze hosting provider, die zelf Magento ervaring heeft, had het niet gevonden. De drie externe integrators die we in die periode hebben aangeschreven hadden het ook niet gevonden. De gesprekken eindigden allemaal op dezelfde manier: iemand keek even, kon zijn vinger er niet op leggen, en ging door. Achteraf gezien was de diagnose niets meer dan goed naar de database kijken voor een afwijking. Ik weet nog steeds niet zeker waarom niemand — ik zelf incluis, zes maanden lang — dat heeft gedaan. ## Wat er daadwerkelijk gebeurde De fix is operationeel simpel zodra je hem ziet. Magento 2's "Update by Schedule" indexing mode is gebouwd rondom materialised view changelogs. Elke indexer subscribet zich op een set source tables; zodra er een rij in een source table wijzigt, wordt er een entry weggeschreven naar een corresponderende `_cl` table voor elke ingeschreven indexer. De indexer cron leest op een tick de nieuwe entries uit elke `_cl` table, verwerkt ze, en ruimt ze op. Het geheel hoort zichzelf in evenwicht te houden: writes stromen de changelog in, de cron draint ze, en de indexes blijven actueel. Het model gaat ervan uit dat de cron kan bijhouden. Met 800K+ producten die dagelijks worden geüpdatet, tegenover het half dozijn indexers dat ingeschreven staat op productwijzigingen, loopt het write-volume per dag in de changelog tabellen op tot in de miljoenen. De indexer cron, die entries sequentieel via PHP verwerkt, kan op de hardware waar hij op zit niet zo snel drainen. De tabellen groeien. Ze blijven groeien. Elke dagelijkse catalogus-import voegt een laag backlog toe die de cron niet meer gaat inhalen, omdat de import van morgen er al aankomt. Wat er gebeurt zodra een `_cl` table voorbij de specifieke drempel groeit die de bijbehorende indexer niet meer aankan — te veel geheugen opgeslokt door de in-flight batch, een query timeout, een lock die nooit vrijkomt, een indexer-proces dat door de OOM killer wordt afgeschoten — is dat de indexer uiteindelijk faalt, herstart, en probeert in te halen vanaf waar hij was gebleven. Tijdens die inhaalslag is de storefront kort inconsistent of niet meer responsief, afhankelijk van welke indexer is omgevallen. In ons geval was de most-pinned altijd `catalogsearch_fulltext`, en dat is ook de indexer waarbij een stall het meest zichtbaar is voor een klant, omdat hij de productzoekfunctie van de storefront afsluit. De diagnose die ik uiteindelijk draaide, was één enkele query tegen `information_schema` die alle `_cl` tables listte op rij-aantal, aflopend gesorteerd. De grootste changelog tabellen kwamen terug op zo'n 902 miljoen rijen — een getal uit mijn hoofd, want ik heb de originele output niet bewaard en de tabellen zijn allang getruncate. De vorm was onmiskenbaar: de indexer cron was niet aan het drainen; hij werd elke dag ingehaald door de catalogus-import van de volgende dag. ## De cron, en wat hij wel en niet oplost De standaard `index` cron group draait niet op deze server. De schedule-mode incremental processing die hij zou doen — uit `_cl` tables lezen, ze drainen, ze opruimen — is precies het ding dat onze update-volume sowieso niet bijhoudt; de cron group is uitgeschakeld, en een nachtelijke full reindex from source doet het werk waarvan de incremental drain verondersteld werd het te doen. Die nachtelijke run is de cron die ik wil doorlopen. Hij voert drie operaties uit, in deze volgorde: 1. **Full reindex** — `bin/magento indexer:reindex` herbouwt elke index direct vanuit de source tables, en omzeilt het changelog mechanisme volledig. 2. **Truncate elke** **`_cl`** **table** — zodra de reindex authoritative indexes heeft geproduceerd, zijn de opgehoopte changelog entries obsoleet en is de ruimte die ze innamen dead weight. 3. **Reset elke** **`mview_state`** **cursor** — `UPDATE mview_state SET version_id = 0` voor elke rij. Dit is precies de stap waar de waarschuwingspost van Max Chadwick over gaat; zonder die stap breekt post-truncate changelog tracking in stilte. De volgorde doet ertoe. Door de reindex eerst te doen, blijven — als de reindex zelf faalt: out-of-memory, lock contention, een query timeout — de opgehoopte changelog entries op hun plek staan en heeft het schedule-mode mechanisme (voor zover het werkt) nog steeds zijn baseline state om op terug te vallen. Als we eerst zouden truncaten en de reindex zou falen, zouden we achterblijven met lege changelog tabellen en indexes in de kapotte staat van vóór de cron, wat strikt slechter is dan waar we begonnen. De destructieve operaties na de reindex sequencen, zet een gedeeltelijke failure om in een silent no-op in plaats van een outage. Ontdaan van de interne wrapper waar ik hem doorheen draai, zien de operaties er zo uit: ```bash #!/usr/bin/env bash set -euo pipefail # Run from the Magento root, with DB credentials configured (e.g. via # ~/.my.cnf or environment) so the mysql calls below pick them up. # 1. Full reindex — rebuild every index directly from source. php bin/magento indexer:reindex # 2. Truncate every changelog table; the freshly rebuilt indexes have # made the accumulated changelog entries irrelevant. TABLES=$(mysql -N -e " SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = DATABASE() AND TABLE_NAME REGEXP '_cl\$'; ") for t in $TABLES; do mysql -e "TRUNCATE TABLE \`$t\`;" done # 3. Reset every mview_state cursor to zero. This is the step Chadwick's # warning is about — without it, post-truncate changelog tracking # silently breaks. mysql -e "UPDATE mview_state SET version_id = 0;" ``` Dit draait elke nacht. De storefront is stabiel sinds we het hebben uitgerold, en zelfs na ongeveer een week normale operatie zit de grootste `_cl` table op zo'n 9 miljoen rijen — ruwweg een honderdste van de 902 miljoen die we vonden op het moment van ontdekking. Een paar neveneffecten klaarden in dezelfde periode ook op — het meest opvallend een terugkerend probleem dat we hadden met prijsupdates die niet doorkwamen naar de storefront, waarbij de price indexer in stilte achterop raakte en klanten oude prijzen te zien kregen. Dat is ook gestopt met gebeuren. Ik kan het niet volledig aan de cron toeschrijven — het kan het oorspronkelijke schedule-mode mechanisme zijn geweest dat achterop raakte, het kan gerelateerd zijn geweest aan de Chadwick failure mode voordat de `version_id`-reset op zijn plek stond, het kan een ongerelateerde coïncidentie zijn geweest met een server- of SQL-restart — maar het is niet teruggekomen sinds de huidige setup draait. Ik wil eerlijk zijn over wat deze fix is. Het is geen root-cause fix. De root cause is dat de indexer-architectuur van Magento 2 niet ontworpen is voor een catalogus van deze omvang met dit update-patroon. De cron koopt tijd door elke nacht als schone lei te behandelen; de onderliggende mismatch tussen het indexing-model van het platform en onze operationele realiteit blijft hetzelfde. Als de catalogus blijft groeien of het dagelijkse update-volume nog verder oploopt, stopt ook dit met werken. De cron is een workaround. Naast deze fix hoort ook een serieuze waarschuwing. Max Chadwick heeft [een post met de titel "Don't TRUNCATE the \_cl tables"](https://maxchadwick.xyz/blog/magento-enterprise-index-lesson-329) waarin hij — terecht — beargumenteert dat naïef truncaten van de changelog tabellen zonder de `version_id` in `mview_state` te resetten, de install in een staat brengt waarin changelog-based indexing in stilte breekt, en zich manifesteert als updates die nooit de storefront bereiken. De cron hierboven doet de `version_id`-reset expliciet, als stap drie; de reset is het stuk dat de specifieke failure mode van Chadwick afsluit. Een lezer die de truncate-stap kopieert zonder de reset, stelt zichzelf bloot aan precies de bug waarvoor Chadwick waarschuwt. Er is nog één operationele tradeoff die het waard is om expliciet te benoemen, voorbij de "het is een workaround"-framing hierboven. Met de standaard incremental indexer cron group uitgeschakeld en een nachtelijke full reindex die al het werk doet, worden de indexes overdag niet actueel gehouden — ze halen 's nachts in. Dat betekent dat er een bekend staleness window is van tot vierentwintig uur voor elke catalogus-wijziging die tussen reindex runs in wordt gemaakt. Voor ons update-patroon, waar leveranciers-feeds op een dagelijkse cadans draaien en de storefront ruim tolerant is voor licht verouderde categorie- en zoekresultaten, zijn dat acceptabele kosten. Voor een shop waarbij de catalogus wijzigingen binnen minuten moet weerspiegelen — flash sales, time-limited inventory, alles waar intra-day freshness toe doet — werkt deze setup niet; je zou óf de incremental cron group moeten laten draaien en een andere manier moeten vinden om het changelog-volume af te handelen, óf vaker dan één keer per nacht moeten rebuilden. De eerlijke conclusie: ik weet nog steeds niet zeker of dit precies de juiste manier is om het aan te pakken. Hij werkt en de uptime is drastisch verbeterd. Die zin is de echte rand van mijn begrip, en de post is met die zin op de pagina nuttiger dan zonder. ## Waarom geen van de experts hem vond Ik wil terugkomen op het sociale deel van dit verhaal, want dat is het deel dat verder strekt dan deze specifieke bug. Vijf aparte partijen hebben naar het probleem gekeken voordat ik het oploste. Het bureau dat het platform had gebouwd. Onze hosting provider. Drie externe Magento integrators. Geen van hen kwam met de changelog-table-invalshoek. De fix is niet obscuur — toen ik het achteraf nakeek, stond hij gewoon op de eerste pagina van de zoekresultaten voor "magento truncate \_cl tables." Max Chadwick heeft erover geschreven (er overigens tegen gewaarschuwd). De mgt-commerce knowledge base heeft [een stap-voor-stap tutorial](https://www.mgt-commerce.com/tutorial/magento-2-index-suspended/) die bijna exact de cron doorloopt die ik heb gebouwd. De Magento GitHub repository heeft [een open issue (#29789)](https://github.com/magento/magento2/issues/29789) waarin het probleem upstream wordt erkend. Er is zelfs [een Packagist module](https://packagist.org/packages/aligent/magento2-indexer-fix) die de fix in een extensie verpakt. De fix is gedocumenteerd. Hij is alleen onbereikbaar vanaf de symptoomkant. Zoek op "magento storefront down at night" of "magento intermittent outage" of "magento indexer stuck" en de changelog-table-invalshoek komt niet naar boven. De expert die je om hulp vraagt komt er vanaf jouw symptoom ook niet, omdat hun mentale model vertrekt vanuit dezelfde set zoektermen als die van jou. De vindbaarheid stort in op de symptoomlaag, niet op de documentatielaag. Het antwoord ligt in het volle zicht onder een zoekterm waar je niet aan hebt gedacht om te proberen, en de experts hebben er niet aan gedacht om hem voor jou te proberen omdat het niet de term is die zij zouden associëren met wat je beschreef. De les die ik hieruit haal — en wat ik anders had gedaan als ik die zes maanden over kon doen — is: minder tijd aan experts vragen besteden en meer tijd kijken naar wat het platform daadwerkelijk voor zich heeft staan. De query tegen `information_schema` was dertig seconden werk. De Magefine module, die ik in plaats daarvan installeerde, was een echte uitgave en een gedeeltelijke fix die zes maanden zelfgenoegzaamheid opleverde. Eerst naar de database kijken had het grootste deel van die tijd bespaard. Dit gaat vaker voorkomen, niet minder. Magento 2.4.8 wordt geleverd met een default-wijziging die alle nieuwe indexers out-of-the-box op Update by Schedule zet, en dat is precies de modus waarin dit hele patroon toeslaat. Meer shops staan op het punt een versie van dit verhaal mee te maken. ## Waar dit nu staat De cron is de operationele modus. Er is geen migratie-tijdlijn. De storefront is stabiel, de indexer houdt het bij, en de fix doet zijn werk. Op de lange termijn denk ik dat het platform niet de juiste match is voor onze use case en gaan we er iets anders omheen ontwerpen. De vorm die ik in gedachten heb is een A/B-style cutover — het nieuwe systeem starten op een van de kleinere storefronts, bewijzen dat het onder live verkeer werkt, en pas daarna nadenken over de grotere migreren. Onze interne tooling heeft de catalogus-data al in een MongoDB backing store staan, dus de data layer voor het nieuwe systeem ligt er; de storefront en de operationele stukken eromheen niet. Er zijn nog geen vaste plannen. De details van de vervangingsarchitectuur zijn niet iets dat ik nu al publiekelijk wil vastleggen. Wat ik publiekelijk wel zeg, is wat ik heb leren weg te ontwerpen: een indexer-patroon dat een generiek flexibility-first datamodel materialiseert in een set gedenormaliseerde indexes via een cron die een changelog moet drainen. Die hele vorm — de EAV-naar-indexer pipeline die Magento 2's catalogus-architectuur definieert — is het ding waar de cron in deze post omheen patcht, en het is precies wat ik niet wil meenemen in iets nieuws. Wat ik er wél in ontwerp, komt in een aparte post, zodra er iets draait om naar te wijzen, niet eerder. Ik schrijf de tweede poging liever op nadat hij contact met de realiteit heeft overleefd dan dat ik me van tevoren aan details commit. ## Het deel waar ik steeds op terugkom Het principe waar ik op dit punt steeds op terugkom, is dat een gedeeltelijke fix die de urgentie wegneemt, slechter is dan helemaal geen fix. De Magefine module was echt een stuk engineering, hij deed echt werk, en hij maakte de storefront stabieler dan hij was geweest. Hij leverde ons ook zes maanden op van "this is fine," en tijdens die zes maanden stopte ik met kijken naar het echte probleem, omdat het echte probleem mij geen slaap meer kostte. De volgende brand wint altijd van de smeulende, en de gedeeltelijke fix is wat de brand tot smeulen terugbrengt. Ik heb geen schone regel die dit in het volgende systeem voorkomt. Het dichtst dat ik bij iets kan komen is: als iets maandenlang te dragen is geweest maar je niet écht hebt begrepen wat het is, behandel dan de tolerantie zelf als het waarschuwingssignaal. De cron in deze post had vijf maanden eerder moeten bestaan dan hij deed. De reden dat hij dat niet deed, is dat er niets genoeg in brand stond om me ertoe aan te zetten hem te bouwen. --- title: "Honderd technische indicator-functies in JavaScript" date: 2026-04-26T15:10:00.000Z updated: 2026-04-27T19:53:41.217Z canonical: https://ninokroesen.nl/blog/honderd-technische-indicator-functies-in-javascript lang: nl author: "Nino Kroesen" --- # Honderd technische indicator-functies in JavaScript Mijn eerste open-source project is een library voor technische analyse genaamd `ta.js` — ongeveer honderd indicator-functies in JavaScript, eerste commit op 2020-08-24, v1.17.0 eerder deze week uitgebracht. Ik ben ermee begonnen omdat ik de indicator-functies sowieso voor mijn eigen werk nodig had, en ze allemaal zelf uitschrijven bleek de schoonste manier om ze te leren. De commits van de eerste drie dagen voegden Money Flow Index, Rate of Change en On-Balance Volume toe; de RSI-fix landde vier dagen later; Stochastics twee weken erin. De library werd gebouwd parallel aan mijn werk door het studieboek heen. Ik heb hem gepubliceerd omdat als iemand het echt zou gebruiken, ze issues konden openen op de functies die ik fout had, en die feedback-loop van buitenaf was goedkoper dan wat ik voor mezelf had kunnen bouwen. De eerlijke lezing van het ding vijfeneenhalf jaar later: ik heb mijn eerste open-source project in JavaScript gebouwd omdat ik toen nog niet wist hoeveel uitvoeringssnelheid uitmaakt in kwantitatieve systemen — de taal bleek verkeerd voor de backtest en goed voor de helft van het werk waar ik nog aan het uitvogelen was wat ik aan het bouwen was. Die tradeoff heb ik aan het begin niet bewust gekozen. Het is iets dat ik later heb geleerd, toen ik backtests op niet-triviale schaal had geprobeerd te draaien en zag dat ze lang genoeg duurden dat de enige eerlijke reactie was om het productiewerk naar een andere stack te verplaatsen. De library bleef waar hij zijn plek nog verdiende, en dat was alles rondom de backtest — data inladen, slicen, een indicator tegen een venster van OHLC plotten in een browsertabblad, itereren op een formule totdat die overeenkwam met wat het studieboek zei dat die moest zijn. ## De vorm Een stuk of honderd standalone pure functies — moving averages, oscillators, bands, volatiliteits-indicatoren, statistiek, chart-transformaties, een PRNG — elk in zijn eigen bestand, elk kort genoeg om in één keer door te lezen. De kleinste moving-average implementatie is 9 regels JavaScript. RSI is 12. De hele library heeft één devDependency (`esbuild`, voor de bundles) en nul runtime dependencies. Hij wordt op drie manieren gedistribueerd: het npm-package, een IIFE-bundle gehost op unpkg die je in een HTML-pagina kunt plakken met een `