<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: C. Wheatley</title>
    <description>The latest articles on DEV Community by C. Wheatley (@bsymbolic).</description>
    <link>https://dev.to/bsymbolic</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3982019%2F8beb5d78-be18-44b4-b2f8-1a9e798a54e2.jpeg</url>
      <title>DEV Community: C. Wheatley</title>
      <link>https://dev.to/bsymbolic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bsymbolic"/>
    <language>en</language>
    <item>
      <title>Browser Video Editor: Trim, Cut, and Export MP4 Without a Server</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Fri, 03 Jul 2026 02:32:22 +0000</pubDate>
      <link>https://dev.to/bsymbolic/browser-video-editor-trim-cut-and-export-mp4-without-a-server-4f2n</link>
      <guid>https://dev.to/bsymbolic/browser-video-editor-trim-cut-and-export-mp4-without-a-server-4f2n</guid>
      <description>&lt;p&gt;Browser Video Editor is a video editor that runs entirely in your browser. You import local clips, trim them, arrange them on a timeline, drop text overlays on top, scrub a live preview, and export a real &lt;code&gt;.mp4&lt;/code&gt; — and the video files never leave your machine. No upload, no backend, no server doing the encoding. I built it with Claude as a pair programmer, and v1 is complete and merged to &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;The shape of it is a Classic non-linear editor: a media bin on the left, a preview canvas in the middle, a properties panel on the right, and a full-width timeline underneath. You click &lt;strong&gt;+ Import video&lt;/strong&gt; to pull a local file into the bin, click a clip to drop it onto the timeline, and from there you can adjust its in/out points, split it at the playhead, reorder clips, delete them, and add text overlays with their own position, size, color, and start/end times. The preview plays, pauses, and scrubs, and every edit redraws immediately.&lt;/p&gt;

&lt;p&gt;Then you hit &lt;strong&gt;Export MP4&lt;/strong&gt; and get an actual H.264 video file with AAC audio — each clip's original sound preserved and stitched in sequence — muxed together in the browser. That last part is the whole point. Plenty of "browser editors" are really thin clients that ship your footage to a render farm. This one does the encode locally, which is why your files stay on your machine.&lt;/p&gt;

&lt;p&gt;The stack is React, TypeScript, Vite, Tailwind, and Zustand, with Vitest for the unit tests. There's deliberately no backend. Export leans on the WebCodecs API, which today means &lt;strong&gt;Chrome or Edge&lt;/strong&gt; — preview and editing work anywhere, but on Firefox and Safari the Export button is disabled with a note explaining why.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The division of labor was the usual one: I decided what the editor should do, and Claude wrote essentially all of the code. We started from a design spec and an implementation plan that split v1 into 16 test-driven tasks, built via subagent-driven development with separate code-review passes, and I verified the result in a real browser before signing off.&lt;/p&gt;

&lt;p&gt;The architecture has one idea holding it together: a single source of truth and a single way to draw a frame. The Zustand store in &lt;code&gt;src/store/editorStore.ts&lt;/code&gt; owns the entire &lt;code&gt;Project&lt;/code&gt; — the clips, the ordered timeline items, the text overlays, the playhead. Nothing else holds state. On top of that sit pure selector functions in &lt;code&gt;src/timeline/selectors.ts&lt;/code&gt; that derive everything you actually render: the timeline layout, and crucially "what is active at time &lt;em&gt;t&lt;/em&gt;". Those selectors are pure, so they're fully unit-tested without any DOM.&lt;/p&gt;

&lt;p&gt;The piece I care most about is &lt;code&gt;Compositor.drawFrame(ctx, project, time, videoFor)&lt;/code&gt;. It renders exactly one frame — the active clip plus whatever overlays are live at that moment — and it is shared by &lt;strong&gt;both&lt;/strong&gt; the preview player and the exporter. The &lt;code&gt;requestAnimationFrame&lt;/code&gt; player in &lt;code&gt;PreviewPlayer.ts&lt;/code&gt; calls it to paint the canvas as you scrub; the exporter calls the same function frame-by-frame as it encodes. That's what makes the editor what-you-see-is-what-you-get: there is no separate "render path" that can drift from the preview. If a text overlay looks right while scrubbing, it looks right in the file, because the same function drew both.&lt;/p&gt;

&lt;p&gt;The export path itself (&lt;code&gt;src/export/Exporter.ts&lt;/code&gt;) is where WebCodecs earns its keep. It walks the timeline at 30fps, and for each frame it seeks the relevant source &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element to the right source time, draws the composited frame to an offscreen canvas, wraps it in a &lt;code&gt;VideoFrame&lt;/code&gt;, and feeds it to a &lt;code&gt;VideoEncoder&lt;/code&gt; configured for H.264 (&lt;code&gt;avc1.42001f&lt;/code&gt;). Audio is handled separately and more bluntly: it allocates one big merged &lt;code&gt;AudioBuffer&lt;/code&gt; via an &lt;code&gt;OfflineAudioContext&lt;/code&gt; (used for its &lt;code&gt;decodeAudioData&lt;/code&gt; to pull in each clip's &lt;code&gt;[in, out]&lt;/code&gt; slice), then a manual loop copies each clip's samples into place at the right timeline offset, chunks that buffer into &lt;code&gt;AudioData&lt;/code&gt; blocks, and runs them through an &lt;code&gt;AudioEncoder&lt;/code&gt; for AAC. Both streams go into &lt;code&gt;mp4-muxer&lt;/code&gt;, which finalizes an in-memory MP4 that gets handed to the user as a download.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Three real ones, all from the export path, all of which had concrete fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebCodecs encoders fall over if you don't respect backpressure.&lt;/strong&gt; A &lt;code&gt;VideoEncoder&lt;/code&gt; will happily accept frames faster than it can encode them, and the encode queue grows until things get ugly — memory balloons, frames stall. The fix is to watch &lt;code&gt;encoder.encodeQueueSize&lt;/code&gt; and stop feeding it when the queue gets too deep. The exporter has a small &lt;code&gt;drainQueue&lt;/code&gt; helper that awaits &lt;code&gt;setTimeout(0)&lt;/code&gt; in a loop until the queue drops below 30 outstanding frames before it submits the next one, for both the video and audio encoders. Without that drain, a longer timeline would push the encoder past where it can keep up. This was one of the issues a code-review pass specifically flagged and fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seeking a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element can silently never finish.&lt;/strong&gt; Exporting works by setting &lt;code&gt;video.currentTime&lt;/code&gt; and waiting for the &lt;code&gt;seeked&lt;/code&gt; event before drawing that frame. The problem is that &lt;code&gt;seeked&lt;/code&gt; doesn't always fire — a seek to a time the browser considers "already there", or an edge near the end of a clip, can just hang, and a hung seek freezes the entire export. The fix is twofold: skip the wait entirely when the requested time is within a millisecond of the current time, and arm a 1000ms &lt;code&gt;setTimeout&lt;/code&gt; fallback that resolves the promise anyway if &lt;code&gt;seeked&lt;/code&gt; never arrives. The export would rather draw a slightly stale frame than deadlock forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio sync is a sample-offset problem, not a timestamp problem.&lt;/strong&gt; It's tempting to encode each clip's audio as its own stream and trust timestamps to line everything up, but the reliable approach turned out to be doing the arithmetic by hand. The exporter allocates one merged buffer sized to the whole timeline, then for each clip computes the in-point sample, the slice length in samples, and the destination offset (&lt;code&gt;start * sampleRate&lt;/code&gt;), and copies samples straight into the merged buffer at that offset — clamping mono sources up to stereo and guarding the bounds. Everything is resampled to a fixed 48kHz / 2-channel target so there's exactly one sample rate to reason about. Building the audio as a single correctly-offset buffer before encoding is what kept the sound aligned with the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;v1 is complete and merged to &lt;code&gt;master&lt;/code&gt;: import, trim, cut/split, a multi-clip reorderable timeline, text overlays, live canvas preview, and in-browser MP4 export with audio. The suite is 27 Vitest tests across the store, the pure selectors, an id helper, and the WebCodecs capability check, and &lt;code&gt;tsc&lt;/code&gt; plus the production build are clean.&lt;/p&gt;

&lt;p&gt;The export was verified, not assumed. When v1 was done, the end-to-end run produced a valid 4.5-second, 1280×720 MP4 with the trim correctly applied and 2-channel / 48kHz AAC audio, confirmed by playing it back in an actual &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element and decoding the audio track with &lt;code&gt;decodeAudioData&lt;/code&gt;. A real file, with real sound, made entirely in the browser.&lt;/p&gt;

&lt;p&gt;Some things were left out of v1 on purpose — transitions, filters and color grading, a separate music track, undo/redo, and project save/load — but the data model leaves room for all of them. This is part of a series on projects built this way; the running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>webcodecs</category>
      <category>video</category>
    </item>
    <item>
      <title>ClawMonitor: A Neon Synthwave System Bar That Watches My Dev Stack</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Fri, 03 Jul 2026 02:27:47 +0000</pubDate>
      <link>https://dev.to/bsymbolic/clawmonitor-a-neon-synthwave-system-bar-that-watches-my-dev-stack-5h11</link>
      <guid>https://dev.to/bsymbolic/clawmonitor-a-neon-synthwave-system-bar-that-watches-my-dev-stack-5h11</guid>
      <description>&lt;p&gt;I wanted to glance up and know, without thinking, whether my machine was on fire — and whether the four background services I actually depend on were still alive. Task Manager makes you go looking. A widget cluttered the desktop. So I built ClawMonitor: a slim, frameless, always-on-top bar pinned to the top edge of the screen, synthwave-styled, that shows CPU / RAM / GPU / network / disk at a glance and — the part no other monitor does — the live up/down status of my dev stack. It's the neon sibling of &lt;a href="https://dev.to/projects/"&gt;ClawPorts&lt;/a&gt;, built the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;ClawMonitor is a single horizontal strip docked to the top of my primary monitor. Left side: CPU %, RAM %, GPU % and temp, network throughput, disk usage. Right side: a row of colored dots for my stack — a local gateway on &lt;code&gt;:18789&lt;/code&gt;, WSL, Docker, and Ollama on &lt;code&gt;:11434&lt;/code&gt; — green when up, dim when down. The look is classic synthwave: cyan / magenta / purple / green neon on a near-black grid, Cascadia Code throughout.&lt;/p&gt;

&lt;p&gt;Hover anywhere on the bar and a three-tile panel slides down: per-core CPU bars and the top processes by load, full GPU detail (VRAM, power draw, fan), RAM with the WSL &lt;code&gt;vmmem&lt;/code&gt; share broken out, and a "Your Stack" tile that spells out each service's port and state. When something redlines — CPU or GPU over 90%, a temperature over 80°C, a disk under its free-space floor, or the gateway going down — the relevant slice pulses red. The whole thing is &lt;strong&gt;click-through&lt;/strong&gt;, so it never blocks the apps underneath, and it &lt;strong&gt;reserves screen space&lt;/strong&gt; like the taskbar so maximized windows start below it instead of getting covered.&lt;/p&gt;

&lt;p&gt;It's built, public, and installed on my machine. The repo is on &lt;a href="https://github.com/denrod25-del/ClawMonitor" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; (MIT), there's a &lt;a href="https://denrod25-del.github.io/ClawMonitor/" rel="noopener noreferrer"&gt;landing page&lt;/a&gt; with a live interactive recreation of the bar, and v0.1.1 ships as an unsigned NSIS installer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Same loop as the rest of these projects: I decided what it should be, Claude wrote nearly all the code as a pair programmer. We brainstormed a spec, turned it into an 18-task plan across milestones, and Claude implemented it test-first with separate reviewer passes. I played the finished build live before signing off. The spec and plan live in &lt;code&gt;docs/superpowers/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The stack is &lt;strong&gt;Electron + &lt;a href="https://github.com/sebhildebrandt/systeminformation" rel="noopener noreferrer"&gt;&lt;code&gt;systeminformation&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, with Vitest for the test suite (35 unit tests). The architecture has a hard split across the Electron process boundary. The main process is a &lt;strong&gt;modular collector orchestrator&lt;/strong&gt;: small readers — &lt;code&gt;cpu&lt;/code&gt;, &lt;code&gt;memory&lt;/code&gt;, &lt;code&gt;gpu&lt;/code&gt;, &lt;code&gt;disk&lt;/code&gt;, &lt;code&gt;network&lt;/code&gt;, &lt;code&gt;sensors&lt;/code&gt;, &lt;code&gt;stack&lt;/code&gt; — each return a normalized slice. The renderer is a frameless transparent page that draws the bar and panel &lt;em&gt;purely&lt;/em&gt; from the latest snapshot pushed over IPC. The UI holds no logic of its own; it's a view of one data object.&lt;/p&gt;

&lt;p&gt;The collectors run on two tiers. A &lt;strong&gt;fast tier&lt;/strong&gt; (default 2s) polls the cheap load metrics. A &lt;strong&gt;slow tier&lt;/strong&gt; (default 8s) polls stack health, because asking whether four services are up involves HTTP probes and &lt;code&gt;tasklist&lt;/code&gt; and doesn't need to fire twice a second. Every collector is wrapped in a timeout so one slow or failed reader can never block the rest — it degrades that slice to &lt;code&gt;null&lt;/code&gt; and the merged snapshot carries on. That &lt;code&gt;mergeSnapshot&lt;/code&gt; step is deliberately forgiving: a failed reader becomes a &lt;code&gt;null&lt;/code&gt; field plus an entry in an &lt;code&gt;errors&lt;/code&gt; map, never a thrown exception that blanks the whole bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A click-through window gets no hover events, so the hover panel is driven from the main process.&lt;/strong&gt; The bar is set to &lt;code&gt;setIgnoreMouseEvents(true)&lt;/code&gt; so clicks pass straight through to whatever's underneath — which is exactly what you want from an always-on overlay, but it also means the DOM never sees a &lt;code&gt;mouseenter&lt;/code&gt;. So the panel isn't CSS hover at all. The main process polls &lt;code&gt;screen.getCursorScreenPoint()&lt;/code&gt; every 120ms, decides whether the cursor is over the bar, and tells the renderer to open or close the panel over IPC. Hover behavior, reimplemented by hand, because the obvious mechanism is unavailable the moment you make the window click-through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reserving screen space is a Win32 call, and it leaks if you're not careful.&lt;/strong&gt; To make maximized apps sit below the bar instead of being covered, ClawMonitor registers as a Windows AppBar — &lt;code&gt;SHAppBarMessage&lt;/code&gt; from &lt;code&gt;shell32&lt;/code&gt;, reached through the &lt;a href="https://koffi.dev/" rel="noopener noreferrer"&gt;&lt;code&gt;koffi&lt;/code&gt;&lt;/a&gt; FFI library because there's no Electron API for it. That reserves the top 34px (in physical pixels, so it has to multiply by the display's scale factor). The trap: if the app force-quits or crashes, Windows does &lt;em&gt;not&lt;/em&gt; reclaim that reservation, and a naive relaunch &lt;em&gt;stacks&lt;/em&gt; a second one on top — 34px becomes 68px becomes a growing dead gap at the top of every screen. The fix is to persist the window handle to disk and, on startup, remove the previous reservation before registering a fresh one, so it self-heals across restarts. (A force-killed dev build can still leak against a separately-installed copy, since they keep separate state — recoverable with a one-line &lt;code&gt;SystemParametersInfo&lt;/code&gt; work-area reset.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows hides CPU temperature from normal apps.&lt;/strong&gt; &lt;code&gt;systeminformation&lt;/code&gt; simply can't read CPU package temp on my box — the ACPI sensor returns null. The honest answer is that you need a kernel-level sensor driver, so ClawMonitor's &lt;code&gt;sensors&lt;/code&gt; collector reads from &lt;strong&gt;LibreHardwareMonitor&lt;/strong&gt;: if you run LHM with its remote web server on, the collector fetches &lt;code&gt;localhost:8085/data.json&lt;/code&gt; and parses out the CPU core temp. If LHM isn't running, the field just hides and everything else works. GPU temperature is the easy case — &lt;code&gt;systeminformation&lt;/code&gt; returns it out of the box (it shells out to nvidia-smi internally), no extra driver needed. I'd rather show "no CPU temp" honestly than fake a number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dev-stack tile is the whole reason it exists.&lt;/strong&gt; A normal monitor tells you the box is busy. It can't tell you &lt;em&gt;why&lt;/em&gt; — that your local gateway died, or that WSL's &lt;code&gt;vmmem&lt;/code&gt; is the thing eating your RAM. The &lt;code&gt;stack&lt;/code&gt; collector probes each service its own way: an HTTP &lt;code&gt;GET&lt;/code&gt; to the gateway and to Ollama, &lt;code&gt;docker ps&lt;/code&gt; for Docker, and a &lt;code&gt;tasklist&lt;/code&gt; filter that sums the &lt;code&gt;vmmem&lt;/code&gt; / &lt;code&gt;vmmemWSL&lt;/code&gt; processes to show WSL's actual memory footprint. Four heterogeneous health checks, normalized into the same up/down shape the bar renders as dots. That's the differentiator — it watches the stack, not just the silicon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;v0.1.1 is built, tested, and installed. The bar renders live at the top of my primary monitor with stable CPU / RAM / GPU+temp / network / disk plus the WSL / gateway / Docker / Ollama dots; the hover panel slides down on cursor-tracking; alerts pulse on redline; the AppBar reserves space and self-heals across restarts. The suite is 35 unit tests across the collectors, the merge/timeout orchestration, the alert logic, and config. It's public on &lt;a href="https://github.com/denrod25-del/ClawMonitor" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; under MIT with a landing page, contributor docs, and an unsigned NSIS installer in Releases.&lt;/p&gt;

&lt;p&gt;The honest caveats: the installer isn't code-signed yet, so SmartScreen warns on first run (signing is a to-do). It's Windows-only — the Electron + &lt;code&gt;systeminformation&lt;/code&gt; foundation is cross-platform, but the AppBar and the WSL tile need platform-specific work, so a Linux port is realistic but unstarted. And v1 is read-only: it shows you the stack, it doesn't yet let you start or stop a service from the bar. That, plus history graphs, is the v1.1 wishlist.&lt;/p&gt;

&lt;p&gt;This is part of an ongoing series on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>electron</category>
      <category>windows</category>
      <category>monitoring</category>
      <category>ai</category>
    </item>
    <item>
      <title>Thirty-Two Projects, One AI Pair Programmer</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Sun, 14 Jun 2026 12:33:00 +0000</pubDate>
      <link>https://dev.to/bsymbolic/thirty-two-projects-one-ai-pair-programmer-2hi5</link>
      <guid>https://dev.to/bsymbolic/thirty-two-projects-one-ai-pair-programmer-2hi5</guid>
      <description>&lt;p&gt;This blog has more than thirty write-ups on it now, and they have almost nothing in common. There are games built in two different engines, a 2,700-year-old aqueduct reconstructed in Blender with a real water simulation, a working search engine, a stack of AI tools that run entirely offline on a single graphics card, and a tool whose entire job is to tell me what's hogging a port. The one thread connecting them is how they were made: each one was built pair-programming with Claude, and each one was built the same way.&lt;/p&gt;

&lt;p&gt;That sameness is the actual story. "AI wrote some code" is not interesting in 2026. What turned out to be interesting — to me, at least — is that a repeatable &lt;em&gt;method&lt;/em&gt; let one hobbyist with limited evenings ship across domains I have no business being competent in at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in here
&lt;/h2&gt;

&lt;p&gt;The range is the point, so a quick tour. On the games side there's &lt;a href="https://dev.to/blog/lava-leap/"&gt;Lava Leap&lt;/a&gt;, an endless climber whose levels are mathematically guaranteed to be beatable, plus a faithful &lt;a href="https://dev.to/blog/space-invaders/"&gt;Space Invaders&lt;/a&gt; clone and a couple of in-progress prototypes. On the 3D side, &lt;a href="https://dev.to/blog/nineveh-aqueduct/"&gt;the Nineveh aqueduct&lt;/a&gt; and a &lt;a href="https://dev.to/blog/atlantis/"&gt;parametric Atlantis&lt;/a&gt; built from a single script. There's a &lt;a href="https://dev.to/blog/book-library/"&gt;local RAG over my own library of books&lt;/a&gt; that answers with citations and never touches the cloud, and several other &lt;a href="https://dev.to/blog/pdf-to-podcast/"&gt;fully-local AI stacks&lt;/a&gt; running on an 8 GB card. There are build-from-source write-ups, desktop tools, a pile of Python data utilities, and &lt;a href="https://dev.to/blog/symbolic/"&gt;a search engine&lt;/a&gt;. The &lt;a href="https://dev.to/projects/"&gt;full list lives here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I am not an expert in most of those areas. I've never shipped a Lua resource, I'm not a graphics programmer, and I'd never compiled a large C++ application from source before this year. The work still got done, and it got done because of the process, not because I suddenly knew all of those things.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually worked
&lt;/h2&gt;

&lt;p&gt;Every project followed the same loop, and the loop is boring on purpose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brainstorm, then write it down.&lt;/strong&gt; Before any code, a back-and-forth to pin down what the thing actually is and what "done" means — ending in a short written spec. This is the step it's most tempting to skip and the one that saved the most time. A vague idea produces vague code; a spec produces a plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan in small, testable pieces.&lt;/strong&gt; The spec becomes a numbered plan: discrete tasks, each one small enough to verify on its own. For a game that might be twenty-eight tasks across eight milestones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build test-first, and verify by running the real thing.&lt;/strong&gt; Claude wrote the great majority of the code, task by task, with tests written before the implementation. Crucially, "it compiles" was never the bar — the bar was watching the actual game run, the actual render come out, the actual MP4 play back with sound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review before moving on.&lt;/strong&gt; Each chunk got looked at with fresh eyes before the next one started.&lt;/p&gt;

&lt;p&gt;The division of labor was consistent across all thirty-two: I decided &lt;em&gt;what&lt;/em&gt; to build and &lt;em&gt;how it should behave&lt;/em&gt; and made the judgment calls; Claude wrote most of the code and did the tireless parts. That split is the whole trick. The model is extraordinary at breadth, at boilerplate, at patiently following a test-driven loop, and at the grind of compiling someone else's C++ project and decoding the error on line 4,000. It is not the one deciding whether the idea is any good, whether the scope is sane, or whether a plausible-looking answer is actually correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked, and what didn't
&lt;/h2&gt;

&lt;p&gt;The honest version, because a post about building with AI that's all upside isn't worth reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked better than I expected.&lt;/strong&gt; Breadth, mostly. I could move from a Phaser game to a Blender fluid sim to a Postgres-backed web app in the same week without paying the usual "I've never done this" tax up front. Build-from-source debugging in particular — the kind of work that's pure friction, where you're three unfamiliar error messages deep in a toolchain you didn't choose — went from "lose a weekend" to "lose an afternoon." And test-driven development, which I am too lazy to do consistently on my own, happens naturally when it's baked into the loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where I had to stay in the driver's seat.&lt;/strong&gt; Taste and scope never delegated well — left alone, the work will happily expand to fill any amount of effort, and someone has to keep saying "that's out of scope" and "ship the small version." And accuracy needs a human or a second pass. When I had the write-ups for this very blog fact-checked against the source code, that check caught real errors in nearly every post: a wrong test count, a config flag I'd described backwards, a credit attributed to the wrong entity, a render described as "photoreal" when it plainly wasn't. Plausible-but-wrong is the failure mode to watch, and it doesn't announce itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blog is part of the experiment
&lt;/h2&gt;

&lt;p&gt;This site was built with the same loop — brainstormed, spec'd, planned, built test-first. It went a step further: there's a small agent that drafts the posts. Point it at a project and it reads that project's notes, its repo, and its commit history, then writes a first draft in the house style. Every draft was then fact-checked against the source before anything was committed, which is the only reason I trust the numbers in these write-ups.&lt;/p&gt;

&lt;p&gt;I mention that not as a flex but because it's the same lesson one more time: the agent doesn't replace the judgment, it removes the friction. It gets a draft onto the page so I can do the part that needs a person — deciding whether it's true and whether it's any good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;If you want the most representative three, I'd read &lt;a href="https://dev.to/blog/lava-leap/"&gt;Lava Leap&lt;/a&gt; for the games, &lt;a href="https://dev.to/blog/nineveh-aqueduct/"&gt;the Nineveh aqueduct&lt;/a&gt; for the 3D work, and &lt;a href="https://dev.to/blog/book-library/"&gt;the local book-library RAG&lt;/a&gt; for the offline-AI stuff. Otherwise the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt; has all of them, and more are going up over the coming weeks.&lt;/p&gt;

&lt;p&gt;None of this required being an expert in thirty-two things. It required a method, a willingness to write the spec before the code, and the discipline to check the work afterward. That's the part worth stealing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>building</category>
      <category>meta</category>
    </item>
    <item>
      <title>The Nineveh Aqueduct: Rebuilding a 2,700-Year-Old Stone Bridge in Blender</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Sun, 14 Jun 2026 12:32:28 +0000</pubDate>
      <link>https://dev.to/bsymbolic/the-nineveh-aqueduct-rebuilding-a-2700-year-old-stone-bridge-in-blender-1icn</link>
      <guid>https://dev.to/bsymbolic/the-nineveh-aqueduct-rebuilding-a-2700-year-old-stone-bridge-in-blender-1icn</guid>
      <description>&lt;p&gt;Around 690 BCE, the Assyrian king Sennacherib built a roughly 50-kilometre canal to carry water from the hills at Khinis down to his capital at Nineveh. Where the canal had to cross a valley near the modern village of Jerwan, his engineers raised a stone aqueduct-bridge on corbelled pointed arches — centuries before Rome figured out the true voussoir arch. I rebuilt a stretch of that bridge in Blender, with Claude driving the modeling and the lighting through the Blender MCP, and then I made the water in the channel an actual fluid simulation rather than a painted-on texture.&lt;/p&gt;

&lt;p&gt;This is a post about doing that in a single session, what made the final warm, hazy look come together, and the one bug that crashed Blender hard enough to cost me forty minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbd2io8u97hrgpwmtyny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbd2io8u97hrgpwmtyny.png" alt="Wide view of the reconstructed Jerwan aqueduct — four corbelled pointed stone arches carrying a channel-topped deck, in warm hazy light" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The historical Jerwan aqueduct is one of the oldest known monumental aqueducts in the world. It was excavated and published by Thorkild Jacobsen and Seton Lloyd in 1935, and what they documented is what I worked from: a long stone bridge carrying a water channel across a wadi, built from limestone blocks, with the canal running along the top of the deck. The arches are &lt;em&gt;corbelled&lt;/em&gt; — built by stepping successive courses of stone inward until they meet — which is what gives them their pointed profile. Sennacherib's masons did this roughly four hundred years before Roman engineers standardized the rounded voussoir arch, so the structure is a genuinely pre-Roman solution to the same problem.&lt;/p&gt;

&lt;p&gt;I didn't try to rebuild all 280 metres of the bridge. I modeled about 80 metres of it — four of the five corbelled arches — at archaeologically plausible proportions: a deck around 22 metres wide, a water channel roughly 3 metres across cut into the top of that deck, a stilling basin upstream, and inscription stones at the abutments (Sennacherib left inscriptions on the real thing). The whole scene came to 35 objects. Where I wasn't sure of an exact dimension I kept the proportions general and grounded in the published description rather than inventing precise numbers the excavation doesn't support.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The split of work was the usual one for these sessions: I decided what to build and judged whether each render looked right, and Claude wrote the Blender Python that actually constructed it, calling &lt;code&gt;execute_blender_code&lt;/code&gt; through the MCP to build geometry, set up materials, place the camera, and render. I was looking at real rendered frames the whole way, not a description of what the scene "should" look like.&lt;/p&gt;

&lt;p&gt;The structure itself is precision modeling — exact dimensions typed in, not eyeballed — with the arches cut into the stone mass using boolean modifiers. The limestone is a procedural shader: a Voronoi texture at object-coordinate scale draws the block joints, tightened down to thin mortar lines so the courses read as stacked masonry rather than noise. Lighting is Cycles on the GPU with the AgX view transform at medium-high contrast, a warm sun placed low (azimuth around 115°, elevation around 18°) to rake across the arches and throw long shadows, and a gradient sky for the warm hazy desert ambient.&lt;/p&gt;

&lt;p&gt;The part I cared most about was the water. It would have been easy to drop a flat plane with a glassy shader into the channel and call it water. Instead the channel water is a &lt;strong&gt;real Mantaflow fluid simulation&lt;/strong&gt;: a domain over the channel, an inflow at the upstream end pushing water through at a set velocity, the stone acting as a collision effector, and the domain set to output a render-ready mesh surface. At a resolution of 128 over that small domain, 180 frames baked in about 17 seconds on my RTX 3070. The domain mesh gets a glass Principled BSDF plus a touch of volume absorption for the warm, slightly silty tint you'd expect from canal water — the simulation replaces the mesh at render time, so what you see flowing in the channel is the solved surface, not a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Changing a Mantaflow parameter mid-bake crashes Blender outright.&lt;/strong&gt; This is the one that cost real time. If you set up a fluid domain, start stepping frames to bake the cache, and then change something like &lt;code&gt;resolution_max&lt;/code&gt; while that's in progress, Blender dies with an &lt;code&gt;EXCEPTION_ACCESS_VIOLATION&lt;/code&gt; — a freed mesh runtime being destructed inside the dependency graph (&lt;code&gt;MeshRuntime::~MeshRuntime&lt;/code&gt;, for the curious). The fix is a discipline rule, not a code change: set &lt;strong&gt;all&lt;/strong&gt; simulation parameters — resolution, inflow velocity, the full frame range — &lt;em&gt;before&lt;/em&gt; you touch &lt;code&gt;frame_set&lt;/code&gt; or start the bake, and never change them once stepping has begun. Save the &lt;code&gt;.blend&lt;/code&gt; before baking, too. After I adopted that, the rebake was clean every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enum names quietly moved in the 5.x line.&lt;/strong&gt; Two of these bit me. The boolean solver enum I reached for from memory — the old &lt;code&gt;FAST&lt;/code&gt; solver — was gone, replaced rather than renamed: the options now are &lt;code&gt;FLOAT&lt;/code&gt;, &lt;code&gt;EXACT&lt;/code&gt;, and &lt;code&gt;MANIFOLD&lt;/code&gt;, and &lt;code&gt;EXACT&lt;/code&gt; is the one that gives you crisp architectural cuts in the stone. Separately, the sky-texture method I reached for had a renamed multi-scatter enum, so rather than fight it I just used a gradient world for the sky, which gave the warm haze I wanted anyway. The lesson both times: probe what the enum actually accepts on &lt;em&gt;this&lt;/em&gt; build instead of assuming the value from memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Camera composition mattered more than simulation quality.&lt;/strong&gt; A 3-metre channel of water sitting between 1-metre stone parapets is genuinely hard to &lt;em&gt;see&lt;/em&gt; — from most angles the parapets hide it completely, and a beautifully simulated surface you can't see is wasted compute. The shots that worked put the camera down at deck level, looking back upstream along the channel, so the water reads as a bright ribbon running into the haze. That framing decision did more for the final image than any bump in solver resolution would have.&lt;/p&gt;

&lt;p&gt;And one workflow rule I re-learned the hard way: &lt;strong&gt;save after every major build chunk, not just once at the start.&lt;/strong&gt; I'd saved the file right after creating the empty scene and then built for a while before the crash above — so the crash took the lot. The rebuild went smoothly precisely because I saved after each block of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0xm0x130tplgmhn0x62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0xm0x130tplgmhn0x62.png" alt="Deck-level view looking upstream along the water channel, the simulated water visible as a bright ribbon between the stone parapets, palms and a lone figure in the warm haze" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two hero stills came out of the session. One is the wide three-quarter view of the bridge with its four corbelled arches in the raking warm light, palms and small figures giving it scale. The other is the deck-level shot looking back upstream, where you can actually see the Mantaflow water running down the channel — the framing that finally made the simulation visible. Both went straight to my iPad for a proper look.&lt;/p&gt;

&lt;p&gt;What I like about it is that the water is &lt;em&gt;earned&lt;/em&gt;. It isn't a texture that happens to look wet; it's a solved fluid surface flowing through a structure built to real proportions from a 1935 excavation report. For a single session of pair-modeling with Claude steering Blender, getting a believable, ~2,700-year-old, pre-Roman stone aqueduct with genuinely simulated water out the other end felt like a fair trade for one hard crash and forty lost minutes.&lt;/p&gt;

&lt;p&gt;This is one of a series of posts on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blender</category>
      <category>3d</category>
      <category>archaeology</category>
      <category>simulation</category>
    </item>
    <item>
      <title>Book Library: A Local RAG That Answers From My Own PDFs</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Sun, 14 Jun 2026 12:23:56 +0000</pubDate>
      <link>https://dev.to/bsymbolic/book-library-a-local-rag-that-answers-from-my-own-pdfs-2h4</link>
      <guid>https://dev.to/bsymbolic/book-library-a-local-rag-that-answers-from-my-own-pdfs-2h4</guid>
      <description>&lt;p&gt;I have a folder full of PDF books I downloaded — a couple hundred of them, mostly programming and technical references. The problem with a couple hundred PDFs is that they're write-only memory: I know the answer is in one of them, I just have no idea which one or what page. Book Library is the fix. It's a chat box in my browser where I ask a question, and it answers using only the text of my books, with citation chips that open the source PDF right at the page it pulled the answer from. No cloud, no API keys, nothing leaves the machine. I built it with Claude as a pair programmer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;It's a retrieval-augmented-generation app over a personal PDF library. You type a question, it finds the most relevant passages across every indexed book, hands those passages to a local language model, and the model writes an answer grounded in them. Every claim comes with a &lt;code&gt;[Title, p.N]&lt;/code&gt; citation, and in the UI those render as little chips you can click to jump straight to the cited page in the original PDF.&lt;/p&gt;

&lt;p&gt;The whole thing runs offline on my RTX 3070. Ollama serves both models on the GPU: &lt;code&gt;qwen2.5:7b-instruct&lt;/code&gt; writes the answers and &lt;code&gt;nomic-embed-text&lt;/code&gt; turns text into the vectors used for search. PyMuPDF extracts the text, ChromaDB stores the embeddings, and a small FastAPI backend ties it together behind a vanilla-JS chat page. The reason I cared about "local" wasn't privacy theater — it's that I wanted to point it at a folder of copyrighted books I own and not ship their contents to anyone's server, and I wanted it to keep working with no subscription and no rate limits.&lt;/p&gt;

&lt;p&gt;The grounding is the point. The system prompt is blunt about it: answer using &lt;em&gt;only&lt;/em&gt; the provided excerpts, cite every claim, and if the excerpts don't contain the answer, say you couldn't find it in the library — do not use outside knowledge. That last instruction is what separates this from just asking a chatbot. A chatbot will confidently fill gaps; this thing is supposed to tell me when my books don't cover something.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The pipeline is the usual RAG shape — ingest, chunk, embed, store, retrieve, cite — but each stage had a small decision that mattered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingest&lt;/strong&gt; walks the PDF folder and pulls text page by page with PyMuPDF. A page that fails to parse yields an empty string instead of killing the whole document, and a book with no extractable text at all (a scanned image with no text layer) is logged and skipped rather than crashing the run. Ingestion is resumable: it writes a JSON manifest after every file, so re-running it only processes books it hasn't seen. That mattered a lot, because indexing the whole library is a 30-to-60-minute job and I add books to the folder over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunking&lt;/strong&gt; is deliberately page-aware. Chunks never span a page boundary, which means every chunk carries an exact page number — and that's what makes the citations trustworthy. Text is sliced into ~1800-character windows with 300 characters of overlap so a sentence split across a boundary still survives in one piece somewhere. 1800 characters is roughly 450 tokens, kept conservative on purpose (more on why in the gotchas).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embed and store&lt;/strong&gt;: each chunk goes through &lt;code&gt;nomic-embed-text&lt;/code&gt; and lands in a ChromaDB collection configured for cosine distance, with the filename, human-readable title, and page number as metadata. Chunk IDs are a SHA-1 of &lt;code&gt;filename::page::index&lt;/code&gt; so re-ingesting is idempotent. Embeddings are added in batches of 64 to keep memory flat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieve and cite&lt;/strong&gt;: a question gets embedded the same way, Chroma returns the top 6 nearest chunks, and anything past a cosine-distance threshold is dropped as "not actually a match." The surviving excerpts are formatted into the prompt with their &lt;code&gt;[Title, p.N]&lt;/code&gt; labels, the model streams an answer back token by token, and a deduplicated citation list rides along in the response. The browser shows the answer as it streams and renders the citations as clickable chips. The entire UI is one static HTML file with a &lt;code&gt;fetch&lt;/code&gt; and a stream reader — no framework.&lt;/p&gt;

&lt;p&gt;The division of labor was the same as my other projects: I decided what it should do and made the design calls, Claude wrote essentially all the code, and I drove it through a brainstorm-then-spec-then-plan flow before any implementation. Both the design doc and the task plan live in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Three real ones, each of which cost actual debugging time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The embedder rejects overlong chunks, and that decided my chunk size.&lt;/strong&gt; &lt;code&gt;nomic-embed-text&lt;/code&gt; has its own context window, and feeding it a chunk that's too long fails with "input length exceeds context length." Dense pages — code listings, tables, CJK text — pack far more into 1800 characters than prose does, so a naive larger chunk size blew up on exactly the technical books I most wanted indexed. The fix was two-part: keep chunks conservative at 1800 chars, and make a single rejected chunk a skip-with-a-note instead of a fatal error. One bad chunk shouldn't lose the whole book.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browsers download PDFs instead of honoring &lt;code&gt;#page=N&lt;/code&gt;.&lt;/strong&gt; The citation chips link to &lt;code&gt;/pdf/&amp;lt;file&amp;gt;#page=42&lt;/code&gt;, expecting the browser's built-in viewer to open at page 42. Instead Chrome kept &lt;em&gt;downloading&lt;/em&gt; the file and ignoring the fragment entirely. The cause: by default the server sent the PDF with a &lt;code&gt;Content-Disposition: attachment&lt;/code&gt; disposition, which tells the browser "save this," not "view this." Serving it with &lt;code&gt;content_disposition_type="inline"&lt;/code&gt; flips it to "open in the viewer," and only then does &lt;code&gt;#page=N&lt;/code&gt; actually jump to the page. A citation you can't click to is worthless, so this was load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LLM context window has to hold all the excerpts at once.&lt;/strong&gt; With six retrieved chunks plus the system prompt plus the question, the default Ollama context was too small and answers would quietly get truncated or lose the earlier excerpts. Setting &lt;code&gt;num_ctx=8192&lt;/code&gt; explicitly gives the model enough room to actually see all six passages it's supposed to be reasoning over. It's an easy thing to forget because nothing errors — the model just silently gets less context than you think it has.&lt;/p&gt;

&lt;p&gt;(A fourth, minor one: ChromaDB's telemetry prints harmless &lt;code&gt;capture()&lt;/code&gt; errors to the console even with telemetry disabled. Cosmetic, but worth knowing it's not your bug.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What works today
&lt;/h2&gt;

&lt;p&gt;It works, and the index has outgrown its first numbers. The live ChromaDB collection now holds roughly 250 books and about 78,000 chunks — up from the 163 books and ~52,000 chunks of the first working version, because the whole point of resumable ingest is that I keep dropping new downloads into the folder and re-running it. The books with no text layer are still skipped; OCR for those scanned PDFs is on the list but not done.&lt;/p&gt;

&lt;p&gt;In the browser I can ask a real question, watch the answer stream in, and click a citation chip to land on the exact page in the source PDF. The answers stay inside the library — when my books genuinely don't cover something, it tells me instead of inventing an answer, which is the behavior I actually wanted.&lt;/p&gt;

&lt;p&gt;It's not perfect. There's no chat memory yet — each question is answered independently — and a few stubborn PDFs don't always honor the page jump. Those are the known edges. But for the thing I built it to do — turning a write-only pile of PDFs into something I can actually ask — it delivers, and it does it entirely on my own GPU with nothing leaving the machine.&lt;/p&gt;

&lt;p&gt;This is one post in a series on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>python</category>
      <category>localllm</category>
      <category>gpu</category>
    </item>
    <item>
      <title>Symbolic: I Built a Search Engine (and Named This Blog After It)</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Sun, 14 Jun 2026 12:23:45 +0000</pubDate>
      <link>https://dev.to/bsymbolic/symbolic-i-built-a-search-engine-and-named-this-blog-after-it-4npi</link>
      <guid>https://dev.to/bsymbolic/symbolic-i-built-a-search-engine-and-named-this-blog-after-it-4npi</guid>
      <description>&lt;p&gt;This blog is called iSymbolic, and that name is not an accident. Symbolic is my search engine — a real, working web search product with its own results page, settings, an advertiser portal, and an admin panel — and it's the project I'm proudest of. The blog is the place I write about what I build; Symbolic is the thing I built that mattered enough to lend its name. There's also a companion browser app that puts Symbolic where searching actually starts. I built it all with Claude as a pair programmer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Symbolic is a web search engine. You land on a clean page — a NASA Earth photo behind a single search box, the tagline "Search without compromise" — you type a query, and you get a page of web results. There's an "I'm Feeling Lucky" button that jumps you straight to the top result, a settings page where you choose your SafeSearch level (strict, moderate, or off, saved to a cookie so it sticks), and paginated results so you can go deeper than the first ten. It's the search experience you already know, rebuilt from the ground up as something I own end to end.&lt;/p&gt;

&lt;p&gt;Under the hood, the actual web index comes from the &lt;a href="https://brave.com/search/api/" rel="noopener noreferrer"&gt;Brave Search API&lt;/a&gt;. I made a deliberate decision early: I was not going to crawl and index the entire web from a home server with an RTX 3070. That's not a weekend project, it's a career. Instead, Symbolic is an independent front end and product layer over Brave's index — my own ranking surface, my own UI, my own ad system, my own policies — sourcing the raw web results from an API built for exactly this. Credit where it's due: Brave does the crawling; Symbolic is everything around it.&lt;/p&gt;

&lt;p&gt;The part that makes it a &lt;em&gt;product&lt;/em&gt; and not just a search box is the advertiser side. Symbolic has a self-serve advertiser portal where a business can sign up, create a text ad with keywords and a bid, and manage their campaigns. Ads are keyword-matched against the search query and shown above and below the organic results. Behind that sits an admin moderation panel: ads don't go live automatically — they sit in a review queue with an approval status, and only approved, active ads ever get served. I'll keep the internals of that panel deliberately vague here, but the shape of it is a real two-sided system: searchers on one side, advertisers on the other, and a human approval gate in between.&lt;/p&gt;

&lt;p&gt;And then there's the companion browser app — the piece that closes the loop. A search engine you have to navigate to first is a search engine you mostly forget to use. The browser-app companion makes Symbolic the place your searches begin, so it's woven into how I actually browse rather than being a tab I have to remember to open. It's a companion to the web product described here, and I'll give it its own full write-up once it's further along — I'd rather under-promise it here than overstate what's shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The stack is modern and intentionally mainstream: &lt;strong&gt;Next.js 16&lt;/strong&gt; with the App Router, &lt;strong&gt;React 19&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode, and &lt;strong&gt;Tailwind CSS v4&lt;/strong&gt;. Data lives in &lt;strong&gt;PostgreSQL&lt;/strong&gt; through &lt;strong&gt;Drizzle ORM&lt;/strong&gt; — with &lt;a href="https://pglite.dev/" rel="noopener noreferrer"&gt;PGlite&lt;/a&gt; running an in-process Postgres locally so I can develop and run migrations without standing up a database server. Authentication for the advertiser portal is handled by &lt;a href="https://clerk.com" rel="noopener noreferrer"&gt;Clerk&lt;/a&gt;, the whole thing is internationalized with &lt;a href="https://next-intl.dev/" rel="noopener noreferrer"&gt;next-intl&lt;/a&gt; (no user-facing string is hard-coded — they all flow through translation namespaces), and &lt;a href="https://arcjet.com/" rel="noopener noreferrer"&gt;Arcjet&lt;/a&gt; provides bot and abuse protection at the edge.&lt;/p&gt;

&lt;p&gt;I didn't start from a blank &lt;code&gt;create-next-app&lt;/code&gt;. Symbolic is built on top of &lt;a href="https://github.com/ixartz/Next-js-Boilerplate" rel="noopener noreferrer"&gt;ixartz's Next.js Boilerplate&lt;/a&gt;, which gave me the entire developer-experience scaffold for free: the linting setup, the testing harness (Vitest for units, Playwright for end-to-end), the i18n wiring, the database tooling, and a sane project structure. That let me spend my time on Symbolic's actual product instead of on plumbing.&lt;/p&gt;

&lt;p&gt;The division of labor was the same one I use on every project: I decided what Symbolic should be and how it should behave, and Claude wrote most of the code. The interesting thing about this build is how disciplined it was. Each major feature went through a written design spec, then an implementation plan, then test-driven execution — there are spec-and-plan documents in the repo for the ad display system, advertiser authentication, ad management, and the admin panel, each one dated, each one preceding the code it describes. The search core itself is small and honest: a typed client that calls the Brave API, normalizes the response into my own &lt;code&gt;SearchResult&lt;/code&gt; shape, and hands it to React Server Components that render the page. Ranking of organic results is Brave's; what I built on top is the ad-selection logic — tokenize the query, find approved active ads whose keywords overlap, order by bid, cap at two slots — and the surrounding product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Three real ones, each grounded in code that's in the repo today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pagination is by offset, not by row.&lt;/strong&gt; My first cut at "next page" did what felt natural — skip N rows and grab the next ten. But the Brave API doesn't page by row offset — its &lt;code&gt;offset&lt;/code&gt; parameter is a &lt;em&gt;page index&lt;/em&gt; (0–9). Asking it to skip 10 rows is not the same as asking it for page 2, and the mismatch produced duplicated and skipped results across page boundaries that looked almost-right, which is the worst kind of wrong. The fix was to stop thinking in row offsets and translate the UI's notion of position into the API's page index before the call goes out. The commit that fixed it is literally titled "paginate search by page index, not row offset, to match Brave API," because that's exactly the lesson: page the way your data source pages, not the way your intuition pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An advertiser's real destination URL must never be the thing your ranking logic touches.&lt;/strong&gt; Ads carry a real click-through URL, but the ad's title and call-to-action don't link there directly. They link to an internal click-tracking route that records the click and then issues a redirect to the real destination. This keeps the raw destination URL out of the rendered result surface and gives me a single, auditable choke point for every ad click — which matters for billing and for catching abuse. It's a small architectural decision (one redirect route) with outsized payoff, and it's the kind of thing that's painful to retrofit, so it went in early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Array overlap in Postgres doesn't come for free in a query builder.&lt;/strong&gt; Ad keywords are stored as a Postgres text array, and matching them against the query tokens is exactly what Postgres's &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; array-overlap operator is for. But Drizzle's typed query builder doesn't expose &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, so I had to drop to a raw SQL fragment — and even then, PGlite needed an explicit &lt;code&gt;::text[]&lt;/code&gt; cast to resolve the operator overload correctly. It's a two-line escape hatch, but it's the kind of thing that compiles, type-checks, and then silently returns nothing until you figure out the cast. The comment explaining why the cast is there is, in this case, more valuable than the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's at
&lt;/h2&gt;

&lt;p&gt;Symbolic is live and real: a working search front end over the Brave index, SafeSearch settings, "I'm Feeling Lucky," a self-serve advertiser portal with sign-up and campaign management, a click-tracked ad system, and an admin moderation queue — all on Next.js 16, React 19, Drizzle, and Postgres, built test-first on top of a battle-tested boilerplate. I'm honest about the scope: the web results are Brave's, not a from-scratch crawler, and that's a feature, not an apology — it let one person ship a genuinely usable search product instead of an unfinished crawler.&lt;/p&gt;

&lt;p&gt;The companion browser app is the next chapter and deserves its own post rather than a paragraph buried at the bottom of this one. When it's ready, I'll write it up the same way I wrote this — what it is, how it was built, and the gotchas I hit on the way.&lt;/p&gt;

&lt;p&gt;This is another entry in the series on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>search</category>
      <category>typescript</category>
      <category>web</category>
    </item>
    <item>
      <title>Lava Leap: Shipping an Endless Climber with an AI Pair Programmer</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Sat, 13 Jun 2026 00:12:38 +0000</pubDate>
      <link>https://dev.to/bsymbolic/lava-leap-shipping-an-endless-climber-with-an-ai-pair-programmer-30f3</link>
      <guid>https://dev.to/bsymbolic/lava-leap-shipping-an-endless-climber-with-an-ai-pair-programmer-30f3</guid>
      <description>&lt;p&gt;Lava Leap is an endless vertical climber: you scale procedurally generated platforms while lava rises from below, and your score is your height plus the coins you grab on the way up. I built it with Claude as a pair programmer over two release cycles, and it's now public on &lt;a href="https://github.com/denrod25-del/lava-leap" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; at v0.2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;The core loop is simple to describe and hard to put down. You run, jump, double jump, wall slide, wall jump, and air dash your way up a tower of platforms that never ends. Some platforms crumble under your feet, some move, and the lava below accelerates the longer you survive. Die, see your score, press Space, go again. Your best run persists in localStorage.&lt;/p&gt;

&lt;p&gt;Around that loop, version 0.2.0 added the things that make a small game feel like a finished one: named zones with their own palettes and lava pacing (the Volcanic Throat's sub-zones turn over roughly every 1000 height units — Magma Vault at 0, The Forge at 1000, Ashfall at 2000, and Obsidian Crown at 3000 — each raising lava speed and biasing toward harder platform types), hand-authored set-piece chunks injected into the random stream, achievements, a daily challenge seed, a cosmetics shop that spends banked coins, procedural chiptune music, eight synthesized sound effects, pause and settings menus, and a crash-recovery overlay so an unhandled error never strands you on a blank canvas.&lt;/p&gt;

&lt;p&gt;The stack is Phaser 3, TypeScript, and Vite, with Vitest for unit tests and Playwright for end-to-end smoke tests. The pixel art player and tiles were generated with PixelLab. Clone the &lt;a href="https://github.com/denrod25-del/lava-leap" rel="noopener noreferrer"&gt;repo&lt;/a&gt; and run &lt;code&gt;npm run dev&lt;/code&gt; to see it move — a game about momentum is better experienced than screenshotted.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The division of labor was consistent: I decided what the game should be, Claude wrote nearly all of the code. We started with a brainstorming session that produced a design spec, then a plan that broke v1 into 28 test-driven tasks across 8 milestones. Claude implemented each milestone with separate reviewer passes, and I played the build live before signing off on each one. v1 shipped in 31 commits; v2 repeated the process with 9 more milestones.&lt;/p&gt;

&lt;p&gt;The architectural decision I care most about is &lt;strong&gt;parametric reachability&lt;/strong&gt;: every generated level is provably climbable. The generator doesn't place platforms randomly and hope. It clamps every next platform inside the player's actual movement envelope — jump height, double-jump extension, dash distance — using reach-budget constants that live in one tuning file alongside the physics values they're derived from. Difficulty scaling raises the lava speed and biases toward crumbling and moving platforms as you climb, but it never places a gap the movement system can't cross. The generator alone has 12 unit tests asserting this. When v2 added hand-authored set-piece chunks, they had to pass a &lt;code&gt;validateChunk&lt;/code&gt; gate that rejects any template exceeding the reach widths — and, after a reviewer pass caught it, any template that puts coins on crumbling platforms, which created sucker bets the player couldn't win.&lt;/p&gt;

&lt;p&gt;The second load-bearing piece is a typed &lt;strong&gt;event spine&lt;/strong&gt;: a small, framework-free event emitter in &lt;code&gt;src/core/events.ts&lt;/code&gt;. Gameplay code emits events (platform landed, coin collected, death) and everything else subscribes — achievements, run analytics, the audio director, score popups. That's what made v2's feature pile tractable: the achievements system never touches the player class. One naming landmine: the field on the game scene is &lt;code&gt;gameEvents&lt;/code&gt;, because &lt;code&gt;events&lt;/code&gt; already exists on &lt;code&gt;Phaser.Scene&lt;/code&gt; and shadowing it breaks the scene lifecycle.&lt;/p&gt;

&lt;p&gt;The juice pass came last and mattered more than I expected: squash-and-stretch on landings, dust particles, screen shake, floating score popups, drifting embers, and a slow-motion beat on death. Even the audio is code — &lt;code&gt;tools/gen-music.mjs&lt;/code&gt; synthesizes the chiptune loops from scratch, so the repo contains no purchased third-party asset packs — sprites are PixelLab-generated and all audio is regenerable from the synthesis scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Three real ones, each of which cost real debugging time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A bare &lt;code&gt;tsc&lt;/code&gt; in the build script silently shadowed our sources.&lt;/strong&gt; The build ran &lt;code&gt;tsc &amp;amp;&amp;amp; vite build&lt;/code&gt;, and &lt;code&gt;tsc&lt;/code&gt; emitted compiled &lt;code&gt;.js&lt;/code&gt; files next to their &lt;code&gt;.ts&lt;/code&gt; sources. Vite resolves &lt;code&gt;.js&lt;/code&gt; before &lt;code&gt;.ts&lt;/code&gt;, so the dev server started serving stale compiled output while we edited the TypeScript — changes just stopped appearing. The fix is &lt;code&gt;"noEmit": true&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt; (the build only needs &lt;code&gt;tsc&lt;/code&gt; as a type-check; Vite does the bundling). The rule we took away: never let stray &lt;code&gt;.js&lt;/code&gt; files sit in &lt;code&gt;src/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't reliably screenshot a WebGL game, so verify behaviorally.&lt;/strong&gt; Phaser's canvas doesn't set &lt;code&gt;preserveDrawingBuffer&lt;/code&gt;, so screenshots taken between frames come back blank or flaky. Synthetic &lt;code&gt;KeyboardEvent&lt;/code&gt;s with &lt;code&gt;keyCode&lt;/code&gt; are ignored by the browser, so you can't fake input that way either. Our fix: in dev builds the game instance is exposed as &lt;code&gt;window.__game&lt;/code&gt;, and verification reads scene and physics state directly — player y-position, lava height, platform counts — instead of pixels. Input is driven by setting Phaser &lt;code&gt;Key.isDown&lt;/code&gt; flags and stepping the player update, or emitting &lt;code&gt;keydown-SPACE&lt;/code&gt; on &lt;code&gt;scene.input.keyboard&lt;/code&gt; for menu handlers. Related: a hidden browser tab stops &lt;code&gt;requestAnimationFrame&lt;/code&gt;, which freezes the whole game loop mid-test — you can pump frames manually with &lt;code&gt;game.step()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scene.start()&lt;/code&gt; stops the scene that calls it.&lt;/strong&gt; Opening Settings from the main menu used &lt;code&gt;scene.start('Settings')&lt;/code&gt;, which silently stopped the Menu scene. Backing out of Settings then revealed... nothing. A black screen, because the menu no longer existed. The fix is that Settings must explicitly &lt;code&gt;scene.start('Menu')&lt;/code&gt; on exit rather than assuming there's a live scene to return to. If you're using Phaser's scene manager for overlay-style screens, &lt;code&gt;launch&lt;/code&gt;/&lt;code&gt;pause&lt;/code&gt; semantics versus &lt;code&gt;start&lt;/code&gt; semantics will bite you exactly once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;v0.2.0 is live and public at &lt;a href="https://github.com/denrod25-del/lava-leap" rel="noopener noreferrer"&gt;github.com/denrod25-del/lava-leap&lt;/a&gt;: the full climb loop, four-stage zone progression, set-pieces, achievements, daily seeds, a shop, procedural music and SFX, pause/settings, and crash recovery. The test suite stands at 51 unit tests across 12 files plus 2 Playwright end-to-end tests, and every milestone in both releases was reviewed and verified live before merge.&lt;/p&gt;

&lt;p&gt;This is the first post in a series on projects built this way — there are about 27 more in the queue. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>phaser</category>
      <category>typescript</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
