<?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: Arsene Muyen Lee</title>
    <description>The latest articles on DEV Community by Arsene Muyen Lee (@arsene_muyenlee_b6090d8d).</description>
    <link>https://dev.to/arsene_muyenlee_b6090d8d</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%2F3692161%2F687c59e7-b09d-4169-b6ce-3627b41b6196.png</url>
      <title>DEV Community: Arsene Muyen Lee</title>
      <link>https://dev.to/arsene_muyenlee_b6090d8d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arsene_muyenlee_b6090d8d"/>
    <language>en</language>
    <item>
      <title>I encoded a 1000-year-old divination system in Go. Every bug was a distinction my types had collapsed.</title>
      <dc:creator>Arsene Muyen Lee</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:41:16 +0000</pubDate>
      <link>https://dev.to/arsene_muyenlee_b6090d8d/i-encoded-a-1000-year-old-divination-system-in-go-every-bug-was-a-distinction-my-types-had-2p56</link>
      <guid>https://dev.to/arsene_muyenlee_b6090d8d/i-encoded-a-1000-year-old-divination-system-in-go-every-bug-was-a-distinction-my-types-had-2p56</guid>
      <description>&lt;p&gt;I spend my day job on cloud platform work — pipelines, idempotent state machines, the usual distributed-systems apparatus. For the last year I've spent evenings on something with no obvious overlap: an engine that computes and reads &lt;strong&gt;Zi Wei Dou Shu&lt;/strong&gt; (紫微斗數), a Chinese astrology system that's been accreting rules since roughly the Song dynasty.&lt;/p&gt;

&lt;p&gt;It turned out to overlap completely. Not because divination is secretly distributed systems, but because Zi Wei is a formal system someone else specified, a thousand years ago, without writing types. My job was to encode it. And every serious bug I hit had the same shape:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The tradition drew a distinction. My data structure didn't. Some code path downstream then confidently stated the opposite of the truth.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a domain-modeling failure mode, and it's a nastier one than it sounds, because the output was always &lt;em&gt;grammatical&lt;/em&gt;. There was no crash, no nil deref, no failing assertion. Just a fluent sentence that a knowledgeable reader would recognize as nonsense.&lt;/p&gt;

&lt;p&gt;Here are the ones worth showing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The system, in one paragraph
&lt;/h2&gt;

&lt;p&gt;A Zi Wei chart is twelve &lt;strong&gt;palaces&lt;/strong&gt; (宮) arranged in a ring, each holding some of ~66 stars. Each palace also carries its own &lt;strong&gt;heavenly stem&lt;/strong&gt; (天干), and each stem produces four &lt;strong&gt;transformations&lt;/strong&gt; (四化 — 祿/權/科/忌, roughly: flourishing, authority, reputation, obstruction). A transformation "flies" from the palace that produced it into some other palace. So a chart isn't twelve independent readings — it's a directed graph with twelve nodes and up to 48 edges, and the interesting claims live on the edges.&lt;/p&gt;

&lt;p&gt;That's the whole domain for this post. The engine computes the graph client-side, sends a summary to a Go backend, and the backend composes a reading from an authored corpus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the struct that knew less than the tradition
&lt;/h2&gt;

&lt;p&gt;A transformation flies from one palace to another. Except sometimes it lands on the palace that produced it. The tradition calls that &lt;strong&gt;自化&lt;/strong&gt; (self-transformation), and it means something &lt;em&gt;opposite&lt;/em&gt; to a normal flight: an outgoing 化 is that palace acting on another area of your life; a 自化 is that energy running on its own and — per the sources — not being retained.&lt;/p&gt;

&lt;p&gt;My wire format didn't care:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// PalaceSlot is one of the 12 palaces.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PalaceSlot&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Stem&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;        &lt;span class="c"&gt;// this palace's own 天干 — the input to Flying&lt;/span&gt;
    &lt;span class="n"&gt;Flying&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;FlyingSihua&lt;/span&gt; &lt;span class="c"&gt;// where each of its 祿/權/科/忌 lands&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;FlyingSihua&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Mutagen&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="c"&gt;// 祿/權/科/忌&lt;/span&gt;
    &lt;span class="n"&gt;ToPalace&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One list. Self-landing edges sat in it alongside everything else, because structurally they &lt;em&gt;are&lt;/em&gt; the same thing: a mutagen and a destination.&lt;/p&gt;

&lt;p&gt;Downstream, a composer walked that list and wrote each entry as &lt;em&gt;"your 祿 flies out to your X."&lt;/em&gt; Which meant a palace whose 祿 self-transformed got told its 祿 flew out &lt;strong&gt;to itself&lt;/strong&gt;. In the paid reading. And in the chat context, which reads the same structure — so the LLM answering follow-up questions was grounded in a fact that contradicted the tradition it was supposedly grounded in.&lt;/p&gt;

&lt;p&gt;The tempting fix is a guard in the composer: &lt;code&gt;if f.ToPalace == p.Name { continue }&lt;/code&gt;. I did that first. It's wrong, because it makes correctness a thing every future consumer has to &lt;em&gt;remember&lt;/em&gt;, and there were already three consumers.&lt;/p&gt;

&lt;p&gt;The fix that holds is splitting it in the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// SplitSelfSihua separates a palace's 自化 from its outbound flights.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;SplitSelfSihua&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="n"&gt;PalaceSlot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;FlyingSihua&lt;/span&gt;&lt;span class="p"&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flying&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;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToPalace&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;outgoing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then making the &lt;em&gt;only&lt;/em&gt; structure composers can reach carry both fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;DomainPalaceFacts&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Palace&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="c"&gt;// Outbound is only the transformations that REACH ANOTHER PALACE. A 化&lt;/span&gt;
    &lt;span class="c"&gt;// landing back on its own palace (自化) is split into Self instead, because&lt;/span&gt;
    &lt;span class="c"&gt;// the two make opposite statements and every consumer here renders Outbound&lt;/span&gt;
    &lt;span class="c"&gt;// with "flies out to your X" phrasing.&lt;/span&gt;
    &lt;span class="n"&gt;Outbound&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;FlyingSihua&lt;/span&gt;
    &lt;span class="n"&gt;Self&lt;/span&gt;     &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;FlyingSihua&lt;/span&gt;
    &lt;span class="n"&gt;Inbound&lt;/span&gt;  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;InboundFlying&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a composer that ignores &lt;code&gt;Self&lt;/code&gt; omits something. It can no longer &lt;em&gt;assert&lt;/em&gt; something false. That's the whole difference, and it's the "make illegal states unrepresentable" idea applied to a domain where the illegal state isn't a nil pointer — it's a sentence.&lt;/p&gt;

&lt;p&gt;I also wrote the spec rule to say &lt;em&gt;why&lt;/em&gt;, because the next person to touch this will otherwise re-merge the fields for convenience:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is not a style preference. A structure that carried both under one name was consumed by a composer that wrote every entry as "flies out to your &lt;code&gt;&amp;lt;palace&amp;gt;&lt;/code&gt;", so a palace whose own 祿 self-transformed was told its 祿 flew out to itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bug 2: matching on the wrong key told a two-year-old about his forties
&lt;/h2&gt;

&lt;p&gt;Life is divided into twelve ten-year &lt;strong&gt;decades&lt;/strong&gt; (大限), each sitting on a palace. To find which decade you're currently in, the obvious move is: ask the chart engine which palace is the current decadal palace, then find the age band sitting on that palace.&lt;/p&gt;

&lt;p&gt;Palace names are the join key. Palace names are also &lt;em&gt;not unique across time&lt;/em&gt; — the ring has twelve positions and twelve bands, and matching by name finds &lt;em&gt;a&lt;/em&gt; band on that palace, not &lt;em&gt;your&lt;/em&gt; band.&lt;/p&gt;

&lt;p&gt;Worse: the engine reports a decadal palace even for someone who hasn't reached their first decade at all (六歲起運 — decades start around age six, so a five-year-old has none). So the lookup happily returned a match seventy years out. A two-year-old's chart said &lt;em&gt;"this year sits inside your 76–85 decade."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix isn't cleverer matching, it's a validity check on the result:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The system SHALL treat a 大限 age band as the active decade only if the band's calendar years can contain the year the reading is about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With one deliberate exception, which I think is the more interesting half:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where the birth year cannot be resolved the palace match SHALL be used as-is: &lt;strong&gt;suppression requires proof the band is wrong, not absence of evidence that it is right.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That asymmetry comes up constantly once you look for it. "I can't verify this" and "I have verified this is wrong" should not produce the same behavior, and a naive &lt;code&gt;if !ok { return }&lt;/code&gt; collapses them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: the pigeonhole that only bites two birth years in ten
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;來因宮&lt;/strong&gt; (origin palace) is defined as: the palace whose own stem equals your birth year's stem. Exactly one palace, reported as a lifelong placement.&lt;/p&gt;

&lt;p&gt;Straightforward scan, right? Find the palace whose &lt;code&gt;Stem&lt;/code&gt; matches &lt;code&gt;YearStem&lt;/code&gt;. That's a one-liner, it passes tests, and it's broken for 20% of humanity.&lt;/p&gt;

&lt;p&gt;Palace stems are assigned by a rule called 五虎遁, which walks &lt;strong&gt;10 stems across 12 branches&lt;/strong&gt;. Pigeonhole: two palaces always repeat the opening stem. For two of the ten birth-year stems, that repeat &lt;em&gt;is&lt;/em&gt; the year stem — 辛 years put 辛 on both 卯 and 丑; 壬 years put 壬 on both 寅 and 子. A naive scan returns two answers for one in five charts, and whichever one it returns first wins.&lt;/p&gt;

&lt;p&gt;The tradition already knew. The 來因宮 never falls in 子 or 丑, &lt;em&gt;precisely because&lt;/em&gt; those are the duplicated positions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// THE TRAP: 五虎遁 walks 10 stems across 12 branches, so two palaces always&lt;/span&gt;
&lt;span class="c"&gt;// repeat the opening stem, and for two of the ten birth years that repeat IS&lt;/span&gt;
&lt;span class="c"&gt;// the year stem. 辛年 puts 辛 on both 卯 and 丑; 壬年 puts 壬 on both 寅 and 子.&lt;/span&gt;
&lt;span class="c"&gt;// A naive scan returns two palaces for one in five charts. The 來因宮 never&lt;/span&gt;
&lt;span class="c"&gt;// falls in 子 or 丑 precisely because those are the duplicated positions, so&lt;/span&gt;
&lt;span class="c"&gt;// excluding them leaves exactly one answer for all ten stems.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;DeriveLaiYinGong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;palace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;branches&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;palaceBranches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&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;branches&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Natal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;YearStem&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Palaces&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;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stem&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Natal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;YearStem&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"子"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"丑"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&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;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines of exclusion. The value is entirely in the comment, and I'd argue the comment is the deliverable — the code without it looks like an arbitrary special case that a future cleanup deletes.&lt;/p&gt;

&lt;p&gt;The general lesson: &lt;strong&gt;when you're implementing someone else's formal system, an apparently arbitrary restriction is usually load-bearing.&lt;/strong&gt; Find out what it's carrying before you optimize it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trusting the client exactly as far as you can re-derive it
&lt;/h2&gt;

&lt;p&gt;The chart is computed in the browser (the engine is a TypeScript library) and the summary is POSTed to Go. Which makes every field on it untrusted input — not in the security sense here so much as the &lt;em&gt;correctness&lt;/em&gt; sense: a version skew, a library change, a reordering, and the backend composes a confident reading from wrong facts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PalaceSlot.Stem&lt;/code&gt; is the sole input to &lt;code&gt;PalaceSlot.Flying&lt;/code&gt;, and &lt;code&gt;Flying&lt;/code&gt; is what reaches the paid reading. Validation checked that &lt;code&gt;Stem&lt;/code&gt; was... a string.&lt;/p&gt;

&lt;p&gt;So the backend re-derives all twelve independently and compares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// crossCheckPalaceStems re-derives all 12 palace 天干 via 五虎遁 and compares&lt;/span&gt;
&lt;span class="c"&gt;// them to what the client sent.&lt;/span&gt;
&lt;span class="c"&gt;//&lt;/span&gt;
&lt;span class="c"&gt;// This is not busywork: PalaceSlot.Stem is the sole input to PalaceSlot.Flying,&lt;/span&gt;
&lt;span class="c"&gt;// Flying is what reaches the paid reading, and nothing checked either of them&lt;/span&gt;
&lt;span class="c"&gt;// until now. It also underwrites palaceBranches: if Summary.Palaces ever stops&lt;/span&gt;
&lt;span class="c"&gt;// arriving in 地支 order, the offsets go wrong and this fires, instead of&lt;/span&gt;
&lt;span class="c"&gt;// 來因宮 quietly naming the wrong palace.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;crossCheckPalaceStems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Mismatch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;branches&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;palaceBranches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&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;branches&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Mismatch&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Palaces&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;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stem&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;palaceStem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Natal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;YearStem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mismatch&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"palaces["&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"].stem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Duplicated logic across two languages, deliberately. It's the cheap version of a parity oracle: the two implementations are independent enough that agreement is evidence, and disagreement surfaces as a named mismatch instead of as a plausible paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I nearly shipped that didn't grade anything
&lt;/h2&gt;

&lt;p&gt;One feature counts how many 化忌 (the obstruction transformation) converge on each palace, and grades the heavy ones. Before shipping it I measured the tier distribution across the reference charts.&lt;/p&gt;

&lt;p&gt;The severe tier — the one whose vocabulary is 凶/破 — covered &lt;strong&gt;11% of individual palaces&lt;/strong&gt;. Fine. But I was also grading &lt;em&gt;axes&lt;/em&gt; (a palace plus its opposite), and across axes the same tier covered &lt;strong&gt;32%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A verdict of that severity applied to a third of the board isn't a verdict. It's a mood. So the rule ships grading single palaces only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The system SHALL grade a single palace by its own count, and SHALL NOT grade the sum across an axis.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The habit that came out of this and that I'd defend generally: &lt;strong&gt;before you ship a rule that classifies things, measure its firing rate on real data.&lt;/strong&gt; A rule that fires on a third of inputs conveys almost nothing, no matter how correct each individual firing is. That's true of lint rules, alert thresholds, fraud heuristics, and — apparently — thousand-year-old divination texts.&lt;/p&gt;

&lt;p&gt;The same feature also ships behind a flag that defaults to off, with "absence of the setting means off," because the reason to disable it is &lt;em&gt;the text reads badly on a real chart&lt;/em&gt; — which you only find out in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The through-line: decline instead of guessing
&lt;/h2&gt;

&lt;p&gt;Written out, the rules I kept adding were all one rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing per-palace stems, birth-year stem, or 命宮 branch → &lt;strong&gt;no 來因宮 named&lt;/strong&gt;, rather than one guessed.&lt;/li&gt;
&lt;li&gt;Chart carries no 生年四化, or client sent no flying data → &lt;strong&gt;neither paragraph written&lt;/strong&gt;, rather than half a contrast.&lt;/li&gt;
&lt;li&gt;No palace on the chart reached by two or more 忌 → &lt;strong&gt;no verdict text at all&lt;/strong&gt;, rather than an empty or hedged section.&lt;/li&gt;
&lt;li&gt;No self-transformation anywhere → &lt;strong&gt;no self-transformation section&lt;/strong&gt;, rather than one saying there isn't one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure mode this guards against is specific to generated text, and I think it's underrated: an LLM-adjacent pipeline given a section header and no facts will fill the section anyway. It will hedge beautifully. The output is indistinguishable from a real reading until someone who knows the domain reads it.&lt;/p&gt;

&lt;p&gt;Empty sections aren't neutral. They're an invitation to fabricate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually generalized
&lt;/h2&gt;

&lt;p&gt;None of this is about astrology, and I'd have learned the same things porting an insurance rating manual or a tax code — any formal system that predates the software and that you don't get to redesign:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A distinction the domain makes must exist in the type, not in each consumer's discipline.&lt;/strong&gt; Guards in consumers scale linearly with consumers; a split field scales with zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join on something unique, and validate the result independently of the join.&lt;/strong&gt; Name-matching found a real band, just not the right one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arbitrary-looking restrictions in an old system are usually load-bearing.&lt;/strong&gt; The exclusion you're about to delete is someone's pigeonhole fix from 400 years ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-derive untrusted input you can re-derive.&lt;/strong&gt; Two independent implementations turn silent wrongness into a named mismatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure a classifier's firing rate before shipping it.&lt;/strong&gt; A severe verdict on a third of cases grades nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Can't verify" and "verified wrong" are different states.&lt;/strong&gt; Don't collapse them into one &lt;code&gt;return&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The engine is part of &lt;a href="https://iyarrow.xyz" rel="noopener noreferrer"&gt;iYarrow&lt;/a&gt;, where the natal chart and reading are free. The casting algorithm it grew out of is open source and separate: &lt;a href="https://github.com/muyen/meihua-yishu" rel="noopener noreferrer"&gt;meihua-yishu&lt;/a&gt;, a 梅花易數 implementation that stays the parity oracle for the divination arithmetic.&lt;/p&gt;

&lt;p&gt;If you've hit the same shape of bug in a domain nobody would call exotic, I'd like to hear it — my strong suspicion is that "fluent and wrong" is much more common than "crashed," and much less reported.&lt;/p&gt;

</description>
      <category>go</category>
      <category>architecture</category>
      <category>programming</category>
      <category>softwaredesign</category>
    </item>
    <item>
      <title>What 963 Commits Taught Me About the 'Vibe to Prod' Gap</title>
      <dc:creator>Arsene Muyen Lee</dc:creator>
      <pubDate>Sun, 04 Jan 2026 05:53:28 +0000</pubDate>
      <link>https://dev.to/arsene_muyenlee_b6090d8d/what-963-commits-taught-me-about-the-vibe-to-prod-gap-9ca</link>
      <guid>https://dev.to/arsene_muyenlee_b6090d8d/what-963-commits-taught-me-about-the-vibe-to-prod-gap-9ca</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why AI-assisted coding is 10% of shipping software&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;I built the core of a media authenticity app in a weekend.&lt;/p&gt;

&lt;p&gt;Firebase Auth. Cryptographic signing. Camera integration. Claude helped me write Go handlers. Cursor generated React components. The demo was &lt;em&gt;chef's kiss&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I felt like a god.&lt;/p&gt;

&lt;p&gt;Then I tried to deploy it. That's when I discovered what 963 commits of pain looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 90% Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Here's what tutorials show you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Idea → Code → "It works!"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what production actually requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Idea → Code → Tests → Security → CI/CD → Infrastructure → Deploy → Monitor → Iterate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That arrow between "Code" and "Tests"? It took me &lt;strong&gt;963 commits&lt;/strong&gt; and nearly a year to cross.&lt;/p&gt;

&lt;p&gt;I'm not exaggerating. Here's my actual git history.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Journey (From My Git Log)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Commit #1: The Euphoria
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fb5f2ea Initial project structure
8750a5c Implement core backend with cryptographic proof system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two commits. Core functionality done. I felt invincible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commits #3-12: The Reality Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f7d4886 Add Docker build and Cloud Build configuration
a965146 Add Docker build and Cloud Build configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, why are there two commits for the same thing? Because the first one didn't work.&lt;/p&gt;

&lt;p&gt;Then came the GitHub Actions saga:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8d21a90 Fix GitHub Actions: Add CLOUDSDK_CORE_PROJECT env var
383e76a Fix GitHub Actions: Use google-github-actions/auth@v2
8159c3f Fix GitHub Actions: Add export_default_credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate commits just to get authentication working in CI. Each one felt like victory. Each one revealed another problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Milestone That Mattered
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;23be044 ✅ Complete CI/CD pipeline working!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that emoji? That's not decoration. That's relief. That commit represented weeks of wrestling with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service account permissions&lt;/li&gt;
&lt;li&gt;Docker layer caching&lt;/li&gt;
&lt;li&gt;Environment variable injection&lt;/li&gt;
&lt;li&gt;Secrets management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code itself? Maybe 200 lines. The debugging? Countless hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Gaps AI Won't Vibe Through
&lt;/h2&gt;




&lt;h3&gt;
  
  
  Gap #1: CI/CD Is a Special Hell
&lt;/h3&gt;

&lt;p&gt;AI can write your GitHub Actions workflow. It cannot debug why your workflow passes locally but fails in CI with a cryptic &lt;code&gt;PERMISSION_DENIED&lt;/code&gt; error at 11pm when you just want to ship.&lt;/p&gt;

&lt;p&gt;My repo has &lt;strong&gt;30+ commits&lt;/strong&gt; with "fix" and "CI" in the message. Each one taught me something documentation didn't.&lt;/p&gt;




&lt;h3&gt;
  
  
  Gap #2: Infrastructure Is Not Code
&lt;/h3&gt;

&lt;p&gt;"Just use Terraform" they say. "Pulumi is easy" they say.&lt;/p&gt;

&lt;p&gt;Sure. But first you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up GCP/AWS accounts and billing&lt;/li&gt;
&lt;li&gt;Configure service accounts with correct IAM roles (not too few, not too many)&lt;/li&gt;
&lt;li&gt;Create Artifact Registry for Docker images&lt;/li&gt;
&lt;li&gt;Maintain separate dev/prod environments that don't accidentally cross&lt;/li&gt;
&lt;li&gt;Wire up secrets management without committing &lt;code&gt;.env&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Configure custom domains and SSL certificates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each bullet is a rabbit hole. None of them ship features.&lt;/p&gt;




&lt;h3&gt;
  
  
  Gap #3: Security Is Invisible Until It Isn't
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;c8b4a6d feat: Complete integration test infrastructure with production-ready authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single commit message hides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT validation that actually validates&lt;/li&gt;
&lt;li&gt;Firebase Auth token verification&lt;/li&gt;
&lt;li&gt;Rate limiting (because someone &lt;em&gt;will&lt;/em&gt; try to hammer your API)&lt;/li&gt;
&lt;li&gt;Security headers: CORS, CSP, HSTS&lt;/li&gt;
&lt;li&gt;Dependency vulnerability scanning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this was "vibed." All of it was required before I could sleep at night.&lt;/p&gt;




&lt;h3&gt;
  
  
  Gap #4: Cross-Platform Is Cross-Painful
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5841cdb fix iPad create proof crash issue
e2b3ec8 android and universal link fixes
c7ff253 android fix like and bookmark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app worked on my iPhone. And my Android test device.&lt;/p&gt;

&lt;p&gt;iPads? Different aspect ratios broke the UI. Different memory constraints caused crashes. Different logs to hunt through.&lt;/p&gt;

&lt;p&gt;"Works on my device" is the new "works on my machine."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Extracted
&lt;/h2&gt;

&lt;p&gt;After 963 commits, I did something I should have done earlier: I separated my business logic from everything else.&lt;/p&gt;

&lt;p&gt;The "everything else" turned out to be around 15,000 lines of infrastructure code. A year of debugging distilled into config files and workflows.&lt;/p&gt;

&lt;p&gt;Here's what's in there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions pipelines&lt;/strong&gt; with security scanning (gosec, Trivy, govulncheck) — because I got tired of finding vulnerabilities in prod&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pulumi infrastructure&lt;/strong&gt; for Cloud Run, Firebase, and networking — because clicking through GCP console doesn't scale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment separation&lt;/strong&gt; that actually separates dev from prod — learned this one the expensive way&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase Auth integration&lt;/strong&gt; that handles edge cases — the "happy path" documentation doesn't mention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured logging with Zap&lt;/strong&gt; — because &lt;code&gt;fmt.Println&lt;/code&gt; stops being funny in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checks and graceful shutdown&lt;/strong&gt; — for when Cloud Run decides to kill your container mid-request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker builds with layer caching&lt;/strong&gt; — because 15-minute builds break your flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-commit hooks&lt;/strong&gt; — to catch the problems I kept committing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GitHub Actions workflows have comments explaining why certain approaches don't work. Those comments are my debugging sessions, preserved in code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Math
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What I Expected&lt;/th&gt;
&lt;th&gt;What It Actually Took&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core features&lt;/td&gt;
&lt;td&gt;1 weekend&lt;/td&gt;
&lt;td&gt;1 weekend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD pipeline&lt;/td&gt;
&lt;td&gt;2 hours&lt;/td&gt;
&lt;td&gt;3 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;A few days&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security hardening&lt;/td&gt;
&lt;td&gt;Add it later&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Manual is fine&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;Console.log&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Weekend project → 2+ months to ship properly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unless you start with production infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;I open-sourced the whole thing: &lt;strong&gt;&lt;a href="https://github.com/muyen/vibe-to-prod" rel="noopener noreferrer"&gt;vibe-to-prod&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's the production infrastructure from my actual app with the business logic stripped out. Go backend, Next.js frontend, Pulumi IaC, GitHub Actions CI/CD, Firebase auth.&lt;/p&gt;

&lt;p&gt;Will it save you 963 commits? No. You'll have your own debugging sessions, your own 2am "why doesn't this work" moments. That's how you actually learn this stuff.&lt;/p&gt;

&lt;p&gt;But maybe it saves you the first 200.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The commit that started this: &lt;code&gt;fb5f2ea Initial project structure&lt;/code&gt;. The commit count when I wrote this article: 963. The number of times I Googled "GitHub Actions PERMISSION_DENIED": countless.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>devops</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
