<?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: Plastik Electrik</title>
    <description>The latest articles on DEV Community by Plastik Electrik (@plastikelectrik).</description>
    <link>https://dev.to/plastikelectrik</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%2F4140326%2Fe8437b33-eb89-4587-91f7-961e918a4cd9.png</url>
      <title>DEV Community: Plastik Electrik</title>
      <link>https://dev.to/plastikelectrik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/plastikelectrik"/>
    <language>en</language>
    <item>
      <title>AI 3D Character Creation — No GPU Required</title>
      <dc:creator>Plastik Electrik</dc:creator>
      <pubDate>Sun, 27 Sep 2026 22:53:29 +0000</pubDate>
      <link>https://dev.to/plastikelectrik/ai-3d-character-creation-no-gpu-required-44mj</link>
      <guid>https://dev.to/plastikelectrik/ai-3d-character-creation-no-gpu-required-44mj</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzp5s2rktorxgheelfow.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzp5s2rktorxgheelfow.png" alt=" " width="760" height="1133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four ways to get a human (or a goblin) into your game&lt;/strong&gt;, running entirely on a CPU-only VPS.&lt;/p&gt;

&lt;p&gt;A while back I wrote about &lt;strong&gt;CHARFORGE&lt;/strong&gt; and &lt;strong&gt;3D GAME ENGINE&lt;/strong&gt; as two apps. &lt;/p&gt;

&lt;p&gt;As of this week, that framing is a little outdated — they're one system now, and the interesting part isn't "generate a face," it's that there are now four completely different ways to get an actor into the game, and the engine doesn't care which one you used — and none of it touches a GPU.&lt;/p&gt;

&lt;p&gt;This post is about that unification, and about the theory + plumbing behind each of the four paths. &lt;/p&gt;

&lt;p&gt;All of it runs on a 6-vCPU, 11 GB RAM VPS. No CUDA, no rented GPU instance, no cloud inference bill. &lt;/p&gt;

&lt;p&gt;That constraint is the whole story, so it's worth saying up front instead of burying it in section 6.&lt;/p&gt;

&lt;p&gt;The core idea: an "actor" is an interface, not a mesh&lt;/p&gt;

&lt;p&gt;The single most important architectural shift in this update is that the game engine no longer thinks in terms of "CHARFORGE characters." &lt;/p&gt;

&lt;p&gt;It thinks in terms of actors, and an actor is anything that can answer a small set of questions: how fast do you move, how do you play an animation, how do you die, how do you aim, where do you look, where does a weapon attach to your hand.&lt;/p&gt;

&lt;p&gt;Once that interface exists, it stops mattering whether the thing behind it is:&lt;/p&gt;

&lt;p&gt;A hand-sculpted CHARFORGE human&lt;br&gt;
An imported GLB/VRM/FBX/OBJ model someone else made&lt;br&gt;
A procedurally grown creature (goblin, robot, alien, four-legged beast)&lt;br&gt;
A CPU-generated human built by Blender + MPFB2, or reconstructed from a single photo&lt;/p&gt;

&lt;p&gt;All four get baked once, rigged, retargeted if needed, and dropped into the shared character library, where the game engine treats them identically. &lt;/p&gt;

&lt;p&gt;That's the theoretical payoff of building around an interface instead of a data format: your roadmap stops being "support format X" and becomes "satisfy the actor contract," which is a much shorter list of problems.&lt;/p&gt;

&lt;p&gt;Let's walk through the four paths.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fws2s8it9zz81ha2uvjy1.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fws2s8it9zz81ha2uvjy1.jpg" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 1: Sculpt a human from morphs (the original CHARFORGE loop)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one I covered before, so briefly: a 19,158-vertex MakeHuman base mesh, 163 bones, deformed via morph weights for gender/age/muscle/weight/ancestry macros plus 136 extra body and facial targets. &lt;/p&gt;

&lt;p&gt;Engine.weights(P) turns a parameter object into a weight vector; the skeleton uses Euler ZXY rotations solved at each bone head:&lt;/p&gt;

&lt;p&gt;M = M_parent · T(h) · R · T(−h)&lt;/p&gt;

&lt;p&gt;All of this — sculpting, skinning, the shader that adds pores and freckles — runs in JavaScript in the browser's CPU. No GPU compute shaders, no WebGPU requirement, just WebGL for display.&lt;/p&gt;

&lt;p&gt;What's new here isn't the math, it's what happens after you finish sculpting: every character now lands in MySQL automatically, not just in browser localStorage. &lt;/p&gt;

&lt;p&gt;That single change removes an entire category of "oh no I cleared my cache" disasters, and it unlocked something more interesting — versioning. &lt;/p&gt;

&lt;p&gt;Every character keeps up to 15 historical versions (throttled to one new snapshot per 5 minutes), restorable, with a 30-day trash instead of instant deletion. &lt;/p&gt;

&lt;p&gt;A character generator turned into a character database, which is a very different product even though the sculpting code barely changed.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkk9xn8k9ys4n25oi3grl.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkk9xn8k9ys4n25oi3grl.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 2: Import someone else's model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the "stop reinventing everyone else's work" path. You can drop in:&lt;/p&gt;

&lt;p&gt;GLB / VRM files as-is — VRM keeps its own material pipeline (via @pixiv/three-vrm), because VRM materials are a whole ecosystem of their own and re-deriving them would be pointless.&lt;/p&gt;

&lt;p&gt;FBX / OBJ, converted to GLB either in-browser or, when that fails, server-side via Blender running headless on CPU.&lt;/p&gt;

&lt;p&gt;CC0 KayKit asset packs, added once per pack, contributing roughly 95 shared animation clips to the whole library.&lt;/p&gt;

&lt;p&gt;The theoretically interesting bit is rig detection and retargeting. Nobody agrees on bone names. &lt;/p&gt;

&lt;p&gt;KayKit, Mixamo, VRM/VRoid, Unreal/MPFB, MakeHuman, and raw Blender exports all name their skeletons differently, so the system does name-based humanoid bone detection across all of them, with a structural fallback (walking the hierarchy by shape rather than by label) for anything unrecognized. &lt;/p&gt;

&lt;p&gt;Once a rig is identified, KayKit's ~95 animation clips get baked once per model in world space, with bone directions aligned, so both T-pose and A-pose rigs animate correctly without per-model manual retargeting.&lt;/p&gt;

&lt;p&gt;There's a small, very "we hit this in production" fix worth mentioning: non-VRM imports were rendering semi-transparent because of how their materials were flagged. &lt;/p&gt;

&lt;p&gt;The fix was blunt and effective — force solid materials on anything non-VRM, with alpha-test at 0.5 for textures that actually need cutouts. &lt;/p&gt;

&lt;p&gt;Sometimes the correct architectural decision is "stop being clever about transparency for models we don't control."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 3: Grow a creature procedurally&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The creature lab doesn't import anything — it generates robots, toon people, goblins, aliens, golems, mushroom folk, beast-men, and four-legged animals from type + seed + sliders, rebuilt live in the browser, no server round-trip at all. This is architecturally the cheapest of the four paths: no file I/O, no conversion, no rig detection, just deterministic procedural generation from a small parameter set — the same "seed in, character out" philosophy the world generator already uses for environments.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbijjj6vlxq3nmp3572e8.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbijjj6vlxq3nmp3572e8.jpg" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 4: Ask a CPU to generate a human for you&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one I'm most proud of, mostly because it sounds like it shouldn't work without a GPU, and yet:&lt;/p&gt;

&lt;p&gt;Realistic human generation via Blender + MPFB2. A job queues on charforge-3d, a dedicated worker running Blender 4.5.14 headless with the MPFB2 add-on (23 skins, 20 outfits) — pure CPU rendering, no CUDA — and produces a fully rigged human mesh. This takes minutes, not seconds; CPU-rendered character generation always will. But it lands automatically in the library when it's done, whether or not anyone's still watching the job.&lt;/p&gt;

&lt;p&gt;Photo → 3D reconstruction. Upload a photo, rembg strips the background, TripoSR reconstructs a 3D mesh from that single image running on torch in CPU mode, and an auto-rigger adds a skeleton. This is genuinely the most "we're living in the future" feature in the whole system, and also the most honest about its limits: single-image reconstruction gives you a rough body and an approximate face and hands. It's not going to fool anyone at close range. But as a "turn a photo into an actor you can drop into a scene in a couple of minutes, with no dedicated hardware" pipeline, it's a small miracle that it works at all.&lt;/p&gt;

&lt;p&gt;Both jobs share the same discipline as the image-generation jobs from the original CHARFORGE pipeline: queue it, poll it, never hold an HTTP connection open for it, and the result gets committed to the library automatically instead of requiring someone to babysit the tab.&lt;/p&gt;

&lt;p&gt;The local AI roster — three services, one CPU, zero GPUs&lt;br&gt;
Service What it does    Hardware&lt;br&gt;
Text — Ollama, llama3.2:3b    Character identity: name, age, occupation, personality, backstory, quote    CPU&lt;br&gt;
Image — PyTorch + diffusers, SD-Turbo Garment fabric generation, AI portrait photos   CPU&lt;br&gt;
3D — Blender 4.5.14 + MPFB2, TripoSR, rembg   Realistic human generation, photo-to-3D, format conversion  CPU&lt;/p&gt;

&lt;p&gt;Three genuinely different AI modalities — language, diffusion, 3D reconstruction — sharing 11 GB of RAM and zero accelerators. Nothing here runs concurrently by accident: text, image, and 3D generation are explicitly taking turns, with Ollama unloaded whenever the 3D or image jobs need the headroom. That's the actual engineering answer to "how do you run three kinds of AI without a GPU" — you don't make each one faster, you make sure they never fight each other for the same memory at the same time.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fho7cl8kui3apzhk30phq.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fho7cl8kui3apzhk30phq.jpg" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the library matters more than any single generator&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the theoretical point I actually want to leave you with: the interesting engineering problem stopped being "how do I generate a 3D character" and became "how do I make four completely different generation methods — none of them GPU-accelerated — produce the same kind of citizen in the same database."&lt;/p&gt;

&lt;p&gt;Every path — sculpted, imported, procedural, or AI-reconstructed — ends up as a row with a kind (human, model, creature), an author, a thumbnail, tags, folders, favorites, and now version history. The game engine's actor interface and the library's shared schema are really the same idea applied twice: define the contract once, and let every producer satisfy it independently. That's what let a photo-to-3D reconstruction — a wildly different pipeline from morph-based sculpting — show up in a game's cast list "like any other character," with zero special-casing in the game engine.&lt;/p&gt;

&lt;p&gt;What's still rough (staying honest)&lt;br&gt;
Photo-to-3D quality is genuinely rough — approximate faces and hands, this is not a scanning solution.&lt;br&gt;
MPFB2 human generation and TripoSR reconstruction both take minutes and compete for the same RAM as the image worker.&lt;br&gt;
Unusual imported rigs fall back to structural detection and can animate imperfectly.&lt;br&gt;
The browser-side editor still does morph/skinning math in JavaScript on the CPU (the game engine already moved to GPU-skinned rendering for playback; the editor hasn't yet).&lt;/p&gt;

&lt;p&gt;None of these are foundational problems — they're the normal tax on ambition when your hardware budget doesn't include an accelerator, and they're all on the roadmap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're building anything that ingests content from multiple sources — user-generated, imported, procedurally generated, AI-reconstructed — the lesson generalizes past 3D characters entirely: don't build four separate pipelines that each know how to be "the" content type. Build one interface that any pipeline can satisfy, and let the diversity of sources become a strength instead of a maintenance burden. And don't assume any of this needs a GPU until you've actually checked — a 3B-parameter LLM, a Turbo-distilled diffusion model, and a single-image 3D reconstructor are all, deliberately, chosen because they tolerate CPU inference. A goblin from the creature lab and a human reconstructed from a photo have almost nothing in common under the hood — and in this system, they can stand in the same lineup, take the same bullet, and neither the engine, the database, nor a single GPU ever had to know they were made differently.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>threejs</category>
      <category>programming</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I Built a Local-AI Character Forge and a Browser Game Engine on a $100/y VPS (No GPU, No Excuses)</title>
      <dc:creator>Plastik Electrik</dc:creator>
      <pubDate>Sat, 26 Sep 2026 14:48:40 +0000</pubDate>
      <link>https://dev.to/plastikelectrik/i-built-a-local-ai-character-forge-and-a-browser-game-engine-on-a-100y-vps-no-gpu-no-excuses-1oj7</link>
      <guid>https://dev.to/plastikelectrik/i-built-a-local-ai-character-forge-and-a-browser-game-engine-on-a-100y-vps-no-gpu-no-excuses-1oj7</guid>
      <description>&lt;p&gt;There's a special kind of stubbornness that leads someone to say "I will generate photorealistic 3D humans, dress them with AI-designed clothing, give them personalities via an LLM, and drop them into a playable FPS/RPG hybrid — on a VPS with 6 vCPUs, 11 GB of RAM, and zero GPU." &lt;/p&gt;

&lt;p&gt;That stubbornness is basically the whole architecture document for this project.&lt;/p&gt;

&lt;p&gt;This post is a tour of two apps I've shipped inside my own CMS, CHARFORGE and 3D GAME ENGINE, why they're built the way they are, and the theory behind the tricks that make "no GPU" survivable instead of fatal.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd8uaovbtrfwzyq1zxle2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd8uaovbtrfwzyq1zxle2.png" alt=" " width="800" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The elevator pitch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CHARFORGE generates realistic 3D human characters entirely in the browser, gives them a name, backstory, and personality via a local LLM, dresses them using an AI garment generator running on the VPS, and can turn the 3D portrait into a photo-realistic image. &lt;/p&gt;

&lt;p&gt;No OpenAI key, no Stability API bill, no vendor lock-in — every AI call happens on hardware I already pay for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3D GAME ENGINE&lt;/strong&gt; sits right underneath it: it takes any character you've forged, lets you assign it a role (enemy, boss, merchant, companion, whatever), drop it into one of 8 procedurally generated environments, and play the result in first or third person, WASD-and-mouse, right in the browser tab.&lt;/p&gt;

&lt;p&gt;Both are "in production" as of this week, which in indie-VPS terms means: it's live, it mostly doesn't fall over, and I've personally watched five AI enemies die in a 25-second playtest with a 57% hit rate. Ship it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Why the browser does the 3D math, not the server&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The single biggest architectural decision in both apps is this: the server never touches a GPU or does any 3D math. &lt;/p&gt;

&lt;p&gt;All mesh deformation, skinning, and rendering happens client-side via Three.js. The VPS's job is to store JSON, serve static assets, and run two small local AI models.&lt;/p&gt;

&lt;p&gt;Why? Because a 6-vCPU box with no GPU is a terrible renderer but a perfectly adequate file server and inference host for small models. Fighting that constraint instead of designing around it is how projects die. So:&lt;/p&gt;

&lt;p&gt;CHARFORGE's human model is a MakeHuman (CC0) base mesh — 19,158 vertices, 163 bones — converted into a custom binary format and deformed by morph weights computed in JavaScript. &lt;/p&gt;

&lt;p&gt;Skinning happens on the CPU, recomputed whenever a parameter changes.&lt;/p&gt;

&lt;p&gt;3D GAME ENGINE bakes each character once — computing the final rest mesh with all morphs applied — and from then on only moves bones. &lt;/p&gt;

&lt;p&gt;That single decision is what lets dozens of actors exist on screen simultaneously: you pay the expensive morph-and-rebuild cost exactly once per unique character, then GPU-skin (via Three.js SkinnedMesh) every clone afterward using SkeletonUtils.&lt;/p&gt;

&lt;p&gt;That "bake once, animate cheaply" pattern is the whole trick behind making a browser feel like it's running a real game engine instead of a tech demo that chugs at 12 fps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2: A skeleton that speaks the same language everywhere&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both apps share a 163-bone skeleton built on Euler rotations in ZXY order, computed at each bone head as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;M = M_parent · T(h) · R · T(−h)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's not decoration — it's the load-bearing piece of the whole cross-app integration. Because CHARFORGE and 3D GAME ENGINE agree on the exact same skeleton convention, a character forged in one app can walk, aim, and swing a sword in the other without a single retargeting step. &lt;/p&gt;

&lt;p&gt;The engine code itself (charforge-engine.js, ~350 lines) is literally shared between both apps rather than duplicated — the game engine imports it directly.&lt;/p&gt;

&lt;p&gt;This is the kind of thing that sounds obvious in a blog post and is not obvious at 2am when you're debugging why a cape is rotating around the wrong axis. &lt;/p&gt;

&lt;p&gt;Getting the bone convention right once, up front, and refusing to let it drift between apps, is worth more than any individual optimization either app does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3: Two local AIs doing very different jobs, on the same box, with no GPU&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part I actually want to brag about. There are three local AI processes involved, and they're triaged by how "AI-shaped" the problem actually is:&lt;/p&gt;

&lt;p&gt;Job Model   Why this one&lt;br&gt;
Character identity (name, backstory, personality, quote)    Ollama running llama3.2:3b&lt;br&gt;
Small enough to run on CPU with tolerable latency; &lt;br&gt;
a name and three sentences of backstory don't need a frontier model&lt;/p&gt;

&lt;p&gt;Garment fabric generation&lt;br&gt;&lt;br&gt;
Stable Diffusion (SD-Turbo), txt2img, CPU, ~512px, 3 steps  Turbo variants trade quality for step count — 3 steps is the difference between "usable" and "the VPS catches fire"&lt;/p&gt;

&lt;p&gt;Photorealistic character photo  SD-Turbo, img2img, 4 steps  Uses the 3D portrait as the starting latent instead of generating from noise, which is both faster and keeps facial identity consistent.&lt;/p&gt;

&lt;p&gt;The theoretical point worth making here: you don't need a GPU if you're honest about which models actually need one. &lt;/p&gt;

&lt;p&gt;A 3B parameter LLM and a "Turbo" distilled diffusion model at low step counts are specifically the tier of model designed to run acceptably on CPU. &lt;/p&gt;

&lt;p&gt;The mistake most people make is trying to run a 70B model or a 50-step SDXL pipeline on a VPS and then concluding "local AI doesn't work without a GPU." &lt;/p&gt;

&lt;p&gt;It's not that local AI needs a GPU — it's that some local AI does, and picking the wrong tier is a self-inflicted wound.&lt;/p&gt;

&lt;p&gt;The practical cost of this choice is real, though: a garment takes ~35–40 seconds, a photo takes ~30–60 seconds, one job runs at a time, and the image worker eats about 4.4 GB of RAM — enough that it explicitly unloads Ollama when it needs the headroom. Text and image AI are, quite literally, taking turns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4: The queueing philosophy — never let AI generation hold a connection open&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With nginx behind Cloudflare enforcing a 60-second timeout, and some jobs taking longer than that, the architecture never lets a single HTTP request wait for an AI job to finish. Every slow job goes:&lt;/p&gt;

&lt;p&gt;POST the job → get a job ID immediately&lt;br&gt;
Client polls GET /job/:id for status, progress, ETA&lt;br&gt;
Server processes one job at a time in its own worker process&lt;/p&gt;

&lt;p&gt;This is unglamorous, "everyone already knows this" system design — but it's exactly the pattern that gets skipped when people bolt AI onto an existing API and then wonder why Cloudflare is killing their requests. If your inference time is unpredictable, your HTTP layer should never assume it's fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5: What the game engine adds on top — a small AI-adjacent behavior tree&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;3D GAME ENGINE isn't just "put the CHARFORGE model in a box." It adds a genuinely game-shaped layer:&lt;/p&gt;

&lt;p&gt;8 AI behaviors per role — hunt, patrol, guard, follow, support, wander, idle, flee — with perception ticking every 0.2s across range, field of view, line of sight, hearing, and even night/fog visibility penalties.&lt;/p&gt;

&lt;p&gt;A pathfinding* on a 1m grid with a hard cap of 4 path computations per frame, because letting 30 NPCs all replan simultaneously is how you turn a game into a slideshow.&lt;/p&gt;

&lt;p&gt;Procedural everything — houses, trees, props, weather, sky — using InstancedMesh for repeated geometry, because instancing 200 identical crates is a rendering non-event, while 200 unique draw calls is not.&lt;br&gt;
Combat math that's satisfyingly explicit: headshots multiply damage ×2.2, explosions fall off by distance, projectiles obey gravity and bounce.&lt;/p&gt;

&lt;p&gt;None of this is exotic AI — it's classic game-AI (perception cones, flee thresholds, alert propagation), and that's the point. &lt;/p&gt;

&lt;p&gt;The flashy AI (LLM identities, diffusion-generated clothing) lives in CHARFORGE; the load-bearing AI in the game engine is deliberately old-school and predictable, because 30 NPCs making decisions every frame is not where you want nondeterministic LLM latency showing up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 6: The deployment discipline that makes any of this survivable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the part that's less "cool AI" and more "how not to destroy your own VPS at 1am," which honestly might be the most reusable lesson in this whole post:&lt;/p&gt;

&lt;p&gt;Every change is packaged as a diff against a known md5 of the live file, never a blind overwrite.&lt;/p&gt;

&lt;p&gt;Backups happen before anything is touched — __backup_gameengine, __backup_editor, __backup_photo, and so on — checked with md5, and never stored inside the publicly served directory (learned that one the hard way, presumably).&lt;/p&gt;

&lt;p&gt;Large payloads are chunked into ~50-line blocks, each verified with md5sum before being joined — because a naive 327-line paste got silently truncated by a terminal buffer, and that's a delightful bug to discover in production.&lt;/p&gt;

&lt;p&gt;Installation is a single &amp;amp;&amp;amp;-chained command that stops at the first failure: dry-run patch → copy → real patch → syntax check (node --check, php -l) → done, or nothing.&lt;br&gt;
Rollback commands are never put in the same code blocks as forward changes — because if you're pasting every block verbatim, you really don't want to accidentally paste the rollback.&lt;/p&gt;

&lt;p&gt;None of this is AI. It's just operational paranoia, and it's the reason "Claude wrote code that patches a live production PHP CMS" wasn't a disaster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's actually still rough (the honest limits section)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the spirit of not writing marketing copy: collisions are flat axis-aligned boxes, there's no mid-match save, no multiplayer, animation is procedural bone-rotation rather than motion-capture (so there's some foot sliding), and AI photo generation is deployed but not yet quality-tuned against real output. &lt;/p&gt;

&lt;p&gt;The roadmap — quality settings, a placement editor, quest scripting, NPC dialogue actually generated live from the local LLM identity, WebSocket multiplayer — is long, and most of it is "depth," not "missing foundations." &lt;/p&gt;

&lt;p&gt;Which, for a phase-1 shipped-on-a-VPS project, feels like the right kind of unfinished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway, if you're building something similar&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If there's a general lesson buried in two technical reports about a character generator and a browser FPS, it's this: local AI on modest hardware works when you match the model tier to the hardware tier, decouple slow inference from your request/response cycle, and put your engineering discipline into deployment safety rather than into pretending the constraints don't exist. &lt;/p&gt;

&lt;p&gt;The LLM doesn't need to be huge. The diffusion model doesn't need 50 steps. &lt;/p&gt;

&lt;p&gt;The server doesn't need a GPU. It needs a queue, a backup script, and the humility to bake a mesh once instead of recomputing it every frame.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Introduction to AI-Driven Programming</title>
      <dc:creator>Plastik Electrik</dc:creator>
      <pubDate>Sat, 26 Sep 2026 09:32:36 +0000</pubDate>
      <link>https://dev.to/plastikelectrik/introduction-to-ai-driven-programming-2g4f</link>
      <guid>https://dev.to/plastikelectrik/introduction-to-ai-driven-programming-2g4f</guid>
      <description>&lt;p&gt;&lt;strong&gt;Welcome to the new era of software development&lt;/strong&gt; where modern tooling transforms how we build robust digital solutions.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0kn4qdqiyotiv7u5e9c.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0kn4qdqiyotiv7u5e9c.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core Logic and Prompt Engineering&lt;/strong&gt;&lt;br&gt;
Mastering core logic now requires precise instructions rather than traditional syntax memorization.&lt;/p&gt;

&lt;p&gt;Define clear objectives for every single prompt.&lt;/p&gt;

&lt;p&gt;Break complex problems into modular, manageable steps.&lt;/p&gt;

&lt;p&gt;Validate generated outputs rigorously against edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Structures in Model Architecture&lt;/strong&gt;&lt;br&gt;
Understanding how information flows through intelligent systems remains essential for scalable software architecture.&lt;/p&gt;

&lt;p&gt;Prioritize clean schemas for seamless model communication.&lt;/p&gt;

&lt;p&gt;Manage context windows efficiently to prevent token overflow.&lt;/p&gt;

&lt;p&gt;Structure inputs logically to guide accurate automated responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging and Iterative Refinement&lt;/strong&gt;&lt;br&gt;
Troubleshooting code today shifts from simple syntax correction toward semantic alignment and behavior tuning.&lt;/p&gt;

&lt;p&gt;Isolate errors by testing specific prompt variations systematically.&lt;/p&gt;

&lt;p&gt;Keep system instructions concise, clear, and highly deterministic.&lt;/p&gt;

&lt;p&gt;Document successful workflow patterns for future project scalability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Algorithmic Thinking in the Age of Automation&lt;/strong&gt;&lt;br&gt;
Developing strong algorithmic foundations ensures you can evaluate, optimize, and correct automated code generation effectively.&lt;/p&gt;

&lt;p&gt;Focus on time and space complexity to keep machine-generated solutions efficient.&lt;/p&gt;

&lt;p&gt;Recognize computational bottlenecks that automated tools might overlook or mismanage.&lt;/p&gt;

&lt;p&gt;Design robust fallback mechanisms for scenarios where models fail or hallucinate logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State Management and Context&lt;/strong&gt;&lt;br&gt;
Maintaining state across interactions is critical for building reliable applications powered by modern language models.&lt;/p&gt;

&lt;p&gt;Implement stateless design patterns where applicable to simplify scaling and testing.&lt;/p&gt;

&lt;p&gt;Track conversation or execution history carefully to prevent context drift over time.&lt;/p&gt;

&lt;p&gt;Secure sensitive data points before passing payloads to external AI interfaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion and Future Outlook&lt;/strong&gt;&lt;br&gt;
Embracing AI-assisted development empowers engineers to build faster while keeping core architectural principles front and center.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwp9kqnza5elpzvejckyk.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwp9kqnza5elpzvejckyk.png" alt="Programming Fundamentals for Beginners" width="800" height="495"&gt;&lt;/a&gt;&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F94ve7w5gcoug8e7xt8fs.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F94ve7w5gcoug8e7xt8fs.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbvkzv9pkh44qij3ombp.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbvkzv9pkh44qij3ombp.jpg" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>promptengineering</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>AI JEV | Calibration Is the Feature: What "90% Confidence" Actually Has to Mean</title>
      <dc:creator>Plastik Electrik</dc:creator>
      <pubDate>Thu, 24 Sep 2026 22:32:54 +0000</pubDate>
      <link>https://dev.to/plastikelectrik/calibration-is-the-feature-what-90-confidence-actually-has-to-mean-538m</link>
      <guid>https://dev.to/plastikelectrik/calibration-is-the-feature-what-90-confidence-actually-has-to-mean-538m</guid>
      <description>&lt;p&gt;A theoretical/practical deep dive into the math behind Jev's confidence scores — and how to check whether your own integration is honest about what it doesn't know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing nobody tells you about confidence scores&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every Jev call comes back with a confidence field. Most integrations treat it the same way they'd treat a gut feeling from a colleague: high number, trust it; low number, escalate. That's a reasonable first instinct, and it's also not remotely enough to build a production system on — because a confidence score is a claim, and claims can be wrong in a specific, measurable, fixable way that has nothing to do with whether the underlying decision was right or wrong.&lt;/p&gt;

&lt;p&gt;This is the part that doesn't show up in a quickstart guide: a confidence score is only useful if it's calibrated, and calibration is a property you have to go check for yourself, on your own data, because it's specific to your criteria, your domain, and how your state object is built. Nobody — not TypeSafe AI, not this article — can hand you a guarantee that your Jev integration's 90% is an honest 90%. You have to measure it.&lt;/p&gt;

&lt;p&gt;This post is about what "calibrated" actually means mathematically, why an accurate model can still be badly calibrated, and how to build the diagnostic tooling yourself in about 40 lines of Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accuracy and calibration are different axes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the distinction that trips people up first: a decision system can be highly accurate and badly calibrated at the same time. These are not the same measurement, and optimizing one doesn't automatically fix the other.&lt;/p&gt;

&lt;p&gt;Accuracy asks: of all the decisions made, how many were correct? Calibration asks a subtler question: of all the decisions made at confidence level X, how many were correct? A system can get 95% of decisions right overall while being wildly overconfident on the 5% it gets wrong — reporting 92% confidence on calls it's actually right about only 60% of the time. That gap is invisible if you only track overall accuracy. It becomes very visible the first time someone downstream trusts a 92%-confidence decision that was actually a coin flip.&lt;/p&gt;

&lt;p&gt;This matters specifically for the confidence-gated routing pattern — the one where you send high-confidence decisions straight through and route low-confidence ones to a human or a more expensive fallback. The entire pattern's safety property depends on the confidence number meaning what it says. If your threshold is 0.85 and your system's actual accuracy at reported-confidence-0.85 is 70%, you are silently auto-approving a much riskier slice of traffic than your threshold implies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defining calibration precisely&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A decision system is perfectly calibrated if, for every confidence level p it reports, the decisions it makes at that confidence level are correct exactly p fraction of the time. Formally, for a binary correctness outcome:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;P(correct | confidence = p) = p    for every p&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nothing is ever perfectly calibrated in practice — this is a target you converge toward, not a bar you clear once. What you actually compute is a deviation from this ideal, and there are two standard tools for it: the reliability diagram and the Expected Calibration Error (ECE).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reliability diagram&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You bucket every decision by its reported confidence (typically into 10 bins: 0.0–0.1, 0.1–0.2, … 0.9–1.0), then for each bucket you plot two numbers: the average reported confidence in that bucket, and the actual accuracy of decisions in that bucket. Perfect calibration is the diagonal line — reported confidence equals actual accuracy, bucket by bucket. Any bucket sitting below the diagonal means the system is overconfident in that range (it says 80%, reality says 65%). Any bucket above the diagonal means it's underconfident (it says 60%, reality says 78% — less dangerous, but it means you're routing decisions to expensive fallback paths that didn't need it).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected Calibration Error (ECE)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reliability diagram is qualitative; ECE turns it into a single number you can track over time and alert on. It's the weighted average gap between confidence and accuracy across all bins:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ECE = Σ (n_b / N) * |accuracy(b) - confidence(b)|&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;where n_b is the number of decisions in bin b, N is the total number of decisions, accuracy(b) is the actual correctness rate in that bin, and confidence(b) is the average reported confidence in that bin. Lower is better; 0 is perfect. As a rough field guide, an ECE under 0.03–0.05 is generally considered well-calibrated for a production decision system; above 0.1 means the confidence field is actively misleading whatever's consuming it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building the calibration checker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the whole thing, assuming you're logging every Jev decision with its reported confidence and — eventually, once ground truth becomes available (a human review, a downstream outcome, a dispute resolution) — whether it was actually correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;python&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_calibration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confidences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;correctness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_bins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    confidences: array of reported confidence values, one per decision, in [0, 1]
    correctness: array of 1/0, whether each decision was actually correct
    Returns per-bin stats and the overall Expected Calibration Error.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;confidences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confidences&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;correctness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;correctness&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bin_edges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_bins&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bin_stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;ece&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;n_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confidences&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_bins&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_edges&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bin_edges&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="c1"&gt;# include the right edge only in the final bin
&lt;/span&gt;        &lt;span class="n"&gt;in_bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confidences&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confidences&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n_bins&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;confidences&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;n_bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;in_bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n_bin&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;bin_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;avg_confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;confidences&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;in_bin&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;accuracy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;correctness&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;in_bin&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accuracy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;avg_confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ece&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_bin&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n_total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt;

        &lt;span class="n"&gt;bin_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_bin&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avg_confidence&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bin_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ece&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;print_reliability_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ece&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Range&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Avg Conf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Accuracy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Gap&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bin_stats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;avg_confidence&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;10.3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;10.3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gap&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;8.3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Expected Calibration Error (ECE): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ece&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ece&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;→ Well calibrated.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;ece&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;→ Mild miscalibration — worth investigating which bins are worst.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;→ Significant miscalibration — do not trust confidence-gated routing until this is fixed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Feed&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;logged&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="n"&gt;once&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;have&lt;/span&gt; &lt;span class="n"&gt;ground&lt;/span&gt; &lt;span class="n"&gt;truth&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;them&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;python&lt;/span&gt;
&lt;span class="n"&gt;confidences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.81&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
&lt;span class="n"&gt;correctness&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;...]&lt;/span&gt;

&lt;span class="n"&gt;bin_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ece&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_calibration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confidences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;correctness&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print_reliability_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ece&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output tells you exactly where the problem lives — not just "our confidence is bad" but "our confidence is bad specifically in the 0.8–0.9 range," which is actionable in a way a single accuracy number never is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where miscalibration actually comes from&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you've measured a gap, the useful next question is why. Three sources show up repeatedly in practice:&lt;/p&gt;

&lt;p&gt;Criteria drift. The written criteria a decision was calibrated against no longer match the traffic hitting it. A support-triage category written six months ago for one product line doesn't cleanly cover the edge cases a new product line introduces — the model is confidently applying old boundaries to new territory.&lt;/p&gt;

&lt;p&gt;Bin sparsity. If only 40 decisions ever land in the 0.95–1.0 bucket, your accuracy estimate for that bucket has a wide confidence interval of its own — don't over-react to a single bad-looking bin without checking n first. This is the most common false alarm in a first calibration report.&lt;/p&gt;

&lt;p&gt;Distribution shift between your golden set and live traffic. If the ground-truth examples you calibrated against skew toward easy, unambiguous cases — which hand-labeled sets often do, because ambiguous cases are exactly the ones humans disagree on and therefore tend to exclude — your live-traffic calibration will look worse than your offline evaluation did, for the boring reason that live traffic is harder than your test set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A practical calibration workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Put this together into something you actually run on a schedule, not just once at launch:&lt;/p&gt;

&lt;p&gt;Log every decision with its confidence and enough context to determine ground truth later (a human review outcome, a downstream signal, a dispute).&lt;br&gt;
Batch ground-truth collection weekly, not per-decision — sampling a representative slice is enough; you don't need ground truth for every single decision, you need enough per bin to make the accuracy estimate meaningful (aim for at least 30-50 per bin before trusting it).&lt;br&gt;
Compute ECE and the full reliability table every week, and alert if ECE crosses your threshold or if any high-traffic bin's gap exceeds roughly 0.15.&lt;br&gt;
When a bin is miscalibrated, don't just lower the routing threshold — that treats the symptom. Go look at why that bin is wrong: pull ten decisions from it and read them. Criteria drift and bin sparsity look different once you actually read the cases.&lt;br&gt;
Re-baseline after every criteria change or model version pin change — a calibration report from before a criteria update tells you nothing about the system running today.&lt;br&gt;
The takeaway&lt;/p&gt;

&lt;p&gt;A confidence field is not a fact about the world — it's a claim the model is making about itself, and like any claim, it can be checked. Confidence-gated routing, cascades, and every other pattern that reads a confidence number and makes a decision based on it are only as trustworthy as that number is calibrated. Computing ECE and a reliability table isn't optional infrastructure for a Jev integration running anything that matters — it's the actual test of whether the "confidence" in "confidence-gated routing" means anything at all.&lt;/p&gt;

</description>
      <category>jev</category>
      <category>ai</category>
      <category>programming</category>
      <category>development</category>
    </item>
    <item>
      <title>Why Most Type-Safe Validation Fails in Production (And How JEV Fixes It)</title>
      <dc:creator>Plastik Electrik</dc:creator>
      <pubDate>Thu, 24 Sep 2026 03:51:55 +0000</pubDate>
      <link>https://dev.to/plastikelectrik/why-most-type-safe-validation-fails-in-production-and-how-jev-fixes-it-4b9o</link>
      <guid>https://dev.to/plastikelectrik/why-most-type-safe-validation-fails-in-production-and-how-jev-fixes-it-4b9o</guid>
      <description>&lt;p&gt;If you've shipped a production system that handles structured data — API payloads, form submissions, config files, event streams — you've probably hit this exact wall:&lt;/p&gt;

&lt;p&gt;Your validation logic works beautifully in development. Tests pass. Types check out. You ship it.&lt;/p&gt;

&lt;p&gt;Then three weeks later, at 2am, something breaks. Not because your code is wrong, but because a piece of real-world data showed up that you never anticipated, and your validation layer had no way to tell you why it failed, where it failed, or what it expected instead.&lt;/p&gt;

&lt;p&gt;This post is about why that keeps happening, and what a different approach — the one behind JEV — actually looks like in practice.&lt;/p&gt;

&lt;p&gt;The Problem With "Pass or Fail" Validation&lt;/p&gt;

&lt;p&gt;Most validation approaches, no matter how sophisticated the schema definitions look, ultimately collapse into a boolean at runtime: valid or invalid.&lt;/p&gt;

&lt;p&gt;That sounds fine until you're the one debugging it. A boolean tells you that something went wrong. It tells you nothing about:&lt;/p&gt;

&lt;p&gt;Which field actually failed&lt;br&gt;
What the system expected instead&lt;br&gt;
Whether this was a hard failure or a borderline, ambiguous case&lt;br&gt;
How to reproduce it later from a log line&lt;/p&gt;

&lt;p&gt;So teams end up bolting workarounds onto their validation layer: custom error objects, ad hoc logging, string-parsing exception messages to figure out what actually happened. All of this is complexity that shouldn't exist in the first place — it exists because the validation layer wasn't designed to be inspected, only to gate.&lt;/p&gt;

&lt;p&gt;Type Safety at Compile Time Isn't the Same as Confidence at Runtime&lt;/p&gt;

&lt;p&gt;This is the part that trips up a lot of engineers, especially coming from a strongly-typed language background. Compile-time type safety guarantees your code is internally consistent. It says nothing about the data your code will actually receive once it's running against real users, real APIs, and real edge cases nobody wrote a test for.&lt;/p&gt;

&lt;p&gt;A User interface with a string email field compiles fine. It doesn't stop "not-an-email", "", or a field that's silently undefined because an upstream service changed its response shape without telling you.&lt;/p&gt;

&lt;p&gt;Runtime validation is where the real risk lives. And most runtime validation tooling treats it as an afterthought bolted onto the API boundary, rather than a first-class part of the system's architecture.&lt;/p&gt;

&lt;p&gt;What JEV Does Differently&lt;/p&gt;

&lt;p&gt;JEV is built around a simple but consequential idea: every validation outcome is a typed, structured decision — not a boolean.&lt;/p&gt;

&lt;p&gt;Instead of asking "did this pass?", JEV asks "what decision did the system make about this input, and can I inspect that decision later?"&lt;/p&gt;

&lt;p&gt;In practice, that means:&lt;/p&gt;

&lt;p&gt;Structured, typed errors instead of generic exceptions you have to string-match against&lt;br&gt;
Reproducible validation logic — the same input produces the same structured outcome, every time, which makes it genuinely testable&lt;br&gt;
A real decision trail — when something fails in production, you're reading a clear, typed record of what happened, not reverse-engineering a stack trace&lt;/p&gt;

&lt;p&gt;Here's a simplified illustration of the difference in mental model:&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Traditional approach: boolean, then guesswork&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ...which field? why?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// JEV-style approach: typed decision object&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jevValidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// result.field, result.reason, result.expected — all typed, all inspectable&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;validation_rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference isn't cosmetic. The second version gives you a data structure you can log, test against, alert on, and reason about six months from now when you've forgotten the details of this specific validation rule.&lt;/p&gt;

&lt;p&gt;Why This Matters More as Systems Grow&lt;/p&gt;

&lt;p&gt;A single validation function with three fields doesn't need this level of rigor. But most real systems don't stay that small.&lt;/p&gt;

&lt;p&gt;As a data model grows — more fields, more optional cases, more integrations feeding data from different sources — the cost of a "pass or fail" validation layer compounds. Every new edge case either:&lt;/p&gt;

&lt;p&gt;Gets silently swallowed by an overly permissive check, or&lt;br&gt;
Triggers a generic error that gives your on-call engineer nothing to work with&lt;/p&gt;

&lt;p&gt;Teams that treat validation as real architecture — not an afterthought — are the ones who scale past this. They're not necessarily working with more complex data models than everyone else. They've just made a deliberate decision that validation outcomes deserve to be first-class, typed, and observable.&lt;/p&gt;

&lt;p&gt;A Few Practical Patterns Worth Adopting&lt;/p&gt;

&lt;p&gt;Regardless of whether you use JEV specifically, a few principles from this approach are worth stealing for your own validation layer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Make your validation outcomes a type, not a boolean.&lt;br&gt;
Even a simple discriminated union ({ outcome: "accepted" | "rejected" | "ambiguous" }) gives you far more to work with than true/false.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Log the decision, not just the failure.&lt;br&gt;
A validation success is also worth logging in a structured way, especially for borderline cases that passed but were close to the boundary. That data is gold when you're tuning validation rules later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Treat validation logic as testable business logic, not glue code.&lt;br&gt;
If your validation rules live scattered across route handlers and middleware, they're not testable in isolation. Pull them out. Test them the same way you'd test any other core logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Design for the debugging session you'll have in six months.&lt;br&gt;
When you're staring at a production incident, what do you wish your validation layer had told you? Build that in now, not after the incident.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Going Deeper&lt;/p&gt;

&lt;p&gt;This post only scratches the surface of how JEV's typed-decision model works and how to actually structure a validation layer this way in a real codebase — the practical patterns, the common mistakes teams make when adopting this mindset, and how to migrate an existing "boolean validation" system incrementally without a risky rewrite.&lt;/p&gt;

&lt;p&gt;I wrote all of that up properly in "JEV for Beginners" — a step-by-step guide that takes you from zero to confidently building with JEV, with real, worked examples rather than toy snippets.&lt;/p&gt;

&lt;p&gt;If this post resonated with a validation headache you've actually had, the book is probably worth your time:&lt;/p&gt;

&lt;p&gt;👉 Get JEV for Beginners on Amazon — available in paperback and Kindle.&lt;br&gt;
&lt;a href="https://link.amazon/B0hlctIpb" rel="noopener noreferrer"&gt;Get your Copy here&lt;/a&gt;&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdv23rxe5fhmy9gy6t5uh.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdv23rxe5fhmy9gy6t5uh.jpg" alt="JEV for Beginners" width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you run into this "boolean validation" trap before? I'd love to hear how your team handled it — drop a comment below.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>jev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
