<?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: Kenji Sakuramoto</title>
    <description>The latest articles on DEV Community by Kenji Sakuramoto (@kenji_sakuramoto_d72920b2).</description>
    <link>https://dev.to/kenji_sakuramoto_d72920b2</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%2F2588616%2F852e6862-979a-4ace-8c82-3ec765a4729f.png</url>
      <title>DEV Community: Kenji Sakuramoto</title>
      <link>https://dev.to/kenji_sakuramoto_d72920b2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenji_sakuramoto_d72920b2"/>
    <language>en</language>
    <item>
      <title>From "this is broken" to a fix PR I merge by hand</title>
      <dc:creator>Kenji Sakuramoto</dc:creator>
      <pubDate>Thu, 15 Oct 2026 04:18:01 +0000</pubDate>
      <link>https://dev.to/kenji_sakuramoto_d72920b2/from-this-is-broken-to-a-fix-pr-i-merge-by-hand-cbh</link>
      <guid>https://dev.to/kenji_sakuramoto_d72920b2/from-this-is-broken-to-a-fix-pr-i-merge-by-hand-cbh</guid>
      <description>&lt;p&gt;A tester on my household-budget app, yen-yen, reported that on the Transactions screen the fixed header covered the first transaction row. It was a layout bug, so nothing threw and my error tracker had nothing to show. The row was still hidden.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kensaurus/mushi-mushi" rel="noopener noreferrer"&gt;Mushi Mushi (虫虫)&lt;/a&gt; is my attempt to close the gap between errors that throw and bugs that users feel. Below is what happens to one report, stage by stage, and why the two steps that touch my repo still wait for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to one report
&lt;/h2&gt;

&lt;p&gt;That tester tapped the widget in the app and typed one sentence: &lt;em&gt;"The fixed header covers the first transaction row…"&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture.&lt;/strong&gt; The report arrives with their note, the route, the browser and viewport, and the last console and network events. A screenshot is attached, and the reporter can remove it before sending. If session replay is on for that session, the last 30 seconds come along too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter.&lt;/strong&gt; Claude Haiku 4.5 makes a cheap first pass and drops spam and noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnose.&lt;/strong&gt; Reports that pass go to a second model (Claude Sonnet 5.5 by default, GPT-5.4 as the fallback). It writes a category, a severity, the likely component, a plain-English summary and a confidence score. It reads code retrieved from my repo while it does this, so it can name a file it actually saw instead of guessing. On the hosted service this takes about 10 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group.&lt;/strong&gt; Each report is embedded and compared with earlier ones by cosine similarity in pgvector. Above 0.82 (the default; each project can change it), the new report joins the existing group instead of opening a new row. Grouped duplicates don't count toward the monthly diagnosis quota. Until early October the per-project threshold was saved but never read, so every project got 0.82. It is read now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix.&lt;/strong&gt; Auto-fix stays off until I enable it for a project. Then I click &lt;strong&gt;Dispatch fix&lt;/strong&gt; on the report. The fix worker loads whole files from the repo at a pinned commit and asks the model for find-and-replace edits through a typed schema. The model gets no tools and no shell. If the edits don't apply, the worker retries once with the exact error, then stops. If any single file's diff would pass 200 changed lines (a per-project setting), the run aborts. Otherwise it opens the PR as a draft and marks it ready for review so CI runs. Every LLM call is traced in Langfuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check.&lt;/strong&gt; The fix's PDCA receipt in the console shows CI on that PR as pending, passed or failed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act.&lt;/strong&gt; I merge it, or I don't. On merge, Mushi marks the report fixed, resolves the linked Sentry issue if the Sentry plugin is on, and sends the person who reported it a "fixed" notice when their report can receive one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the header report, the diagnosis read: &lt;em&gt;"Header stays fixed and covers the first transaction row when scrolling on Transactions; content clipped under sticky chrome on iOS + Android"&lt;/em&gt; (severity high, confidence 0.95). The draft fix it opened is public: &lt;a href="https://github.com/kensaurus/yen-yen/pull/68" rel="noopener noreferrer"&gt;yen-yen #68&lt;/a&gt;. I closed it without merging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I still click merge
&lt;/h2&gt;

&lt;p&gt;The loop could run without me: CI already runs on every fix PR. I kept the merge manual anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  The grades are useful, not final
&lt;/h3&gt;

&lt;p&gt;A separate background job, the judge, re-reads classified reports in batches. It scores accuracy, severity calibration, component tagging and repro quality from 0 to 1, and says whether it agrees with the overall call. Those scores feed an A/B test between classifier prompts, so a better prompt can win. They don't change a report's status, and they aren't a reason to merge a fix unread. A severity that is off, or a fix aimed at the wrong component, is exactly what a loop with nobody watching would ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  A wrong fix costs more than a slow one
&lt;/h3&gt;

&lt;p&gt;When someone reports a bug and later sees it marked fixed, they learn that the app is looked after. An automatic fix that changes the wrong thing and breaks something else teaches them the opposite. The fix worker's header comment states the rule: "human still confirms merge — nothing auto-merges." I made the path from report to reviewable PR fast and left the merge to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's open source
&lt;/h2&gt;

&lt;p&gt;The SDKs are MIT: &lt;code&gt;@mushi-mushi/web&lt;/code&gt; for any site, plus React, Vue, Svelte, Angular, React Native, Capacitor and Node. The CLI and the MCP server are MIT too. iOS, Android and Flutter SDKs are in preview. The server (Supabase Edge Functions, Postgres and pgvector) is AGPLv3. You can self-host it for your own product for free, with your own LLM key and no diagnosis cap. If you run a modified server as a service for other people, AGPL §13 asks you to publish your changes, or you can take a commercial license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source
&lt;/h2&gt;

&lt;p&gt;The fix worker, the judge and the grouping code described above are in &lt;a href="https://github.com/kensaurus/mushi-mushi" rel="noopener noreferrer"&gt;github.com/kensaurus/mushi-mushi&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've caught user-reported bugs that never threw anything, I'd like to hear how.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I wrote this post with help from an AI assistant. The numbers, the quoted report and its diagnosis come from the Mushi repo and the public yen-yen PR linked above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A 147,000-word interactive book as a free SPA, and the stack behind it</title>
      <dc:creator>Kenji Sakuramoto</dc:creator>
      <pubDate>Thu, 23 Apr 2026 07:48:48 +0000</pubDate>
      <link>https://dev.to/kenji_sakuramoto_d72920b2/i-shipped-a-147000-word-interactive-book-as-a-free-spa-heres-the-stack-55i0</link>
      <guid>https://dev.to/kenji_sakuramoto_d72920b2/i-shipped-a-147000-word-interactive-book-as-a-free-spa-heres-the-stack-55i0</guid>
      <description>&lt;p&gt;&lt;em&gt;&lt;a href="https://kensaur.us/the-wanting-mind/" rel="noopener noreferrer"&gt;the wanting mind&lt;/a&gt;&lt;/em&gt; is a free 147,000-word interactive book: no ads, no account, in English, Japanese, Chinese and Thai. It is a React 19 SPA with Supabase behind it and Capacitor for the mobile apps. ElevenLabs reads the chapters, fal.ai made the chapter art, Veo made the hero videos, and Claude translated the chapters. This post covers what worked, what I'd do again, and the stack.&lt;/p&gt;

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

&lt;p&gt;A long-form book about &lt;em&gt;why we're never satisfied&lt;/em&gt;: the economics, neuroscience and Buddhist philosophy of &lt;code&gt;taṇhā&lt;/code&gt; ("thirst"). At 147,000 words, reading it cover to cover takes about eleven hours. Most people won't do that, so the app gives them other ways in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Narrated chapters.&lt;/strong&gt; ElevenLabs voices, with 12 narrator personas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 3D knowledge graph&lt;/strong&gt; of the concepts, people and chapters, built with &lt;code&gt;react-three-fiber&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22 simulations&lt;/strong&gt; you can play with: the Easterlin paradox, Minsky moments, &lt;code&gt;r &amp;gt; g&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four languages&lt;/strong&gt;, switchable while you read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An "essence" view&lt;/strong&gt;: each chapter compressed to one quote over its chapter image, shareable as a card.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read it: &lt;a href="https://kensaur.us/the-wanting-mind/" rel="noopener noreferrer"&gt;kensaur.us/the-wanting-mind&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;React 19 + Vite 8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast dev loop; a prerender step after the build for SEO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TypeScript 6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The book's content lives in typed data files, so a typo in a chapter reference fails the build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Tailwind CSS 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Design tokens in one file, dark mode with &lt;code&gt;dark:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3D&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;reagraph&lt;/strong&gt; on react-three-fiber&lt;/td&gt;
&lt;td&gt;The knowledge graph and the closing constellation scene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Capacitor 8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The same React build for iOS and Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth + DB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Supabase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Accounts and cloud sync, with row-level security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;English narration of all 36 chapters, about 20 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;fal.ai&lt;/strong&gt; (nano-banana-2)&lt;/td&gt;
&lt;td&gt;Chapter essence images, batch-generated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Veo 3.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;35 looping hero videos and a cover video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chapter by chapter, into Japanese, Chinese and Thai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;S3 + CloudFront&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static files; no server to keep running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sentry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Errors on readers' devices reach me without anyone having to report them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Firebase Cloud Messaging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Notifications to signed-in readers' devices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Three things that mattered more than I expected
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Prerendering chapter HTML
&lt;/h3&gt;

&lt;p&gt;An SPA serves one HTML shell for every route. Google can render JavaScript, but other crawlers and link previews often read only that shell. So &lt;code&gt;scripts/prerender-chapter-html.mjs&lt;/code&gt; runs after &lt;code&gt;vite build&lt;/code&gt;. It copies &lt;code&gt;dist/index.html&lt;/code&gt; once for every chapter and feature page in all four locales, and gives each copy its own &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, description, canonical URL and OpenGraph tags. The deploy uploads each file to the matching S3 key, so a crawler or link preview asking for &lt;code&gt;/ja/ch-4&lt;/code&gt; gets that chapter's title and description in the first response. The chapter text itself still renders in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Quote cards drawn in the browser
&lt;/h3&gt;

&lt;p&gt;Every chapter has an "essence", one sentence that sums it up. &lt;code&gt;generateQuoteCardBlob&lt;/code&gt; in &lt;code&gt;src/components/QuoteCard.tsx&lt;/code&gt; draws it as a 1080×1080 card on a Canvas: the chapter image as the backdrop, a translucent overlay, the quote, and the book's name. It runs in the browser with no server. Readers can save the card or share it with &lt;code&gt;navigator.share()&lt;/code&gt; on mobile.&lt;/p&gt;

&lt;p&gt;For social posts I call the same function from a Playwright script (&lt;code&gt;scripts/generate-quote-cards.mjs&lt;/code&gt;), so the cards I post are the cards readers get.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Store listings from one file
&lt;/h3&gt;

&lt;p&gt;The store listings exist in four locales on both stores, and editing them by hand is slow and easy to get wrong. Two scripts do it now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scripts/asc-update-listing.mjs&lt;/code&gt; signs a JWT with the App Store Connect API key, reads the listing copy from &lt;code&gt;docs/store-listings.md&lt;/code&gt;, and pushes name, subtitle, promotional text, description and keywords for each locale.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/play-update-listing.mjs&lt;/code&gt; does the same through the Google Play Publisher API with a service account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;docs/store-listings.md&lt;/code&gt; is the one place the listing copy is edited.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you build something similar
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pick one mobile shell (Capacitor or Tauri) and stay with it.&lt;/li&gt;
&lt;li&gt;Prerender before you write growth posts. The posts link to pages, and the pages should show their content to whatever opens them.&lt;/li&gt;
&lt;li&gt;Add error reporting on the first day, even for a personal project.&lt;/li&gt;
&lt;li&gt;Generate marketing images from the app's own code, so they match what readers see.&lt;/li&gt;
&lt;li&gt;Keep the content (chapters, essences, graph nodes) as plain TypeScript files in git. For a single author, that is enough of a CMS.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The source isn't public yet. If a translation reads wrong, the feedback button in the app reaches me directly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I wrote this post with help from an AI assistant. The figures come from the project's repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
