<?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: Ishant gupta</title>
    <description>The latest articles on DEV Community by Ishant gupta (@ishantgupta).</description>
    <link>https://dev.to/ishantgupta</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%2F3903810%2F42ed8e39-059d-45e4-b8d8-cb367edbb952.webp</url>
      <title>DEV Community: Ishant gupta</title>
      <link>https://dev.to/ishantgupta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ishantgupta"/>
    <language>en</language>
    <item>
      <title>Everything Was Green. The AI Was Still Wrong. Building Veritas: a SigNoz-powered risk engine for AI agents</title>
      <dc:creator>Ishant gupta</dc:creator>
      <pubDate>Sat, 18 Jul 2026 13:30:22 +0000</pubDate>
      <link>https://dev.to/ishantgupta/everything-was-green-the-ai-was-still-wrong-44j2</link>
      <guid>https://dev.to/ishantgupta/everything-was-green-the-ai-was-still-wrong-44j2</guid>
      <description>&lt;h1&gt;
  
  
  Everything Was Green. The AI Was Still Wrong.
&lt;/h1&gt;

&lt;p&gt;Three days before I wanted to call this done, I was staring at the SigNoz Logs Explorer trying to figure out why my OTLP logs weren't showing up. Traces had landed fine from the first run. Logs? Nothing. Not even an error, which honestly is worse than an error — at least an error gives you something to grep for.&lt;/p&gt;

&lt;p&gt;That's kind of the whole point of this post, actually. The agent can be dead wrong and none of your dashboards will notice, because they were never built to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap I kept running into
&lt;/h2&gt;

&lt;p&gt;Standard observability tells you three things: is it up, is it fast, did it throw a 500. That's it. An AI agent can pass all three checks and still hallucinate a fact, retrieve zero relevant documents and answer anyway, or execute a plan that was broken from the first step. None of that shows up as an error. It shows up as a decision — and decisions don't come with status codes attached.&lt;/p&gt;

&lt;p&gt;I kept coming back to one rule while building Veritas: it can't just be a black box scoring another black box. If it flags a run and I can't explain why in plain English, that's not accountability, that's just a second opinion nobody can argue with. So everything the score is based on has to already exist on the trace, and every point has to be something a human could look at and say "yeah, that's fair" or "no, that's wrong."&lt;/p&gt;

&lt;p&gt;That's also why nothing in the risk engine is machine-learned. Not because ML is bad, just — for this specific job, I wanted something I could point at and explain, not something I'd have to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;Veritas reads OpenTelemetry data out of a self-hosted SigNoz instance and turns it into a Semantic Risk score from 0–100 for each agent run. Then it builds a Replay Timeline so you can jump straight to the step where things fell apart instead of scrolling through logs like you're doing archaeology. It doesn't store any telemetry itself — SigNoz stays the source of truth, Veritas just reads off it.&lt;/p&gt;

&lt;p&gt;My FastAPI agent has five stages: planner, retriever, tool, memory, response. Each one is its own OTel span nested under a root &lt;code&gt;agent.run&lt;/code&gt; span, and each carries attributes like &lt;code&gt;tool.success&lt;/code&gt;, &lt;code&gt;retrieval_score&lt;/code&gt;, &lt;code&gt;response.confidence&lt;/code&gt;. Six heuristics turn those attributes into the score, with weights that live in one file:&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="n"&gt;RETRIEVAL_ZERO_DOCS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;span class="n"&gt;TOOL_FAILURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;PLANNER_ZERO_TASKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;span class="n"&gt;CONFIDENCE_VERY_LOW_PENALTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;LATENCY_HIGH_PENALTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it against five scenarios through the actual engine (not typed in by hand):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Healthy&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;LOW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retriever issue&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;MODERATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool timeout&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planner failure&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple failures&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;CRITICAL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small thing, but every weight is a multiple of 5, so the engine can only ever spit out multiples of 5. I'd written an early planning doc with "illustrative" scores like 48 and 84 in it, and when I noticed that months later I realized the engine literally cannot produce those numbers. I left the real output alone instead of fudging it to match the doc — a demo that's quietly tuned to look verified is exactly the thing this whole project is supposed to catch, so it would've been a bit much to do that here.&lt;/p&gt;

&lt;p&gt;Here's the Replay Timeline on a real tool-timeout run, score 75, HIGH. Retriever comes back with one document instead of several, the policy lookup times out at 330ms, confidence drops to 0.41 — each of those shows up as its own line instead of getting buried somewhere in a log file.&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%2F4c0n83knpeov1khclap0.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%2F4c0n83knpeov1khclap0.png" alt="Replay Timeline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click into a flagged run and you get a full incident report: root cause, what was expected vs. what happened, a plain-English chain of why, and the business impact. All of it comes straight out of the same risk breakdown — no extra model call, no second AI judging the first one.&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%2Fpoythqzdn65ggl3j3684.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%2Fpoythqzdn65ggl3j3684.png" alt="Incident report"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SigNoz
&lt;/h2&gt;

&lt;p&gt;Before any of the risk-scoring stuff, I had a much more boring problem: I needed somewhere to actually put the telemetry. I'd built agent pipelines before without any real observability layer — just print statements and a prayer — and this time I wanted to do it properly, which meant OpenTelemetry, which meant picking a backend to send it to.&lt;/p&gt;

&lt;p&gt;My first instinct was the usual stack everyone reaches for: Prometheus for metrics, Loki for logs, Tempo for traces, Grafana to glue it all together and pretend it's one tool. I got about an hour into setting that up before I realized I was going to spend the whole weekend wiring four services together and configuring their scrape intervals and label schemas, and none of that time would go toward the actual idea, which was scoring agent runs, not learning YAML. So I went looking for something that treated traces, logs, and metrics as one thing instead of three things you correlate by hand, and SigNoz kept coming up. Self-hosted, OTLP-native, one docker-compose to get a working instance, and — this is the part that mattered most — logs and traces share the same trace_id in the same UI, so you can pivot from a span straight to the log lines that happened during it without switching tools or copy-pasting an ID between two different dashboards.&lt;/p&gt;

&lt;p&gt;That single decision is basically the reason Veritas works the way it does. The Replay Timeline only exists because I could pull a trace and its logs together as one query instead of stitching two systems' outputs together after the fact. If I'd gone with the four-tool stack, I think I would've spent the entire build just writing glue code to keep Tempo's trace IDs and Loki's log labels in sync, and the actual risk engine — the part I actually cared about — would've been the last 10% of the weekend instead of the first 80%. SigNoz didn't give me the scoring logic, that's still just Python and a handful of &lt;code&gt;if&lt;/code&gt; statements, but it gave me a place to point that logic at where everything I needed was already sitting next to everything else.&lt;/p&gt;

&lt;p&gt;Here's the raw Logs Explorer mid-debug, with &lt;code&gt;risk_score&lt;/code&gt; and &lt;code&gt;risk_level&lt;/code&gt; sitting right there as structured fields on the log line, not something I had to reconstruct from a separate metrics panel:&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%2Fot86voyq8ig2eh0ddffv.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%2Fot86voyq8ig2eh0ddffv.png" alt="SigNoz Logs Explorer showing structured agent.run logs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The stuff that actually broke
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Logs that went nowhere.&lt;/strong&gt; Traces worked from run one. Logs didn't. I wired up the OTLP log handler, hit the endpoint, checked the explorer — nothing. No error, no warning. My first guess was &lt;code&gt;propagate = False&lt;/code&gt; somewhere blocking records before they hit the root logger's handler, so I grepped the whole logging setup for the word &lt;code&gt;propagate&lt;/code&gt;. Zero hits. Theory dead, and no error message to chase down instead. What ended up working was attaching the handler directly to each &lt;code&gt;veritas.agent.*&lt;/code&gt; logger by name instead of the root logger. Logs started flowing immediately. Downside: lines duplicate now, since both handlers fire on the same record.&lt;/p&gt;

&lt;p&gt;I went back and checked this against the actual OpenTelemetry Python docs afterward, rather than just trusting my memory of why the fix worked, and it turns out the documented setup is the opposite of what I did — you're supposed to attach the handler once, to the root logger, and named child loggers should reach it automatically through Python's default &lt;code&gt;propagate=True&lt;/code&gt;. The OTel docs also mention a much more common cause for this exact symptom: if &lt;code&gt;logging.basicConfig()&lt;/code&gt; gets called anywhere before the OTel logging integration is set up, the integration's formatting silently never takes effect. So if you hit this same wall — logs go nowhere, nothing in the console — check your init order first. That's the documented issue. Mine might have just been working around the same root cause from a different angle, honestly not 100% sure. Leaving the account here as it happened rather than rewriting it to sound like I knew what I was doing at the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A route that didn't exist.&lt;/strong&gt; The Replay Timeline kept throwing 404s and every instinct said "broken import somewhere." It wasn't that — &lt;code&gt;app/trace/[id]/page.tsx&lt;/code&gt; genuinely hadn't been created yet. Components were built, the route just wasn't wired up.&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%2F79tdwlcycfxsr0jod9k6.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%2F79tdwlcycfxsr0jod9k6.png" alt="Terminal showing repeated GET /trace/high-risk 404 before the route file existed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once that got fixed, I found the trace lookup only recognized two hardcoded keys — &lt;code&gt;high-risk&lt;/code&gt; and &lt;code&gt;low-risk&lt;/code&gt;. Five other demo fixtures were sitting fully generated on disk and completely invisible to the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SigNoz's percentile gap.&lt;/strong&gt; A P50/P90 latency panel threw &lt;code&gt;Function with name 'histogramQuantile' does not exist&lt;/code&gt;. Turned out to be a real version mismatch in the specific SigNoz/ClickHouse build I'd self-hosted — nothing wrong with my config. I ended up rebuilding every percentile panel as a simple sum-divided-by-count average instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zooming out to the fleet view
&lt;/h2&gt;

&lt;p&gt;Past a single run, there's a fleet-level dashboard: risk distribution, latency by stage, which agents are failing most, and some rule-based recommendations keyed off whatever's breaking most often right now.&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%2F1d3ibazaz5vuck8g3ivm.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%2F1d3ibazaz5vuck8g3ivm.png" alt="Veritas Fleet Overview: risk distribution, latency breakdown, top risks today"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth being upfront about: the numbers on that page are simulated 24-hour traffic across three agents, and it says so right on the page. Only the Refund Agent card links to a real, clickable trace — Research and Support Agent don't yet. Didn't want someone to click around and find that out on their own without me having said it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself three weeks ago
&lt;/h2&gt;

&lt;p&gt;"No errors" is not the same as "working." I know that now, but at the time it just felt like relief — like, okay, good, nothing's on fire. Took me embarrassingly long to realize that silence is its own red flag, and arguably a worse one than an actual error, because at least an error gives you a line number.&lt;/p&gt;

&lt;p&gt;The planning doc thing I found almost by accident — I had it open in another tab while writing this up, not even looking for anything, and just noticed 48 and 84 sitting in there. Had this small "wait, my engine literally can't make that number" moment. Every weight's a multiple of 5. It can't output 48. Those were just numbers I'd typed in early on because they sounded roughly right, from before any of this was actually running. Nobody would have noticed if I'd just quietly loosened the engine to allow numbers like that instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  One line
&lt;/h2&gt;

&lt;p&gt;I don't think AI agents need less autonomy. I think they need the same kind of accountability every other system had to earn before anyone trusted it with production traffic.&lt;/p&gt;

&lt;p&gt;Veritas is open source: &lt;a href="https://github.com/ishantgupta30/veritas-flight-recorder" rel="noopener noreferrer"&gt;https://github.com/ishantgupta30/veritas-flight-recorder&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Every Sports App Resets Your Streak Eventually. Mine Can't. 🔒⚡</title>
      <dc:creator>Ishant gupta</dc:creator>
      <pubDate>Sat, 11 Jul 2026 16:43:04 +0000</pubDate>
      <link>https://dev.to/ishantgupta/every-sports-app-resets-your-streak-eventually-mine-cant-hln</link>
      <guid>https://dev.to/ishantgupta/every-sports-app-resets-your-streak-eventually-mine-cant-hln</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Loyalty Ledger — a fan loyalty tracker where your check-in streak, badges, and history live on Solana instead of some app's database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://loyalty-ledger-blond.vercel.app" rel="noopener noreferrer"&gt;https://loyalty-ledger-blond.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the problem I kept coming back to. Every sports app wants you to check in, engage, "prove your loyalty" — collect points, build a streak, unlock a badge. Cool. Except every single one of them throws that history away the second you stop opening the app. Switch apps and your streak resets to zero. Get banned, or the app shuts down, or they just quietly decide to wipe inactive accounts one day — and your history is just... gone. Because it was never actually yours. It was a number sitting in someone else's database, and they could reset it, inflate it, or delete it whenever they felt like it. You had zero say in it. &lt;/p&gt;

&lt;p&gt;And that bugged me way more than it probably should have. Like — we figured out how to make ownership portable for money, for domain names, for digital art. But "I've supported Argentina since 2019" 🇦🇷 still lives and dies inside one company's backend, and nobody's really questioned that.&lt;/p&gt;

&lt;p&gt;So I kept the weekend scope deliberately small: prove one fan's loyalty to one team, for real, end to end — instead of sketching ten features that are all half-fake. You connect a wallet, pick a sport and team, and check in. FIFA World Cup is the fully working path here ⚽ — that check-in sends an actual transaction that creates or updates a program-owned account, not a row in my database somewhere. Your streak count, your badge tier, the actual badge tokens — none of it exists anywhere I control. Which honestly felt a little weird to build, in a good way.&lt;/p&gt;

&lt;p&gt;Once that core loop worked, I built the rest of the identity around it: a Fan Passport that shows your streak, a derived "Fan Score," your tier (Rookie → Devoted → Veteran → Legend 🏆), a progress bar toward the next tier, an achievements grid with locked/unlocked states, a recent-activity feed pulled from real on-chain history, and a leaderboard ranking actual fans by actual streaks. There's also a "Demo Preview" toggle that fills the passport with sample numbers, clearly labeled as sample data — because a screen full of zeros just doesn't demo well, and I'd rather be upfront about that than pretend it's real.&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%2F7ugb6l1ytoe80rxw574f.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%2F7ugb6l1ytoe80rxw574f.png" alt="demo" width="800" height="1203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NBA and "Other International Sport" are in the app too, same UI and flow, but honestly? They're stubbed. Sample fixtures, no chain writes, and the app says so directly instead of hiding it. I'd rather ship one path that's completely real than three that are all a little bit fake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://loyalty-ledger-blond.vercel.app" rel="noopener noreferrer"&gt;https://loyalty-ledger-blond.vercel.app&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ishantgupta30/Loyalty-Ledger" rel="noopener noreferrer"&gt;https://github.com/ishantgupta30/Loyalty-Ledger&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%2Fwcitptto5bvwhuy3ea0i.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%2Fwcitptto5bvwhuy3ea0i.png" alt="📸 SCREENSHOT: Fan Passport with real (or Demo Preview) data — streak, Fan Score, tier stars, achievements. Your hero image." width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To actually try it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;a href="https://phantom.app/" rel="noopener noreferrer"&gt;Phantom&lt;/a&gt; if you don't have it, and switch it to &lt;strong&gt;Devnet&lt;/strong&gt; — Settings → Developer Settings → Change Network → Devnet. This is the step almost everyone misses first, myself included the first time 🙃&lt;/li&gt;
&lt;li&gt;Grab free devnet SOL from the &lt;a href="https://faucet.solana.com/" rel="noopener noreferrer"&gt;faucet&lt;/a&gt; to cover transaction fees, which run a fraction of a cent per check-in. No real money touches this project anywhere.&lt;/li&gt;
&lt;li&gt;Open the app and click Connect Wallet, top right.&lt;/li&gt;
&lt;li&gt;Pick FIFA World Cup and a team.&lt;/li&gt;
&lt;li&gt;Hit check-in and approve the transaction Phantom pops up.&lt;/li&gt;
&lt;li&gt;Watch the streak and Fan Score update, confetti fires 🎉, and if you crossed a threshold, a badge-unlocked toast shows up.&lt;/li&gt;
&lt;li&gt;Scroll down and the full Fan Passport fills in — tier, progress bar, achievements, recent activity, your actual rank on that team's leaderboard.&lt;/li&gt;
&lt;li&gt;Once you've crossed tier 1 (3 check-ins), hit Claim Badge — it mints a real SPL token straight to your wallet. Go check Phantom's token list after, it's just sitting there. Permanently. Not a UI element pretending to be a thing 👀&lt;/li&gt;
&lt;/ol&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%2Fw8e9ssj224v3ydbg4u9x.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%2Fw8e9ssj224v3ydbg4u9x.png" alt="📸 SCREENSHOT: Phantom's transaction-approval popup mid check-in — the single most convincing frame, proves it's a real signed tx" width="728" height="1202"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub Repository:&lt;br&gt;
&lt;a href="https://github.com/ishantgupta30/Loyalty-Ledger" rel="noopener noreferrer"&gt;https://github.com/ishantgupta30/Loyalty-Ledger&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The account design
&lt;/h3&gt;

&lt;p&gt;The core piece is a small Anchor program. Every fan gets a PDA — a Program Derived Address, an account owned by the program itself, not by me — keyed to &lt;code&gt;(wallet, sport, team)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;FandomRecord&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;streak_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;last_checkin_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;highest_tier_claimed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;#[max_len(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;sport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;#[max_len(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&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;Check-in doesn't touch a database at all. It fires a &lt;code&gt;check_in&lt;/code&gt; instruction that either creates this account (first check-in) or updates it — extends the streak, or resets it if too much time passed between check-ins. That's basically the whole point of this project in one sentence: the streak isn't something my frontend can fake, inflate, or quietly reset, because it was never my frontend's data to begin with. Anyone — not just this app — can read the account directly and verify exactly what it says. No trust required.&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%2Ffc7bq0qtfemg8aqgxqgw.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%2Ffc7bq0qtfemg8aqgxqgw.png" alt="SCREENSHOT: Solana Explorer page for a confirmed check_in tx" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The leaderboard problem (this one got me)
&lt;/h3&gt;

&lt;p&gt;This took way longer than it had any right to. My first cut of the account only stored &lt;code&gt;wallet&lt;/code&gt;, &lt;code&gt;streak_count&lt;/code&gt;, and &lt;code&gt;last_checkin_ts&lt;/code&gt; — sport and team lived purely in the PDA seeds. Which is fine if you already know a wallet and want to look up their record. But it's a total dead end if you want to ask "who are the top Argentina fans," because seeds only let you derive one specific account when you already know the inputs. You can't run that backwards to enumerate every account matching a team. Learned that one the hard way mid-build.&lt;/p&gt;

&lt;p&gt;So I ended up adding &lt;code&gt;sport&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt; as actual fields on the account data, not just seed inputs. Which meant a full account-struct change, a rebuild, and a redeploy — genuinely annoying mid-weekend — but it's what makes a real &lt;code&gt;getProgramAccounts&lt;/code&gt; call (filtered client-side by sport/team) actually able to answer "rank every Argentina fan by streak" using real chain state instead of a cached guess or a fake list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Solana, specifically
&lt;/h3&gt;

&lt;p&gt;I wanted this to be a project where pulling out the blockchain would actually break something — not just remove a buzzword from the pitch. Check-ins are frequent and individually pretty worthless — potentially one per match, per fan, across a lot of fans over a tournament. That only works as a real product (not a novelty) if each check-in is near-free and confirms fast enough to feel like a UI action instead of a bank transfer. Solana's one of the few chains where both of those are true at once. A check-in here confirms in about a second and costs a fraction of a cent — which is the actual, boring, practical reason "check in every match, forever" is something you could really build, instead of something that only works in a demo video.&lt;/p&gt;

&lt;p&gt;There's a side effect of the PDA design I honestly didn't fully plan for going in: because the fan record is owned by the program and not by my database, any other app that knows the program id can read it directly. No API key. No integration meeting with me. No trusting my numbers. A ticketing platform could check the same record to offer a loyalty discount. A streaming service could unlock a perk off the same tier. None of that needs my permission or my infrastructure staying online. That's a meaningfully different thing from "an app that happens to use Solana" — and it kind of just fell out of choosing PDAs over a normal database, rather than being the original plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually broke along the way
&lt;/h3&gt;

&lt;p&gt;Being honest here instead of pretending this all went smoothly: I burned a genuinely embarrassing amount of time on devnet SOL. The official faucet rate-limits per IP, and I hit that wall hard while iterating on deploys — every account-struct change meant a full redeploy, and every redeploy needed enough SOL to cover rent for a fresh program buffer. I ended up bouncing between the CLI faucet, a couple of backup faucets, and eventually just asking around, before I could get back to actually building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Solana&lt;/strong&gt; — the PDA-per-fan account design and the on-chain leaderboard aren't decorative. Pull Solana out of this project and the core claim — that a fan's streak can't be faked, inflated, or reset by the app itself — just stops being true. That was the whole point of building it this way instead of with a normal backend.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>blockchain</category>
      <category>solana</category>
    </item>
    <item>
      <title>Git-regret 💀 — I Finally Finished the CLI That Reads Your Shame</title>
      <dc:creator>Ishant gupta</dc:creator>
      <pubDate>Tue, 02 Jun 2026 08:31:21 +0000</pubDate>
      <link>https://dev.to/ishantgupta/git-regret-i-finally-finished-the-cli-that-reads-your-shame-ngf</link>
      <guid>https://dev.to/ishantgupta/git-regret-i-finally-finished-the-cli-that-reads-your-shame-ngf</guid>
      <description>&lt;p&gt;&lt;em&gt;This is an official submission for the GitHub Finish-Up-A-Thon Challenge.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself: &lt;a href="https://ishantgupta30.github.io/git-regret/" rel="noopener noreferrer"&gt;https://ishantgupta30.github.io/git-regret/&lt;/a&gt;
&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%2F6q23xflr3yp0zpsjpm9f.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%2F6q23xflr3yp0zpsjpm9f.png" alt="page" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🕒 Introduction: The Ghost in the &lt;code&gt;.git&lt;/code&gt; Directory
&lt;/h2&gt;

&lt;p&gt;Every developer has a digital graveyard.&lt;/p&gt;

&lt;p&gt;It is a hidden directory, usually tucked away in a generic &lt;code&gt;/Developer/Projects&lt;/code&gt; folder, filled with half-baked ideas, abandoned repositories, and code that was written at 3:00 AM under the influence of intense caffeine and false confidence.&lt;/p&gt;

&lt;p&gt;For 18 months, my personal digital graveyard contained a repository called &lt;code&gt;git-audit-tool&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you opened that repository and ran &lt;code&gt;git log&lt;/code&gt;, you would find a brief, tragic story told in three commits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;initial commit - hackathon night&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wip add secret detection - crashes on big repos&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;giving up for tonight, too tired - will finish later&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last commit sat there untouched for a year and a half.&lt;/p&gt;

&lt;p&gt;The code inside was embarrassing.&lt;/p&gt;

&lt;p&gt;It was a single Python file that hardcoded execution limits, contained broken placeholders, and regularly threw unhandled exceptions if you pointed it at a repository with a mature history.&lt;/p&gt;

&lt;p&gt;But the core problem never went away:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nobody actually checks their git history until something goes terribly wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We push API keys.&lt;/p&gt;

&lt;p&gt;We squash terrible commit messages into production branches.&lt;/p&gt;

&lt;p&gt;We leave panic-driven &lt;code&gt;fix: layout again&lt;/code&gt; commits scattered across our history.&lt;/p&gt;

&lt;p&gt;When the GitHub Finish-Up-A-Thon was announced, I realized it was time to stop closing the tab.&lt;/p&gt;

&lt;p&gt;It was time to take this broken, abandoned script and turn it into a production-ready CLI.&lt;/p&gt;

&lt;p&gt;This is the story of how I completely rewrote that technical mess into &lt;strong&gt;git-regret&lt;/strong&gt;—a tool that scans repository history for mistakes, calculates project "regret metrics," and generates GitHub Copilot remediation plans automatically.&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.amazonaws.com%2Fuploads%2Farticles%2F2z6kxx3u9vufz2gbq12p.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%2F2z6kxx3u9vufz2gbq12p.png" alt="demo" width="800" height="797"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🛑 The Before: One Function, Zero Architecture, Infinite Shame
&lt;/h2&gt;

&lt;p&gt;Before discussing the rebuild, we need to look at the starting line.&lt;/p&gt;

&lt;p&gt;The original script was a masterclass in anti-patterns.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One file&lt;/li&gt;
&lt;li&gt;Zero tests&lt;/li&gt;
&lt;li&gt;Fragile parsing&lt;/li&gt;
&lt;li&gt;Hardcoded limits&lt;/li&gt;
&lt;li&gt;Empty TODOs everywhere
&lt;/li&gt;
&lt;/ul&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;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="c1"&gt;# wip - trying to add secret detection
# TODO: this crashes on big repos lol
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--oneline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&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="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;not a git repo&lt;/span&gt;&lt;span class="sh"&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;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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="s"&gt;checking &lt;/span&gt;&lt;span class="si"&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;commits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; commits...&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;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&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="s"&gt;bad commit: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sha&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;msg&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="k"&gt;if&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;run_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔍 Anatomy of a Broken Script
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Magic Number Limitation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--oneline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-20&lt;/span&gt;&lt;span class="sh"&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 tool literally refused to inspect more than 20 commits.&lt;/p&gt;




&lt;h3&gt;
  
  
  Fragile String Parsing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser assumed every log line would always have the same structure.&lt;/p&gt;

&lt;p&gt;Not exactly resilient engineering.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Phantom Feature
&lt;/h3&gt;

&lt;p&gt;The tool claimed to perform secret detection.&lt;/p&gt;

&lt;p&gt;The implementation?&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="c1"&gt;# TODO secret detection here
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  The README
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# git-audit-tool&lt;/span&gt;

TODO: write this.

&lt;span class="gu"&gt;## Status&lt;/span&gt;
Gave up. Crashes on any repo &amp;gt; 10 commits. Will fix later.
&lt;span class="p"&gt;
-&lt;/span&gt; [ ] fix the crash
&lt;span class="p"&gt;-&lt;/span&gt; [ ] add secret detection
&lt;span class="p"&gt;-&lt;/span&gt; [ ] make it actually useful
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time I looked at the repository, the technical debt felt bigger than the project itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Designing git-regret
&lt;/h2&gt;

&lt;p&gt;To build a utility people would actually use, I had to stop thinking like a script writer and start thinking like a systems engineer.&lt;/p&gt;

&lt;p&gt;The vision centered around three pillars:&lt;/p&gt;

&lt;h2&gt;
  
  
  1️⃣ Multi-Dimensional Analysis
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Repository Scan Engine]
       │
       ├── HIGH SEVERITY 🚨
       │      ├── Secret Leaks
       │      └── Fix Chains
       │
       └── MEDIUM SEVERITY ⚠️
              ├── WIP Commits
              ├── Regret Keywords
              └── Giant Commits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2️⃣ Decoupled Architecture
&lt;/h2&gt;

&lt;p&gt;Analysis and presentation should never depend on one another.&lt;/p&gt;




&lt;h2&gt;
  
  
  3️⃣ AI-Powered Remediation
&lt;/h2&gt;

&lt;p&gt;Finding mistakes is useful.&lt;/p&gt;

&lt;p&gt;Generating the exact fix plan is even better.&lt;/p&gt;

&lt;p&gt;This became the foundation of the &lt;code&gt;--copilot&lt;/code&gt; feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤖 Pairing with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;A surprising amount of CLI development is boilerplate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regex creation&lt;/li&gt;
&lt;li&gt;Test fixtures&lt;/li&gt;
&lt;li&gt;Argument parsing&lt;/li&gt;
&lt;li&gt;State tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub Copilot helped accelerate all of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Challenge #1: Secret Detection
&lt;/h2&gt;

&lt;p&gt;I needed a reliable set of patterns for identifying leaked credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;I am building a high-performance Python static analysis tool for git history. Generate compiled regex patterns for AWS keys, Stripe live keys, GitHub tokens, private SSH keys, database URLs, and common API secret assignments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Result
&lt;/h3&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;re&lt;/span&gt;

&lt;span class="n"&gt;SECRET_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_access_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AKIA[A-Z0-9]{16}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stripe_live_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_[0-9a-zA-Z]{24}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github_pat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ghp_[0-9a-zA-Z]{36}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pem_private_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-----BEGIN[A-Z ]+PRIVATE KEY-----&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(mongodb|postgresql|postgres)://[^:]+:[^@]+@[^/]+&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This became the foundation of the scanning engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⛓️ Challenge #2: Detecting Fix Chains
&lt;/h2&gt;

&lt;p&gt;One common signal of rushed development is a series of consecutive patch commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fix: layout bug
fix: layout bug try 2
fix: forgot import
bugfix: typo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These should usually be squashed into a single commit.&lt;/p&gt;

&lt;p&gt;I asked Copilot to generate a state machine capable of identifying runs of three or more consecutive fix commits.&lt;/p&gt;

&lt;p&gt;The resulting implementation correctly handled edge cases and trailing chains without introducing off-by-one errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Challenge #3: Automated Testing
&lt;/h2&gt;

&lt;p&gt;Testing Git tooling is difficult because you cannot safely mutate a real repository during unit tests.&lt;/p&gt;

&lt;p&gt;I used Copilot to generate a Pytest fixture that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates a temporary repository.&lt;/li&gt;
&lt;li&gt;Configures dummy Git identities.&lt;/li&gt;
&lt;li&gt;Generates synthetic commit histories.&lt;/li&gt;
&lt;li&gt;Runs assertions against isolated repositories.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result was a reproducible testing environment covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secret detection&lt;/li&gt;
&lt;li&gt;Empty histories&lt;/li&gt;
&lt;li&gt;Pagination&lt;/li&gt;
&lt;li&gt;Fix chains&lt;/li&gt;
&lt;li&gt;WIP detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By release day, the project had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;35 assertions&lt;/li&gt;
&lt;li&gt;10 test scenarios&lt;/li&gt;
&lt;li&gt;Full isolated execution&lt;/li&gt;
&lt;/ul&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%2F8fsqmmdbtb7sgdagdo9g.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%2F8fsqmmdbtb7sgdagdo9g.png" alt="A full remediation plan" width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ Architecture
&lt;/h2&gt;

&lt;p&gt;The rewrite introduced strict separation between components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────┐
│ cli.py                     │
│ Flag parsing               │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│ analyzer.py                │
│ Repository analysis        │
└─────────────┬──────────────┘
              │
              ▼
┌────────────────────────────┐
│ ui.py                      │
│ Rich rendering + prompts   │
└────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This architecture makes the analysis engine reusable in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;li&gt;CI systems&lt;/li&gt;
&lt;li&gt;Future integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without modification.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 The Result: git-regret
&lt;/h2&gt;

&lt;p&gt;Installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;git-regret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git-regret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╭──────────────────────────────╮
│ git-regret 💀                │
│ Unbreak your past commits.   │
╰──────────────────────────────╯

Scanned 142 commits

Found:
🚨 1 HIGH
⚠️ 2 MEDIUM

[HIGH] Secret Leak Detected

Commit: a3f910d

Message:
hotfix: override api connection auth

Detail:
Leaked Stripe key detected

Suggested Fix:
Rotate credential and purge history.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠️ The Killer Feature: &lt;code&gt;--copilot&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git-regret &lt;span class="nt"&gt;--copilot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool generates a GitHub Copilot prompt tailored to the repository findings.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an elite principal engineer auditing my repository.

Detected:
- secret_leak
- trailing_fix_chain

Affected commits:
- a3f910d
- 84b2c11

Please:

1. Provide an interactive rebase plan.
2. Show git filter-repo commands.
3. Draft replacement commit messages.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of searching Stack Overflow for hours, developers get an immediate remediation workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Before vs After
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Old Script&lt;/th&gt;
&lt;th&gt;git-regret&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codebase&lt;/td&gt;
&lt;td&gt;87 Lines&lt;/td&gt;
&lt;td&gt;520+ Lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Single File&lt;/td&gt;
&lt;td&gt;Modular&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;35 Assertions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analysis Rules&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;print()&lt;/td&gt;
&lt;td&gt;Rich&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error Handling&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;td&gt;Robust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Integration&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Copilot Prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔮 Roadmap
&lt;/h2&gt;

&lt;p&gt;Planned features include:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;git-regret install-hook&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Automatically install pre-commit protections.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;git-regret --ci&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Generate GitHub Actions workflows automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.gitregret.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Custom organizational rules and policies.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏁 Conclusion
&lt;/h2&gt;

&lt;p&gt;Finishing an abandoned project taught me something important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An old idea is not necessarily a bad idea. Sometimes it is simply waiting for a better implementation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The original project failed because it lacked structure.&lt;/p&gt;

&lt;p&gt;By introducing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decoupled architecture&lt;/li&gt;
&lt;li&gt;Automated testing&lt;/li&gt;
&lt;li&gt;GitHub Copilot assistance&lt;/li&gt;
&lt;li&gt;Clear design boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I transformed a forgotten hackathon script into a production-ready developer tool.&lt;/p&gt;

&lt;p&gt;If you have an abandoned repository sitting in your projects folder, this challenge is your sign to revisit it.&lt;/p&gt;

&lt;p&gt;You might discover that the hardest part was simply finishing.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Audit Your Repository Today
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;git-regret

&lt;span class="c"&gt;# Generate a report and Copilot remediation plan&lt;/span&gt;
git-regret &lt;span class="nt"&gt;--copilot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Documentation &amp;amp; Website:&lt;/strong&gt; &lt;a href="https://ishantgupta30.github.io/git-regret/" rel="noopener noreferrer"&gt;https://ishantgupta30.github.io/git-regret/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/ishantgupta30/git-regret" rel="noopener noreferrer"&gt;https://github.com/ishantgupta30/git-regret&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚡ Get Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;git-regret
git-regret &lt;span class="nt"&gt;--copilot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If git-regret helps you uncover a secret, clean up a fix chain, or finally understand what happened in your repository six months ago, consider giving the project a ⭐.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>I Built a Space App That Gives You Real-Time Planetary Data — Powered by Gemma 4, No Backend</title>
      <dc:creator>Ishant gupta</dc:creator>
      <pubDate>Mon, 11 May 2026 18:36:34 +0000</pubDate>
      <link>https://dev.to/ishantgupta/i-built-a-space-app-that-gives-you-real-time-planetary-data-powered-by-gemma-4-no-backend-3823</link>
      <guid>https://dev.to/ishantgupta/i-built-a-space-app-that-gives-you-real-time-planetary-data-powered-by-gemma-4-no-backend-3823</guid>
      <description>&lt;h2&gt;
  
  
  I Built a Retro Space Mission Control Powered by Gemma 4 — and It Talks Like a NASA Commander 🚀
&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%2Fsoknamjxqkt9ht7ll0v6.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%2Fsoknamjxqkt9ht7ll0v6.png" alt="best" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What happens when you give an open-source AI model the keys to the solar system?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've been obsessed with space since I was a kid. But every space app I found was either too boring (just static facts) or too complex (needs a PhD to use).&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Cosmos Explorer&lt;/strong&gt; — a retro, terminal-aesthetic space mission control center that runs entirely in the browser, powered by &lt;strong&gt;Gemma 4&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://cosmos-explorer-ishant.netlify.app/" rel="noopener noreferrer"&gt;Live Demo → cosmos-explorer-ishant.netlify.app&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
🔗 &lt;strong&gt;&lt;a href="https://github.com/ishantgupta30/cosmos-explorer" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No downloads. No installs. Just open it and feel like you're at NASA.&lt;/p&gt;
&lt;h2&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%2Fp3h7gepx9awq8zr620dl.gif" alt="Astronaut floating in space" width="220" height="124"&gt;
&lt;/h2&gt;
&lt;h2&gt;
  
  
  The Idea: What if space exploration felt alive?
&lt;/h2&gt;

&lt;p&gt;Static facts are boring. I wanted something that &lt;em&gt;reacted&lt;/em&gt; — that felt intelligent, cinematic, and immersive.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;Gemma 4&lt;/strong&gt; came in.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding descriptions, I let Gemma 4 generate real-time mission briefings, reveal hidden planetary secrets, narrate the night sky like a planetarium, and compare worlds with the insight of an actual scientist.&lt;/p&gt;

&lt;p&gt;Gemma 4 isn't just a feature in this app. &lt;strong&gt;It's the soul of it.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What is Cosmos Explorer?
&lt;/h2&gt;

&lt;p&gt;It's a 7-in-one space dashboard built as a single HTML file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🌍 Solar System&lt;/td&gt;
&lt;td&gt;Animated real-physics orbits, click any planet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🤖 Mission Briefings&lt;/td&gt;
&lt;td&gt;Gemma 4 generates NASA-style planet briefings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💡 Discovery Mode&lt;/td&gt;
&lt;td&gt;Gemma 4 reveals obscure scientific secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🛰️ Satellites&lt;/td&gt;
&lt;td&gt;Live mission data for Hubble, JWST, Voyager 1 &amp;amp; more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📡 ISS Tracker&lt;/td&gt;
&lt;td&gt;Real orbital physics, updates every second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔭 Sky CAM&lt;/td&gt;
&lt;td&gt;Point your camera at the sky — Gemma 4 narrates it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚖️ Planet Compare&lt;/td&gt;
&lt;td&gt;Gemma 4 analyzes any two worlds side by side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let me walk you through each one.&lt;/p&gt;


&lt;h2&gt;
  
  
  Feature 1: The Solar System — Click a Planet, Get a Mission Briefing
&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%2Fbtvs9oxj0gmwmmawi6f7.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%2Fbtvs9oxj0gmwmmawi6f7.png" alt="Solar system view with Saturn selected and Gemma mission briefing" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The moment you click a planet, Gemma 4 kicks in. No loading spinner for 5 seconds. Just instant, cinematic text that typewriters across the screen like a real mission computer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma's output for Saturn:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TARGET ACQUIRED: SATURN. Ring system spans 282,000 km yet only 10-100 
meters thick. Density lower than water — it would float. Enceladus moon 
sprays water geysers at 1,400 km/h. Titan has liquid methane lakes. 
145 moons confirmed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't hardcoded. Gemma 4 writes this fresh every time. I just gave it a persona:&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="nf"&gt;callGemma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a NASA mission commander. Cite specific instruments and missions 
   by exact name. Begin with TARGET ACQUIRED:. Be cinematic. 2-3 sentences maximum.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`Generate a mission briefing for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;planetName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That system prompt is doing a LOT of work. The model picks up on tone, format, and scientific precision instantly. That's the power of a 27B instruction-tuned model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature 2: Discovery Mode — Secrets Gemma Reveals
&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%2Flkytzsy68wqmbpmqabjz.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%2Flkytzsy68wqmbpmqabjz.png" alt="Discovery mode showing Gemma revealing a hidden fact about Saturn's rings" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every planet has a &lt;strong&gt;Discovery Mode&lt;/strong&gt; button. Press it, and Gemma 4 digs up something genuinely obscure — not the usual "Saturn has rings" stuff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real output for Saturn:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;DISCOVERY: Saturn's rings are geologically young — only 10 to 100 million years old, formed when dinosaurs walked on Earth. Cassini measured ring mass and dust accumulation rates, shocking scientists who expected billion-year-old rings. The rings may disappear in 100 million years as Saturn's gravity pulls them inward.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I prompted it as a planetary scientist with access to mission data:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a planetary scientist revealing a genuinely obscure fact. 
 Choose something highly specific. Cite missions, instruments, or datasets. 
 2-3 sentences. Begin with "DISCOVERY: "&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I love here: Gemma 4 doesn't just recite Wikipedia. It &lt;em&gt;connects&lt;/em&gt; facts. The dinosaur comparison? That's Gemma making an analogy on its own.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature 3: Sky CAM — Your Camera Becomes a Planetarium
&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%2Fsf0uxwjd7r7wgx560b9r.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%2Fsf0uxwjd7r7wgx560b9r.png" alt="Sky CAM view showing Orion constellation overlay with Gemma AI analysis panel" width="799" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the feature I'm most proud of.&lt;/p&gt;

&lt;p&gt;Point your device camera at the sky (or just use the direction/tilt sliders), and Sky CAM overlays constellation lines in real time. But the real magic? Hit &lt;strong&gt;"GEMMA AI ANALYSIS"&lt;/strong&gt; and watch this happen:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Turn your gaze southward, where Orion strides across the winter sky — Betelgeuse blazing with the light of a star 700 times larger than our own Sun, a red supergiant in its final cosmic act. In 2019, Betelgeuse dimmed so dramatically that astronomers worldwide predicted imminent supernova — a reminder that even celestial constants can surprise us. The ancient Egyptians saw Orion as Osiris, god of death and resurrection, his belt stars aligned with the three pyramids of Giza."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I gave Gemma 4 a very specific persona here:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a live planetarium narrator with deep astronomy expertise. 
 Be vivid, poetic, scientifically precise, and cinematic.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt also feeds in the current compass direction, tilt angle, and which constellations are visible. Gemma uses all of that context to write something &lt;em&gt;specific&lt;/em&gt; to what you're actually looking at.&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;128K context window&lt;/strong&gt; of Gemma 4 shines — I can pass rich contextual data and get nuanced, location-aware responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature 4: Planet Comparison — Gemma as Data Analyst
&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%2F4bok8padlj5z65xr1e1p.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%2F4bok8padlj5z65xr1e1p.png" alt="Planet comparison tool showing Mercury vs Mars with Gemma analysis" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pick any two planets, hit ANALYZE, and Gemma 4 breaks down the comparison like a scientist explaining it to a curious friend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mercury vs Mars:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;SIZE CONTRAST: Mars is 1.4× wider than Mercury. DISTANCE: Mercury orbits at 57.9M KM from the Sun, while Mars orbits at 227.9M KM — a vast difference in solar energy received. TEMPERATURE: Mercury surface reads -180→430°C vs Mars's -125→20°C — Mercury and Mars represent extreme thermal environments.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The structured breakdown format comes entirely from the prompt. I didn't template the output — Gemma 4 just... organizes it that way when you ask it to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature 5: The Space Q&amp;amp;A Chat
&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%2Fnlgedl8bc2x5tgelgyi6.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%2Fnlgedl8bc2x5tgelgyi6.png" alt="Space Q&amp;amp;A chat showing Gemma answering a question about the ISS" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the bottom of the Solar System view, there's a chat bar. Ask anything about space.&lt;/p&gt;

&lt;p&gt;The system prompt is simple but effective:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are an expert space scientist. Answer questions about planets, moons, 
 satellites, black holes, and galaxies enthusiastically. Keep answers 2-4 
 sentences with specific numbers and surprising facts.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "specific numbers" instruction is key — it stops Gemma from giving vague answers and forces it to ground responses in real data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Gemma 4? The Model Selection Decision
&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%2Fgdvoovussjewdz1i5ib3.gif" 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%2Fgdvoovussjewdz1i5ib3.gif" alt="Thinking hard" width="480" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was a deliberate choice, not a default.&lt;/p&gt;

&lt;p&gt;I needed a model that could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Switch personas instantly&lt;/strong&gt; — NASA commander, planetarium narrator, planetary scientist, data analyst. All in the same app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Produce short, precise outputs&lt;/strong&gt; — The UI doesn't have space for essays. 2-3 sentences max.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work reliably via API&lt;/strong&gt; — The whole app runs client-side, so I needed a stable, fast endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle creative + factual tasks&lt;/strong&gt; — Mission briefings need drama. Discovery Mode needs accuracy. Both at once is hard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I went with &lt;strong&gt;&lt;code&gt;gemma-4-27b-it&lt;/code&gt;&lt;/strong&gt; (the 27B dense model via Google AI Studio's Gemini API).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 27B and not 2B or 4B?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The smaller models are amazing for edge/mobile deployment — but they struggled with the tonal range this app demands. A 4B model would give me a decent mission briefing &lt;em&gt;or&lt;/em&gt; a poetic sky narration, but not both reliably. The 27B model handles persona-switching cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not the 26B MoE?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The MoE is built for high-throughput reasoning. My use case is lots of &lt;em&gt;short&lt;/em&gt; creative + factual bursts, not long-chain reasoning. The dense 27B was the better fit here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The killer feature: instruction following&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every Gemma 4 call in this app has a very specific system prompt. The model follows them precisely — it doesn't ramble, doesn't break character, doesn't exceed the sentence limit I set. That level of instruction-following made the entire UI feel polished.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture (The Short Version)
&lt;/h2&gt;

&lt;p&gt;The entire app is &lt;strong&gt;one HTML file&lt;/strong&gt;. No frameworks, no build step, no server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cosmos Explorer
├── Canvas-based solar system (vanilla JS + requestAnimationFrame)
├── Real orbital mechanics (Kepler's equations)
├── ISS tracker (simulated TLE physics)
├── NASA images via JPL/APOD APIs
├── Space news via curated data
└── Gemma 4 via Gemini API (gemma-4-27b-it)
    ├── Mission briefings (on planet click)
    ├── Discovery Mode (on button press)
    ├── Sky CAM narration (on analyze)
    ├── Planet comparison (on analyze)
    └── Space Q&amp;amp;A (persistent chat)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Gemma integration is surprisingly clean — one &lt;code&gt;callGemma()&lt;/code&gt; function that every AI feature uses:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callGemma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;systemText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://generativelanguage.googleapis.com/v1beta/models/gemma-4-27b-it:generateContent?key=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;system_instruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;systemText&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userText&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;generationConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxOutputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;maxTokens&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&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="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parts&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;That's it. Every Gemma feature in the entire app flows through that one function. Clean, simple, powerful.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Moment That Made Me Go "Whoa"
&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%2Frzs0vzzfds0vgl2kk8bl.gif" 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%2Frzs0vzzfds0vgl2kk8bl.gif" alt="Mind blown reaction" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During testing, I clicked on Earth and asked Gemma to generate a mission briefing.&lt;/p&gt;

&lt;p&gt;It said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"TARGET ACQUIRED: EARTH. The only confirmed biosphere in the known universe. Magnetic field generated by liquid iron outer core deflects solar wind at 400 km/s. 71% surface liquid water. 8.1 billion crew members aboard. Protect at all costs."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;"8.1 billion crew members aboard. Protect at all costs."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I didn't prompt that. The model extrapolated from the NASA commander persona and added something genuinely moving. That's the difference between a language model and a &lt;em&gt;creative&lt;/em&gt; language model.&lt;/p&gt;

&lt;p&gt;That line stayed in the app.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exoplanet Explorer&lt;/strong&gt; — Gemma 4 generating habitability reports for Kepler catalog planets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mission Planner&lt;/strong&gt; — Ask Gemma to plan a hypothetical mission to any planet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Night Sky Events&lt;/strong&gt; — Gemma narrating upcoming eclipses, conjunctions, meteor showers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline mode&lt;/strong&gt; — Gemma 4 2B/4B running locally via WebGPU (no API key needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The offline idea excites me most. The 4B model is small enough to run in the browser — imagine a space education tool that works with zero internet, perfect for schools and rural areas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;🚀 Live Demo:&lt;/strong&gt; &lt;a href="https://cosmos-explorer-ishant.netlify.app/" rel="noopener noreferrer"&gt;cosmos-explorer-ishant.netlify.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open it, click Saturn, hit Discovery Mode, then go to Sky CAM and press Gemma AI Analysis. That sequence will show you everything Gemma 4 can do in about 60 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;People talk about AI like it's a search engine replacement. But Gemma 4 did something different here — it became a &lt;em&gt;narrator&lt;/em&gt;. It gave this app a voice, a personality, a sense of drama.&lt;/p&gt;

&lt;p&gt;Space is the most dramatic story humanity has ever told. I just needed an AI good enough to tell it.&lt;/p&gt;

&lt;p&gt;Gemma 4 was that AI. 🌌&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with vanilla JS, Canvas API, and Gemma 4 via Google AI Studio.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;No frameworks were harmed in the making of this app.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Thanks for reading!!&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#gemma4&lt;/code&gt; &lt;code&gt;#googleai&lt;/code&gt; &lt;code&gt;#javascript&lt;/code&gt; &lt;code&gt;#space&lt;/code&gt; &lt;code&gt;#webdev&lt;/code&gt; &lt;code&gt;#ai&lt;/code&gt; &lt;code&gt;#opensource&lt;/code&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>I Tried to Run Google's Most Enterprise Database on My MacBook. Here's What Actually Happened.</title>
      <dc:creator>Ishant gupta</dc:creator>
      <pubDate>Wed, 29 Apr 2026 13:57:01 +0000</pubDate>
      <link>https://dev.to/ishantgupta/i-tried-to-run-googles-most-enterprise-database-on-my-macbook-heres-what-actually-happened-36gf</link>
      <guid>https://dev.to/ishantgupta/i-tried-to-run-googles-most-enterprise-database-on-my-macbook-heres-what-actually-happened-36gf</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At Google Cloud NEXT '26, buried inside announcement #68 of 260 total announcements, was a single sentence that stopped me cold:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Run it anywhere — across multiple clouds, on-premises, or on your laptop."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"It" is &lt;strong&gt;Google Spanner&lt;/strong&gt; — the database that powers Gmail, Google Ads, Google Pay, and YouTube. The system Walmart, Goldman Sachs, and Shopify depend on for their most critical workloads.&lt;/p&gt;

&lt;p&gt;They said I could run it on my laptop. So I tried. Here's the honest account of what happened — including the part where Google locked me out before I even started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spoiler: Spanner Omni itself requires whitelisting. But what I could run locally surprised me more than I expected.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Spanner Omni Actually Is
&lt;/h2&gt;

&lt;p&gt;Spanner Omni is Google's answer to a question nobody thought they'd ask: what if you could take the database that runs Google's entire ad business and make it deployable anywhere — on AWS, on-premises, in an air-gapped data center, or on a developer's MacBook?&lt;/p&gt;

&lt;p&gt;The hard engineering problem wasn't the database itself. It was &lt;strong&gt;TrueTime&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Original Spanner's legendary consistency guarantees depend on atomic clocks and GPS hardware physically embedded inside Google's data centers. That hardware is what lets Spanner know, with mathematical certainty, the order in which transactions happened across data centers on opposite sides of the planet.&lt;/p&gt;

&lt;p&gt;You cannot ship GPS satellites in a Docker image.&lt;/p&gt;

&lt;p&gt;So Google built a software-defined TrueTime alternative — a reimplementation that provides error-bounded time synchronization without specialized hardware, relying on the insight that Spanner can tolerate weaker uncertainty bounds because it overlaps time-uncertainty waits with other database work.&lt;/p&gt;

&lt;p&gt;That is not a minor detail. That's a fundamental reimagining of one of distributed systems' most famous components, shipped quietly alongside 259 other announcements.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part Google Didn't Advertise: It's Not Public Yet
&lt;/h2&gt;

&lt;p&gt;The blog post says "available today in preview." What it doesn't prominently say: the Docker image requires Google to whitelist your account before you can pull it.&lt;/p&gt;

&lt;p&gt;I found this out the hard way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull us-docker.pkg.dev/cloud-spanner-omni-preview/release/spanner-omni:2026.r1-beta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even after configuring gcloud auth, the pull failed with the same authorization error: permission denied on the registry. The preview is real, but access is controlled. If you want in, you need to contact Google directly.&lt;/p&gt;

&lt;p&gt;This is worth knowing before you block off an evening.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Can Do Right Now: The Spanner Emulator
&lt;/h2&gt;

&lt;p&gt;While Spanner Omni stays behind Google's velvet rope, there's a fully functional local Spanner environment that IS publicly available right now: the &lt;strong&gt;Cloud Spanner Emulator&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull gcr.io/cloud-spanner-emulator/emulator

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 9010:9010 &lt;span class="nt"&gt;-p&lt;/span&gt; 9020:9020 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; spanner-emulator &lt;span class="se"&gt;\&lt;/span&gt;
  gcr.io/cloud-spanner-emulator/emulator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It pulled cleanly and started in under 2 seconds:&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.amazonaws.com%2Fuploads%2Farticles%2Fd2qkrp3elpfkg00onriv.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%2Fd2qkrp3elpfkg00onriv.png" alt="Docker ps showing spanner-emulator running" width="800" height="39"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure gcloud to point to your local emulator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;auth/disable_credentials &lt;span class="nb"&gt;true
&lt;/span&gt;gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project test-project
gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;api_endpoint_overrides/spanner http://localhost:9020/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run a real query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud spanner databases execute-sql test-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SELECT 'Spanner is running on my MacBook' AS message"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F1kq9fb3xgxsigkyjm464.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%2F1kq9fb3xgxsigkyjm464.png" alt="Terminal showing Spanner is running on my MacBook" width="800" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That line printing in my terminal felt genuinely surreal. This is the same database engine that handles Google's global ad auction, running entirely offline on a MacBook Air.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark: How Fast Is It Locally?
&lt;/h2&gt;

&lt;p&gt;I didn't just want to run a hello-world query. I wanted real numbers.&lt;/p&gt;

&lt;p&gt;I inserted 1,000 rows using a shell loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 1000&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;gcloud spanner rows insert &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test-instance &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test-db &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Users &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"UserId=&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;,Name=User&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;,Email=user&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;@test.com"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F0tafsi9802ggmvwco5db.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%2F0tafsi9802ggmvwco5db.png" alt="Insert loop running with commit timestamps" width="799" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then timed a COUNT query across all 1,000 rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;time &lt;/span&gt;gcloud spanner databases execute-sql test-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SELECT COUNT(*) FROM Users"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a LIMIT 100 fetch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;time &lt;/span&gt;gcloud spanner databases execute-sql test-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM Users LIMIT 100"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Frtt0s7wnz50mb0i1ekzp.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%2Frtt0s7wnz50mb0i1ekzp.png" alt="LIMIT 100 query returning 100 rows in 0.37 seconds" width="800" height="1613"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Result: 100 rows fetched in 0.37 seconds on a MacBook Air.&lt;/strong&gt; COUNT across 1,000 rows returned near-instantly. For a database engine designed for planet-scale distributed workloads, that's more than responsive enough for real local development.&lt;/p&gt;


&lt;h2&gt;
  
  
  ACID Transactions Working Locally
&lt;/h2&gt;

&lt;p&gt;This is Spanner's actual superpower — strong consistency across distributed nodes. I wanted to verify it works locally too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud spanner databases execute-sql test-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"UPDATE Users SET Name = 'Ishant Gupta' WHERE UserId = 1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-partitioned-dml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immediately queried it back:&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.amazonaws.com%2Fuploads%2Farticles%2Fvmz2gxvsi82x8pguyizw.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%2Fvmz2gxvsi82x8pguyizw.png" alt="UPDATE statement modifying 1 row" width="798" height="134"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud spanner databases execute-sql test-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM Users WHERE UserId = 1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fiowkvbqnxv6ao8o7razw.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%2Fiowkvbqnxv6ao8o7razw.png" alt="SELECT showing Ishant Gupta updated" width="800" height="130"&gt;&lt;/a&gt;&lt;br&gt;
The change was there instantly. Then I ran aggregations across all 1,000 rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud spanner databases execute-sql test-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SELECT COUNT(*) as TotalUsers, 
        MAX(UserId) as MaxId, MIN(UserId) as MinId 
        FROM Users"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fo04xmzmi8alh3vx1v240.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%2Fo04xmzmi8alh3vx1v240.png" alt="TotalUsers 1000 MaxId 1000 MinId 1" width="798" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;COUNT, MAX, MIN — all correct, all returned in under a second. This isn't a mock. It's real Spanner consistency semantics running entirely offline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vector Search: The AI Angle
&lt;/h2&gt;

&lt;p&gt;One of the biggest NEXT '26 announcements was Spanner's support for vector search — making it viable as an AI retrieval database with no ETL pipeline needed.&lt;/p&gt;

&lt;p&gt;I created the schema locally to see if the emulator would accept it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;Documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;DocId&lt;/span&gt; &lt;span class="n"&gt;INT64&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;Embedding&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DocId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fedkcdcw6bwrmzn54kl56.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%2Fedkcdcw6bwrmzn54kl56.png" alt="Vector search schema with Documents table created" width="799" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It accepted the schema instantly. Note: actual vector similarity search queries require cloud Spanner — but designing and validating your AI data model locally before touching production is exactly the workflow Google is targeting with these announcements.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Story: Solving the Parity Problem
&lt;/h2&gt;

&lt;p&gt;Here's what I think is genuinely underreported about both the emulator and Spanner Omni:&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.amazonaws.com%2Fuploads%2Farticles%2Frj4apmspjtl9o849dayr.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%2Frj4apmspjtl9o849dayr.png" alt="Spanner dev workflow: local emulator to CI/CD to production with same SQL" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before this, building on Spanner meant one of three painful options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mocking the database (fast but inaccurate)&lt;/li&gt;
&lt;li&gt;Substituting SQLite (wrong SQL dialect, wrong behavior)&lt;/li&gt;
&lt;li&gt;Pushing every change to a cloud dev environment and waiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a local Spanner running, the queries that work in your terminal work in production. The schema you design locally is the schema you deploy. The ACID semantics you rely on locally are the semantics you get in prod.&lt;/p&gt;

&lt;p&gt;For AI applications specifically, this matters even more. Spanner now supports vector search, Spanner Graph for relationship queries, and full-text search — all in one database. You can prototype an entire AI retrieval stack locally, for free, before spending a rupee on cloud credits.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Honest Take
&lt;/h2&gt;

&lt;p&gt;Spanner Omni as announced is not what you can run today unless Google whitelists you. That gap between the blog post and actual access is worth calling out — not as a criticism, but as something every developer should know before they try.&lt;/p&gt;

&lt;p&gt;But the emulator is real, it works, and the numbers speak for themselves: 1,000 rows, 0.37 second fetches, real ACID transactions, and vector-ready schema — all running on a MacBook Air that was also playing Spotify.&lt;/p&gt;

&lt;p&gt;The 2012 Spanner paper felt like reading about infrastructure from another planet. Running &lt;code&gt;SELECT * FROM Users&lt;/code&gt; locally in 2026 means that planet is now your dev environment.&lt;/p&gt;

&lt;p&gt;If you're building anything data-intensive — especially with AI retrieval — run these two Docker commands this weekend and see how it fits your stack. I'm waiting on Spanner Omni access; when it lands, I'll write the follow-up this article deserves.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;References:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://cloud.google.com/blog/products/databases/introducing-spanner-omni" rel="noopener noreferrer"&gt;Introducing Spanner Omni&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://docs.cloud.google.com/spanner-omni/overview" rel="noopener noreferrer"&gt;Spanner Omni Docs&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://cloud.google.com/spanner/docs/emulator" rel="noopener noreferrer"&gt;Cloud Spanner Emulator&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://cloud.google.com/blog/topics/google-cloud-next/google-cloud-next-2026-wrap-up" rel="noopener noreferrer"&gt;Google Cloud NEXT '26 Announcements&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>cloudnextchallenge</category>
      <category>googlecloud</category>
    </item>
  </channel>
</rss>
