How to Extract Structured Data from A Website

Four-tier web extraction decision framework: API feeds, JS-rendered, strict automation, authenticated workflows

TinyFish agents are cloud-based browser sessions that navigate any website — static, JavaScript-rendered, or requiring multi-step workflows — and return machine-readable structured data without requiring you to manage browser infrastructure, proxy rotation, or session handling.

The right tool for structured data extraction depends entirely on what kind of website you’re dealing with. This guide presents a four-tier framework: most sites fall into Tier 1 or 2, where you don’t need TinyFish at all. Tier 3 and 4 are where managed infrastructure earns its cost.

Prerequisites for the code examples: Python 3.8+. Install: pip install requests feedparser playwright tinyfish

Responsible use note: Always review a site’s terms of service before automated data extraction. Public-facing data for research, competitive intelligence, and non-commercial analysis is generally low-risk. Extraction at scale for data resale or targeting platform users requires careful legal review.

What Counts as “Structured Data Extraction”?

The goal is machine-readable output — JSON, CSV, a typed object — rather than raw HTML or a PDF dump. A scraper that returns <div class="price">$29.99</div> isn’t done; a scraper that returns { "price": 29.99, "currency": "USD" } is.

The right tool depends on what kind of site you’re dealing with. Tier 1 sites are trivially easy. Tier 4 requires the most capability. Most real-world extraction projects span multiple tiers — a single pipeline might hit Tier 1 sources for some data and Tier 3 sources for others.

Four Tiers of Website Complexity

Tier Site type What makes it hard Right tool Code complexity
1 Has an API or RSS feed Nothing — use the API requests + JSON Trivial
2 JS-rendered, no API Content loads after JS executes Playwright (local) or TinyFish Fetch (managed) Low
3 Strict automation requirements at scale Infrastructure gaps cause failures in production TinyFish Fetch with browser Medium
4 Authenticated or multi-step workflow Session state, conditional navigation, decisions TinyFish Web Agent Handled for you

Tiers 3 and 4 aren’t edge cases — they’re where production data pipelines typically land once you move beyond toy examples.

Four-tier web extraction decision framework: API feeds, JS-rendered, strict automation, authenticated workflows

Tier 1 — Sites with APIs or RSS Feeds

Always check for an official API before writing a scraper. Many sites that look like scraping targets have well-documented APIs that return exactly the structured data you need.

Where to look: /robots.txt, the site footer (“Developers” / “API” links), documentation subdomains, RapidAPI, or just searching "[site name] API documentation".

import requests

# Example: a site that returns JSON at a predictable endpoint
response = requests.get(
    "https://data-source.example.com/api/v1/products",
    headers={"Authorization": "Bearer your_api_key"},
    params={"category": "electronics", "format": "json"}
)
products = response.json()
print(products[0])  # {'id': '123', 'name': 'Widget A', 'price': 29.99}

RSS feeds are Tier 1 for content monitoring:

import feedparser

feed = feedparser.parse("https://news-source.example.com/feed.xml")
articles = [{"title": e.title, "url": e.link, "published": e.published} for e in feed.entries]

This is the best case. It’s fast, free, respects the site’s intended access pattern, and won’t break when page layouts change.

Tier 2 — JavaScript-Rendered Pages

When a site loads content after JavaScript executes, requests alone returns an empty or incomplete page. You need a browser that runs JavaScript.

Two equally valid approaches, each with a clear use case:

Playwright — the right choice when you’re running locally, need a controlled environment, or are integrating into an existing test suite. Free to use, open-source, excellent documentation.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://js-rendered-site.example.com/catalog")
    page.wait_for_selector(".product-card")  # wait for JS to render

    products = page.evaluate("""
        () => Array.from(document.querySelectorAll('.product-card')).map(el => ({
            name: el.querySelector('.name').innerText,
            price: el.querySelector('.price').innerText,
        }))
    """)
    browser.close()

TinyFish Fetch API — the right choice when you want zero infrastructure overhead: no browser process to manage, no selector maintenance when the site updates, no proxy setup. Returns markdown or structured text from the live rendered page.

import requests, os

response = requests.post(
    "https://api.fetch.tinyfish.ai",
    headers={"X-API-Key": os.environ["TINYFISH_API_KEY"]},
    json={"urls": ["https://js-rendered-site.example.com/catalog"], "format": "markdown"}
)
results = response.json().get("results", [])
content = results[0]["text"] if results else ""  # full page content after JS renders

For simple JS-rendered pages where content is fully visible after load, either approach works. Playwright gives you more control; TinyFish Fetch eliminates infrastructure management.

Tier 3 — Sites with Strict Automation Requirements

Playwright requires significant infrastructure work to run reliably on sites with strict automation requirements at scale. The gaps compound:

  • Proxy management — single-IP requests fail rate limits; rotating proxies require sourcing, billing, and maintenance
  • Session handling — requests with stale or suspicious session fingerprints get challenged more frequently
  • Concurrency — running 50 Playwright instances locally saturates memory; running them in cloud containers requires container orchestration

TinyFish Fetch API handles the infrastructure layer — proxy routing, session management, and request handling at the infrastructure level. Your extraction code stays the same.

import requests, os

response = requests.post(
    "https://api.fetch.tinyfish.ai",
    headers={"X-API-Key": os.environ["TINYFISH_API_KEY"]},
    json={
        "urls": ["https://strict-requirements-site.example.com/listings"],
        "format": "markdown"
    }
)

# Check for errors — failed URLs don't consume credits
result_data = response.json()
errors = result_data.get("errors", [])
results = result_data.get("results", [])
if results and not errors:
    content = results[0]["text"]
else:
    print("Fetch incomplete. Errors:", errors)

The Fetch API is free on all TinyFish plans — zero credits consumed per request. You pay only for Web Agent steps (Tier 4). For Tier 3 use cases, this means running extraction at scale without per-request credit costs.

For running extraction across many URLs in parallel, see how to run 1,000 parallel web requests with the TinyFish Fetch API.

Tier 4 — Authenticated or Multi-Step Workflows

When extraction requires navigating through multiple pages, making decisions based on page content, or maintaining session state across requests — this is where a goal-directed web agent is the right abstraction.

Framing note: Tier 4 workflows are appropriate when you’re accessing accounts and portals you are authorized to use — your own organization’s tools, accounts you operate, or systems where you have explicit access rights.

from tinyfish import TinyFish
import os

client = TinyFish()  # reads TINYFISH_API_KEY from env

# Example: extract a structured report from a multi-step portal
result = client.agent.run(
    url="https://portal.example.com",
    goal="""Navigate to the Analytics section and extract the monthly summary.
    Return JSON with: { month, revenue, units_sold, top_products: string[] }
    Use the authorized credentials already stored in Vault."""
)

# COMPLETED means the session ran — not that the goal succeeded.
# Check both infrastructure-level and goal-level results:
if hasattr(result, 'status') and result.status == "FAILED":
    print("Infrastructure error:", getattr(result, 'error', 'unknown'))
else:
    data = result.result or {}
    if data.get("status") == "failure":
        print("Agent could not complete goal:", data.get("reason"))
    else:
        print("Extracted:", data)

The key distinction from Tier 3: the Web Agent doesn’t just fetch a URL — it understands a goal, navigates through steps to reach it, and returns structured output. Session state, conditional navigation (“if the report isn’t available yet, check back in 10 minutes”), and multi-page workflows are handled by the agent, not by your code.

Choosing the Right Approach

Does the site have an API or RSS feed?
  → Yes: Use the API (Tier 1). No further tooling needed.
  → No: Is the content JavaScript-rendered?
       → No (static HTML): requests + BeautifulSoup for simple cases.
       → Yes: Is this a local/controlled environment?
              → Yes: Playwright (Tier 2a).
              → No, or you need managed scale: TinyFish Fetch API (Tier 2b or 3).
                   → Does the workflow require multi-step navigation or session state?
                          → Yes: TinyFish Web Agent (Tier 4).
                          → No: TinyFish Fetch API (Tier 3).
Use case Best tool
Site has a public API Use the API
Static HTML, simple extraction requests + BeautifulSoup
JS-rendered, local or test environment Playwright
JS-rendered, production at scale TinyFish Fetch
Strict automation requirements at scale TinyFish Fetch (browser: true)
Multi-step workflow or session state TinyFish Web Agent

Most production extraction projects combine multiple tiers. An e-commerce monitoring pipeline might use a site’s API for product catalog (Tier 1), TinyFish Fetch for pricing pages (Tier 3), and TinyFish Web Agent for authenticated inventory portals (Tier 4).

The right extraction tool is the one that matches the complexity of what you’re extracting. For Tier 1 and 2, the answer is almost always free open-source tools. For Tier 3 and 4, managed infrastructure pays for itself in engineering time avoided. Start with the simplest tool that works, and upgrade when the complexity demands it.

FAQ

What is the simplest way to extract structured data from a website?

The simplest approach is checking whether the site has a public API or RSS feed first — if it does, use that directly and skip browser automation entirely. For sites without APIs, Python’s requests library works for static HTML. JavaScript-rendered sites require a browser; Playwright for local use, TinyFish Fetch API for managed-scale extraction.

When does Playwright break at scale?

Playwright is reliable for local and small-scale use. In production environments running hundreds or thousands of daily extractions, the infrastructure requirements compound: proxy sourcing and rotation, browser process memory limits, session fingerprint freshness, and container orchestration for concurrency. TinyFish Fetch handles this infrastructure layer, keeping your extraction code simple.

What is the TinyFish Fetch API and how is it different from requests?

TinyFish Fetch API runs a full browser session and returns the page content after JavaScript execution — similar to what Playwright returns, but without the infrastructure overhead. Unlike Python’s requests, which only performs an HTTP GET and returns the raw server response, TinyFish Fetch renders the complete DOM including dynamically loaded content. The Fetch API is free on all TinyFish plans — zero credits per successful request.

When do I need the Web Agent instead of the Fetch API?

Use the Web Agent when your extraction involves decisions, navigation across multiple pages, or session state that must persist across steps. If you’re fetching a single URL and parsing the response, Fetch is sufficient. If you need to “navigate to the quarterly report, download the CSV, and extract the revenue line” — that’s a multi-step goal requiring the Web Agent.

Is extracting publicly visible data legal?

Public-facing data extraction for research, competitive intelligence, and non-commercial purposes is generally low-risk in most jurisdictions. Terms of service are contractual (not legal) restrictions and their enforceability varies by context. Data that requires authentication — content behind login pages — is in a different category: only access portals and accounts you are authorized to use. Consult legal counsel for commercial data products or any extraction at significant scale.

How do I extract structured data from a site that requires login?

Use TinyFish Web Agent with Vault, which stores credentials securely and injects them into the browser session. The agent navigates the authentication flow using credentials for accounts you are authorized to access, then continues to the target content. This is appropriate for your own organizational tools, SaaS dashboards, and portals where you hold the account.

Related Reading:

Want to scrape the web without getting blocked? Try TinyFish — a browser API built for AI agents and developers.

LinkedIn recorta 5% de su plantilla y despidos tech 2026 superan 100.000

Los despidos tech 2026 dejaron de ser una serie de titulares aislados para convertirse en la fotografía estructural del año. Reuters confirmó el 18 de mayo que LinkedIn planea recortar el 5% de su plantilla global, una decisión que llega cuando el contador acumulado de despidos en el sector tecnológico ya superó los 100.000 puestos en lo que va del año.

La cifra no es marginal. Tiene impacto directo sobre miles de familias, sobre el mercado de talento en LATAM y Europa, y sobre la narrativa pública del sector. Y, sobre todo, convive con una contradicción que merece análisis: las mismas compañías que recortan plantilla están firmando los compromisos de inversión más grandes de su historia.

TL;DR

  • Reuters reportó el 18 de mayo que LinkedIn planea recortar el 5% de su plantilla global como parte de una reestructuración alineada con su pivote a IA.- Los despidos acumulados en tech superaron 100.000 puestos en lo que va de 2026, según trackers como Layoffs.fyi y reportes financieros de TradingView.- La ronda actual continúa olas previas de 160.000 (2022), 260.000 (2023) y 150.000 (2024-2025) en grandes plataformas digitales.- El recorte coincide con un capex récord en IA: Meta, Alphabet, Microsoft y NVIDIA proyectan más de $350B combinados en 2026.- Los ajustes golpean middle management, marketing, ventas y operaciones; ingeniería ML, infraestructura y MLOps mantienen contrataciones selectivas.- Microsoft, dueña de LinkedIn desde 2016, ya había recortado más de 6.000 empleados en oleadas previas durante el primer trimestre.- Las reestructuraciones combinan retiro voluntario, paquetes de salida y eliminación de vacantes para suavizar la cifra final reportada.

Qué pasó

De acuerdo con el reporte original de Reuters, replicado el 18 de mayo por TradingView, Yahoo Finance y MarketWatch, LinkedIn comunicó internamente que ejecutará una reducción cercana al 5% de su plantilla global. A la escala en la que opera la red profesional, eso se traduce en algo más de mil puestos eliminados, distribuidos entre operaciones, ventas, marketing y áreas de soporte. La empresa no ha hecho público un desglose por país.

La comunicación se inscribe en una ola más amplia. Trackers especializados como Layoffs.fyi, que llevan inventario manual de los anuncios públicos desde 2022, contabilizan más de 100.000 puestos eliminados en empresas tecnológicas durante 2026, con LinkedIn como el último capítulo visible. El número es preocupante por sí mismo, pero su lectura cambia cuando se lo compara con el contexto financiero del mismo período.
Las reestructuraciones de 2026 golpean roles de oficina más que ingeniería de plataforma.

Contexto: el ciclo de despidos desde 2022

Para entender qué significa este recorte, conviene mirar atrás. El ciclo actual de despidos tech arrancó en el segundo semestre de 2022, cuando la caída de la publicidad digital, la corrección bursátil y el cambio de tasas de interés rompieron la inercia expansiva de la pandemia. Aquel año, Layoffs.fyi registró más de 160.000 puestos eliminados.

2023 fue todavía peor. Con Meta entrando en el llamado año de la eficiencia, con Amazon recortando AWS y dispositivos, con Google preparando Bard y luego Gemini, el número se acercó a 260.000 puestos eliminados en el año. Buena parte de esos despidos se ejecutaron en oleadas trimestrales, una mecánica que se mantuvo en 2024 y 2025.

Lo que llama la atención de 2026 no es el volumen total, sino su composición. Las primeras olas de 2022 y 2023 eliminaron principalmente posiciones que se habían sobrecontratado durante la pandemia: reclutadores, customer success, expansiones internacionales sobredimensionadas. Las olas de 2026 atacan al middle management, a las funciones corporativas tradicionales y a equipos de producto en líneas de negocio maduras. Es un recorte cualitativamente distinto.

Los números detrás de la cifra

El umbral de 100.000 despidos tech acumulados a mediados de mayo de 2026 implica un ritmo cercano a 22.000 puestos eliminados por mes en lo que va del año. Si la cadencia se sostiene, el sector cerraría 2026 por encima de 250.000 puestos eliminados, una cifra comparable con la del peor año del ciclo, 2023.

El detalle empresa por empresa muestra concentración. Microsoft acumula más de 6.000 puestos eliminados antes del anuncio de LinkedIn, en oleadas que afectaron equipos de Azure, Xbox, gaming y la división de dispositivos. Meta volvió a anunciar ajustes selectivos en su plataforma de Reality Labs. Google ejecutó recortes en Search Ads y en su grupo de hardware. Amazon redujo equipos en AWS y en su división de salud.

graph LR
    A["Capex IA 2026: 350B USD"] --> B["Inversion en GPUs y datacenters"]
    B --> C["Reestructuracion de areas no IA"]
    C --> D["Despidos: 100.000+"]
    D --> E["Liberacion de payroll"]
    E --> A

El caso LinkedIn: Microsoft, IA y la silla musical

LinkedIn ocupa un lugar particular en este mapa. Comprada por Microsoft en 2016 por 26.200 millones de dólares, la red profesional opera de manera relativamente autónoma dentro del grupo. Pero su negocio depende de tres líneas que están siendo redefinidas por la IA generativa al mismo tiempo: anuncios B2B, suscripciones de reclutamiento (Recruiter) y servicios de aprendizaje (LinkedIn Learning).

En anuncios, los grandes presupuestos B2B se están desplazando hacia formatos generativos y a publicidad performativa más eficiente. En reclutamiento, los ATS modernos con cribado por IA empiezan a competir con la propia función de búsqueda de LinkedIn. En aprendizaje, el contenido generado y los tutores conversacionales de Copilot, ChatGPT o Claude reducen el atractivo de cursos pregrabados. El recorte del 5% es la respuesta operativa: reducir costo fijo en marketing y operaciones para liberar caja que pueda reinvertirse en producto y en integración con la pila Copilot de Microsoft.

💭 Clave: el recorte de LinkedIn no es una señal de que el negocio esté roto. Es una señal de que la matriz, Microsoft, está reorganizando dólares: los que ahorra en headcount de divisiones maduras los redirige al capex de infraestructura de IA, donde la prioridad estratégica es 2026-2028.

La paradoja: capex récord en IA, recortes récord en personal

El dato que vuelve esta noticia particularmente relevante para el lector técnico es la coexistencia de dos curvas que, en cualquier otro ciclo, habrían sido contradictorias. Por un lado, Meta, Alphabet, Microsoft y NVIDIA proyectan en sus guidances un capex agregado superior a los 350.000 millones de dólares para 2026, dirigido sobre todo a centros de datos, GPUs Vera Rubin de NVIDIA, refrigeración líquida y redes ópticas de muy alta velocidad. Por otro, esas mismas compañías recortan plantilla.

La explicación operativa es simple. El capex se contabiliza en el balance y se amortiza a varios años, mientras que el opex de payroll impacta directamente sobre el margen del año en curso. Cada empleado eliminado en una división madura libera entre 150.000 y 250.000 dólares anuales de payroll cargado. Ese ahorro permite mostrar márgenes consistentes ante los analistas mientras el capex sube de manera vertiginosa.
El capex de 2026 se concentra en GPUs, redes ópticas y refrigeración líquida.
La narrativa pública también se beneficia: anunciar despidos junto con inversiones masivas en IA permite a los CEOs presentar el ajuste como una transformación estratégica y no como una corrección de costos. Es la misma lógica que llevó a Meta a hablar del año de la eficiencia mientras lanzaba Llama 3, o a Google a vender la integración de Gemini como justificación parcial de los recortes en Search Ads.

Impacto en LATAM y trabajo remoto

Los anuncios oficiales suelen hablar de plantilla global sin desglose geográfico, pero los reportes internos filtrados a medios como Bloomberg muestran un patrón claro: las posiciones remotas internacionales son las primeras en caer. Los centros de excelencia abiertos durante 2020-2022 en Ciudad de México, Bogotá, Buenos Aires, San José y Medellín están perdiendo peso relativo.

Tres factores convergen. Primero, la fortaleza del dólar erosionó la ventaja de costo que tenían esos hubs frente a equipos en Estados Unidos. Segundo, los gerentes intermedios remotos son los más fáciles de eliminar sin afectar inmediatamente la producción técnica. Tercero, el endurecimiento regulatorio en visas estadounidenses redujo la fricción para concentrar talento en sedes locales en lugar de distribuirlo.

⚠️ Ojo: si trabajás como contractor o staff aug para una Big Tech desde LATAM, conviene anticipar que los contratos atados a equipos de middle management o marketing tienen mayor riesgo de no renovación en los próximos dos trimestres. Los roles de IC en plataforma, ML y data engineering siguen con demanda alta.

El efecto sobre el ecosistema startup latinoamericano es ambiguo. Por un lado, libera talento senior que puede mover el sector local; por otro, presiona los salarios a la baja en categorías donde las startups locales no pueden competir con la rampa de paquete de una FAANG aunque haya recortes.

Qué sigue

El próximo punto de inflexión llegará con los reportes del segundo trimestre, que las grandes tecnológicas publicarán en julio. La pregunta clave es si el capex en IA empieza a traducirse en revenue significativo o si las compañías mantienen el discurso de inversión a futuro mientras profundizan recortes en otras áreas. Si las divisiones de cloud crecen al 30% interanual o por encima, la narrativa se sostiene; si caen por debajo del 20%, es probable una segunda ola de despidos antes de fin de año.

A nivel sectorial, conviene seguir tres indicadores: la evolución del índice de despidos publicado mensualmente por Layoffs.fyi, las guidance trimestrales de capex en los reportes financieros y los anuncios de cierre o consolidación de centros de excelencia internacionales. Los tres marcarán la temperatura real del ajuste en lo que resta de 2026.

📖 Resumen en Telegram: Ver resumen

Preguntas frecuentes

¿Cuántos despidos tech hay confirmados en 2026?

Más de 100.000 puestos eliminados en lo que va del año según trackers manuales como Layoffs.fyi, que recopilan anuncios públicos de empresas. La cifra real es probablemente mayor porque no todas las reducciones se anuncian formalmente.

¿Cuántos empleados va a despedir LinkedIn exactamente?¿Por qué Microsoft recorta plantilla si invierte cientos de miles de millones en IA?

Porque las dos cifras viven en líneas distintas del balance. El capex en IA se amortiza a varios años y se financia en parte con caja generada por las divisiones maduras. El recorte de headcount opex en esas mismas divisiones libera margen anual y permite sostener la presentación financiera ante los inversores.

¿Qué roles están más expuestos al recorte?

Middle management, marketing, ventas, operaciones, soporte L1, posiciones remotas internacionales en divisiones maduras y equipos de producto en líneas de negocio que no están directamente conectadas con IA generativa o plataforma. Ingeniería ML, MLOps, infraestructura y seguridad mantienen contratación activa.

¿Es buen momento para emigrar a una Big Tech desde LATAM?

Depende del rol. Para perfiles IC senior en plataforma, ML o data engineering, sigue habiendo demanda y movimiento. Para perfiles managers, marketing o producto en líneas de negocio tradicionales, el riesgo de rotación o no renovación es alto. Conviene priorizar equipos vinculados directamente a productos de IA en producción.

¿Esto va a continuar el resto de 2026?

Probablemente sí, al menos hasta que los reportes del segundo y tercer trimestre muestren si el capex en IA se traduce en revenue. Si las cifras de cloud y suscripciones IA crecen como esperan los analistas, los despidos podrían amainar; si decepcionan, es esperable una segunda ola entre septiembre y noviembre.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

With Great Productivity Comes Great Responsibility – Uncle Ben

1. The Productivity Boom and the Hidden Tax

There is no denying it: agent-based coding with tools like Cursor, GitHub Copilot, and modern Large Language Models (LLMs) has fundamentally shifted the baseline of software development velocity. Tasks that used to consume an entire afternoon—writing boilerplate middleware, mapping complex TypeScript types, or generating comprehensive test suites—are now handled by an agent in less than thirty seconds. We are operating at a level of productivity that was unimaginable a few years ago.

But this hyper-velocity introduces a silent, compounding problem: Technical Debt Creep.

When an agent generates code at lightning speed, it optimizes for immediate completion—making the current prompt happy. It does not think about your system’s architectural integrity six months down the line. If we treat agents as flawless execution machines, we aren’t actually moving faster; we are simply deferring a massive architectural tax that we will have to pay with interest during future maintenance cycles.

2. The Two Paths for Managing Tech Debt

To survive in an agent-driven ecosystem without building a fragile codebase, you cannot rely on passive acceptance. You must actively manage the technical debt before it merges into your main branch. Depending on the scope of the feature you are building, you have two core operational options to enforce architectural sanity:

Option 1: Preventative Guardrails (The Markdown Blueprint)

For day-to-day velocity, the most effective way to prevent technical debt is to stop the agent from generating it in the first place. This means creating structured, persistent Markdown files (such as .cursorrules or system prompt guidelines) that constrain the AI’s boundaries at all times.

  • How it works: You explicitly document your absolute design restrictions before writing a line of code. For example: “Never introduce third-party UI wrappers; always default to native Express routes; use standard Web API protocols; stick strictly to our established Postgres database schemas.”
  • The Benefit: By embedding your architectural boundaries directly into the agent’s context window, you eliminate entire classes of over-engineered boilerplate before they ever hit your editor. The AI is forced to work within your clean framework from prompt number one.

Option 2: Active Auditing (The Deep-Dive Review)

For complex, fluid, or highly specific integration cases where global rules aren’t enough, you must switch to the active auditing protocol. When an agent hands you a complex solution, your immediate instinct shouldn’t be to hit “Accept” and test the browser. It should be to introduce deliberate friction, slow down, and dissect the change through a strict verification process:

  • Audit the Changelog First: Before looking at raw syntax, look at the high-level change summary. What files did it touch? Did it sneak in a new npm package? Did a minor logic fix result in modifications across five different directories? A bloated changelog is the first warning sign of a leaky abstraction.
  • Inspect the Raw Files Ruthlessly: Never rely solely on the agent’s summary or inline diff highlights. Open the modified files themselves. Review the explicit code changes, evaluate the architectural decisions, and check what was deleted.

A Note on the Learning Curve:
If you are shifting to this level of scrutiny for the first time, it might sound incredibly daunting—like you’re spending more time playing warden than actually developing. But don’t let that scare you off.

Just like traditional code review, this is a muscle that builds over time. As you co-author more features with your agent, you will naturally develop an intuitive sense of what needs a rigorous, line-by-line audit and what requires far less effort. A simple utility function or a predictable unit test can often be greenlit quickly; a complex state synchronization or an integration with a streaming endpoint is your cue to slow down and put on the auditor’s hat. You aren’t slowing down your entire day; you are learning exactly where to apply the brakes.

3. Decoupling from the “Surrender Protocol”

The most critical moment in this workflow happens when you realize the agent’s current architectural trajectory is wrong, and you explicitly instruct it to change paths or swap methodologies.

For example, when you tell an agent: “This wrapper abstraction is getting too messy. Let’s abandon this library approach and build a clean, manual Server-Sent Events (SSE) route in Express instead.”

Invariably, the agent will instantly drop its previous conviction and offer its standard surrender protocol:

“You are right! I apologize for the oversight. Dropping the wrapper and moving to a native Express SSE implementation is a much cleaner, more robust approach. Let me rewrite that for you…”

While this response feels validating to our engineering ego, it is actually a major red flag. The agent isn’t agreeing because it had a sudden epiphany about clean architecture; it is agreeing because its objective function is designed to match your intent and minimize user friction.

When the agent gives you the “You are right!” concession, you must re-verify these new agreements with extreme skepticism. Frequently, even after promising a clean pivot, the agent will generate code that still carries remnants or structural habits of the messy abstraction it just apologized for. Its context window is still polluted by the previous failed iterations.

4. Conclusion: The AI is the Accelerator, You are the Anchor

Coding agents are spectacular tools for amplifying an engineer’s reach, but they possess zero long-term accountability. They will never have to debug a production outage at 3:00 AM caused by a leaky abstraction they generated—you will.

When an agent agrees with your technical pivot, thank it for the speed, but verify the receipt ruthlessly. True quality isn’t built on how fast an agent can type “You are right”; it’s built on the engineer’s willingness to slow down, look at the files, and enforce architectural sanity.

Dabri: Linux Speech-to-Text, the Unix Way

Backstory:

A year ago I built a speech-to-text assistant for myself, because on Linux this has always been a pain and there still was not really anything solid. And a system-wide “keyboard” for inserting voice into any window is a superpower, a productivity boost, and a daily necessity.

Today there are already many new STT projects, but Dabri still feels irreplaceable, because:

  • Most voice-to-text tools are either cloud-first (↓no privacy), browser-first (not a native part of the desktop), or app-first (↑heavy, ↓poor automation).
  • There are a lot of Python scripts in the field, which are good for glue and prototypes, but become fragile once they grow into a long-running service with packaging, hotkeys, audio, IPC, and native distribution.

Dabri’s answer: one small Go binary, a quiet user-space daemon, CLI commands, Unix socket IPC, and optional WebSocket for pipelines.

It is packaged as a cross-distro AppImage
and also as native packages for Fedora (COPR) and Arch Linux (AUR).

Linux has great building blocks: shell, window managers, pipes, hotkeys, system services, clipboard tools. Voice input should feel like one more native building block, not like a separate product living outside the desktop.

A good STT tool should be quiet, invokable, scriptable, and easy to automate.

The simplest automation example:

Add this to ~/.config/autostart (XDG autostart) if you are on a DE like GNOME / KDE / XFCE:

[Desktop Entry]
Name=Dabri
Exec=dabri
Icon=io.github.ashbuk.dabri
Type=Application
Terminal=false
X-GNOME-Autostart-enabled=true

Or add this line to your Hyprland config:

exec-once = dabri

and Dabri will be available as soon as your computer starts.

Advanced automations

Save voice notes to a file:

dabri start
# speak...
dabri stop >> ~/voice-notes.md

Pipe transcript into any tool — translate, summarize, look something up:

text=$(dabri stop)
echo "$text" | wl-copy          # copy to Wayland clipboard
# or: echo "$text" | xclip -sel clip   # X11

Voice-controlled smart home — trigger Home Assistant actions by speaking:

transcript=$(dabri --json stop | jq -r '.data.transcript')
[[ "$transcript" == *"lights off"* ]] && curl -X POST http://homeassistant.local/api/lights/off

The WebSocket server opens localhost:8080 for remote integrations — useful for IoT pipelines and home automation hubs that prefer HTTP/WS over Unix sockets. Enable it in config:

web_server:
  enabled: true
  port: 8080
  host: localhost

Binding a button for recording and choosing a model, like other settings, is available through the tray or CLI. The Whisper.cpp Large V3 Turbo model is basically state of the art right now. It works beautifully with multi-language input and is very accurate and fast on newer laptops.

Why the rename?

I rebranded my project from Speak-to-AI to Dabri… because with the AI boom, SEO became a real pain, the project was simply getting lost in search.
A descriptive, domain-like name may look useful, but it mostly works only while the project is needed just by you. Warm references in blogs, mentions in online Linux magazines, and the first 100 ⭐ stars on GitHub pushed me to rename it – hopefully, the ship catches a new wind 🙂

Thanks for the attention,
Asher

Les Design Patterns essentiels

Les Design Patterns Essentiels : Guide du Développeur

Les design patterns sont des solutions éprouvées à des problèmes récurrents en programmation. Ils représentent les meilleures pratiques développées par des générations de programmeurs.

Qu’est-ce qu’un Design Pattern ?

Un design pattern décrit :

  • Un problème récurrent
  • Une solution générale réutilisable
  • Les conséquences de cette solution

Les Patterns Essentiels

1. Singleton Pattern

Problème : Garantir qu’une classe n’a qu’une seule instance.

class DatabaseConnection:
    _instance = None
    _initialized = False

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if not self._initialized:
            self.connection = self._create_connection()
            self._initialized = True

    def _create_connection(self):
        # Simulation de connexion
        return "Connection établie"

    def query(self, sql):
        return f"Exécution de : {sql}"

# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True

Version thread-safe :

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

2. Factory Pattern

Problème : Créer des objets sans spécifier leur classe exacte.

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

class Cat(Animal):
    def make_sound(self):
        return "Meow!"

class Bird(Animal):
    def make_sound(self):
        return "Tweet!"

class AnimalFactory:
    @staticmethod
    def create_animal(animal_type: str) -> Animal:
        animals = {
            'dog': Dog,
            'cat': Cat,
            'bird': Bird
        }

        animal_class = animals.get(animal_type.lower())
        if animal_class:
            return animal_class()
        else:
            raise ValueError(f"Type d'animal inconnu: {animal_type}")

# Usage
factory = AnimalFactory()
dog = factory.create_animal('dog')
cat = factory.create_animal('cat')

print(dog.make_sound())  # Woof!
print(cat.make_sound())  # Meow!

3. Observer Pattern

Problème : Notifier plusieurs objets lors d’un changement d’état.

from abc import ABC, abstractmethod
from typing import List

class Observer(ABC):
    @abstractmethod
    def update(self, message: str):
        pass

class Subject:
    def __init__(self):
        self._observers: List[Observer] = []

    def attach(self, observer: Observer):
        if observer not in self._observers:
            self._observers.append(observer)

    def detach(self, observer: Observer):
        if observer in self._observers:
            self._observers.remove(observer)

    def notify(self, message: str):
        for observer in self._observers:
            observer.update(message)

class EmailNotifier(Observer):
    def __init__(self, email: str):
        self.email = email

    def update(self, message: str):
        print(f"Email envoyé à {self.email}: {message}")

class SMSNotifier(Observer):
    def __init__(self, phone: str):
        self.phone = phone

    def update(self, message: str):
        print(f"SMS envoyé au {self.phone}: {message}")

class NewsPublisher(Subject):
    def __init__(self):
        super().__init__()
        self._latest_news = ""

    def publish_news(self, news: str):
        self._latest_news = news
        self.notify(f"Nouvelle actualité: {news}")

# Usage
publisher = NewsPublisher()

email_notifier = EmailNotifier("[email protected]")
sms_notifier = SMSNotifier("+33612345678")

publisher.attach(email_notifier)
publisher.attach(sms_notifier)

publisher.publish_news("Nouvelle fonctionnalité lancée!")

4. Strategy Pattern

Problème : Choisir un algorithme à l’exécution.

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float) -> str:
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number: str):
        self.card_number = card_number

    def pay(self, amount: float) -> str:
        return f"Paiement de {amount}€ par carte {self.card_number[-4:]}"

class PayPalPayment(PaymentStrategy):
    def __init__(self, email: str):
        self.email = email

    def pay(self, amount: float) -> str:
        return f"Paiement de {amount}€ via PayPal ({self.email})"

class BankTransferPayment(PaymentStrategy):
    def __init__(self, account_number: str):
        self.account_number = account_number

    def pay(self, amount: float) -> str:
        return f"Virement de {amount}€ vers {self.account_number}"

class ShoppingCart:
    def __init__(self):
        self.items = []
        self.payment_strategy = None

    def add_item(self, item: str, price: float):
        self.items.append((item, price))

    def set_payment_strategy(self, strategy: PaymentStrategy):
        self.payment_strategy = strategy

    def checkout(self):
        total = sum(price for _, price in self.items)
        if self.payment_strategy:
            return self.payment_strategy.pay(total)
        else:
            raise ValueError("Aucune stratégie de paiement définie")

# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Souris", 29.99)

# Paiement par carte
cart.set_payment_strategy(CreditCardPayment("1234567812345678"))
print(cart.checkout())

# Changement de stratégie
cart.set_payment_strategy(PayPalPayment("[email protected]"))
print(cart.checkout())

5. Decorator Pattern

Problème : Ajouter des fonctionnalités à un objet dynamiquement.

from abc import ABC, abstractmethod

class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass

    @abstractmethod
    def description(self) -> str:
        pass

class SimpleCoffee(Coffee):
    def cost(self) -> float:
        return 2.0

    def description(self) -> str:
        return "Café simple"

class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

class MilkDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.5

    def description(self) -> str:
        return self._coffee.description() + " + lait"

class SugarDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.2

    def description(self) -> str:
        return self._coffee.description() + " + sucre"

class VanillaDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.8

    def description(self) -> str:
        return self._coffee.description() + " + vanille"

# Usage
coffee = SimpleCoffee()
print(f"{coffee.description()}: {coffee.cost()}€")

# Ajout de lait
coffee_with_milk = MilkDecorator(coffee)
print(f"{coffee_with_milk.description()}: {coffee_with_milk.cost()}€")

# Ajout de sucre et vanille
fancy_coffee = VanillaDecorator(SugarDecorator(coffee_with_milk))
print(f"{fancy_coffee.description()}: {fancy_coffee.cost()}€")

6. Command Pattern

Problème : Encapsuler une requête comme un objet.

from abc import ABC, abstractmethod
from typing import List

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass

class Light:
    def __init__(self, location: str):
        self.location = location
        self.is_on = False

    def turn_on(self):
        self.is_on = True
        print(f"Lumière {self.location} allumée")

    def turn_off(self):
        self.is_on = False
        print(f"Lumière {self.location} éteinte")

class LightOnCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self):
        self.light.turn_on()

    def undo(self):
        self.light.turn_off()

class LightOffCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self):
        self.light.turn_off()

    def undo(self):
        self.light.turn_on()

class RemoteControl:
    def __init__(self):
        self.commands: List[Command] = []
        self.last_command: Command = None

    def set_command(self, command: Command):
        self.commands.append(command)

    def press_button(self, slot: int):
        if 0 <= slot < len(self.commands):
            command = self.commands[slot]
            command.execute()
            self.last_command = command

    def press_undo(self):
        if self.last_command:
            self.last_command.undo()

# Usage
living_room_light = Light("salon")
bedroom_light = Light("chambre")

remote = RemoteControl()
remote.set_command(LightOnCommand(living_room_light))
remote.set_command(LightOffCommand(living_room_light))
remote.set_command(LightOnCommand(bedroom_light))

remote.press_button(0)  # Allume le salon
remote.press_button(2)  # Allume la chambre
remote.press_undo()     # Annule la dernière action

Patterns Avancés

7. Builder Pattern

class Pizza:
    def __init__(self):
        self.size = ""
        self.dough = ""
        self.toppings = []

    def __str__(self):
        return f"Pizza {self.size}, pâte {self.dough}, garnie de: {', '.join(self.toppings)}"

class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()

    def set_size(self, size: str):
        self.pizza.size = size
        return self

    def set_dough(self, dough: str):
        self.pizza.dough = dough
        return self

    def add_topping(self, topping: str):
        self.pizza.toppings.append(topping)
        return self

    def build(self) -> Pizza:
        return self.pizza

# Usage
pizza = (PizzaBuilder()
         .set_size("grande")
         .set_dough("fine")
         .add_topping("mozzarella")
         .add_topping("tomates")
         .add_topping("basilic")
         .build())

print(pizza)

Quand Utiliser Chaque Pattern

Pattern Utilisation Avantages
Singleton Configuration globale Une seule instance
Factory Création d’objets complexes Flexibilité
Observer Notifications Couplage faible
Strategy Algorithmes interchangeables Extensibilité
Decorator Fonctionnalités additionnelles Composition
Command Actions réversibles Historique

Anti-Patterns à Éviter

1. Singletons Partout

# Mauvais
class Logger(Singleton):
    pass

class Config(Singleton):
    pass

# Mieux : Injection de dépendance
class Service:
    def __init__(self, logger: Logger, config: Config):
        self.logger = logger
        self.config = config

2. Factory Trop Complexe

# Mauvais
class ComplexFactory:
    def create(self, type, size, color, material, style, ...):
        # Trop de paramètres
        pass

# Mieux : Builder Pattern
class ProductBuilder:
    def with_size(self, size): return self
    def with_color(self, color): return self
    # ...

Conclusion

Les design patterns sont des outils puissants qui :

  • Améliorent la communication entre développeurs
  • Fournissent des solutions éprouvées
  • Rendent le code plus maintenable
  • Facilitent les tests et la refactorisation

L’important n’est pas de tous les connaître, mais de comprendre quand et comment les appliquer. Un bon développeur sait reconnaître les situations où un pattern apporte une réelle valeur ajoutée.

Testing IndexedDB Schema Migrations in Offline-First PWAs

Migration behavior in production build: download a private pain tracker

This is the migration-safety stop in the failure-mode and testing path.

Read first:
Service Worker Failure Modes in Offline-First PWAs
and
Rollback Patterns in Offline-First PWAs

Then continue to:
Offline Queue Replay and Idempotency in Offline-First PWAs

If you want privacy-first, offline health tech to exist without surveillance funding it: sponsor the build → https://paintracker.ca/sponsor

If you want the privacy boundary that makes migration fidelity matter, add:
Trust Boundaries in Client-Side Health Apps

IndexedDB migrations look straightforward right up until real users keep
their data for months.

In a fresh test database, everything feels clean.

The new schema opens.
The upgrade callback runs.
The happy path passes.

That is the easy part.

The hard part is what happens when the app updates on a device carrying
old drafts, stale references, interrupted writes, half-finished queues,
or records created by code you already forgot you shipped.

That is where migration testing stops being a storage detail and starts
becoming a trust boundary.

Because once an offline-first app stores real user history locally, a
bad migration does not just break a test.

It rewrites memory.

The real risk is not schema change

The real risk is silent damage.

If the app crashes loudly during an upgrade, at least you know something
went wrong.

If the app opens successfully but drops a field, misreads a record,
or detaches related data from its references, that is worse.

Now the system looks functional while carrying false history forward.

That is why migration testing has to check more than “did the database
open?”

It has to check whether meaning survived.

Fresh databases prove almost nothing

One of the easiest ways to lie to yourself is to test only against a
brand-new database.

That tells you the latest schema can initialize.

It does not tell you whether upgrades are safe.

Real migration testing needs historical fixtures.

Version 1 data.
Version 2 data.
Malformed edge cases you know used to exist.
Partially populated records.
Unexpected nulls.
Old optional fields that later became required.

If you do not test against old shapes, then you are not really testing
migration behavior.

You are testing installation.

Those are not the same thing.

Test the shape and the meaning

A migration test should not stop at structural validity.

Sure, you should verify that records match the new schema.

But that is only the first layer.

You also need to verify that the record still means what it meant before
the upgrade.

Did drafts stay attached to the right entity?
Did timestamps remain interpretable?
Did queued actions still point to the correct records?
Did attachments keep their references?
Did flags and defaults preserve prior user intent rather than rewriting
it?

Schema correctness is not enough if the migration preserved bytes but
lost the user’s history.

Partial failure is where real migrations are judged

This is the part teams skip because it is awkward.

What happens if the upgrade starts and does not finish cleanly?

What happens if one object store changes and the next one fails?

What happens if the browser is closed mid-upgrade?

What happens if the app throws after rewriting records but before
finalizing related references?

If your tests never model partial failure, then your migration story is
too optimistic for offline-first software.

Real devices lose power.
Tabs get killed.
Users close the app.
Storage operations throw.

The migration path has to survive ugly timing, not just ideal timing.

Long-delayed clients are not edge cases

One of the hardest realities in offline-first systems is the user who
skips several versions.

They do not upgrade from version 5 to version 6.

They upgrade from version 5 to version 11.

That means migration testing cannot assume every intermediate release ran
in order on the device.

You need to know whether:

  • the upgrade path can move across multiple versions safely,
  • the app can still interpret very old local records,
  • old feature data can be preserved or explicitly retired without
    ambiguity,
  • queued work created under older assumptions still degrades safely.

If your migration tests only cover the immediately previous version, you
are testing the release train, not the real world.

Test against bad data on purpose

A protective migration suite should include ugly fixtures intentionally.

Not because the app should accept every corrupted record forever.

Because real local data is messy.

Browsers crash.
Old bugs leave strange shapes behind.
Optional fields become required later.
Manual imports create awkward combinations.

Migration tests should include:

  • missing fields,
  • extra fields,
  • invalid enum values,
  • orphaned references,
  • stale queue entries,
  • duplicate identifiers,
  • records that are valid enough to exist but not clean enough to trust.

That is where you learn whether the migration fails soft or silently
corrupts the state model.

Rollback safety belongs in migration testing too

Migration testing is not only about moving forward.

It is also about understanding what happens if the release needs to be
pulled back.

Can the previous version tolerate the newly written records for one
release window?

If not, is that explicit in the rollout plan?

Do you snapshot before destructive rewrites?

Do you retain enough metadata to restore meaning if the migration proves
wrong in the wild?

If those answers are unknown, the migration is not well tested enough to
ship confidently.

A good migration test suite usually covers five things

1. Fresh install

Prove the newest schema initializes correctly.

2. Upgrade from every supported historical version

Prove old local states land in the new shape without losing meaning.

3. Partial failure and interruption

Prove the app fails safely when upgrade steps do not complete.

4. Compatibility of related state

Prove queues, drafts, references, and attachments still line up after the
migration.

5. Recovery behavior

Prove the app can explain what happened, preserve what is safe, and avoid
continuing with corrupted assumptions.

That is a much higher bar than a single upgrade callback test.

It is also much closer to reality.

The user never sees the migration directly

That is what makes this dangerous.

Users do not watch the upgrade transaction happen.

They only see the aftermath.

Their notes are there or not.

Their queue resumes cleanly or not.

Their saved state still makes sense or not.

So the migration test suite is one of the few places where you can catch
history loss before it becomes part of the product.

That is why this is not just a database concern.

It is product integrity work.

The deeper rule

Offline-first apps have to carry old local reality forward without
falsifying it.

That means schema migrations need more than correctness on a clean
machine.

They need proof under messy data, delayed upgrades, partial failure, and
mixed-version history.

If the migration test suite cannot demonstrate that, then the app is not
really proving upgrade safety.

It is just hoping the user’s device is kinder than production usually is.

Next in the failure-mode path:
Offline Queue Replay and Idempotency in Offline-First PWAs

Support this work

Which Automations Need Human Approval? 5 That Do, 5 That Don’t.

TL;DR: Whether an automation needs human approval comes down to two variables: blast radius and reversibility. Five action types (outbound emails, CRM updates, social posts, payments, calendar invites) should stay gated; five others (internal alerts, logging, email labeling, drafts, file transforms) can run from day one. The gray zone in between earns autonomy by building a clean track record.

Whether a workflow step needs human approval depends almost entirely on what the action does in the world, not on how good the AI is.

An AI that drafts a wrong reply costs you the second it takes to delete the draft. An AI that sends that same reply could cost you a deal you’ve been working for months, and you don’t find out until the prospect goes quiet. Same model, same prompt, same workflow shape. Different blast radius.

Get this wrong in either direction and it costs you: too many approval steps and you’ve replicated the manual work you were trying to escape; too few and you’ve handed control of your client relationships to a probabilistic system with no safety net.

Here’s a practical framework for thinking about where the line should be, with ten concrete examples to make it tangible.

Two variables that determine the answer

Before going through the list, it helps to have a consistent way of evaluating any step: blast radius (how bad is the outcome if the AI gets this wrong?) and reversibility (can you undo it easily?).

Small blast radius, easy to reverse: strong candidate for autonomous execution. Large blast radius, hard to reverse: needs a human checkpoint before it fires, regardless of how confident the AI seems.

That framing handles most workflow automation approval decisions cleanly. Where it doesn’t is the middle, steps with a medium blast radius and partial reversibility. More on those at the end.

Five that should always have approval

1. Outbound emails to clients, prospects, or partners.

Once an email is sent, it’s sent. The recipient has seen it, formed an impression, and possibly already replied. If an AI misclassified a prospect as a warm lead and sent an aggressive follow-up, that email can’t be unsent. If it responded to a support complaint with a generic template, it can’t take back the irritation it caused. The Air Canada chatbot case is the extreme version: an autonomous chatbot committed to a refund policy that didn’t exist, Air Canada tried to disclaim responsibility, and a tribunal held them liable anyway. Outbound communication creates commitments. Those deserve a human eye before they leave your account.

2. CRM deal stage or contact data changes.

Your pipeline is a record of where things actually stand. If an AI incorrectly advances a deal from “proposal sent” to “verbal agreement” because it misread an email tone as positive, your forecasting and follow-up cadence both adjust to a false signal. By the time you notice, you might have delayed reaching out to close, missed a check-in, or sent premature onboarding materials. CRM data drives behavior downstream, and corrupted data corrupts every decision it informs.

3. Social media posts.

Public content carries a different blast radius than internal records. A post that goes out at the wrong time, in the wrong tone, or in response to something that just shifted context can be deleted, but not before people have seen it, or screenshotted it. For solopreneurs where your personal brand and your business brand are the same thing, a single off-tone automated post can do disproportionate damage. The approval step here takes fifteen seconds. The alternative is monitoring every queue every day and hoping nothing fires at a bad moment.

4. Invoice or payment-related actions.

Any automation that creates, sends, or modifies financial documents needs a human checkpoint. Sending an invoice to the wrong client, for the wrong amount, or at the wrong billing interval is the kind of mistake that surfaces awkwardly, sometimes weeks later when reconciliation reveals the discrepancy. Payment automations carry legal and accounting implications that a misclassification can’t simply be “corrected” without a paper trail. Keep this class of actions fully supervised until the workflow has a long, clean track record.

5. Calendar invites or scheduling on your behalf.

An AI that sends a meeting invite to a prospect you weren’t ready to approach, books two meetings at the same time, or schedules a call before you’ve confirmed availability creates commitments that require awkward cancellations to undo. Calendar actions are technically reversible, but the impression left by botched scheduling isn’t. For service-based solopreneurs, how you handle scheduling is part of how clients assess your professionalism.

Five that can run autonomously from day one

1. Internal Slack or notification messages to yourself.

If the AI sends you a wrong notification, you dismiss it. No external impact, no commitment made, no relationship affected. Internal alerts, summaries, and status updates are exactly what automation was made for. Let them run.

2. Logging to a spreadsheet or database.

Writing a record that an event occurred, a form submission came in, a call happened, or a task completed carries minimal risk. The log entry can be corrected, deleted, or ignored. Even a systematic misclassification produces a fixable dataset, not an external consequence. If your workflow ends in writing to a log, it doesn’t need approval.

3. Email labeling and folder organization.

Sorting incoming emails into folders, applying labels, or flagging for follow-up affects only your own inbox. The worst outcome is a mislabeled email you have to find manually. Let the AI sort your inbox and review the categorization rules occasionally, not every individual action.

4. Creating drafts (not sending them).

Having the AI draft a reply, prepare a document, or generate a proposal is genuinely useful precisely because nothing goes out until you review it. The draft is the output; you’re still the one who decides whether and how it gets used. This is a good pattern for getting AI help with outbound communication while keeping the actual send gated.

5. Data formatting and file transformations.

Converting a CSV to a specific format, reformatting a report, extracting structured data from an uploaded document: these are deterministic operations where the AI’s role is parsing and transforming, not deciding. If the transformation is wrong, the input file still exists and you run it again. Nothing external changes.

The gray zone: where a track record earns autonomy

Between these two categories is a range of steps where the right answer depends on context and history. Routing a new lead to a specific pipeline stage might be low-risk if you have a high volume of clearly-defined lead types and a simple routing rule, or high-risk if your pipeline stages drive automated follow-up sequences that are hard to interrupt.

Confidence scoring handles this precisely. Start those gray-zone steps in supervised mode, approval required. As executions accumulate, you’ll see which inputs the AI handles consistently and which ones it struggles with. The steps that earn a clean track record can graduate to autonomous execution. The ones that don’t stay in your queue, where they belong.

This is the core logic behind the automation trust ladder: you don’t have to decide up front whether a step is safe enough to automate fully. You start supervised, collect evidence, and make the decision based on actual performance rather than theoretical confidence.

Worth noting: approvals on Rills are always free. Adding a review step to a gray-zone action doesn’t increase your bill. The cost of being cautious is just your time reviewing, which shrinks as patterns emerge. There’s no financial pressure to skip oversight on steps you’re not sure about.

A simple rule of thumb

When you’re building a new workflow and you’re not sure whether a step needs approval, ask: if the AI gets this wrong, who finds out and how quickly?

If the answer is “I find out immediately and fix it in under a minute with no external impact,” let it run. If the answer is “a client finds out before I do,” add the approval step. That covers most cases without much analysis.

Originally published on the Rills blog. Rills is the autonomous decision layer for solopreneurs and micro-teams: AI proposes, humans approve via a mobile swipe queue, workflows graduate from supervised to autonomous as they earn it. Approvals are always free, you only pay when the AI takes a real action.

Introducing roo-smarter_csv — A Drop-In Roo CSV Backend That’s 3–4.6x Faster

Roo is great at hiding the differences between CSV, XLSX, ODS, and friends behind one spreadsheet-style API, but its CSV processing is slow.

Meet roo-smarter_csv — a drop-in backend that makes Roo’s CSV path 3–4.6× faster and significantly more robust against messy real-world data, without changing a single line of your existing Roo code.

gem 'roo-smarter_csv'
require 'roo-smarter_csv'

spreadsheet = Roo::Spreadsheet.open('people.csv')
spreadsheet.cell(2, 1)   # => "John"
spreadsheet.row(2)       # => ["John", 30, "[email protected]", 50000]

That’s the whole integration. Roo::Spreadsheet.open, cell, row, column, each, parse, first_row / last_row, first_column / last_column, to_csv — all the methods you already use keep working exactly as before. Under the hood, SmarterCSV does the parsing.

Why a new backend?

For CSV specifically, Roo delegates to Ruby’s built-in CSV library, and there are three long-standing problems with Ruby CSV:

  1. It’s slow. Ruby’s CSV is the bottleneck in many real-world Roo CSV pipelines.
  2. It requires manual configuration. You need to provide the parameters for column and row separators, amongst other things.
  3. It’s fragile against real-world data. Inconsistent separators, BOMs, mixed quote styles, embedded newlines, and numeric coercion edge cases can cause silent data corruption — see 10 Ways Ruby’s CSV.read Can Silently Corrupt or Lose Your Data for a tour.

roo-smarter_csv swaps SmarterCSV in as the parser while keeping Roo’s spreadsheet API as the public model. SmarterCSV has been around for 14 years — a battle-tested library you can rely on. You get its speed, robustness, and automatic detection of parameters — and nothing about how you call Roo changes.

Performance: 3–4.6× faster than Roo::CSV

Speedup measured against Roo’s built-in CSV backend, using SmarterCSV 1.17.1:

File Speedup
PEOPLE_IMPORT_B.csv 2.98×
uscities.csv 4.22×
uszips.csv 4.45×
worldcities.csv 4.58×
embedded_newlines_60k.csv 3.84×
heavy_quoting_60k.csv 3.42×
many_empty_fields_60k.csv 3.36×
sample_100k.csv 3.17×
sensor_data_50krows_50cols.csv 3.23×
tab_separated_60k.tsv 3.14×
utf8_multibyte_60k.csv 3.17×

The speedup holds up across the awkward shapes Ruby’s CSV tends to choke on — heavy quoting, embedded newlines, many empty fields, UTF-8 multibyte content, tab-separated input. SmarterCSV’s C extension does the heavy lifting; the Roo grid model is populated from the parsed rows.

For deeper benchmark background, see the recent SmarterCSV 1.16 release notes and the SmarterCSV 1.15.2 benchmark write-up.

Benefits beyond speed

The performance number is the headline, but the robustness improvements are arguably more valuable on a production import path.

Automatic separator detection

SmarterCSV’s col_sep: :auto and row_sep: :auto are the effective defaults. CSV exports from Excel, MySQL, PostgreSQL, Google Sheets, and assorted European tools use different separators (,, ;, t) and different line endings (n, rn, r). With Ruby’s CSV you have to know upfront — guess wrong and you get one giant row, or one column with embedded commas. With roo-smarter_csv you usually don’t need to specify anything:

Roo::Spreadsheet.open('us_export.csv')        # comma-separated, LF
Roo::Spreadsheet.open('eu_export.csv')        # semicolon-separated, CRLF
Roo::Spreadsheet.open('mysql_export.tsv', extension: :csv)   # tab-separated

All three Just Work.

Automatic numeric conversion

convert_values_to_numeric: true is on by default. Cells containing "30" become 30, "1.5" becomes 1.5. With Ruby’s CSV you’d write the coercion yourself, or pass converters: :numeric and pray you don’t have ZIP codes with leading zeros (Ruby CSV mangles those – SmarterCSV handles those correctly).

spreadsheet.cell(2, 2)   # => 30        (Integer, not "30")
spreadsheet.cell(2, 4)   # => 1.5       (Float, not "1.5")

UTF-8 BOM handling

Excel loves to write a UTF-8 BOM at the start of CSV files. Ruby’s CSV will happily put a <0xfeff> at the start of your first header. SmarterCSV strips the BOM transparently — your first column header is the actual header.

Robust quote handling

SmarterCSV 1.16 ships RFC 4180–compliant quote boundary handling by default, plus quote_escaping: :auto that handles both "" (RFC) and " (MySQL, PostgreSQL COPY TO) escape conventions row-by-row. Mid-field quotes (5'10", O'Brien) no longer toggle quoted mode and silently corrupt rows.

Same spreadsheet model

Critically, none of this changes how Roo presents the data. SmarterCSV row hashes are an internal parsing representation; Roo still stores everything in its coordinate-based cell grid, so cell(row, col), row(n), column(n), first_row, last_row, each, parse, and to_csv all behave exactly as Roo users expect.

Blank rows stay addressable, too — roo-smarter_csv sets remove_empty_hashes: false so Roo’s row numbering matches the file even when rows are empty.

Installation

# Gemfile
gem 'roo-smarter_csv'
bundle install
# Anywhere in your app's boot path (config/application.rb, an initializer, etc.)
require 'roo-smarter_csv'

require "roo-smarter_csv" loads both roo and smarter_csv and registers Roo::SmarterCSV as Roo’s CSV handler. From that point on, every Roo::Spreadsheet.open(...) on a CSV file routes through SmarterCSV.

Usage examples

Drop-in replacement

require 'roo'
require 'roo-smarter_csv'

csv = Roo::Spreadsheet.open('people.csv')

csv.cell(2, 1)      # => "John"
csv.cell(2, 2)      # => 30
csv.row(2)          # => ["John", 30, "[email protected]", 50000]
csv.first_row       # => 1
csv.last_row        # => 4

csv.each do |row|
  # process row
end

TSV (tab-separated)

csv = Roo::Spreadsheet.open(
  'people.tsv',
  extension: :csv,
  csv_options: { col_sep: "t" }
)

StringIO / in-memory input

io = StringIO.new("Name,AgenAlice,30nBob,25n")
csv = Roo::Spreadsheet.open(io, extension: :csv)
csv.row(2)   # => ["Alice", 30]

Passing SmarterCSV options directly

csv = Roo::Spreadsheet.open(
  'data.csv',
  smarter_csv: {
    col_sep:    ';',
    quote_char: '"',
    encoding:   'utf-8',
  }
)

Options: two namespaces, clear precedence

roo-smarter_csv understands two option namespaces and resolves them in a predictable order.

smarter_csv: — the primary namespace

Anything SmarterCSV accepts can go here:

Roo::Spreadsheet.open('data.csv',
  smarter_csv: {
    col_sep: ';',
    row_sep: "n",
    quote_char: '"',
    encoding: 'utf-8',
  })

csv_options: — Roo compatibility namespace

If you already pass csv_options: to Roo, the following four keys are bridged into the effective SmarterCSV options:

  • col_sep
  • row_sep
  • quote_char
  • encoding

No other Roo options are treated as CSV parser settings.

Precedence rules

  1. Start with SmarterCSV defaults.
  2. Apply roo-smarter_csv Roo-compatibility overrides (notably remove_empty_hashes: false).
  3. Copy the supported keys from csv_options: into the effective SmarterCSV options.
  4. Apply smarter_csv: on top.
  5. If the same key exists in both places, smarter_csv: wins and a warning is emitted.
Roo::Spreadsheet.open(
  'data.csv',
  csv_options:  { col_sep: ';'  },
  smarter_csv:  { col_sep: "t" }   # ← wins, warning emitted
)

This means existing Roo code that passes csv_options: keeps working unchanged, and you can opt into the full SmarterCSV option surface whenever you want.

Effective defaults

When you pass no options at all, the effective configuration is:

  • col_sep: :auto — auto-detect separator
  • row_sep: :auto — auto-detect line endings
  • quote_char: '"'
  • downcase_header: true
  • strings_as_keys: false
  • convert_values_to_numeric: true
  • remove_empty_hashes: false (Roo-compat override)
  • headers_in_file: true

That covers the vast majority of real-world CSV inputs without any configuration.

What it does not change

roo-smarter_csv is intentionally narrow in scope:

  • It only affects CSV. Roo’s XLSX, ODS, and other backends are untouched.
  • It preserves Roo’s coordinate model. SmarterCSV’s hash-of-symbols rows are an internal parsing artifact — the public API is still spreadsheet-style cells, rows, and columns.
  • It preserves Roo’s single-sheet CSV behavior. A CSV file is still a single sheet.
  • It preserves to_csv export for the in-memory spreadsheet representation.

If you’ve been using Roo’s CSV path and the rest of your code expects Roo’s grid API, nothing in your code needs to change.

When to reach for it

roo-smarter_csv is the right choice when:

  • You already have a Roo-based pipeline and don’t want to rewrite it.
  • You import CSV files from heterogeneous sources (different tools, locales, separator conventions).
  • Your imports are big enough that a 3–4× speedup matters.
  • You’ve been hitting silent data quality bugs caused by Ruby CSV’s defaults.

If you’re starting fresh and don’t need Roo’s multi-format abstraction, use SmarterCSV directly — you get the same speed plus a richer hash-based API (chunked processing, instrumentation hooks, bad-row quarantine, key_mapping, column selection, and more). See Switch from Ruby CSV to SmarterCSV in 5 Minutes.

New to SmarterCSV?

That’s it ✨

gem 'roo-smarter_csv'
require 'roo-smarter_csv'

Two lines, no API change, 3–4.6× faster CSV imports and much better tolerance for real-world data.

Issues, feedback, and PRs welcome.

Zed IDE Is 1.0 — and the Electron Era Just Ended

Zed shipped 1.0 today. After five years of work, more than a million lines of Rust, and a public preview that hundreds of thousands of developers have been running daily, Nathan Sobo and the Zed Industries team posted the milestone on April 29, 2026. I have been a JetBrains lifer for the better part of fifteen years. I tried VS Code. I tried Cursor. I ran code-server on a remote box for a while. None of them stuck. Zed has stuck, and the version-number news is a good excuse to say why.

Clarification, before the rest of this gets confusing. JetBrains IDEs are not built on Electron. The IntelliJ Platform is native-on-JVM, a stack from a much earlier generation of IDE engineering — closer in lineage to Eclipse than to VS Code, with its own different (and heavier) set of tradeoffs that I get into in the JetBrains/Cursor/code-server section below. When the next paragraph says “the product underneath is Electron,” it is talking about the post-2014 wave of new code editors that built on Atom and then VS Code and then Cursor. JetBrains is the standing exception to that wave, not part of it.

The short version: for over a decade, every “new” code editor announcement has been the same product wearing different hats. The hat is whatever the announcement is selling — AI, collaboration, themes, a different vendor — and the product underneath is Electron. Same Chromium per window. Same JavaScript on the editor critical path. Same multi-gigabyte memory footprint by lunchtime. Sublime Text held the line on native code editors through the 2010s, but it was a closed-source one-developer product without a real collaboration or AI story. Zed is the first credible open-source, AI-native, GPU-accelerated production editor in the last decade that restarted from below the browser, and 1.0 is the moment the bet finally pays off in public.

What Zed actually is

Zed is a native, GPU-accelerated, Rust-built code editor with its own custom UI framework called GPUI. There is no Electron. No Chromium. No Node runtime sitting between the keystroke and the screen. Rendering goes directly to Metal on macOS, DirectX on Windows, and Vulkan on Linux through a small set of optimised primitives — rectangles, shadows, text, icons, images — drawn the way a video game draws its UI.

The editor is open source. It was open-sourced on January 24, 2024 under GPLv3, with the server-side collaboration components under AGPL and GPUI itself released under the Apache 2.0 license. That last detail matters: it means the rendering framework that makes Zed fast is available for any other project that wants to build a high-performance native UI in Rust, without inheriting the editor’s copyleft.

It runs on macOS (since the original preview), Linux as of July 2024, and Windows since October 15, 2025. The framework is designed to render at up to 120 frames per second, with the ceiling reached on Apple Silicon and modern Linux GPUs. On a 2024-vintage MacBook Pro that sounds like marketing copy until you actually see a scroll bar that doesn’t tear and a cursor that lands the same frame you press the key.

The team, and the Atom lesson

The three founders are Nathan Sobo, Antonio Scandurra, and Max Brunsfeld. The names matter because the trajectory matters.

Sobo joined GitHub in late 2011 as one of the first two engineers on what became Atom, a hackable text editor written in JavaScript and shipped on Electron. Atom was the first big editor to put Electron on the map; for several years it was the canonical example of “build a desktop app with web technology.” It hit a million-plus active users and shaped a generation of developer tooling. Brunsfeld joined the Atom team in 2013. While there, he created Tree-sitter, an incremental parsing library that is now the syntax engine inside GitHub’s own code-search infrastructure, Neovim, Helix, and most modern editors that take parsing seriously. Scandurra joined in 2014 and co-architected Teletype, Atom’s collaborative-editing feature — an early production use of CRDTs (conflict-free replicated data types) for real-time text editing.

Atom was killed by the company that built it. GitHub announced the sunset on June 8, 2022 and discontinued the editor on December 15, 2022, pointing users at VS Code and Codespaces. The reason given was prioritisation; the reason most developers who watched it happen suspected was simpler. VS Code had won the Electron-editor race, and Atom was the loser in a category that GitHub was no longer willing to fund two horses in.

The lesson the founders drew, as the team has stated explicitly in the 1.0 announcement, was that Electron’s architecture was the ceiling — not the editor’s product decisions, but the platform underneath them. My read of that lesson: a web-stack editor, no matter how well written, will pay a per-keystroke and per-frame tax that a native one will not. Zed Industries was founded in 2021, and the project was announced publicly in 2023. Brunsfeld and Scandurra came along, and the architecture was specifically not the architecture of Atom.

Why the architecture matters

The hard claim Zed is making is that a native, GPU-rendered editor is not “VS Code but a bit faster.” It is a different category of responsiveness, and the gap is not subtle.

Antonio Scandurra’s engineering post from March 7, 2023, “Leveraging Rust and the GPU to render user interfaces at 120 FPS,” walks through the GPUI design. The framework treats the screen as a video-game scene rather than a DOM. Each frame, the CPU describes the scene as a flat list of typed primitives; the GPU draws the entire frame in parallel through optimised shaders. There is no layout-recalculation thread, no garbage collector eating frame budget at random intervals, no Chromium compositor between the editor’s intent and the pixel.

The numbers that show up in independent reviews are consistent in their order of magnitude. One commonly cited 2026 benchmark places Zed at roughly 0.4 to 0.6 seconds cold start against VS Code’s 1.3, a 2-millisecond keystroke latency against approximately 25, and resident memory in the low hundreds of megabytes against multi-gigabyte VS Code footprints on large monorepos. Different reviewers measure differently, and your mileage will vary by hardware and project size, but the consistent finding is that the gap is one order of magnitude on memory and roughly that on input latency. That is not a tweak. It is a different kind of editor.

You can feel this without a benchmark. Open a 100,000-line monorepo cold. Watch the file tree appear. Type in the global search box. The thing the editor industry has accepted as “the cost of doing business” — the small but persistent friction of an editor that is always on the edge of catching up to your typing — is not present.

What 1.0 actually ships with

Per the 1.0 announcement, the launch is not so much a feature unveiling as a declaration that the editor is ready for the developers who passed on it during the preview. Most of what makes Zed Zed has been shipping incrementally over the last eighteen months.

The agent panel runs multiple AI agents in parallel inside the editor. The “parallel agents” feature shipped on April 22, 2026, one week before 1.0; you can have one agent refactoring a function in one file while another writes tests in another, both visible in their own threads, both editing through Zed’s multibuffer review system rather than blind diff-applying. The agents themselves are pluggable. Zed supports the Agent Client Protocol (ACP), an open standard the team published on August 27, 2025 that decouples the editor from the model. Claude Agent, OpenAI’s Codex, the open-source OpenCode runner, and Cursor’s agent all speak ACP today. JetBrains joined the ACP initiative on October 6, 2025, which is a small but notable detail — the legacy IDE vendor decided not to fork its own protocol.

Edit prediction shipped on February 13, 2025. It is not autocomplete. It is a model trained to predict the next edit at keystroke granularity, displayed inline as a subtle ghost-text overlay you accept with Tab. After a few hours it stops feeling like a feature and starts feeling like the editor is keeping up.

A native debugger shipped on June 18, 2025, integrated into the editor’s panel system rather than living in a side window. Native git integration — branch picker, commit viewer, diff staging, blame in the gutter — landed on March 12, 2025. Tree-sitter-powered syntax everywhere, of course. CRDT-based real-time collaboration is inherited directly from the founders’ Teletype work at Atom and predates the AI features by years. Vim and Helix keymaps are first-class. There is a settings UI now, for those of us who never quite memorised every JSON key.

The 1.0 release also marks the launch of Zed for Business: centralised billing, role-based access controls, team management. This is the part that pays for the editor everyone else uses for free. It is also the moment the project becomes a business in the way it always intended to be — selling services around an open-source product, not licensing the product itself.

JetBrains, Cursor, code-server: the alternatives I actually tried

I want to be specific about the comparison, because “VS Code is bad” is not what this article is saying.

JetBrains remains, in my honest read, the gold standard for full-IDE features in several languages. IntelliJ’s refactoring engine understands cross-module Java better than most things on the market; the database tooling is hard to beat; Kotlin support is, unsurprisingly, first-class. The cost is a JVM startup time you can plan a coffee break around and a memory footprint that competes with the operating system. After fifteen years of paying that cost without complaint — and after watching JetBrains hesitate on its own AI story until very recently — I stopped being willing to pay it for everything.

Cursor is VS Code with an excellent AI integration bolted on top. The AI is genuinely good. The editor underneath is the same Electron baseline, with the same multi-gigabyte memory profile and the same Chromium frame budget. If your bottleneck is “I want better AI in VS Code,” Cursor is a real answer. If your bottleneck is “I want an editor that does not feel like a browser tab,” Cursor cannot solve that, because it is a browser tab.

code-server runs VS Code in an actual browser, hosted on a remote box you control. It is genuinely useful for some workflows — running an IDE on a beefy server while editing from a thin laptop, or sharing a development environment with a team. It is also another layer of latency on top of an already-Electron editor, and the experience reflects that.

The point of listing these is not to dunk on any of them. It is that for a decade, every credible alternative to “open VS Code on your laptop” has been some flavour of “open VS Code somewhere else.” Zed is the first option in a long time that does not start with that as the foundation.

What’s next: DeltaDB and the bigger bet

Sequoia Capital led a $32 million Series B in Zed Industries on August 20, 2025, bringing total funding to over $42 million on top of the original $10 million Series A from Redpoint Ventures and others.

The headline use of those funds is DeltaDB: an operation-based version control system using CRDTs to track edits at character-level granularity, designed to interoperate with git rather than replace it. The pitch is that real-time collaboration between humans and AI agents — multiple agents, multiple humans, the same codebase — needs a substrate that records intent at finer resolution than a git commit. The team has said DeltaDB will itself be open-sourced, with an optional paid synchronisation service for teams that don’t want to run their own.

The public roadmap lists the rest of the bet. Instant project sharing — hand someone a link, they’re editing your code in their browser, no setup. Code history as agent context, so the agent can see how a function got the way it did. Async branch collaboration. A plan mode for the agent that writes a strategy before touching code. Hands-free coding via voice. Notebook-style interactive cells. Zed on the Web, opening any project from any device.

Read it as roadmap, not as ship-list. Some of it will land in 2026; some of it will slip; some of it may quietly disappear. What I take from the published roadmap is that the team is not declaring the editor done. 1.0 is the moment the foundation is solid. The interesting work is what gets built on top of it.

The honest caveats

Switching editor mid-project is rarely a good idea, and Zed is not a perfect drop-in replacement for everything VS Code or JetBrains does today. A short list of what to expect:

  • Extension ecosystem is small. Zed’s extension model is WASM-based, sandboxed, technically nicer than VS Code’s Node-based one. The breadth is not there yet. If you depend on a specific niche VS Code extension, search the Zed extensions directory before you commit to switching, and assume the answer is sometimes “not yet.”
  • Language intelligence is uneven. Rust, TypeScript, Python, Go are excellent. JetBrains-grade Java refactoring across multimodule projects, or the depth of IntelliJ’s database tooling, is not yet there. If those are your daily bread, Zed is not a one-for-one swap today.
  • Terms-of-service language deserves a careful read. The Hacker News thread on the 1.0 launch surfaced concerns about the breadth of rights granted around source-code processing for telemetry and AI features. Several of those defaults are configurable; one or two of them require an opt-out. Read the ToS, set the AI defaults consciously, and turn off telemetry if that’s your preference. The team is generally responsive on these issues, but the launch-day version is the version you sign up for.
  • Hardware sensitivity. GPUI is brilliant on Apple Silicon and on modern Linux GPUs. On older hardware or some Windows configurations, the picture is more uneven; the 120 FPS claim is upper-bound, not floor.
  • Some workflows still feel rough. The search-results-in-tabs UX has friction for anyone used to a Telescope-style modal. A few remote-development scenarios are still shy of the comfortable VS Code Remote experience. These are addressable, but they are real today.

None of these are dealbreakers, in the sense that I have switched my daily work to Zed and have not switched back. They are the kind of caveat you would want to know before clearing an afternoon to migrate a serious project.

What this all means

The thing that has been true about code editors for the last ten years is that the architecture stopped mattering, because everyone shipped on the same architecture. The conversation moved up the stack. Themes, AI, collaboration features, plugin marketplaces — those are the layers everyone has been competing on, because the layer underneath was a settled question. You build on Electron, and you negotiate with its constraints.

Zed is a real bet that the architecture is back on the table. The team is not making the bet from outside the industry; they are the people who built Atom, who put Electron on the editor map, and who have spent the last five years deciding it was the wrong foundation for the editor they wanted next. That is a different kind of credibility than “we have a new idea.” It is the credibility of people who have already done the thing they are now arguing against.

I am not going to tell you to switch tomorrow. Most of the audience for this piece is in the middle of something, and editor switches mid-something are how projects pick up two weeks of unrelated friction.

I am going to tell you that this is the first time in over a decade that the architectural ground under the code editor has shifted, and 1.0 is the day it shifted publicly. A native, open-source, GPU-accelerated, AI-native editor with a real team, real funding, real users, and real momentum is now the alternative to “VS Code but with extras.” That alternative did not exist yesterday. It exists today.

Open the download page. Try it on the project you don’t mind a little friction on. Spend a week. The decision you make at the end of the week is yours. The thing I want on the record is that the choice is real now, and it has been a long time since that was true.

Why AI Makes Readability More Important, Not Less

Software development has always had an asymmetry: code can be written faster than it can be understood.

Code is written once but read repeatedly during reviews, debugging sessions, refactors, and production incidents. The real constraint has always been understanding code rather than typing it.

AI dramatically widens that asymmetry. Tools like GitHub Copilot and Claude Code can generate thousands of lines of code in minutes, but the speed at which humans understand code hasn’t changed.

Before AI, the speed of writing code naturally constrained how quickly systems could grow. AI removes much of that friction. But human comprehension speed hasn’t changed. Systems can now grow faster than teams can understand them — or keep them coherent.

The asymmetry isn’t new, but the magnitude is.

Systems Can Now Grow Faster Than Teams Can Understand

Discussions of AI-generated code often focus on correctness or “AI slop.” That risk is real. But a larger risk is structural: how quickly disorder can accumulate as systems grow faster than humans can maintain them.

AI removes much of the friction from writing code. AI can generate large portions of features from a handful of prompts. But every line of code still has to be read, reviewed, debugged, refactored, and extended. Even correct code increases cognitive load, because it still increases the amount of code humans must understand.

A messy system with 20k lines of code is annoying. A messy system with 500k AI-generated lines of code becomes extremely difficult to understand.

AI increases code volume while increasing the speed at which structural entropy, like small inconsistencies and duplicated code blocks, can accumulate.

As that entropy grows, so does the system’s cognitive surface area: the amount of structure developers must mentally explore to understand the code.

As systems grow faster, the team’s mental model can struggle to keep up.

The Bottleneck Shifts

When developers switch to using AI tools, they spend less time manually writing code and more time analyzing AI-generated code:

  • reviewing it
  • validating that it actually behaves correctly
  • integrating it correctly into the system
  • refactoring it into something readable
  • aligning it with existing patterns in the codebase

The bottleneck shifts from typing code to understanding, reviewing, and shaping the generated code to fit the system.

As code volume increases, code review becomes more cognitively demanding. Large AI-generated changes increase review fatigue, making it harder for both the author and the reviewer to fully understand and validate every change.

AI Introduces Structural Drift

AI generates code from the local context it sees rather than a full understanding of the system’s architecture. As a result, it does not reliably maintain global structural consistency across the system. Small inconsistencies can accumulate across many changes, such as:

  • slightly different abstractions
  • duplicate logic
  • inconsistent naming
  • subtle architectural drift

Individually, these differences are small. But across hundreds of changes they compound.

Over time, these inconsistencies increase the cognitive entropy — the amount of disorder a reader must make sense of — of the system. The structure becomes harder to recognize. The patterns become less predictable. When patterns become unpredictable, understanding slows down.

Humans Maintain Coherence

In addition to validating correctness, developers must ensure AI-generated code fits the system’s existing structure and conventions. This maintains system coherence. By coherence, I mean:

  • The structure of the new code makes sense
  • The patterns remain consistent internally and across the system
  • The boundaries of methods, classes, and packages remain clear
  • Responsibilities remain understandable
  • Complexity remains manageable
  • Naming remains accurate and meaningful
  • Similar problems are solved in similar ways

Readable code is required to make that possible.

AI does not automatically maintain system coherence. As codebases grow faster through AI code generation, a larger part of the developer’s job becomes counteracting structural drift. Without that effort, the system becomes harder to understand over time, even if every individual piece works.

Readable structure allows coherence to survive as the system grows.

AI Can Also Help Maintain Readability

AI can also help with the work of maintaining coherence. With enough context and a human in the loop to validate their proposals, frontier models are surprisingly good at:

  • structural refactors
  • naming improvements
  • package reorganization
  • large mechanical changes

I’ve had good results asking Claude Code to analyze an architectural layer of a codebase and propose an updated package and class structure based on the business functionality it detects — adding breadcrumbs for me. (More on using AI to improve a hard-to-navigate codebase in a future article.) It saves time by handling the analysis and tedious cut-and-paste, and the return on investment multiplies every time someone works in that part of the system and benefits from the improved navigability.

The Constraint Hasn’t Changed

AI dramatically reduces the cost of producing code. But the fundamental constraint in software development has never been typing. It has always been understanding. Human comprehension speed does not scale with code generation speed.

Whether a system remains understandable depends on its coherence: the consistency of its structure, boundaries, and patterns. Readable structure is what preserves that coherence as a system evolves.

Some companies are experimenting with AI-driven development loops where agents generate specifications, write implementations, run tests, and even review their own changes. In those environments, humans may not read every line of code. But even highly automated systems still rely on humans when something goes wrong. Production incidents, unexpected behavior, and architectural changes require developers to understand what the system is doing and why.

Whether code is written manually, with AI assistance, or through AI-driven development loops, understandable systems are still necessary for the humans maintaining them.

But what actually determines how difficult a system is to understand?

In my next article, I’ll go into more depth about cognitive surface area: the property of a system that determines how much of it developers must mentally explore to understand or change behavior.

AI was my editor, but these ideas are my own.

This article is part of a broader series exploring how code structure, navigability, and cohesion align with cognitive limits.

If you’re interested in the deeper dive, the full series is here:
Designing Systems for Human Brains